"""线程解析兜底单测:provider 过滤 + 共享 home 回退。 背景(生产实测 2026-10-28):app-server 按进程 provider 过滤 thread/list,不带 sub2api 注入的进程看不到 sub2api 下建的 thread;本地 worker 又用共享 home 而非租户 home。 旧解析只查租户 home + 缺省 provider,本地任务一律 404 thread not found。本批修法:home(租户→共享)× provider(缺省→sub2api)按序穷尽,首个非空即停,并把 provider 原样带给后续的 thread-read/rename/archive CLI。 不跑真实模型、不调真实 host.py:_run_host_cli 与 _local_worker_codex_home 均以 stub 替换。 """ import json import os import sys import tempfile import unittest from pathlib import Path from unittest import mock try: import cloud.server as server_mod from cloud.server import ( CloudControlPlaneHandler, _local_worker_codex_home, _run_host_cli, ) except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) import cloud.server as server_mod from cloud.server import ( CloudControlPlaneHandler, _local_worker_codex_home, _run_host_cli, ) def _handler(tmp): h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler) srv = mock.Mock() srv.tenants_root = os.path.join(tmp, "tenants") srv.host_worktrees_dir = os.path.join(tmp, "worktrees") h.server = srv return h def _list_result(threads): return {"data": threads} class RunHostCliTestCase(unittest.TestCase): def test_model_provider_goes_before_subcommand(self): seen = {} class Proc: returncode = 0 stdout = json.dumps({"data": []}) def fake_run(cmd, **kwargs): seen["cmd"] = cmd return Proc() with mock.patch.object(server_mod.subprocess, "run", fake_run): ok, _ = _run_host_cli(["thread-list", "--cwd", "/wt"], "/home/x", model_provider="sub2api") self.assertTrue(ok) cmd = seen["cmd"] self.assertLess(cmd.index("--model-provider"), cmd.index("thread-list")) self.assertEqual(cmd[cmd.index("--model-provider") + 1], "sub2api") def test_no_provider_arg_when_none(self): seen = {} class Proc: returncode = 0 stdout = json.dumps({"data": []}) def fake_run(cmd, **kwargs): seen["cmd"] = cmd return Proc() with mock.patch.object(server_mod.subprocess, "run", fake_run): ok, _ = _run_host_cli(["thread-list"], "/home/x") self.assertTrue(ok) self.assertNotIn("--model-provider", seen["cmd"]) class LocalWorkerHomeTestCase(unittest.TestCase): def test_reads_host_config_next_to_host_py(self): with tempfile.TemporaryDirectory() as tmp: with open(os.path.join(tmp, "host-config-host.json"), "w", encoding="utf-8") as f: json.dump({"codex_home": "/srv/shared-home"}, f) with mock.patch.object( server_mod, "_host_py_path", return_value=os.path.join(tmp, "host.py")): self.assertEqual(_local_worker_codex_home(), "/srv/shared-home") def test_relative_home_resolves_against_config_dir(self): with tempfile.TemporaryDirectory() as tmp: with open(os.path.join(tmp, "host-config-host.json"), "w", encoding="utf-8") as f: json.dump({"codex_home": "work/home"}, f) with mock.patch.object( server_mod, "_host_py_path", return_value=os.path.join(tmp, "host.py")): self.assertEqual(_local_worker_codex_home(), os.path.join(tmp, "work/home")) def test_missing_config_returns_none(self): with tempfile.TemporaryDirectory() as tmp: with mock.patch.object( server_mod, "_host_py_path", return_value=os.path.join(tmp, "host.py")): self.assertIsNone(_local_worker_codex_home()) class ListFallbackTestCase(unittest.TestCase): def _run(self, calls, env_sub2api=True): """calls: {(home, provider): threads-or-error}。 home 用标记串:'tenant' / 'shared',stub 按 _run_host_cli 收到的 codex_home 参数映射。 """ def fake_cli(argv, home, model_provider=None, **kwargs): key = ("tenant" if "tenants" in home else "shared", model_provider) calls.append(key) val = self.script.get(key) if isinstance(val, Exception): return False, str(val) return True, _list_result(val if val is not None else []) return fake_cli def test_shared_home_plus_sub2api_finds_thread(self): thread = {"id": "t-1", "updatedAt": 100, "cwd": "/wt"} self.script = { ("tenant", None): [], ("tenant", "sub2api"): [], ("shared", None): [], ("shared", "sub2api"): [thread], } calls = [] with tempfile.TemporaryDirectory() as tmp: h = _handler(tmp) task = {"id": "task-1", "project_id": "proj-1"} env = {"SUB2API_BASE_URL": "https://example.invalid"} with mock.patch.dict(os.environ, env), \ mock.patch.object(server_mod, "_run_host_cli", self._run(calls)), \ mock.patch.object( server_mod, "_local_worker_codex_home", return_value=os.path.join(tmp, "shared")): threads, home, _cwd, err, provider = \ h._list_task_threads(task) self.assertIsNone(err) self.assertEqual(threads, [thread]) self.assertEqual(provider, "sub2api") self.assertIn("shared", home) self.assertEqual(calls[-1], ("shared", "sub2api")) def test_tenant_hit_stops_early(self): thread = {"id": "t-9", "updatedAt": 5} self.script = {("tenant", None): [thread]} calls = [] with tempfile.TemporaryDirectory() as tmp: h = _handler(tmp) task = {"id": "task-1", "project_id": "proj-1"} with mock.patch.dict(os.environ, {}, clear=False), \ mock.patch.object(server_mod, "_run_host_cli", self._run(calls)), \ mock.patch.object( server_mod, "_local_worker_codex_home", return_value=os.path.join(tmp, "shared")): threads, _home, _cwd, err, provider = \ h._list_task_threads(task) self.assertIsNone(err) self.assertEqual(threads, [thread]) self.assertIsNone(provider) self.assertEqual(calls, [("tenant", None)]) def test_all_empty_resolves_to_404_not_502(self): self.script = {} with tempfile.TemporaryDirectory() as tmp: h = _handler(tmp) task = {"id": "task-1", "project_id": "proj-1"} with mock.patch.dict( os.environ, {"SUB2API_BASE_URL": "https://example.invalid"}), \ mock.patch.object(server_mod, "_run_host_cli", self._run([])), \ mock.patch.object( server_mod, "_local_worker_codex_home", return_value=os.path.join(tmp, "shared")): _t, _h, _c, err, _p = h._resolve_task_thread(task) self.assertEqual(err, (404, "thread not found")) def test_host_py_failure_stays_502(self): self.script = {} with tempfile.TemporaryDirectory() as tmp: h = _handler(tmp) task = {"id": "task-1", "project_id": "proj-1"} with mock.patch.object( server_mod, "_run_host_cli", return_value=(False, "host.py not found")), \ mock.patch.object( server_mod, "_local_worker_codex_home", return_value=None): _t, _h, _c, err, _p = h._resolve_task_thread(task) self.assertEqual(err[0], 502) if __name__ == "__main__": unittest.main()