"""覆盖率 78%->85% 第 4 步:cloud/server.py 剩余缺口的真实 HTTP 测试。 沿用 test_coverage85_server2 的 create_server + http.client 模式: 每个测试用独立临时 SQLite 与临时端口,不跑模型、不联网。 重点:_dispatch 的 404/限流分支、_dispatch_project 各子路由的 权限拒绝与错方法分支、identity/memory/kb/cost 等子路由、 continue/wait/followup/result-full 的校验分支、create_task 的 幂等与附件校验分支。 """ import http.client import json import os import sqlite3 import tempfile import threading import unittest from cloud.auth import AuthStore from cloud.server import create_server class Server3TestCase(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.db_path = os.path.join(self.tmp.name, "control.db") self.auth = AuthStore(self.db_path) self.owner_id, self.owner_key = self.auth.create_user("owner") self._old_admins = os.environ.get("CODEX_GLOBAL_ADMINS") os.environ["CODEX_GLOBAL_ADMINS"] = self.owner_id self.viewer_id, self.viewer_key = self.auth.create_user("viewer") self.outsider_id, self.outsider_key = self.auth.create_user( "outsider") self.server = create_server(self.db_path, "127.0.0.1", 0) self.port = self.server.server_address[1] self.thread = threading.Thread( target=self.server.serve_forever, daemon=True) self.thread.start() status, self.project = self._req( "POST", "/projects", {"name": "demo"}, self.owner_key) self.assertEqual(status, 201) self.auth.set_member( self.project["id"], self.owner_id, self.viewer_id, "viewer") def tearDown(self): self.server.shutdown() self.server.server_close() self.thread.join(timeout=5) try: self.auth.conn.close() except Exception: pass self.tmp.cleanup() if self._old_admins is None: os.environ.pop("CODEX_GLOBAL_ADMINS", None) else: os.environ["CODEX_GLOBAL_ADMINS"] = self._old_admins def _req(self, method, path, body=None, key=None, headers=None, raw_body=None): conn = http.client.HTTPConnection( "127.0.0.1", self.port, timeout=10) hdrs = dict(headers or {}) if raw_body is not None: payload = raw_body elif body is not None: payload = json.dumps(body).encode("utf-8") hdrs.setdefault("Content-Type", "application/json") else: payload = None if key is not None: hdrs["X-API-Key"] = key try: conn.request(method, path, body=payload, headers=hdrs) resp = conn.getresponse() data = resp.read() finally: conn.close() try: parsed = json.loads(data.decode("utf-8")) if data else None except ValueError: parsed = None return resp.status, parsed def _pid(self): return self.project["id"] def _create_task(self, key, **extra): body = {"project_id": self._pid(), "prompt": "做一个加法函数"} body.update(extra) status, task = self._req("POST", "/tasks", body, key) self.assertEqual(status, 201, task) return task def _set_status(self, tid, status): conn = sqlite3.connect(self.db_path) try: conn.execute("UPDATE tasks SET status = ? WHERE id = ?", (status, tid)) conn.commit() finally: conn.close() class DispatchBranchesTest(Server3TestCase): def test_oauth_and_keys_and_webhook_404s(self): pid = self._pid() cases = [ ("POST", "/oauth/github/login", None, 404), ("POST", "/oauth/github/callback", {}, 400), ("GET", "/oauth/github/bindings/extra", None, 404), ("DELETE", "/users/me/keys/a/b", None, 404), ("POST", "/users/me/keys/bogus-action", {}, 404), ("GET", "/github-app/nope", None, 404), ("GET", "/webhooks/github", None, 404), ("POST", "/webhooks/nope", {}, 404), ("GET", "/users/me/memories/abc", None, 404), ("GET", "/tasks/nope/bogus-suffix", None, 404), ("PUT", "/projects/%s/members" % pid, {}, 404), ("GET", "/projects/%s/bogus" % pid, None, 404), ] for method, path, body, want in cases: with self.subTest(path=path): status, _ = self._req(method, path, body, self.owner_key) self.assertEqual(status, want) def test_rate_limiter_recreated_when_missing(self): # server.rate_limiter 置空后下一请求重建限流器,不 500。 self.server.rate_limiter = None status, body = self._req("GET", "/projects", key=self.owner_key) self.assertEqual(status, 200) self.assertIsNotNone(self.server.rate_limiter) def test_unauthenticated_and_rate_limited(self): status, body = self._req("GET", "/projects") self.assertEqual(status, 401) # 新用户的新 key:setUp 的请求不计入它的窗口。 _fresh_id, fresh_key = self.auth.create_user("fresh") self.server.rate_limit_per_minute = 1 try: status, _ = self._req("GET", "/projects", key=fresh_key) self.assertEqual(status, 200) status, body = self._req("GET", "/projects", key=fresh_key) self.assertEqual(status, 429) self.assertEqual(body["error"], "rate_limited") finally: self.server.rate_limit_per_minute = None def test_health_with_broken_db(self): # 第二台 server:建好后把库文件换成目录,_connect 失败, # health 仍 200 但 db_ok 为 False(不炸 500)。 db2 = os.path.join(self.tmp.name, "db2.sqlite") bad = create_server(db2, "127.0.0.1", 0) port = bad.server_address[1] th = threading.Thread(target=bad.serve_forever, daemon=True) th.start() try: os.remove(db2) os.mkdir(db2) conn = http.client.HTTPConnection( "127.0.0.1", port, timeout=10) try: conn.request("GET", "/health") resp = conn.getresponse() data = json.loads(resp.read().decode("utf-8")) finally: conn.close() self.assertEqual(resp.status, 200) self.assertFalse(data["db_ok"]) finally: bad.shutdown() bad.server_close() th.join(timeout=5) def test_result_full_variants(self): task = self._create_task(self.owner_key) tid = task["id"] status, body = self._req("GET", "/tasks/%s/result-full" % tid, key=self.owner_key) self.assertEqual(status, 404, body) conn = sqlite3.connect(self.db_path) try: conn.execute( "UPDATE tasks SET result_text_full = ? WHERE id = ?", ("full-result-text", tid)) conn.commit() finally: conn.close() status, body = self._req("GET", "/tasks/%s/result-full" % tid, key=self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["result_text_full"], "full-result-text") status, _ = self._req("GET", "/tasks/%s/result-full" % tid, key=self.outsider_key) self.assertEqual(status, 403) status, _ = self._req("GET", "/tasks/nope/result-full", key=self.owner_key) self.assertEqual(status, 404) class ProjectSubrouteSweepTest(Server3TestCase): def test_outsider_403_sweep(self): pid = self._pid() routes = [ ("POST", "/projects/%s/identity-memories" % pid, {"kind": "fact", "content": "x"}), ("GET", "/projects/%s/identity-memories" % pid, None), ("DELETE", "/projects/%s/identity-memories/m1" % pid, None), ("POST", "/projects/%s/identity-memories/m1/supersede" % pid, {}), ("POST", "/projects/%s/memory" % pid, {"goal": "g", "conclusion": "c"}), ("GET", "/projects/%s/memory" % pid, None), ("POST", "/projects/%s/memory/1/supersede" % pid, {}), ("POST", "/projects/%s/kb/index" % pid, {"path": "a.md", "content": "hi"}), ("GET", "/projects/%s/kb/search?q=hi" % pid, None), ("GET", "/projects/%s/cost" % pid, None), ("POST", "/projects/%s/push/subscriptions" % pid, {}), ("GET", "/projects/%s/push/subscriptions" % pid, None), ("DELETE", "/projects/%s/push/subscriptions/s1" % pid, None), ("POST", "/projects/%s/automations" % pid, {}), ("GET", "/projects/%s/automations" % pid, None), ("GET", "/projects/%s/automations/a1" % pid, None), ("PATCH", "/projects/%s/automations/a1" % pid, {}), ("POST", "/projects/%s/automations/a1/cancel" % pid, {}), ("GET", "/projects/%s/automations/a1/bind" % pid, None), ("POST", "/projects/%s/automations/a1/bind" % pid, {}), ("DELETE", "/projects/%s/automations/a1/bind" % pid, None), ("POST", "/projects/%s/automations/a1/heartbeat" % pid, {}), ("GET", "/projects/%s/triage" % pid, None), ("GET", "/projects/%s/todos" % pid, None), ("POST", "/projects/%s/todos" % pid, {"title": "t"}), ("GET", "/projects/%s/todos/t1" % pid, None), ("PATCH", "/projects/%s/todos/t1" % pid, {}), ("DELETE", "/projects/%s/todos/t1" % pid, None), ("GET", "/projects/%s/skills" % pid, None), ("GET", "/projects/%s/apps" % pid, None), ("GET", "/projects/%s/mcp-servers" % pid, None), ("POST", "/projects/%s/mcp-servers/reload" % pid, {}), ("POST", "/projects/%s/mcp-servers/s1/oauth-login" % pid, {}), ] for method, path, body in routes: with self.subTest(path=path): status, resp = self._req(method, path, body, self.outsider_key) self.assertEqual(status, 403, resp) def test_wrong_method_404_sweep(self): pid = self._pid() routes = [ ("PUT", "/projects/%s/identity-memories" % pid, {}), ("PUT", "/projects/%s/memory" % pid, {}), ("GET", "/projects/%s/kb/index" % pid, None), ("POST", "/projects/%s/kb/search" % pid, {}), ("POST", "/projects/%s/cost" % pid, None), ("PUT", "/projects/%s/automations" % pid, {}), ("DELETE", "/projects/%s/automations/a1" % pid, None), ("GET", "/projects/%s/automations/a1/cancel" % pid, None), ("PUT", "/projects/%s/automations/a1/bind" % pid, {}), ("GET", "/projects/%s/automations/a1/heartbeat" % pid, None), ("POST", "/projects/%s/triage" % pid, {}), ("PUT", "/projects/%s/todos" % pid, {}), ("POST", "/projects/%s/skills" % pid, {}), ("POST", "/projects/%s/apps" % pid, {}), ("GET", "/projects/%s/mcp-servers/reload" % pid, None), ("GET", "/projects/%s/mcp-servers/s1/oauth-login" % pid, None), ("POST", "/projects/%s/mcp-servers/../oauth-login" % pid, {}), ] for method, path, body in routes: with self.subTest(path=path): status, _ = self._req(method, path, body, self.owner_key) self.assertEqual(status, 404) def test_owner_ok_read_routes(self): pid = self._pid() for path in ["/projects/%s/skills" % pid, "/projects/%s/apps" % pid, "/projects/%s/mcp-servers" % pid, "/projects/%s/triage" % pid, "/projects/%s/cost" % pid, "/projects/%s/automations" % pid, "/projects/%s/todos" % pid, "/projects/%s/identity-memories" % pid, "/projects/%s/memory" % pid, "/projects/%s/kb/search?q=hi" % pid, "/projects/%s/push/subscriptions" % pid]: with self.subTest(path=path): status, body = self._req("GET", path, key=self.owner_key) self.assertEqual(status, 200, body) class IdentityMemoryFlowTest(Server3TestCase): def test_identity_memory_create_supersede_delete(self): pid = self._pid() base = "/projects/%s/identity-memories" % pid status, body = self._req("POST", base, {"kind": "fact", "content": "喜欢深色"}, self.owner_key) self.assertEqual(status, 201, body) mid = body["id"] status, body = self._req("GET", base, key=self.owner_key) self.assertEqual(status, 200) self.assertTrue(any(m["id"] == mid for m in body["memories"])) # 坏 kind 400;不存在的记忆 supersede/delete 404。 status, body = self._req("POST", base, {"kind": "bogus-kind", "content": "x"}, self.owner_key) self.assertEqual(status, 400) status, _ = self._req("POST", base + "/nope/supersede", {}, self.owner_key) self.assertEqual(status, 404) status, body = self._req("POST", base + "/%s/supersede" % mid, {"replacement_content": "喜欢浅色"}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["status"], "superseded") status, body = self._req("DELETE", base + "/%s" % mid, key=self.owner_key) self.assertEqual(status, 200) status, _ = self._req("DELETE", base + "/%s" % mid, key=self.owner_key) self.assertEqual(status, 404) def test_project_memory_create_list_supersede(self): pid = self._pid() base = "/projects/%s/memory" % pid status, body = self._req("POST", base, {"goal": "g"}, self.owner_key) self.assertEqual(status, 400) status, body = self._req("POST", base, {"goal": "做周报", "conclusion": "已做"}, self.owner_key) self.assertEqual(status, 201, body) mid = body["id"] status, body = self._req("GET", base + "?top_k=abc", key=self.owner_key) self.assertEqual(status, 400) status, body = self._req("GET", base, key=self.owner_key) self.assertEqual(status, 200) self.assertEqual(len(body["summaries"]), 1) status, body = self._req("POST", base + "/abc/supersede", {}, self.owner_key) self.assertEqual(status, 400) status, body = self._req("POST", base + "/99999/supersede", {}, self.owner_key) self.assertEqual(status, 404) status, body = self._req("POST", base + "/%s/supersede" % mid, {}, self.owner_key) self.assertEqual(status, 200) self.assertEqual(body["status"], "deprecated") def test_kb_index_search_validation(self): pid = self._pid() base = "/projects/%s/kb" % pid status, _ = self._req("POST", base + "/index", {"content": "hi"}, self.owner_key) self.assertEqual(status, 400) status, body = self._req("POST", base + "/index", {"path": "a.md", "content": "hello"}, self.owner_key) self.assertEqual(status, 201, body) status, body = self._req("GET", base + "/search?q=hello", key=self.owner_key) self.assertEqual(status, 200, body) class ContinueTaskTest(Server3TestCase): def test_continue_validation_and_success(self): task = self._create_task(self.owner_key) tid = task["id"] # 进行中任务 409。 status, body = self._req("POST", "/tasks/%s/continue" % tid, {"message": "继续"}, self.owner_key) self.assertEqual(status, 409) self._set_status(tid, "completed") cases = [ ({"message": ""}, 400), ({"message": "x" * 20001}, 400), ({"message": "/goal "}, 400), ({"message": "继续", "effort": "ultra"}, 400), ({"message": "继续", "model": 123}, 400), ({"message": "继续", "approval": "sometimes"}, 400), ({"message": "继续", "child_model": 123}, 400), ({"message": "继续", "child_effort": "ultra"}, 400), ({"message": "继续", "plan": True, "approval": "full"}, 400), ({"message": "继续", "plan": True, "goal_mode": True}, 400), ] for body, want in cases: with self.subTest(body=body): status, _ = self._req("POST", "/tasks/%s/continue" % tid, body, self.owner_key) self.assertEqual(status, want) status, body = self._req("POST", "/tasks/%s/continue" % tid, {"message": "继续做"}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["status"], "queued") self.assertEqual(body["resume_message"], "继续做") # 外人 403;不存在 404;merged 409。 status, _ = self._req("POST", "/tasks/%s/continue" % tid, {"message": "继续"}, self.outsider_key) self.assertEqual(status, 403) status, _ = self._req("POST", "/tasks/nope/continue", {"message": "继续"}, self.owner_key) self.assertEqual(status, 404) self._set_status(tid, "merged") status, _ = self._req("POST", "/tasks/%s/continue" % tid, {"message": "继续"}, self.owner_key) self.assertEqual(status, 409) def test_continue_goal_message_and_quota_402(self): task = self._create_task(self.owner_key) tid = task["id"] self._set_status(tid, "completed") status, body = self._req("POST", "/tasks/%s/continue" % tid, {"message": "/goal 收齐周报"}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["goal_mode"], 1) self.assertEqual(body["goal"], "收齐周报") # 配额 0 的属主:校验通过后 402,不改状态(配额行 # 由 init_db 回填默认额度,已存在时 UPDATE 而非 INSERT)。 conn = sqlite3.connect(self.db_path) try: conn.execute( "UPDATE user_quotas SET quota_usd_micros = 0" " WHERE user_id = ?", (self.owner_id,)) conn.commit() finally: conn.close() status, body = self._req("POST", "/tasks/%s/continue" % tid, {"message": "再继续"}, self.owner_key) self.assertEqual(status, 402, body) class TaskWaitTest(Server3TestCase): def test_wait_validation_and_immediate(self): parent = self._create_task(self.owner_key) child = self._create_task(self.owner_key) # 子任务挂到父任务下(coordinator 语义外的直接写库, # 只为测 wait 的缺省子任务路径)。 conn = sqlite3.connect(self.db_path) try: conn.execute( "UPDATE tasks SET parent_task_id = ? WHERE id = ?", (parent["id"], child["id"])) conn.commit() finally: conn.close() self._set_status(child["id"], "completed") base = "/tasks/%s/wait" % parent["id"] cases = [ ({"timeout_seconds": "x"}, 400), ({"timeout_seconds": 121}, 400), ({"task_ids": "not-a-list"}, 400), ({"task_ids": []}, 400), ({"task_ids": ["a"] * 9}, 400), ({"task_ids": ["ghost"]}, 404), ({"task_ids": [child["id"]], "after_cursor": "bogus"}, 400), ] for body, want in cases: with self.subTest(body=body): status, _ = self._req("POST", base, body, self.owner_key) self.assertEqual(status, want) # 已终态子任务:缺省路径立即返回,不等超时。 status, body = self._req("POST", base, {"timeout_seconds": 5}, self.owner_key) self.assertEqual(status, 200, body) self.assertFalse(body["timed_out"]) # 跨项目子任务 403。 conn = sqlite3.connect(self.db_path) try: conn.execute( "INSERT INTO tasks(id, project_id, prompt, status," " created_by, created_at, updated_at) VALUES(" "'alien', 'other-proj', 'x', 'completed'," " 'someone', 1000, 1000)") conn.commit() finally: conn.close() status, _ = self._req("POST", base, {"task_ids": ["alien"]}, self.owner_key) self.assertEqual(status, 403) def test_wait_too_many_children_default(self): parent = self._create_task(self.owner_key) conn = sqlite3.connect(self.db_path) try: for i in range(9): conn.execute( "INSERT INTO tasks(id, project_id, prompt, status," " parent_task_id, created_by, created_at," " updated_at) VALUES(?, ?, 'x', 'queued', ?, ?," " 1000, 1000)", ("kid-%d" % i, self._pid(), parent["id"], self.owner_id)) conn.commit() finally: conn.close() status, body = self._req("POST", "/tasks/%s/wait" % parent["id"], {"timeout_seconds": 5}, self.owner_key) self.assertEqual(status, 400) self.assertEqual(body["count"], 9) class FollowupTest(Server3TestCase): def test_followup_create_list_validation(self): task = self._create_task(self.owner_key) tid = task["id"] base = "/tasks/%s/followups" % tid status, body = self._req("POST", base, {"body": ""}, self.owner_key) self.assertEqual(status, 400) status, body = self._req("POST", base, {"body": "请继续"}, self.owner_key) self.assertEqual(status, 201, body) status, body = self._req("GET", base, key=self.owner_key) self.assertEqual(status, 200) self.assertEqual(len(body["followups"]), 1) status, _ = self._req("POST", base, {"body": "x"}, self.outsider_key) self.assertEqual(status, 403) status, _ = self._req("POST", "/tasks/nope/followups", {"body": "x"}, self.owner_key) self.assertEqual(status, 404) # queued 任务的 /goal 消息:原地转 goal,返回 200。 status, body = self._req("POST", base, {"body": "/goal 收邮件"}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["delivery"], "goal_converted") status, body = self._req("GET", "/tasks/%s" % tid, key=self.owner_key) self.assertEqual(body["goal_mode"], 1) self.assertEqual(body["goal"], "收邮件") class CreateTaskVariantsTest(Server3TestCase): def test_idempotency_replay_returns_existing(self): task = self._create_task(self.owner_key, idempotency_key="idem-1") status, body = self._req( "POST", "/tasks", {"project_id": self._pid(), "prompt": "另一段话", "idempotency_key": "idem-1"}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["id"], task["id"]) def test_no_time_limit_flag(self): task = self._create_task(self.owner_key, no_time_limit=True) self.assertEqual(task["no_time_limit"], 1) task = self._create_task(self.owner_key) self.assertEqual(task["no_time_limit"], 0) def test_upload_id_validation_errors(self): base = {"project_id": self._pid(), "prompt": "x"} cases = [ (dict(base, upload_id=123), 400), (dict(base, upload_id="ghost", executor="cloud"), 400), (dict(base, upload_id="ghost"), 400), ] for body, want in cases: with self.subTest(body=body): status, _ = self._req("POST", "/tasks", body, self.owner_key) self.assertEqual(status, want) # 他人的包 403;已用 409;过期 400;目录缺失 400;空包 400。 conn = sqlite3.connect(self.db_path) try: conn.execute( "INSERT INTO uploads(id, created_by, status," " created_at) VALUES('up-other', ?, 'ready', ?)", (self.outsider_id, 1000000000)) conn.execute( "INSERT INTO uploads(id, created_by, status," " created_at) VALUES('up-used', ?, 'used', ?)", (self.owner_id, 1000000000)) conn.execute( "INSERT INTO uploads(id, created_by, status," " created_at) VALUES('up-old', ?, 'ready', 1000)", (self.owner_id,)) conn.commit() finally: conn.close() # created_at 用当前时间戳,避免“过期”分支被其它分支抢先: # 上面 up-other/up-used 的 created_at 需改回现在。 conn = sqlite3.connect(self.db_path) try: import time as _time now = _time.time() conn.execute( "UPDATE uploads SET created_at = ? WHERE id IN" " ('up-other', 'up-used')", (now,)) conn.commit() finally: conn.close() for uid, want in [("up-other", 403), ("up-used", 409), ("up-old", 400)]: with self.subTest(uid=uid): status, _ = self._req("POST", "/tasks", dict(base, upload_id=uid), self.owner_key) self.assertEqual(status, want) if __name__ == "__main__": unittest.main()