test(review): 新增 pytest 离线回归网,锁定 dwg_review 既有行为
在改动审查流水线(确定性检查前置 / 结论结构化 / 证据交叉校验)之前, 先立一张离线、确定性、秒级的回归网。 - tests/fakes.py:假 LLM + 假 MCP 基座 + 响应构造器(ok_text/ok_image/err) + make_ctx/make_evidence,绝不触网、不读密钥 - 10 个模块共 134 用例: mcp_client / llm_client / cad_tools / checklist / config / planning / evidence / review / report / annotator - pytest.ini(testpaths=tests)+ requirements-dev.txt + 根 conftest.py (sys.path + RENDER_WAIT=0 / REPORT_DIR=tmp fixture)
This commit is contained in:
1 parent
8c07c7cad6
commit
0656397a68
15 files changed
+1710
No files matched your search
@@ -0,0 +1,118 @@
|
||||
"""`dwg_review.phases.report` 单元测试。"""
|
||||
|
||||
import json
|
||||
|
||||
from dwg_review.phases.report import extract_issues, save_report, summarize
|
||||
from tests.fakes import FakeLlmClient, make_ctx
|
||||
|
||||
|
||||
def findings_fixture():
|
||||
return [
|
||||
{"frame": 1, "sheet": "A1", "finding": "图框1:文字重叠"},
|
||||
{"frame": 2, "sheet": "A3", "finding": "图框2:未发现明显问题"},
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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"]
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# extract_issues
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_extract_issues_empty_returns_empty_list():
|
||||
llm = FakeLlmClient(text_script=["[]"])
|
||||
ctx = make_ctx(llm=llm)
|
||||
assert extract_issues(ctx, [], log=lambda *a: None) == []
|
||||
assert llm.calls == []
|
||||
|
||||
|
||||
def test_extract_issues_parses_json_array():
|
||||
payload = '[{"frame": 1, "handle": "A", "severity": "high", "message": "x"}]'
|
||||
llm = FakeLlmClient(text_script=[payload])
|
||||
ctx = make_ctx(llm=llm)
|
||||
issues = extract_issues(ctx, findings_fixture(), log=lambda *a: None)
|
||||
assert issues == [{"frame": 1, "handle": "A", "severity": "high", "message": "x"}]
|
||||
|
||||
|
||||
def test_extract_issues_strips_code_fence():
|
||||
fenced = '```json\n[{"frame": 1, "message": "y"}]\n```'
|
||||
llm = FakeLlmClient(text_script=[fenced])
|
||||
ctx = make_ctx(llm=llm)
|
||||
assert extract_issues(ctx, findings_fixture(), log=lambda *a: None) == [
|
||||
{"frame": 1, "message": "y"}
|
||||
]
|
||||
|
||||
|
||||
def test_extract_issues_returns_empty_on_invalid_json():
|
||||
logs = []
|
||||
llm = FakeLlmClient(text_script=["这不是 JSON"])
|
||||
ctx = make_ctx(llm=llm)
|
||||
assert extract_issues(ctx, findings_fixture(), log=logs.append) == []
|
||||
assert any("结构化抽取失败" in line for line in logs)
|
||||
|
||||
|
||||
def test_extract_issues_returns_empty_when_not_a_list():
|
||||
llm = FakeLlmClient(text_script=['{"frame": 1}'])
|
||||
ctx = make_ctx(llm=llm)
|
||||
assert extract_issues(ctx, findings_fixture(), log=lambda *a: None) == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# save_report
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_save_report_writes_md_and_json(tmp_reports):
|
||||
findings = findings_fixture()
|
||||
plan = {"frames": [{"bbox": [0, 0, 10, 10]}]}
|
||||
issues = [{"frame": 1, "handle": "A", "message": "x"}]
|
||||
|
||||
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_issues_default_to_empty(tmp_reports):
|
||||
_, json_path = save_report("md", findings_fixture(), {"frames": []}, log=lambda *a: None)
|
||||
assert json.loads(json_path.read_text(encoding="utf-8"))["issues"] == []
|
||||
|
||||
|
||||
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()
|
||||
Reference in new issue
Block a user