test_source_lineage.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. from __future__ import annotations
  2. from copy import deepcopy
  3. from fastapi.testclient import TestClient
  4. import pytest
  5. from app import main
  6. from app.card_details.common import context_input_fields
  7. from app.module_audit.log_context import locate_log_module
  8. from app.source_lineage.comparison import _normalize_business, _source_module_ids
  9. from app.source_lineage.projector import _all_bindings
  10. from app.source_lineage.resolver import SourceResolver
  11. client = TestClient(main.app)
  12. @pytest.fixture(autouse=True)
  13. def clear_source_inspector_caches():
  14. main._inspector_cache.clear()
  15. main._build_view_cache.clear()
  16. yield
  17. main._inspector_cache.clear()
  18. main._build_view_cache.clear()
  19. def _module(payload: dict, module_id: str) -> dict:
  20. return next(item for item in payload["modules"] if item["id"] == module_id)
  21. def _row_bindings(module: dict, row_index: int = 0) -> list[dict]:
  22. ids = set(module["rows"][row_index]["bindingIds"])
  23. return [binding for binding in module["bindings"] if binding["id"] in ids]
  24. def _event_detail(summary: dict, *, input_content=None, output_content=None):
  25. return {
  26. **summary,
  27. "input": {"contentType": "json", "content": input_content, "truncated": False},
  28. "output": {"contentType": "json", "content": output_content, "truncated": False},
  29. }
  30. def test_inspector_view_planning_fields_have_exact_independent_bindings(
  31. monkeypatch, current_bundle
  32. ):
  33. bundle = deepcopy(current_bundle)
  34. event = {
  35. "id": 60,
  36. "event_seq": 1,
  37. "event_type": "tool_call",
  38. "event_name": "think_and_plan",
  39. "agent_role": "main",
  40. "agent_depth": 0,
  41. "status": "completed",
  42. "started_at": "2026-07-13T09:59:00",
  43. "ended_at": "2026-07-13T09:59:01",
  44. }
  45. bundle["events"].insert(0, event)
  46. detail = _event_detail(
  47. event,
  48. input_content={
  49. "thought_summary": "先确定承重结构",
  50. "thought": "完整思考原文",
  51. "plan": "1. 写入方向\n2. 开始本轮",
  52. "action": "保存创作目标",
  53. },
  54. output_content={"ok": True},
  55. )
  56. calls = []
  57. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  58. monkeypatch.setattr(
  59. main.provider,
  60. "load_event_details",
  61. lambda _build_id, ids: calls.append(list(ids)) or {60: deepcopy(detail)},
  62. )
  63. monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, _event_id: deepcopy(detail))
  64. main._inspector_cache.clear()
  65. response = client.get("/api/script-builds/88001/inspector-view/event:60")
  66. assert response.status_code == 200
  67. payload = response.json()
  68. assert payload["schemaVersion"] == "inspector-source-v2"
  69. assert payload["businessProjection"]["businessSections"][0]["content"] == "先确定承重结构"
  70. assert calls == [[60]]
  71. paths = {
  72. module["id"]: _row_bindings(module)[0]["selector"].get("path")
  73. for module in payload["modules"]
  74. }
  75. assert paths == {
  76. "summary": "/input/content/thought_summary",
  77. "thought": "/input/content/thought",
  78. "plan": "/input/content/plan",
  79. "action": "/input/content/action",
  80. }
  81. assert list(payload["sources"]) == ["event:60"]
  82. def test_optional_objective_and_creative_blocks_use_stable_semantic_ids():
  83. objective = _normalize_business({
  84. "blocks": [
  85. {"type": "items", "title": "根据以下信息做出的决策", "items": [{"label": "账号", "value": "A"}]},
  86. {"type": "summary", "title": "当前保存的创作方向", "value": "方向原文"},
  87. ]
  88. }, card_kind="objective")
  89. assert [item["_semanticKey"] for item in objective] == [
  90. "block:inputs",
  91. "block:direction",
  92. ]
  93. assert _source_module_ids("objective", "block:inputs", 1) == ["inputs"]
  94. assert _source_module_ids("objective", "block:direction", 2) == ["direction"]
  95. creative = _normalize_business({
  96. "blocks": [
  97. {"type": "items", "title": "可确认的上游信息", "items": [{"label": "案例", "value": "A"}]},
  98. {"type": "creative-process", "title": "创作处理", "steps": [{"action": "改写"}]},
  99. {"type": "summary", "title": "产出的候选表", "value": "候选 A"},
  100. {"type": "notice", "title": "记录说明", "value": "过程只保存了一部分"},
  101. ]
  102. }, card_kind="creative")
  103. assert [item["_semanticKey"] for item in creative] == [
  104. "block:available",
  105. "block:process",
  106. "block:candidate",
  107. "block:record-note",
  108. ]
  109. assert [
  110. _source_module_ids("creative", item["_semanticKey"], index)
  111. for index, item in enumerate(creative, 1)
  112. ] == [["available"], ["process"], ["candidate"], ["record-note"]]
  113. def test_context_input_fields_classifies_business_refs_as_database_records():
  114. context = {
  115. "inputs": [
  116. {"label": "多路决策", "summary": "采用方案 1", "detailRef": "multipath-decision:21"},
  117. {"label": "数据取舍", "summary": "采用两条证据", "detailRef": "data-decision:11"},
  118. {"label": "领域事实", "summary": "事实 A", "detailRef": "domain-info:31"},
  119. {"label": "开启本轮", "summary": "目标 A", "detailRef": "event:60", "relation": "direct-read"},
  120. ]
  121. }
  122. event = {"id": 60, "event_name": "begin_round"}
  123. fields = context_input_fields(context, "test", [event])
  124. assert [item["source"]["kind"] for item in fields] == [
  125. "database", "database", "database", "runtime-event"
  126. ]
  127. assert [item["source"]["label"] for item in fields[:3]] == [
  128. "script_build_multipath_decision",
  129. "script_build_data_decision",
  130. "script_build_domain_info",
  131. ]
  132. assert [item["source"]["fieldPath"] for item in fields] == [
  133. "decision", "decision", "content", "input.content.goal"
  134. ]
  135. def test_implementation_task_sections_bind_to_exact_task_spans(
  136. monkeypatch, current_bundle
  137. ):
  138. bundle = deepcopy(current_bundle)
  139. task = """交付说明:输出一份完整候选表
  140. 任务目标:形成三段式文章结构
  141. method: 先搭建段落骨架
  142. 目标:让读者理解因果关系
  143. 必用素材:柔性排产案例
  144. 前情提要:本轮已核实领域事实
  145. 重要约束:不改动事实数字
  146. 禁止事项:不得伪造引用"""
  147. event = next(item for item in bundle["events"] if item["id"] == 50)
  148. event["inputData"] = {"task": task}
  149. detail = _event_detail(event, input_content={"task": task}, output_content={"summary": "完成"})
  150. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  151. monkeypatch.setattr(
  152. main.provider,
  153. "load_event_details",
  154. lambda _build_id, ids: {50: deepcopy(detail)} if 50 in ids else {},
  155. )
  156. main._inspector_cache.clear()
  157. response = client.get(
  158. "/api/script-builds/88001/inspector-view/round:1:branch:1:task"
  159. )
  160. assert response.status_code == 200
  161. payload = response.json()
  162. expected_modules = {
  163. "task-notes-1": "deliverable",
  164. "task-scope-2": "scope",
  165. "task-method-3": "method",
  166. "task-goal-4": "goal",
  167. "task-materials-5": "constraints",
  168. "task-context-6": "constraints",
  169. "task-constraints-7": "constraints",
  170. "task-avoid-8": "avoid",
  171. }
  172. for module_id, source_module_id in expected_modules.items():
  173. module = _module(payload, module_id)
  174. bindings = _row_bindings(module)
  175. assert bindings, module_id
  176. assert all(binding["resolution"] == "resolved" for binding in bindings)
  177. assert all(
  178. binding["selector"]["kind"] == "text-span"
  179. for binding in bindings
  180. if binding["sourceId"] == "event:50"
  181. )
  182. if source_module_id != "scope":
  183. event_binding = next(binding for binding in bindings if binding["sourceId"] == "event:50")
  184. assert event_binding["selector"]["kind"] == "text-span"
  185. assert event_binding["selector"]["path"] == "/input/content/task"
  186. section = next(
  187. item
  188. for item in payload["businessProjection"]["businessSections"]
  189. if item["id"] == module_id
  190. )
  191. assert event_binding["selector"]["exactText"] == section["content"]
  192. def test_inspector_view_query_binds_each_condition_and_case(monkeypatch, current_bundle):
  193. bundle = deepcopy(current_bundle)
  194. event = next(item for item in bundle["events"] if item["id"] == 53)
  195. detail = _event_detail(
  196. event,
  197. input_content={"keyword": "因果结构", "top_k": 2},
  198. output_content={
  199. "count": 2,
  200. "results": [
  201. {"post_id": "A", "account": "账号A", "data": "Case A"},
  202. {"post_id": "B", "account": "账号B", "data": "Case B"},
  203. ],
  204. },
  205. )
  206. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  207. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {53: deepcopy(detail)})
  208. monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, _event_id: deepcopy(detail))
  209. main._inspector_cache.clear()
  210. payload = client.get("/api/script-builds/88001/inspector-view/event:53").json()
  211. purpose = _module(payload, "conditions")
  212. result = _module(payload, "results")
  213. status = _module(payload, "outcome")
  214. assert [_row_bindings(purpose, index)[0]["selector"]["path"] for index in range(2)] == [
  215. "/input/content/keyword",
  216. "/input/content/top_k",
  217. ]
  218. assert [_row_bindings(result, index)[0]["selector"]["path"] for index in range(2)] == [
  219. "/output/content/results/0",
  220. "/output/content/results/1",
  221. ]
  222. assert len({binding["id"] for binding in result["bindings"]}) == 2
  223. assert all(
  224. binding["selector"].get("path") == "/output/content"
  225. and binding["transform"]["operation"] == "retrieval-outcome-v1"
  226. for binding in _row_bindings(status)
  227. )
  228. assert result["bindings"][0]["evidence"] == {
  229. "availability": "returned-to-agent",
  230. "adoption": "not-recorded",
  231. "confidence": "deterministic",
  232. }
  233. assert result["bindings"][0]["transform"]["operation"] == "retrieval-result-item-v1"
  234. assert all(binding["resolution"] == "resolved" for binding in status["bindings"])
  235. assert not any(":error:" in binding["id"] for binding in status["bindings"])
  236. status_binding_ids = {binding["id"] for binding in status["bindings"]}
  237. assert all(
  238. set(row["bindingIds"]).issubset(status_binding_ids)
  239. for row in status["rows"]
  240. )
  241. @pytest.mark.parametrize(
  242. ("event_name", "output_content", "expected_paths"),
  243. [
  244. (
  245. "get_domain_info",
  246. {"domain_info": [{"id": 1, "content": "事实 A"}, {"id": 2, "content": "事实 B"}]},
  247. [
  248. "/output/content/domain_info/0",
  249. "/output/content/domain_info/1",
  250. ],
  251. ),
  252. (
  253. "get_script_snapshot",
  254. {"script_build": {"status": "running"}, "paragraphs": []},
  255. ["/output/content"],
  256. ),
  257. ],
  258. )
  259. def test_direct_tool_result_paths_follow_the_shared_business_contract(
  260. monkeypatch, current_bundle, event_name, output_content, expected_paths
  261. ):
  262. bundle = deepcopy(current_bundle)
  263. event = {
  264. "id": 60,
  265. "event_seq": 60,
  266. "event_type": "tool_call",
  267. "event_name": event_name,
  268. "agent_role": "main",
  269. "agent_depth": 0,
  270. "status": "completed",
  271. "ended_at": "2026-07-13T10:00:01",
  272. }
  273. bundle["events"].append(event)
  274. detail = _event_detail(event, input_content={}, output_content=output_content)
  275. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  276. monkeypatch.setattr(
  277. main.provider,
  278. "load_event_details",
  279. lambda _build_id, ids: {60: deepcopy(detail)} if 60 in ids else {},
  280. )
  281. main._inspector_cache.clear()
  282. payload = client.get("/api/script-builds/88001/inspector-view/event:60").json()
  283. results = _module(payload, "results")
  284. paths = [
  285. _row_bindings(results, index)[0]["selector"].get("path")
  286. for index in range(len(results["rows"]))
  287. ]
  288. assert paths == expected_paths
  289. def test_generic_event_with_saved_body_never_falls_back_to_false_missing(
  290. monkeypatch, current_bundle
  291. ):
  292. bundle = deepcopy(current_bundle)
  293. event = {
  294. "id": 60,
  295. "event_seq": 60,
  296. "event_type": "tool_call",
  297. "event_name": "save_script_direction",
  298. "agent_role": "main",
  299. "agent_depth": 0,
  300. "status": "completed",
  301. }
  302. bundle["events"].insert(0, event)
  303. detail = _event_detail(
  304. event,
  305. input_content={"direction": "完整创作方向"},
  306. output_content="创作脚本方向已保存",
  307. )
  308. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  309. monkeypatch.setattr(
  310. main.provider,
  311. "load_event_details",
  312. lambda _build_id, ids: {60: deepcopy(detail)} if 60 in ids else {},
  313. )
  314. main._inspector_cache.clear()
  315. payload = client.get("/api/script-builds/88001/inspector-view/event:60").json()
  316. assert payload["sources"]["event:60"]["resultState"] == "present"
  317. bindings = [
  318. binding
  319. for module in payload["modules"]
  320. for binding in module["bindings"]
  321. ]
  322. assert bindings
  323. assert all(binding["sourceId"] == "event:60" for binding in bindings)
  324. assert all(binding["resolution"] == "resolved" for binding in bindings)
  325. def test_data_decision_and_final_result_bind_to_real_database_fields(
  326. monkeypatch, current_bundle
  327. ):
  328. bundle = deepcopy(current_bundle)
  329. bundle["record"].update({"status": "success", "summary": "构建摘要"})
  330. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  331. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
  332. main._inspector_cache.clear()
  333. decision = client.get(
  334. "/api/script-builds/88001/inspector-view/data-decision:11"
  335. ).json()
  336. source_module = _module(decision, "block-1")
  337. for index, row in enumerate(source_module["rows"]):
  338. row_bindings = _row_bindings(source_module, index)
  339. assert len(row_bindings) == 1
  340. assert row_bindings[0]["selector"]["path"] == f"/sources/{index}"
  341. assert row_bindings[0]["evidence"]["adoption"] == "explicitly-adopted"
  342. assert decision["sources"][row_bindings[0]["sourceId"]]["locator"]["table"] == "script_build_data_decision"
  343. assert len(_row_bindings(_module(decision, "block-2"))) == 1
  344. final = client.get(
  345. "/api/script-builds/88001/inspector-view/run:final-result"
  346. ).json()
  347. conclusion = _module(final, "conclusion")
  348. summary = _module(final, "summary")
  349. assert final["businessProjection"]["businessSections"][0]["content"] == "构建已完成。"
  350. assert _row_bindings(conclusion)[0]["selector"]["path"] == "/status"
  351. assert _row_bindings(conclusion)[0]["transform"]["kind"] == "parsed"
  352. assert _row_bindings(summary)[0]["selector"]["path"] == "/summary"
  353. stats = _module(final, "statistics")
  354. assert _row_bindings(stats)[0]["selector"]["kind"] == "members"
  355. assert _row_bindings(stats)[0]["selector"]["members"]
  356. assert _row_bindings(stats)[0]["selector"]["reducer"] == "Round / Branch 记录计数"
  357. def test_every_visible_module_has_a_resolved_or_explicit_unresolved_binding(
  358. monkeypatch, current_bundle
  359. ):
  360. bundle = deepcopy(current_bundle)
  361. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  362. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
  363. main._inspector_cache.clear()
  364. payload = client.get(
  365. "/api/script-builds/88001/inspector-view/round:1:goal"
  366. ).json()
  367. assert payload["modules"]
  368. for module in payload["modules"]:
  369. assert module["rows"]
  370. for row in module["rows"]:
  371. assert row["businessSelector"]
  372. bindings = _row_bindings(module, module["rows"].index(row))
  373. assert bindings
  374. for binding in bindings:
  375. assert binding["resolution"] in {"resolved", "partial", "missing", "unsafe"}
  376. if binding["resolution"] == "missing":
  377. assert binding["selector"]["kind"] == "unresolved"
  378. assert binding["selector"]["reason"]
  379. if binding["resolution"] == "unsafe":
  380. assert binding["evidence"]["confidence"] == "unconfirmed"
  381. assert binding["sourceId"] in payload["sources"]
  382. def test_business_projection_is_the_same_projection_as_readable_inspector(
  383. monkeypatch, current_bundle
  384. ):
  385. bundle = deepcopy(current_bundle)
  386. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  387. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
  388. main._inspector_cache.clear()
  389. for detail_ref in (
  390. "run:objective",
  391. "round:1:goal",
  392. "round:1:plan",
  393. "data-decision:11",
  394. "round:1:branch:1:task",
  395. "round:1:result",
  396. "run:final-result",
  397. ):
  398. old = client.get(f"/api/script-builds/88001/activities/{detail_ref}")
  399. source = client.get(f"/api/script-builds/88001/inspector-view/{detail_ref}")
  400. assert old.status_code == 200, detail_ref
  401. assert source.status_code == 200, detail_ref
  402. expected = {
  403. key: value
  404. for key, value in old.json().items()
  405. if key not in {"technical", "changes"}
  406. }
  407. assert source.json()["businessProjection"] == expected, detail_ref
  408. def test_zero_domain_facts_are_a_complete_empty_query_not_missing(
  409. monkeypatch, current_bundle
  410. ):
  411. bundle = deepcopy(current_bundle)
  412. bundle["domainInfo"] = []
  413. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  414. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
  415. main._inspector_cache.clear()
  416. payload = client.get(
  417. "/api/script-builds/88001/inspector-view/round:1:branch:2:output"
  418. ).json()
  419. assert payload["businessProjection"]["businessSections"][0]["content"] == (
  420. "已查询完成,本方案没有新增领域事实"
  421. )
  422. output = _module(payload, "output")
  423. binding = _row_bindings(output)[0]
  424. source = payload["sources"][binding["sourceId"]]
  425. assert binding["resolution"] == "resolved"
  426. assert source["completeness"] == "complete"
  427. assert source["resultState"] == "empty"
  428. assert source["locator"] == {
  429. "table": "script_build_domain_info",
  430. "filters": {"branch_id": 2},
  431. "resultCount": 0,
  432. }
  433. assert source["rawRecord"] == []
  434. round_result = client.get(
  435. "/api/script-builds/88001/inspector-view/round:1:result"
  436. ).json()
  437. domain = _module(round_result, "facts")
  438. domain_source = round_result["sources"][_row_bindings(domain)[0]["sourceId"]]
  439. assert domain_source["resultState"] == "empty"
  440. assert domain_source["rawRecord"]["value"] == []
  441. def test_completeness_scan_includes_item_level_bindings():
  442. modules = [
  443. {
  444. "bindings": [{"id": "module", "resolution": "resolved"}],
  445. "business": {
  446. "items": [
  447. {
  448. "bindings": [
  449. {
  450. "id": "child",
  451. "resolution": "missing",
  452. "selector": {"kind": "unresolved", "reason": "missing"},
  453. }
  454. ]
  455. }
  456. ]
  457. },
  458. }
  459. ]
  460. assert {item["resolution"] for item in _all_bindings(modules)} == {
  461. "resolved",
  462. "missing",
  463. }
  464. def test_direct_tool_group_has_one_item_binding_per_call(monkeypatch, current_bundle):
  465. bundle = deepcopy(current_bundle)
  466. summary = next(item for item in bundle["events"] if item["id"] == 51)
  467. detail = _event_detail(summary, input_content={}, output_content={"paragraphs": [1, 2]})
  468. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  469. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {51: deepcopy(detail)})
  470. monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, _event_id: deepcopy(detail))
  471. main._inspector_cache.clear()
  472. payload = client.get(
  473. "/api/script-builds/88001/inspector-view/retrieval-direct:51"
  474. ).json()
  475. activities = _module(payload, "activities")
  476. assert len(activities["rows"]) == 1
  477. activity_binding = _row_bindings(activities)[0]
  478. assert activity_binding["selector"]["kind"] == "whole-record"
  479. assert activity_binding["transform"]["operation"] == "activity-summary-v1"
  480. def test_whole_event_and_parsed_text_fallback_are_resolved():
  481. event = _event_detail(
  482. {
  483. "id": 91,
  484. "event_name": "script_evaluator",
  485. "status": "completed",
  486. "duration_ms": 20,
  487. },
  488. input_content={"task": "评审两个方案"},
  489. output_content={"summary": "原始 Markdown **结论**"},
  490. )
  491. resolver = SourceResolver(bundle={}, event_details={91: event})
  492. whole = resolver.binding_for_event(91, binding_id="whole", path="")
  493. assert whole["selector"] == {"kind": "whole-record"}
  494. assert whole["resolution"] == "resolved"
  495. parsed = resolver.text_span_binding(
  496. 91,
  497. binding_id="parsed",
  498. path="output.content.summary",
  499. exact_text="清理后的结论",
  500. role="output",
  501. )
  502. assert parsed["selector"] == {
  503. "kind": "json-pointer",
  504. "path": "/output/content/summary",
  505. }
  506. assert parsed["transform"]["kind"] == "parsed"
  507. assert parsed["resolution"] == "resolved"
  508. def test_round_goal_and_plan_bind_their_real_save_events(monkeypatch, current_bundle):
  509. bundle = deepcopy(current_bundle)
  510. begin = {
  511. "id": 60,
  512. "event_seq": 60,
  513. "event_type": "tool_call",
  514. "event_name": "begin_round",
  515. "round_index": 1,
  516. "status": "completed",
  517. }
  518. revision = {
  519. "id": 61,
  520. "event_seq": 61,
  521. "event_type": "tool_call",
  522. "event_name": "record_multipath_plan",
  523. "round_index": 1,
  524. "status": "completed",
  525. }
  526. bundle["events"].extend([begin, revision])
  527. details = {
  528. 60: _event_detail(begin, input_content={"goal": bundle["rounds"][0]["goal"]}, output_content={"round_index": 1}),
  529. 61: _event_detail(revision, input_content={"paths": bundle["rounds"][0]["multipath_plan"], "mode": "分工", "note": "保存规划"}, output_content={"saved": True}),
  530. }
  531. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  532. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, ids: {event_id: deepcopy(details[event_id]) for event_id in ids if event_id in details})
  533. main._inspector_cache.clear()
  534. goal = client.get("/api/script-builds/88001/inspector-view/round:1:goal").json()
  535. goal_block = _module(goal, "goal")
  536. entered = next(binding for binding in _row_bindings(goal_block) if binding["sourceId"] == "event:60")
  537. assert entered["selector"]["path"] == "/input/content/goal"
  538. assert entered["evidence"]["availability"] == "produced-by-run"
  539. plan = client.get("/api/script-builds/88001/inspector-view/round:1:plan").json()
  540. formation = _module(plan, "basis")
  541. revisions = next(binding for binding in _row_bindings(formation) if binding["sourceId"] == "event:61")
  542. assert revisions["selector"]["path"] == "/input/content"
  543. assert revisions["evidence"]["availability"] == "produced-by-run"
  544. def test_round_result_review_uses_the_real_event_record(monkeypatch, current_bundle):
  545. bundle = deepcopy(current_bundle)
  546. summary = next(item for item in bundle["events"] if item["id"] == 41)
  547. detail = _event_detail(
  548. summary,
  549. input_content={"task": "评审第 1 轮主脚本"},
  550. output_content={"summary": summary["agentOutputData"]["summary"]},
  551. )
  552. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  553. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, ids: {41: deepcopy(detail)} if 41 in ids else {})
  554. main._inspector_cache.clear()
  555. payload = client.get("/api/script-builds/88001/inspector-view/round:1:result").json()
  556. review_bindings = [binding for binding in _all_bindings(payload["modules"]) if binding["sourceId"] == "event:41"]
  557. assert review_bindings
  558. assert all(binding["selector"]["kind"] == "whole-record" for binding in review_bindings)
  559. assert all(binding["resolution"] == "resolved" for binding in review_bindings)
  560. def test_log_window_loader_ignores_events_without_msg_id(monkeypatch):
  561. captured = []
  562. monkeypatch.setattr(
  563. main.audit_repository,
  564. "load_event_log_windows",
  565. lambda build_id, anchors: captured.extend(anchors) or {},
  566. )
  567. result = main._safe_load_log_windows(443, {9: {"msg_id": None, "event_seq": 77}})
  568. assert captured == []
  569. assert result == {}
  570. def test_log_context_requires_the_exact_event_msg_id():
  571. content = """[FOLD:第一段]
  572. [MSG_ANCHOR:msg_id=message-a:seq=7]
  573. 内容 A
  574. [FOLD:第二段]
  575. [TOOL_ANCHOR:msg_id=message-b:seq=7:tool=test]
  576. 内容 B
  577. """
  578. matched = locate_log_module(content, {"msg_id": "message-b", "event_seq": 7})
  579. assert matched is not None
  580. assert matched["msgId"] == "message-b"
  581. assert "内容 B" in matched["content"]
  582. assert "内容 A" not in matched["content"]
  583. assert locate_log_module(content, {"msg_id": None, "event_seq": 7}) is None
  584. assert locate_log_module(content, {"msg_id": "message-c", "event_seq": 7}) is None
  585. def test_missing_review_and_decision_cards_have_resolved_empty_sources(
  586. monkeypatch, current_bundle
  587. ):
  588. bundle = deepcopy(current_bundle)
  589. bundle["events"] = [
  590. event
  591. for event in bundle["events"]
  592. if event.get("event_name") != "script_multipath_evaluator"
  593. ]
  594. bundle["multipathDecisions"] = []
  595. monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
  596. monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
  597. main._inspector_cache.clear()
  598. main._build_view_cache.clear()
  599. execution = client.get("/api/script-builds/88001/execution-view").json()
  600. batch = execution["rounds"][0]["convergenceBatches"][0]
  601. refs = [
  602. batch["missingReview"]["detailRef"],
  603. batch["missingDecision"]["detailRef"],
  604. ]
  605. for detail_ref in refs:
  606. response = client.get(
  607. f"/api/script-builds/88001/inspector-view/{detail_ref}"
  608. )
  609. assert response.status_code == 200
  610. payload = response.json()
  611. assert payload["businessProjection"]["businessSections"][0]["id"] == "status"
  612. bindings = _row_bindings(_module(payload, "status"))
  613. assert bindings
  614. assert all(binding["resolution"] == "resolved" for binding in bindings)
  615. assert any(
  616. source["resultState"] == "empty"
  617. for source in payload["sources"].values()
  618. )