| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- from __future__ import annotations
- from copy import deepcopy
- from importlib import import_module
- from fastapi.testclient import TestClient
- from app import main
- from app.module_audit.service import ModuleAuditService
- class _Provider:
- mode = "test"
- def __init__(self, bundle):
- self.bundle = deepcopy(bundle)
- self.load_bundle_calls = 0
- def load_bundle(self, _script_build_id: int):
- self.load_bundle_calls += 1
- return deepcopy(self.bundle)
- def load_event_detail(self, _script_build_id: int, event_id: int):
- event = next(item for item in self.bundle["events"] if item["id"] == event_id)
- return {
- **deepcopy(event),
- "input": {"content": deepcopy(event.get("inputData"))},
- "output": {
- "content": deepcopy(
- event.get("agentOutputData") or event.get("output_preview")
- )
- },
- }
- class _Repository:
- def __init__(self, bundle):
- self.events = {item["id"]: deepcopy(item) for item in bundle["events"]}
- self.load_event_calls = 0
- self.load_log_calls = 0
- def load_event(self, _script_build_id: int, event_id: int):
- self.load_event_calls += 1
- event = deepcopy(self.events.get(event_id))
- if event is None:
- return None
- event["eventBody"] = {
- "input_content_type": "application/json",
- "input_content": '{"task":"完整派发任务"}',
- "output_content_type": "text/plain",
- "output_content": "完整运行返回",
- }
- return event
- def load_log(self, _script_build_id: int):
- self.load_log_calls += 1
- return (
- "[AGENT_TASK:script_implementer]\n"
- "[MSG_ANCHOR:msg_id=task-50:seq=10]\n"
- "完整派发任务\n"
- "[TOOL_ANCHOR:msg_id=tool-51:seq=11]\n"
- "后续工具调用"
- )
- def _service(current_bundle):
- return ModuleAuditService(
- provider=_Provider(current_bundle), repository=_Repository(current_bundle)
- )
- def test_audit_index_is_lightweight_and_collects_full_flow_modules(current_bundle):
- payload = _service(current_bundle).index(88001)
- module_ids = {item["id"] for item in payload["modules"]}
- assert "run:objective" in module_ids
- assert "round:1:branch:1:task" in module_ids
- assert "event:53" in module_ids
- assert "round:1:result" in module_ids
- assert not any(item["displayVariant"] == "collapsed-summary" for item in payload["modules"])
- assert "round:1:summary" not in module_ids
- assert "round:1:branch:1:summary" not in module_ids
- assert "eventBody" not in str(payload)
- assert "contexts" not in payload
- def test_audit_reuses_completed_run_snapshot_between_index_and_detail(current_bundle):
- service = _service(current_bundle)
- service.index(88001)
- service.detail(88001, "round:1:branch:1:task")
- assert service.provider.load_bundle_calls == 1
- def test_audit_reuses_completed_module_detail(current_bundle):
- service = _service(current_bundle)
- first = service.detail(88001, "round:1:branch:1:task")
- event_calls = service.repository.load_event_calls
- log_calls = service.repository.load_log_calls
- second = service.detail(88001, "round:1:branch:1:task")
- assert second is first
- assert service.repository.load_event_calls == event_calls
- assert service.repository.load_log_calls == log_calls
- def test_branch_task_audit_separates_card_event_and_fallback_sources(current_bundle):
- current_bundle["events"][4]["msg_id"] = "task-50"
- current_bundle["events"][4]["inputData"] = {
- "task": "完整的实现 Agent 派发任务"
- }
- payload = _service(current_bundle).detail(88001, "round:1:branch:1:task")
- assert payload["actualUse"]["card"]["title"] == "实现任务"
- reads = payload["actualReads"]
- assert any(
- item["consumer"] == "card"
- and item["fieldPath"] == "impl_task"
- and item["disposition"] == "selected"
- for item in reads
- )
- event_read = next(
- item
- for item in reads
- if item["consumer"] == "inspector"
- and item["fieldPath"] == "inputData.task"
- )
- assert event_read["value"] == "完整的实现 Agent 派发任务"
- assert event_read["disposition"] == "selected"
- assert event_read["transformation"] == "直接摘取"
- assert event_read["source"]["sourceKind"] == "database"
- assert event_read["sourceKey"] == "database:event:50"
- assert event_read["usageAreas"] == [
- "Inspector 业务详情",
- "Inspector 技术详情",
- ]
- assert event_read["usageRatio"] == {
- "calculable": True,
- "value": 1.0,
- "label": "100.0%",
- }
- assert event_read["readId"].startswith("read:")
- assert event_read["contextRefs"] == [
- {
- "contextId": "event:50",
- "sourceKey": "database:event:50",
- "contextFieldPath": "inputData.task",
- "relation": "direct",
- "exact": True,
- }
- ]
- fallback_read = next(
- item
- for item in reads
- if item["consumer"] == "inspector"
- and item["fieldPath"] == "impl_task"
- )
- assert fallback_read["disposition"] == "fallback"
- assert fallback_read["sourcePriority"] == "备用来源"
- assert fallback_read["usageRatio"]["calculable"] is False
- assert any(
- item["consumer"] == "inspector"
- and item["fieldPath"] == "technical.branch"
- and item["transformation"] == "完整记录读取"
- for item in reads
- )
- assert any(
- context["kind"] == "event-record"
- and context["content"]["eventBody"]["output_content"] == "完整运行返回"
- and isinstance(context["usedFragments"], list)
- for context in payload["contexts"]
- )
- assert not any(item["label"] == "table" for item in reads)
- assert any(
- context["kind"] == "function-result"
- and context["source"]["sourceKind"] == "function"
- for context in payload["contexts"]
- )
- assert any(
- context["kind"] == "log-module"
- and context["source"]["sourceKind"] == "log"
- and context["relatedSourceKey"] == "database:event:50"
- for context in payload["contexts"]
- )
- assert any(context["kind"] == "log-module" for context in payload["contexts"])
- def test_audit_routes_serve_page_assets_and_full_stream(monkeypatch, current_bundle):
- module_audit_router = import_module("app.module_audit.router")
- audit_service = _service(current_bundle)
- monkeypatch.setattr(module_audit_router, "service", audit_service)
- client = TestClient(main.app)
- page = client.get("/audit/88001")
- assert page.status_code == 200
- assert 'data-run-id="88001"' in page.text
- assert 'id="module-jump"' in page.text
- assert "只看使用字段" in page.text
- assert 'id="source-filter"' in page.text
- assert client.get("/audit-assets/styles.css").status_code == 200
- assert client.get("/audit-assets/app.js").status_code == 200
- index = client.get("/api/script-builds/88001/module-audit")
- assert index.status_code == 200
- assert "eventBody" not in index.text
- detail = client.get(
- "/api/script-builds/88001/module-audit/round%3A1%3Abranch%3A1%3Atask"
- )
- assert detail.status_code == 200
- assert detail.json()["id"] == "round:1:branch:1:task"
- stream = client.get("/api/script-builds/88001/module-audit-stream")
- assert stream.status_code == 200
- assert stream.headers["content-type"].startswith("application/x-ndjson")
- assert '"type":"index"' in stream.text
- assert '"type":"detail"' in stream.text
- assert '"type":"complete"' in stream.text
- def test_audit_page_collapses_large_blocks_without_truncating_source():
- script = (
- main.__file__.replace("main.py", "module_audit/static/app.js")
- )
- with open(script, encoding="utf-8") as handle:
- source = handle.read()
- assert "count > 800" in source
- assert "slice(0, length)" in source
- assert "jsonText(value)" in source
- assert 'contentBox("技术详情"' in source
- assert 'contentBox("业务详情"' in source
- assert 'class="businessPart${' in source
- assert 'class="correspondenceRow"' in source
- assert "contextsForGroups" in source
- assert "correlationMark" in source
- assert "setCorrelationFocus" in source
- assert "businessSegments" in source
- assert "cardSegments" in source
- assert "populateModuleJump" in source
- assert "jumpToModule" in source
- assert "scrollIntoView" in source
- assert "renderCompactReads" in source
- assert "renderCompactContexts" in source
- assert "实际读取值" in source
- assert "原始字段值" in source
- assert "原始记录位置" in source
- assert "定位原始字段" in source
- assert "data-locate-read-id" in source
- assert "data-read-ids" in source
- assert "module-audit-stream" in source
- assert "requestAnimationFrame" in source
- assert 'get("module")' in source
- assert "data-load-module" not in source
- assert "IntersectionObserver" not in source
- assert "Inspector 业务详情 ·" not in source
- assert "Inspector 读取 · table" not in source
- assert "来源投影" not in source
- assert "数据库:" in source and "函数:" in source and "日志:" in source
|