Files
cad-agent/tests/test_report.py
T
gjm f7fa8f14d9 feat(review): 审查结论改为结构化 JSON,移除二次 LLM 抽取
- checklist.py:系统提示加入结构化输出契约(只输出 {summary, issues[]},
  引用 handle 必须真实存在、不得编造)
- review.py:review_frame 返回 {summary, issues, raw};仅在「不再请求工具」的回合
  解析结论,中间工具回合的 content 永不解析;强制收口那次 chat 不传 tools
- report.py:删除 extract_issues 与 _EXTRACT_SYSTEM(不再需要第二次 LLM 调用);
  新增 format_finding(用 .get,顺带修复缺 frame 键 KeyError);save_report 的
  issues 省略时从 findings 拍平
- __main__.py:主流程改为 确定性检查 → review_frame → build_finding(合并+校验),
  issues 由 flatten_issues 派生

顺带修复既有 gap:summarize 硬下标 KeyError、extract_issues 单行围栏解析失败。
2026-10-14 19:53:41 +08:00

144 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""`dwg_review.phases.report` 单元测试。"""
import json
from dwg_review.phases.report import format_finding, save_report, summarize
from tests.fakes import FakeLlmClient, make_ctx
def issue(**over):
base = {
"frame": 1, "category": "文字", "severity": "high", "message": "文字重叠",
"handle": "A1", "source": "llm", "verified": True,
}
base.update(over)
return base
def findings_fixture():
return [
{
"frame": 1, "sheet": "A1", "summary": "该图框存在文字重叠",
"issues": [issue()],
"counts": {"deterministic": 0, "llm": 1, "unverified": 0},
},
{
"frame": 2, "sheet": "A3", "summary": "未发现明显问题",
"issues": [],
"counts": {"deterministic": 0, "llm": 0, "unverified": 0},
},
]
# ---------------------------------------------------------------------------
# format_finding
# ---------------------------------------------------------------------------
def test_format_finding_renders_summary_and_issues():
text = format_finding(findings_fixture()[0])
assert "图框 1" in text and "图幅 A1" in text
assert "该图框存在文字重叠" in text
assert "[high][LLM] 文字重叠" in text
assert "handle=A1" in text
def test_format_finding_marks_deterministic_source():
finding = {"frame": 1, "sheet": "A1", "summary": "s",
"issues": [issue(source="deterministic", handle="Z1")]}
assert "[确定性]" in format_finding(finding)
def test_format_finding_marks_unverified():
finding = {"frame": 1, "sheet": "A1", "summary": "s",
"issues": [issue(handle=None, verified=False)]}
text = format_finding(finding)
assert "未在证据中定位" in text
assert "handle=" not in text
def test_format_finding_tolerates_missing_keys():
text = format_finding({})
assert "图框 None" in text # 不再 KeyError(修复既有硬下标 gap)
# ---------------------------------------------------------------------------
# summarize
# ---------------------------------------------------------------------------
def test_summarize_empty_returns_message_without_llm():
llm = FakeLlmClient(text="不应被调用")
ctx = make_ctx(llm=llm)
out = summarize(ctx, [], log=lambda *a: None)
assert "未识别到可审查的图框" in out
assert llm.calls == []
def test_summarize_uses_chat_text():
llm = FakeLlmClient(text="## 总报告")
ctx = make_ctx(llm=llm)
assert summarize(ctx, findings_fixture(), log=lambda *a: None) == "## 总报告"
assert len(llm.calls) == 1
assert "图框 1" in llm.calls[0]["user"]
assert "图框 2" in llm.calls[0]["user"]
def test_summarize_input_includes_issue_lines():
llm = FakeLlmClient(text="x")
ctx = make_ctx(llm=llm)
summarize(ctx, findings_fixture(), log=lambda *a: None)
user = llm.calls[0]["user"]
assert "[high][LLM] 文字重叠" in user
def test_summarize_falls_back_to_raw_input_on_error():
class Boom(FakeLlmClient):
def chat_text(self, system, user, **kwargs):
raise RuntimeError("汇总炸了")
logs = []
ctx = make_ctx(llm=Boom())
out = summarize(ctx, findings_fixture(), log=logs.append)
assert "图框 1" in out and "图框 2" in out
assert any("汇总失败" in line for line in logs)
def test_summarize_tolerates_finding_without_frame_key():
# 修复既有硬下标 gap:缺 frame 键不再 KeyError
llm = FakeLlmClient(text="ok")
ctx = make_ctx(llm=llm)
out = summarize(ctx, [{"summary": "无 frame 键", "issues": []}], log=lambda *a: None)
assert out == "ok"
# ---------------------------------------------------------------------------
# save_report
# ---------------------------------------------------------------------------
def test_save_report_writes_md_and_json(tmp_reports):
findings = findings_fixture()
plan = {"frames": [{"bbox": [0, 0, 10, 10]}]}
issues = [issue()]
md_path, json_path = save_report("# 报告", findings, plan, issues, log=lambda *a: None)
assert md_path.exists() and json_path.exists()
assert md_path.read_text(encoding="utf-8") == "# 报告"
data = json.loads(json_path.read_text(encoding="utf-8"))
assert data["findings"] == findings
assert data["issues"] == issues
assert data["frames"] == plan["frames"]
assert "generated_at" in data
def test_save_report_derives_issues_from_findings(tmp_reports):
findings = findings_fixture()
_, json_path = save_report("md", findings, {"frames": []}, log=lambda *a: None)
data = json.loads(json_path.read_text(encoding="utf-8"))
assert data["issues"] == [issue()] # 拍平自 findings
def test_save_report_creates_missing_directory(tmp_path, monkeypatch):
from dwg_review import config
target = tmp_path / "deep" / "nested"
monkeypatch.setattr(config, "REPORT_DIR", target)
save_report("md", [], {"frames": []}, log=lambda *a: None)
assert target.is_dir()