test_v8_builder.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. from copy import deepcopy
  2. from app.execution_builder import ExecutionViewBuilder
  3. from app.inspector_projection import project_activity_detail, project_event_detail, project_round_detail
  4. def test_v8_contract_separates_multipath_review_main_decision_and_overall_review(current_bundle):
  5. view = ExecutionViewBuilder().from_bundle(current_bundle)
  6. assert view["schemaVersion"] == "8"
  7. assert view["dataShape"] == "current"
  8. first = view["rounds"][0]
  9. assert len(first["convergenceBatches"]) == 2
  10. assert [batch["branchIds"] for batch in first["convergenceBatches"]] == [[1, 2], [3]]
  11. assert [batch["review"]["eventId"] for batch in first["convergenceBatches"]] == [39, 40]
  12. assert [batch["decision"]["recordId"] for batch in first["convergenceBatches"]] == [21, 22]
  13. assert first["convergenceBatches"][0]["decision"]["branchOutcomes"] == [
  14. {"branchId": 1, "status": "merged", "label": "已采用", "reasoning": "方案 1 的当前处理理由"},
  15. {"branchId": 2, "status": "discarded", "label": "未采用", "reasoning": "方案 2 的当前处理理由"},
  16. ]
  17. assert first["overallEvaluation"]["detailRef"] == "event:41"
  18. assert first["branches"][0]["dataDecisions"][0]["decision"] == "采用案例中的三段结构"
  19. assert all(decision["recordId"] > 0 for branch in first["branches"] for decision in branch["dataDecisions"])
  20. assert "dataDecisions" not in first["convergenceBatches"][0]
  21. assert "evaluation" not in first["branches"][0]
  22. assert "disposition" not in first["branches"][0]
  23. retrieval = first["branches"][0]["retrievalStage"]
  24. assert retrieval["directToolGroups"][0]["calls"][0]["businessLabel"] == "当前主脚本"
  25. assert retrieval["agentRuns"][0]["businessLabel"] == "解构 Case Agent"
  26. assert retrieval["agentRuns"][0]["querySummary"]["empty"] == 1
  27. review_projection = first["convergenceBatches"][0]["review"]["decisionProjection"]
  28. main_projection = first["convergenceBatches"][0]["decision"]["decisionProjection"]
  29. data_projection = first["branches"][0]["dataDecisions"][0]["decisionProjection"]
  30. overall_projection = first["overallEvaluation"]["decision"]
  31. assert (review_projection["decisionType"], review_projection["authority"]) == (
  32. "evaluation",
  33. "recommendation",
  34. )
  35. assert (main_projection["decisionType"], main_projection["authority"]) == (
  36. "tradeoff",
  37. "final",
  38. )
  39. assert (data_projection["decisionType"], data_projection["authority"]) == (
  40. "tradeoff",
  41. "implementation-scope",
  42. )
  43. assert (overall_projection["decisionType"], overall_projection["authority"]) == (
  44. "evaluation",
  45. "recommendation",
  46. )
  47. def test_multipath_review_keeps_grouped_branches_and_comparison_matrix(current_bundle):
  48. review_event = next(item for item in current_bundle["events"] if item["id"] == 39)
  49. review_event["agentOutputData"]["summary"] = """### 分支评估 branch_id=1(领域信息路)
  50. **该支结论**:部分通过
  51. **目标推进**:
  52. - 事实链完整。
  53. **未通过/存疑项**:
  54. - 元素尚未全部落库。
  55. **有据扎实度**:来源可指位。
  56. ### 分支评估 branch_id=2(内容路)
  57. **该支结论**:部分通过
  58. **目标推进**:
  59. - 骨架已形成。
  60. **未通过/部分通过项**:
  61. - 数量红线违规。
  62. **有据扎实度**:与快照一致。
  63. ### 对比与建议
  64. | 维度 | branch1 | branch2 | 差异依据 |
  65. |------|---------|---------|----------|
  66. | 目标契合度 | 高 | 高 | 各自完成主任务 |
  67. **采纳建议**:组合两个方案。"""
  68. first = ExecutionViewBuilder().from_bundle(current_bundle)["rounds"][0]
  69. projection = first["convergenceBatches"][0]["review"]["decisionProjection"]
  70. blocks = projection["detail"]["blocks"]
  71. assert [block["type"] for block in blocks[:2]] == [
  72. "evaluation-branches",
  73. "evaluation-matrix",
  74. ]
  75. branches = blocks[0]["branches"]
  76. assert [item["subject"] for item in branches] == ["方案 1", "方案 2"]
  77. assert branches[1]["achievements"] == ["骨架已形成。"]
  78. assert branches[1]["problems"] == ["数量红线违规。"]
  79. assert blocks[1]["rows"][0]["criterion"] == "目标契合度"
  80. def test_same_round_multipath_decisions_remain_separate_and_ordered(current_bundle):
  81. view = ExecutionViewBuilder().from_bundle(current_bundle)
  82. batches = [item["decision"] for item in view["rounds"][0]["convergenceBatches"]]
  83. assert [item["recordId"] for item in batches] == [21, 22]
  84. assert batches[0]["decision"] != batches[1]["decision"]
  85. assert batches[0]["node"]["detailRef"] == "multipath-decision:21"
  86. def test_comma_separated_review_scope_joins_matching_main_decision(current_bundle):
  87. review_event = next(item for item in current_bundle["events"] if item["id"] == 39)
  88. review_event["inputData"]["task"] = "评估对象:branch_id = 1, 2(分工)"
  89. first = ExecutionViewBuilder().from_bundle(current_bundle)["rounds"][0]
  90. batch = next(
  91. item
  92. for item in first["convergenceBatches"]
  93. if item.get("decision") and item["decision"]["recordId"] == 21
  94. )
  95. assert batch["association"] == "exact-branch-set-and-time"
  96. assert batch["review"]["eventId"] == 39
  97. assert batch["branchIds"] == [1, 2]
  98. assert batch["missingReview"] is None
  99. assert batch["missingDecision"] is None
  100. def test_review_without_main_decision_remains_a_review_only_batch(current_bundle):
  101. current_bundle["multipathDecisions"] = []
  102. view = ExecutionViewBuilder().from_bundle(current_bundle)
  103. batches = view["rounds"][0]["convergenceBatches"]
  104. assert [item["association"] for item in batches] == ["review-only", "review-only"]
  105. assert [item["review"]["eventId"] for item in batches] == [39, 40]
  106. assert all(item["decision"] is None for item in batches)
  107. assert all(item["missingDecision"] for item in batches)
  108. def test_unscoped_same_round_review_is_reported_as_unlinked_not_missing(current_bundle):
  109. current_bundle["events"] = [
  110. item
  111. for item in current_bundle["events"]
  112. if item.get("event_name") != "script_multipath_evaluator"
  113. ]
  114. current_bundle["events"].append(
  115. {
  116. "id": 4995,
  117. "event_seq": 99,
  118. "event_type": "agent_invoke",
  119. "event_name": "script_multipath_evaluator",
  120. "round_index": 1,
  121. "status": "ok",
  122. "inputData": {"task": "请评审本轮候选方案"},
  123. "agentOutputData": {"summary": "评审内容完整,但任务未保存方案范围。"},
  124. }
  125. )
  126. first = ExecutionViewBuilder().from_bundle(current_bundle)["rounds"][0]
  127. missing = first["convergenceBatches"][0]["missingReview"]
  128. assert missing["card"]["primary"]["value"] == (
  129. "发现同轮评审,但无法安全关联到具体候选"
  130. )
  131. assert missing["sourceNotice"] == "association-incomplete"
  132. assert missing["unscopedReviewRefs"] == ["event:4995"]
  133. def test_main_canvas_translates_agent_terms_without_changing_recorded_decision(current_bundle):
  134. current_bundle["multipathDecisions"][0]["decision"] = "选择 Branch 1 合入 Base。"
  135. first = ExecutionViewBuilder().from_bundle(current_bundle)["rounds"][0]
  136. batch = first["convergenceBatches"][0]["decision"]
  137. assert batch["decision"] == "选择 Branch 1 合入 Base。"
  138. assert batch["node"]["card"]["primary"]["value"] == "选择方案 1 合入主脚本。"
  139. assert "方案 1 合入主脚本" in first["summary"]["decision"]["summary"]
  140. def test_round_summary_is_structured_and_keeps_multiple_decision_batches(current_bundle):
  141. current_bundle["rounds"][0]["goal"] = (
  142. "将三段骨架扩展到可模拟完整观感的程度\n"
  143. "① 为各段补充具体形式和脚本元素\n"
  144. "② 将详细执行描述改成可执行画面\n"
  145. "③ 拆分核心呈现段的二级子段\n"
  146. "④ 保持三个方案互不依赖"
  147. )
  148. view = ExecutionViewBuilder().from_bundle(current_bundle)
  149. summary = view["rounds"][0]["summary"]
  150. assert summary["goal"]["headline"].startswith("将三段骨架扩展到可模拟完整观感的程度")
  151. assert summary["goal"]["itemCount"] == 0
  152. assert summary["goal"]["previewItems"] == []
  153. assert summary["goal"]["truncated"] is False
  154. assert summary["approach"]["mode"] == "分工"
  155. assert summary["approach"]["candidateCount"] == 3
  156. assert summary["decision"]["batchCount"] == 2
  157. assert summary["decision"]["branchIds"] == [1, 2, 3]
  158. assert summary["output"]["mergedContentCount"] == 1
  159. assert summary["output"]["domainFactCount"] == 1
  160. assert view["rounds"][0]["result"]["artifactRef"] == "artifact:round:1"
  161. def test_round_detail_keeps_complete_goal_out_of_compact_summary(current_bundle):
  162. full_goal = "核心目标\n① 第一项很长的具体任务\n② 第二项很长的具体任务\n③ 第三项很长的具体任务\n④ 第四项只在详情中完整保留"
  163. current_bundle["rounds"][0]["goal"] = full_goal
  164. view = ExecutionViewBuilder().from_bundle(current_bundle)
  165. first = view["rounds"][0]
  166. detail = project_round_detail(88001, first, current_bundle)
  167. assert first["summary"]["goal"]["previewItems"] == []
  168. assert detail["businessSections"][0]["content"] == full_goal
  169. def test_branch_task_detail_uses_complete_dispatch_task_instead_of_card_preview(current_bundle):
  170. full_task = "目标:" + "完整创作任务。" * 80
  171. current_bundle["events"].append({
  172. "id": 99,
  173. "event_seq": 99,
  174. "event_type": "agent_invoke",
  175. "event_name": "script_implementer",
  176. "agent_role": "script_implementer",
  177. "round_index": 1,
  178. "branch_id": 1,
  179. "status": "ok",
  180. "inputData": {"task": full_task},
  181. })
  182. view = ExecutionViewBuilder().from_bundle(current_bundle)
  183. task_card = view["rounds"][0]["branches"][0]["task"]["card"]["primary"]
  184. detail = project_activity_detail(
  185. 88001,
  186. "round:1:branch:1:task",
  187. view,
  188. current_bundle,
  189. )
  190. assert detail["businessSections"][0]["title"] == "目标"
  191. assert detail["businessSections"][0]["content"] == "完整创作任务。" * 80
  192. assert len(detail["businessSections"][0]["content"]) > 140
  193. assert task_card["label"] == "目标"
  194. assert task_card["value"] == "完整创作任务。" * 80
  195. assert "…" not in task_card["value"]
  196. def test_final_result_has_independent_detail_and_current_artifact_entry(current_bundle):
  197. current_bundle["record"]["status"] = "success"
  198. current_bundle["record"]["summary"] = "已形成可交付的主脚本。"
  199. view = ExecutionViewBuilder().from_bundle(current_bundle)
  200. final = view["finalResult"]
  201. detail = project_activity_detail(
  202. 88001, "run:final-result", view, current_bundle
  203. )
  204. assert final["detailRef"] == "run:final-result"
  205. assert final["artifactRef"] == "artifact:base:current"
  206. assert final["card"]["primary"]["value"] == "已形成可交付的主脚本。"
  207. assert [item["title"] for item in detail["businessSections"]] == [
  208. "构建结论",
  209. "构建摘要或失败原因",
  210. "轮次与方案统计",
  211. "最终主脚本规模",
  212. "版本说明",
  213. ]
  214. assert detail["changes"]["artifactRef"] == "artifact:base:current"
  215. assert detail["changes"]["exactness"] == "current-only"
  216. def test_round_result_uses_dedicated_aggregate_detail(current_bundle):
  217. view = ExecutionViewBuilder().from_bundle(current_bundle)
  218. detail = project_activity_detail(
  219. 88001, "round:1:result", view, current_bundle
  220. )
  221. assert [item["title"] for item in detail["businessSections"]] == [
  222. "本轮目标完成情况",
  223. "方案处置",
  224. "领域事实",
  225. "整体评审",
  226. "主脚本变化",
  227. "下一步",
  228. ]
  229. assert "方案 1:已采用" in detail["businessSections"][1]["content"]
  230. assert "柔性排产可降低" in detail["businessSections"][2]["content"]
  231. assert detail["changes"]["artifactRef"] == "artifact:base:current"
  232. def test_retrieval_agent_detail_prefers_lazy_loaded_complete_event(current_bundle):
  233. view = ExecutionViewBuilder().from_bundle(current_bundle)
  234. full_task = "查找并对比可说明因果结构的解构 Case,保留完整限制条件。"
  235. full_screening = "### 初筛整理\n- 两个案例可用。\n- 一个案例因结构不符而舍弃。"
  236. event_detail = {
  237. "id": 52,
  238. "event_name": "retrieve_data_decode_case",
  239. "input": {"content": {"task": full_task}},
  240. "output": {"content": {"summary": full_screening}},
  241. }
  242. detail = project_activity_detail(
  243. 88001,
  244. "retrieval-agent:52",
  245. view,
  246. current_bundle,
  247. event_detail=event_detail,
  248. )
  249. sections = {item["title"]: item["content"] for item in detail["businessSections"]}
  250. assert sections["完整取数任务"] == full_task
  251. assert "一个案例因结构不符而舍弃" in sections["初筛整理"]
  252. assert "成功为空 1 次" in sections["原始命中"]
  253. def test_first_three_main_agent_cards_and_details_use_real_decision_context(current_bundle):
  254. direction = """## 选题价值主张
  255. 用真实工厂解释柔性排产的价值。
  256. ## 账号价值主张
  257. 保持专业、可核验的表达。
  258. ## 创作总目标
  259. ### 目标 1
  260. - 目标语句:让读者看懂柔性排产如何减少浪费。
  261. ### 目标 2
  262. - 目标语句:用可核验的行业事实支撑结论。
  263. ## 领域评估维度
  264. - 维度名:数据可信度
  265. """
  266. current_bundle["record"]["script_direction"] = direction
  267. first_round = current_bundle["rounds"][0]
  268. first_round["goal"] = "完成主结构。\n① 补齐机制拆解\n② 核实行业数据"
  269. paths = first_round["multipath_plan"]
  270. current_bundle["events"] = [
  271. {
  272. "id": 100,
  273. "event_seq": 0,
  274. "event_type": "agent_scope",
  275. "event_name": "main",
  276. "agent_role": "main",
  277. "agent_depth": 0,
  278. "scope_event_id": 100,
  279. "status": "ok",
  280. },
  281. *[
  282. {
  283. "id": event_id,
  284. "event_seq": 1,
  285. "event_type": "tool_call",
  286. "event_name": name,
  287. "agent_role": "main",
  288. "agent_depth": 0,
  289. "scope_event_id": 100,
  290. "parent_event_id": 100,
  291. "status": "ok",
  292. "ended_at": "2026-07-13T09:59:01",
  293. }
  294. for event_id, name in (
  295. (101, "get_topic_detail"),
  296. (102, "get_account_script_section_patterns"),
  297. (103, "get_account_script_persona_points"),
  298. )
  299. ],
  300. {
  301. "id": 108,
  302. "event_seq": 2,
  303. "event_type": "tool_call",
  304. "event_name": "think_and_plan",
  305. "agent_role": "main",
  306. "agent_depth": 0,
  307. "scope_event_id": 100,
  308. "parent_event_id": 100,
  309. "status": "ok",
  310. "inputData": {
  311. "thought_number": 1,
  312. "thought_summary": "先锁定创作目标,再竞争首轮结构。",
  313. "thought": "完整分析账号结构、选题价值与领域下限。",
  314. "plan": "保存创作目标,然后开启首轮。",
  315. "action": "写入创作总目标",
  316. },
  317. "ended_at": "2026-07-13T09:59:02",
  318. },
  319. {
  320. "id": 104,
  321. "event_seq": 3,
  322. "event_type": "tool_call",
  323. "event_name": "save_script_direction",
  324. "agent_role": "main",
  325. "agent_depth": 0,
  326. "scope_event_id": 100,
  327. "parent_event_id": 100,
  328. "status": "ok",
  329. "inputData": {"script_direction": direction},
  330. "outputData": {"success": True},
  331. "ended_at": "2026-07-13T09:59:02",
  332. },
  333. {
  334. "id": 105,
  335. "event_seq": 4,
  336. "event_type": "tool_call",
  337. "event_name": "begin_round",
  338. "agent_role": "main",
  339. "agent_depth": 0,
  340. "scope_event_id": 100,
  341. "parent_event_id": 100,
  342. "round_index": 0,
  343. "status": "ok",
  344. "outputData": {"success": True, "round_index": 1},
  345. "ended_at": "2026-07-13T09:59:03",
  346. },
  347. {
  348. "id": 106,
  349. "event_seq": 5,
  350. "event_type": "tool_call",
  351. "event_name": "get_script_snapshot",
  352. "agent_role": "main",
  353. "agent_depth": 0,
  354. "scope_event_id": 100,
  355. "parent_event_id": 100,
  356. "round_index": 1,
  357. "status": "ok",
  358. "outputData": {"count": 0},
  359. "ended_at": "2026-07-13T09:59:04",
  360. },
  361. {
  362. "id": 107,
  363. "event_seq": 6,
  364. "event_type": "tool_call",
  365. "event_name": "record_multipath_plan",
  366. "agent_role": "main",
  367. "agent_depth": 0,
  368. "scope_event_id": 100,
  369. "parent_event_id": 100,
  370. "round_index": 1,
  371. "status": "ok",
  372. "inputData": {
  373. "paths": paths,
  374. "mode": "分工",
  375. "note": first_round["plan_note"],
  376. },
  377. "outputData": {"success": True, "round_index": 1},
  378. "ended_at": "2026-07-13T09:59:05",
  379. },
  380. *current_bundle["events"],
  381. ]
  382. view = ExecutionViewBuilder().from_bundle(current_bundle)
  383. planning = view["planning"]
  384. objective = view["objective"]
  385. first = view["rounds"][0]
  386. assert planning["title"] == "创作规划解析"
  387. assert planning["detailRef"] == "event:108"
  388. assert planning["card"]["primary"]["value"] == "先锁定创作目标,再竞争首轮结构。"
  389. planning_event = next(item for item in current_bundle["events"] if item["id"] == 108)
  390. planning_detail = project_event_detail(88001, planning_event)
  391. assert [section["title"] for section in planning_detail["businessSections"]] == [
  392. "规划概要",
  393. "完整思考",
  394. "执行计划",
  395. "下一步",
  396. ]
  397. assert planning_detail["businessSections"][1]["content"] == "完整分析账号结构、选题价值与领域下限。"
  398. assert objective["card"]["primary"]["value"] == "让读者看懂柔性排产如何减少浪费。"
  399. assert [line["label"] for line in objective["card"]["secondary"]] == [
  400. "决策前读取",
  401. "目标结构",
  402. ]
  403. assert objective["card"]["secondary"][0]["value"] == "选题 · 账号段落模式 · 账号人设"
  404. assert "2 个创作目标" in objective["card"]["secondary"][1]["value"]
  405. assert "完成主结构" in first["goal"]["card"]["primary"]["value"]
  406. assert "补齐机制拆解" in first["goal"]["card"]["primary"]["value"]
  407. assert first["goal"]["card"]["secondary"][0]["label"] == "形成前可见"
  408. assert first["plan"]["node"]["card"]["secondary"][0]["label"] == "各路任务"
  409. assert first["plan"]["runtimeRevisionRefs"] == ["event:107"]
  410. assert "decisionContext" not in str(view)
  411. objective_detail = project_activity_detail(88001, "run:objective", view, current_bundle)
  412. goal_detail = project_activity_detail(88001, "round:1:goal", view, current_bundle)
  413. plan_detail = project_activity_detail(88001, "round:1:plan", view, current_bundle)
  414. objective_titles = [block["title"] for block in objective_detail["blocks"]]
  415. assert objective_titles == ["当前保存的创作方向", "根据以下信息做出的决策"]
  416. assert objective_detail["blocks"][0]["value"] == direction.strip()
  417. assert objective_detail["blocks"][0]["presentation"] == "document"
  418. assert "关键约束" not in objective_titles
  419. assert "构建总结" not in str(objective_detail["blocks"])
  420. goal_titles = [block["title"] for block in goal_detail["blocks"]]
  421. assert goal_titles[0] == "本轮目标构成"
  422. assert goal_detail["blocks"][0]["presentation"] == "document"
  423. assert goal_detail["blocks"][0]["value"] == current_bundle["rounds"][0]["goal"]
  424. assert "核心目标" not in str(goal_detail["blocks"][0])
  425. plan_block = plan_detail["blocks"][0]
  426. assert plan_block["type"] == "implementation-plan"
  427. assert plan_block["title"] == "本轮实施路线"
  428. assert plan_block["routes"][0]["target"] == "文章结构"
  429. assert any(block["title"] == "根据以下信息做出的决策" for block in plan_detail["blocks"])
  430. assert "人设详细原文" not in str(view)
  431. def test_running_round_waits_for_main_agent_without_claiming_record_loss(current_bundle):
  432. view = ExecutionViewBuilder().from_bundle(current_bundle)
  433. second = view["rounds"][1]
  434. batch = second["convergenceBatches"][0]
  435. assert batch["missingDecision"]["kind"] == "execution"
  436. assert batch["missingDecision"]["card"]["primary"]["value"] == "等待主 Agent 比较候选方案"
  437. assert "sourceNotice" not in batch["missingDecision"]
  438. assert second["branches"][0]["outcome"]["label"] == "等待主 Agent 处置"
  439. assert not any(item["id"] == "missing-multipath-decisions" for item in view["warnings"])
  440. def test_stopped_round_is_process_incomplete_not_record_missing(current_bundle):
  441. stopped = deepcopy(current_bundle)
  442. stopped["record"]["status"] = "stopped"
  443. stopped["multipathDecisions"] = []
  444. stopped["events"] = []
  445. view = ExecutionViewBuilder().from_bundle(stopped)
  446. first = view["rounds"][0]
  447. batch = first["convergenceBatches"][0]
  448. assert batch["missingDecision"]["card"]["primary"]["value"] == "构建停止前尚未形成多路决策"
  449. assert batch["missingDecision"]["sourceNotice"] == "process-incomplete"
  450. assert first["overallEvaluation"]["card"]["primary"]["value"] == "构建停止前尚未进行整体评审"
  451. assert first["overallEvaluation"]["sourceNotice"] == "process-incomplete"
  452. open_branch = view["rounds"][1]["branches"][0]
  453. assert open_branch["outcome"]["label"] == "构建停止时尚未处置"
  454. assert open_branch["outcome"]["sourceNotice"] == "process-incomplete"
  455. assert not any(item["id"] == "missing-multipath-decisions" for item in view["warnings"])
  456. def test_completed_round_without_main_decision_is_a_real_record_gap(current_bundle):
  457. completed = deepcopy(current_bundle)
  458. completed["record"]["status"] = "success"
  459. completed["multipathDecisions"] = []
  460. completed["events"] = []
  461. view = ExecutionViewBuilder().from_bundle(completed)
  462. first = view["rounds"][0]
  463. batch = first["convergenceBatches"][0]
  464. assert batch["missingDecision"]["card"]["primary"]["value"] == "构建已经结束,但未找到主 Agent 多路决策记录"
  465. assert batch["missingDecision"]["sourceNotice"] == "record-missing"
  466. assert first["overallEvaluation"]["card"]["primary"]["value"] == "构建已经结束,但未找到明确的整体评审结论"
  467. assert first["overallEvaluation"]["sourceNotice"] == "record-missing"
  468. assert first["overallEvaluation"]["status"] == "not-evaluated"
  469. assert first["state"] == "not-evaluated"
  470. assert first["result"]["status"] == "not-evaluated"
  471. assert any(item["id"] == "missing-multipath-decisions" for item in view["warnings"])
  472. def test_domain_information_is_an_independent_output(current_bundle):
  473. view = ExecutionViewBuilder().from_bundle(current_bundle)
  474. branch = next(item for item in view["rounds"][0]["branches"] if item["branchId"] == 2)
  475. assert branch["pathType"] == "领域信息"
  476. assert branch["output"]["type"] == "domain-info"
  477. assert branch["output"]["factCount"] == 1
  478. assert branch["output"]["summary"] == "新增 1 条已核实领域事实"
  479. assert branch["status"] == "discarded"
  480. def test_runtime_event_only_supplies_explicit_evaluation_and_keeps_orphan_agent_unassigned(current_bundle):
  481. view = ExecutionViewBuilder().from_bundle(current_bundle)
  482. first = view["rounds"][0]
  483. assert first["overallEvaluation"]["sourceNotice"] == "runtime-associated"
  484. assert first["state"] == "partially-passed"
  485. assert view["rounds"][1]["state"] == "running"
  486. assert [item["eventId"] for item in view["unassigned"]["runtimeEvents"]] == [42]
  487. def test_branch_zero_rows_cannot_enter_new_projection(current_bundle):
  488. current_bundle["dataDecisions"].append({
  489. "id": 999,
  490. "round_index": 1,
  491. "branch_id": 0,
  492. "decision": "危险旧决策内容",
  493. "sources": [],
  494. })
  495. view = ExecutionViewBuilder().from_bundle(current_bundle)
  496. serialized = str(view)
  497. assert "危险旧决策内容" not in serialized
  498. assert [item["decision"]["recordId"] for item in view["rounds"][0]["convergenceBatches"]] == [21, 22]
  499. def test_old_record_is_visible_but_not_reinterpreted(current_bundle):
  500. legacy = deepcopy(current_bundle)
  501. legacy["events"] = []
  502. legacy["multipathDecisions"] = []
  503. legacy["domainInfo"] = []
  504. for branch in legacy["branches"]:
  505. branch["path_type"] = None
  506. view = ExecutionViewBuilder().from_bundle(legacy)
  507. assert view["dataShape"] == "legacy-incomplete"
  508. assert all(not any(batch.get("decision") for batch in round_["convergenceBatches"]) for round_ in view["rounds"])
  509. assert any(item["id"] == "legacy-shape" for item in view["warnings"])
  510. def test_main_payload_does_not_embed_event_bodies_or_candidate_snapshot(current_bundle):
  511. current_bundle["events"][0]["output_body"] = "secret complete report"
  512. view = ExecutionViewBuilder().from_bundle(current_bundle)
  513. serialized = str(view)
  514. assert "secret complete report" not in serialized
  515. assert "candidate_snapshot" not in serialized
  516. assert "script_build_event_body" not in serialized
  517. def test_inspector_exposes_business_decisions_and_only_real_changes(current_bundle):
  518. view = ExecutionViewBuilder().from_bundle(current_bundle)
  519. first = view["rounds"][0]
  520. round_detail = project_round_detail(88001, first, current_bundle)
  521. data_detail = project_activity_detail(88001, "data-decision:11", view, current_bundle)
  522. main_detail = project_activity_detail(88001, "multipath-decision:21", view, current_bundle)
  523. goal_detail = project_activity_detail(88001, "round:1:goal", view, current_bundle)
  524. assert round_detail["changes"]["exactness"] == "current-only"
  525. assert data_detail["detailKind"] == "agent-decision"
  526. assert [block["title"] for block in data_detail["blocks"]] == [
  527. "参与判断",
  528. "明确取舍",
  529. "理由",
  530. ]
  531. assert main_detail["detailKind"] == "agent-decision"
  532. assert "最终决定" in [block["title"] for block in main_detail["blocks"]]
  533. assert "评审建议与最终决定" in [
  534. block["title"] for block in main_detail["blocks"]
  535. ]
  536. assert "changes" not in goal_detail
  537. def test_explicit_next_round_translation_is_shown_as_recommendation_handling(current_bundle):
  538. changed = deepcopy(current_bundle)
  539. changed["multipathDecisions"][0]["reasoning"] = (
  540. "方案 1 直接采用。方案 2 的数据优势很有价值,"
  541. "但不直接合并,决定在下一轮转译为增强目标。"
  542. )
  543. view = ExecutionViewBuilder().from_bundle(changed)
  544. rows = view["rounds"][0]["convergenceBatches"][0]["decision"][
  545. "decisionProjection"
  546. ]["body"]["reviewComparison"]
  547. assert "handling" in rows[1]
  548. assert "下一轮转译" in rows[1]["handling"]
  549. def test_every_projected_business_section_is_readable_text(current_bundle):
  550. view = ExecutionViewBuilder().from_bundle(current_bundle)
  551. refs = [
  552. "run:objective",
  553. "data-decision:11",
  554. "multipath-decision:21",
  555. "domain-info:31",
  556. "round:1:goal",
  557. "round:1:plan",
  558. "round:1:result",
  559. "round:1:branch:1",
  560. "round:1:branch:1:task",
  561. "round:1:branch:1:retrieval",
  562. "round:1:branch:1:output",
  563. ]
  564. details = [project_round_detail(88001, view["rounds"][0], current_bundle)]
  565. details.extend(project_activity_detail(88001, ref, view, current_bundle) for ref in refs)
  566. for detail in details:
  567. if detail.get("detailKind") == "agent-decision":
  568. assert detail["blocks"]
  569. _assert_business_blocks_hide_technical_fields(detail["blocks"])
  570. else:
  571. assert detail["businessSections"]
  572. assert all(isinstance(section["content"], str) for section in detail["businessSections"])
  573. assert all(
  574. isinstance(section["content"], str)
  575. for section in (detail.get("changes") or {}).get("sections", [])
  576. )
  577. def _assert_business_blocks_hide_technical_fields(blocks):
  578. serialized = str(blocks)
  579. for forbidden in (
  580. "script_build_id",
  581. "branch_id",
  582. "paragraph_id",
  583. "post_id",
  584. "event ID",
  585. "Tool 名",
  586. "path_type",
  587. "Base",
  588. "patch",
  589. ):
  590. assert forbidden not in serialized