Only pure computation/validation sub-blocks of _handle_github_webhook, _create_followup, _deliver_to_thread and _task_chat_history are extracted; all DB writes, audits, external calls and side-effect blocks stay in the callers, error strings/codes and response shapes verbatim. _deliver_to_thread uses the shared goal classifier only for /goal input: its non-goal terminal path still goes to CAS revive (not 409) and its merged path keeps its own wording. New tests: test_longfunc_remaining.py (15).
209 lines
8.8 KiB
Python
209 lines
8.8 KiB
Python
"""剩余长函数小提取单测:只钉住新抽出的纯块,不跑真实模型回合、
|
||
不联网、不连生产密钥。
|
||
|
||
覆盖(均取自原函数逐字逻辑):
|
||
- 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()
|