test_module_audit.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. from __future__ import annotations
  2. from copy import deepcopy
  3. from importlib import import_module
  4. from fastapi.testclient import TestClient
  5. from app import main
  6. from app.module_audit.service import ModuleAuditService
  7. class _Provider:
  8. mode = "test"
  9. def __init__(self, bundle):
  10. self.bundle = deepcopy(bundle)
  11. self.load_bundle_calls = 0
  12. def load_bundle(self, _script_build_id: int):
  13. self.load_bundle_calls += 1
  14. return deepcopy(self.bundle)
  15. def load_event_detail(self, _script_build_id: int, event_id: int):
  16. event = next(item for item in self.bundle["events"] if item["id"] == event_id)
  17. return {
  18. **deepcopy(event),
  19. "input": {"content": deepcopy(event.get("inputData"))},
  20. "output": {
  21. "content": deepcopy(
  22. event.get("agentOutputData") or event.get("output_preview")
  23. )
  24. },
  25. }
  26. class _Repository:
  27. def __init__(self, bundle):
  28. self.events = {item["id"]: deepcopy(item) for item in bundle["events"]}
  29. self.load_event_calls = 0
  30. self.load_log_calls = 0
  31. def load_event(self, _script_build_id: int, event_id: int):
  32. self.load_event_calls += 1
  33. event = deepcopy(self.events.get(event_id))
  34. if event is None:
  35. return None
  36. event["eventBody"] = {
  37. "input_content_type": "application/json",
  38. "input_content": '{"task":"完整派发任务"}',
  39. "output_content_type": "text/plain",
  40. "output_content": "完整运行返回",
  41. }
  42. return event
  43. def load_log(self, _script_build_id: int):
  44. self.load_log_calls += 1
  45. return (
  46. "[AGENT_TASK:script_implementer]\n"
  47. "[MSG_ANCHOR:msg_id=task-50:seq=10]\n"
  48. "完整派发任务\n"
  49. "[TOOL_ANCHOR:msg_id=tool-51:seq=11]\n"
  50. "后续工具调用"
  51. )
  52. def _service(current_bundle):
  53. return ModuleAuditService(
  54. provider=_Provider(current_bundle), repository=_Repository(current_bundle)
  55. )
  56. def test_audit_index_is_lightweight_and_collects_full_flow_modules(current_bundle):
  57. payload = _service(current_bundle).index(88001)
  58. module_ids = {item["id"] for item in payload["modules"]}
  59. assert "run:objective" in module_ids
  60. assert "round:1:branch:1:task" in module_ids
  61. assert "event:53" in module_ids
  62. assert "round:1:result" in module_ids
  63. assert not any(item["displayVariant"] == "collapsed-summary" for item in payload["modules"])
  64. assert "round:1:summary" not in module_ids
  65. assert "round:1:branch:1:summary" not in module_ids
  66. assert "eventBody" not in str(payload)
  67. assert "contexts" not in payload
  68. def test_audit_reuses_completed_run_snapshot_between_index_and_detail(current_bundle):
  69. service = _service(current_bundle)
  70. service.index(88001)
  71. service.detail(88001, "round:1:branch:1:task")
  72. assert service.provider.load_bundle_calls == 1
  73. def test_audit_reuses_completed_module_detail(current_bundle):
  74. service = _service(current_bundle)
  75. first = service.detail(88001, "round:1:branch:1:task")
  76. event_calls = service.repository.load_event_calls
  77. log_calls = service.repository.load_log_calls
  78. second = service.detail(88001, "round:1:branch:1:task")
  79. assert second is first
  80. assert service.repository.load_event_calls == event_calls
  81. assert service.repository.load_log_calls == log_calls
  82. def test_branch_task_audit_separates_card_event_and_fallback_sources(current_bundle):
  83. current_bundle["events"][4]["msg_id"] = "task-50"
  84. current_bundle["events"][4]["inputData"] = {
  85. "task": "完整的实现 Agent 派发任务"
  86. }
  87. payload = _service(current_bundle).detail(88001, "round:1:branch:1:task")
  88. assert payload["actualUse"]["card"]["title"] == "实现任务"
  89. reads = payload["actualReads"]
  90. assert any(
  91. item["consumer"] == "card"
  92. and item["fieldPath"] == "impl_task"
  93. and item["disposition"] == "selected"
  94. for item in reads
  95. )
  96. event_read = next(
  97. item
  98. for item in reads
  99. if item["consumer"] == "inspector"
  100. and item["fieldPath"] == "inputData.task"
  101. )
  102. assert event_read["value"] == "完整的实现 Agent 派发任务"
  103. assert event_read["disposition"] == "selected"
  104. assert event_read["transformation"] == "直接摘取"
  105. assert event_read["source"]["sourceKind"] == "database"
  106. assert event_read["sourceKey"] == "database:event:50"
  107. assert event_read["usageAreas"] == [
  108. "Inspector 业务详情",
  109. "Inspector 技术详情",
  110. ]
  111. assert event_read["usageRatio"] == {
  112. "calculable": True,
  113. "value": 1.0,
  114. "label": "100.0%",
  115. }
  116. assert event_read["readId"].startswith("read:")
  117. assert event_read["contextRefs"] == [
  118. {
  119. "contextId": "event:50",
  120. "sourceKey": "database:event:50",
  121. "contextFieldPath": "inputData.task",
  122. "relation": "direct",
  123. "exact": True,
  124. }
  125. ]
  126. fallback_read = next(
  127. item
  128. for item in reads
  129. if item["consumer"] == "inspector"
  130. and item["fieldPath"] == "impl_task"
  131. )
  132. assert fallback_read["disposition"] == "fallback"
  133. assert fallback_read["sourcePriority"] == "备用来源"
  134. assert fallback_read["usageRatio"]["calculable"] is False
  135. assert any(
  136. item["consumer"] == "inspector"
  137. and item["fieldPath"] == "technical.branch"
  138. and item["transformation"] == "完整记录读取"
  139. for item in reads
  140. )
  141. assert any(
  142. context["kind"] == "event-record"
  143. and context["content"]["eventBody"]["output_content"] == "完整运行返回"
  144. and isinstance(context["usedFragments"], list)
  145. for context in payload["contexts"]
  146. )
  147. assert not any(item["label"] == "table" for item in reads)
  148. assert any(
  149. context["kind"] == "function-result"
  150. and context["source"]["sourceKind"] == "function"
  151. for context in payload["contexts"]
  152. )
  153. assert any(
  154. context["kind"] == "log-module"
  155. and context["source"]["sourceKind"] == "log"
  156. and context["relatedSourceKey"] == "database:event:50"
  157. for context in payload["contexts"]
  158. )
  159. assert any(context["kind"] == "log-module" for context in payload["contexts"])
  160. def test_audit_routes_serve_page_assets_and_full_stream(monkeypatch, current_bundle):
  161. module_audit_router = import_module("app.module_audit.router")
  162. audit_service = _service(current_bundle)
  163. monkeypatch.setattr(module_audit_router, "service", audit_service)
  164. client = TestClient(main.app)
  165. page = client.get("/audit/88001")
  166. assert page.status_code == 200
  167. assert 'data-run-id="88001"' in page.text
  168. assert 'id="module-jump"' in page.text
  169. assert "只看使用字段" in page.text
  170. assert 'id="source-filter"' in page.text
  171. assert client.get("/audit-assets/styles.css").status_code == 200
  172. assert client.get("/audit-assets/app.js").status_code == 200
  173. index = client.get("/api/script-builds/88001/module-audit")
  174. assert index.status_code == 200
  175. assert "eventBody" not in index.text
  176. detail = client.get(
  177. "/api/script-builds/88001/module-audit/round%3A1%3Abranch%3A1%3Atask"
  178. )
  179. assert detail.status_code == 200
  180. assert detail.json()["id"] == "round:1:branch:1:task"
  181. stream = client.get("/api/script-builds/88001/module-audit-stream")
  182. assert stream.status_code == 200
  183. assert stream.headers["content-type"].startswith("application/x-ndjson")
  184. assert '"type":"index"' in stream.text
  185. assert '"type":"detail"' in stream.text
  186. assert '"type":"complete"' in stream.text
  187. def test_audit_page_collapses_large_blocks_without_truncating_source():
  188. script = (
  189. main.__file__.replace("main.py", "module_audit/static/app.js")
  190. )
  191. with open(script, encoding="utf-8") as handle:
  192. source = handle.read()
  193. assert "count > 800" in source
  194. assert "slice(0, length)" in source
  195. assert "jsonText(value)" in source
  196. assert 'contentBox("技术详情"' in source
  197. assert 'contentBox("业务详情"' in source
  198. assert 'class="businessPart${' in source
  199. assert 'class="correspondenceRow"' in source
  200. assert "contextsForGroups" in source
  201. assert "correlationMark" in source
  202. assert "setCorrelationFocus" in source
  203. assert "businessSegments" in source
  204. assert "cardSegments" in source
  205. assert "populateModuleJump" in source
  206. assert "jumpToModule" in source
  207. assert "scrollIntoView" in source
  208. assert "renderCompactReads" in source
  209. assert "renderCompactContexts" in source
  210. assert "实际读取值" in source
  211. assert "原始字段值" in source
  212. assert "原始记录位置" in source
  213. assert "定位原始字段" in source
  214. assert "data-locate-read-id" in source
  215. assert "data-read-ids" in source
  216. assert "module-audit-stream" in source
  217. assert "requestAnimationFrame" in source
  218. assert 'get("module")' in source
  219. assert "data-load-module" not in source
  220. assert "IntersectionObserver" not in source
  221. assert "Inspector 业务详情 ·" not in source
  222. assert "Inspector 读取 · table" not in source
  223. assert "来源投影" not in source
  224. assert "数据库:" in source and "函数:" in source and "日志:" in source