coverage85-step4c: server 长尾端点与子路由测试(+19 项)
This commit is contained in:
1 parent
21a5fd3a2a
commit
3b7d45c9a2
1 file changed
+614
@@ -0,0 +1,614 @@
|
||||
"""覆盖率 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()
|
||||
Reference in new issue
Block a user