"""剩余长函数小提取单测:只钉住新抽出的纯块,不跑真实模型回合、 不联网、不连生产密钥。 覆盖(均取自原函数逐字逻辑): - webhook:评论解析的 review 回退、fix-review prompt 模板与 2000 字截断; - followup:请求体校验的成功/错误串、/goal 前置判定的各分支; - deliver:非 goal 门禁(goal 任务 409、merged 409),以及关键语义—— 门禁绝不拦截终态任务(终态走下面的 CAS 复活,不是 409); - chat_history:秒->毫秒换算、用户条目、链路条目(fu- 回声跳过、 无 ts 贴上一条有 ts 的条目之后且 ts_ms 为 null)。 原路由行为不变由已有的 webhook / followup / thread / chat_history 套件覆盖,本文件不重复测路由,只测抽出子块本身。 """ import os import sys import unittest from pathlib import Path try: from cloud.server import ( _chat_history_trace_entries, _chat_history_user_entries, _classify_goal_followup, _parse_github_comment_with_review_fallback, _seconds_to_ms, _thread_non_goal_guard, _validate_followup_body, _webhook_fix_review_prompt, ) except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.server import ( _chat_history_trace_entries, _chat_history_user_entries, _classify_goal_followup, _parse_github_comment_with_review_fallback, _seconds_to_ms, _thread_non_goal_guard, _validate_followup_body, _webhook_fix_review_prompt, ) def _task(**over): task = {"status": "running", "goal_mode": False, "verify_command": None, "plan": None, "goal": None, "prompt": "p"} task.update(over) return task class WebhookExtractionTestCase(unittest.TestCase): def test_review_payload_verbatim_semantics(self): # 原样语义(注意,不是“修”成 7):现行 parser 里 issue 缺失 # 并不使首解返回 None(只使 pr_number 为 None),故回退块 # 在当前 parser 下不可达;抽出原样保留该块,不擅自改行为。 payload = { "action": "created", "comment": {"body": "/fix-review 请修", "user": {"login": "a"}}, "pull_request": {"number": 7}, "repository": {"full_name": "o/r"}, } result = _parse_github_comment_with_review_fallback(payload) self.assertEqual(result["pr_number"], None) self.assertEqual(result["comment"], "/fix-review 请修") self.assertIsNone(_parse_github_comment_with_review_fallback( {"action": "edited", "comment": {"body": "/fix-review"}, "pull_request": {"number": 7}})) def test_issue_comment_direct_and_non_trigger(self): direct = { "action": "created", "comment": {"body": "/fix-review", "user": {"login": "a"}}, "issue": {"number": 3}, "repository": {"full_name": "o/r"}, } self.assertEqual( _parse_github_comment_with_review_fallback(direct)["pr_number"], 3) self.assertIsNone(_parse_github_comment_with_review_fallback( {"action": "created", "comment": {"body": "hello"}})) def test_fix_review_prompt_truncates_comment(self): result = {"repo": "o/r", "pr_number": 9, "author": "a", "comment": "x" * 2500} prompt = _webhook_fix_review_prompt(result) self.assertTrue(prompt.startswith( "[webhook /fix-review] repo=o/r pr=9 author=a\n")) self.assertEqual(len(prompt.split("\n", 1)[1]), 2000) class FollowupBodyTestCase(unittest.TestCase): def test_ok_strips_and_defaults_queued(self): text, mode, err = _validate_followup_body({"body": " hi "}) self.assertEqual((text, mode, err), ("hi", "queued", None)) def test_direct_mode_kept(self): text, mode, err = _validate_followup_body( {"body": "hi", "mode": "direct"}) self.assertEqual((text, mode, err), ("hi", "direct", None)) def test_body_required_variants(self): for body in ({}, {"body": ""}, {"body": " "}, {"body": 123}): _t, _m, err = _validate_followup_body(body) self.assertEqual(err, (400, {"error": "body required"})) def test_too_long_and_bad_mode(self): _t, _m, err = _validate_followup_body({"body": "x" * 20001}) self.assertEqual(err, (400, {"error": "body too long"})) _t, _m, err = _validate_followup_body( {"body": "hi", "mode": "weird"}) self.assertEqual(err, (400, { "error": "mode must be queued or direct"})) class GoalClassifyTestCase(unittest.TestCase): def test_goal_empty_and_mainline_and_plan(self): self.assertEqual( _classify_goal_followup(_task(), ""), ("error", 400, {"error": "goal 目标不能为空"})) action, code, payload = _classify_goal_followup( _task(goal_mode=True, goal="g"), "新目标") self.assertEqual((action, code), ("error", 409)) self.assertEqual(payload["error"], "主线已有 goal,/goal 不会重复创建") self.assertEqual(payload["goal"], "g") self.assertEqual( _classify_goal_followup(_task(plan="p"), "新目标"), ("error", 400, {"error": "plan 模式与 goal 模式冲突"})) def test_goal_merged_queued_terminal_running(self): self.assertEqual( _classify_goal_followup(_task(status="merged"), "新目标")[0], "error") self.assertEqual( _classify_goal_followup(_task(status="queued"), "新目标")[0], "convert_queued") self.assertEqual( _classify_goal_followup(_task(status="completed"), "新目标")[0], "revive_terminal") self.assertEqual( _classify_goal_followup(_task(status="running"), "新目标")[0], "continue") def test_non_goal_only_queued_or_running_continues(self): self.assertEqual( _classify_goal_followup(_task(status="queued"), None)[0], "continue") action, code, payload = _classify_goal_followup( _task(status="completed"), None) self.assertEqual((action, code), ("error", 409)) self.assertEqual(payload["error"], "后续消息仅支持进行中任务") class ThreadGuardTestCase(unittest.TestCase): def test_goal_task_and_merged_blocked(self): self.assertEqual( _thread_non_goal_guard(_task(verify_command="v")), (409, {"error": "goal 任务暂不支持后续消息"})) self.assertEqual( _thread_non_goal_guard(_task(status="merged"))[0], 409) def test_terminal_not_blocked_by_guard(self): # 关键:门禁只拦 goal/merged,终态任务必须放行到 CAS 复活。 self.assertIsNone(_thread_non_goal_guard(_task(status="completed"))) self.assertIsNone(_thread_non_goal_guard(_task(status="running"))) class ChatHistoryExtractionTestCase(unittest.TestCase): def test_seconds_to_ms(self): self.assertEqual(_seconds_to_ms(1.5), 1500) self.assertEqual(_seconds_to_ms(0), 0) self.assertIsNone(_seconds_to_ms(None)) self.assertIsNone(_seconds_to_ms("bad")) def test_user_entries(self): task = {"prompt": "做一件事", "created_at": 10.0} cont = [{"id": "c1", "task_id": "t", "project_id": "p", "body": "继续", "created_by": "u", "created_at": 11.0}] fu = [{"id": "f1", "task_id": "t", "project_id": "p", "body": "后续", "mode": "queued", "status": "pending", "created_by": "u", "sender_task_id": None, "created_at": 12.0, "updated_at": 12.0}] entries = _chat_history_user_entries(task, cont, fu) self.assertEqual( [e[2]["id"] for e in entries], ["task-prompt", "continue-c1", "followup-f1"]) self.assertEqual(entries[0][2]["ts_ms"], 10000) self.assertEqual(entries[2][2]["label"], "后续消息") def test_trace_entries_skip_echo_and_inherit_ts(self): entries = _chat_history_trace_entries([ {"id": "fu-abc", "kind": "message", "text": "回声"}, "not-a-dict", {"id": "m1", "kind": "message", "text": "你好", "ts": 2000}, {"id": "m2", "kind": "message", "text": "无 ts"}, ], start_ms=1000) self.assertEqual([e[2]["id"] for e in entries], ["m1", "m2"]) self.assertEqual(entries[0][1], 2000) self.assertEqual(entries[0][2]["ts_ms"], 2000) self.assertEqual(entries[1][1], 2000) # 贴在上一条有 ts 之后 self.assertIsNone(entries[1][2]["ts_ms"]) if __name__ == "__main__": unittest.main()