"""`dwg_review.checks` 单元测试:确定性检查前置。""" from dwg_review import checks from dwg_review.checks import ( check_duplicate_entities, check_zero_length, derive_from_evidence, make_issue, run_deterministic_checks, ) from dwg_review.mcp_client import McpError, McpToolError from tests.fakes import FakeMcpClient, make_ctx, make_evidence, ok_text QUIET = lambda *a: None # noqa: E731 WINDOW = {"min_x": 0.0, "min_y": 0.0, "max_x": 10.0, "max_y": 10.0} # --------------------------------------------------------------------------- # make_issue # --------------------------------------------------------------------------- def test_make_issue_minimal(): issue = make_issue("几何", "medium", "零长度") assert issue == {"category": "几何", "severity": "medium", "message": "零长度", "source": "deterministic"} def test_make_issue_with_handles(): issue = make_issue("几何", "low", "重复", handle="A", handles=["A", "B"]) assert issue["handle"] == "A" assert issue["handles"] == ["A", "B"] def test_make_issue_omits_empty_handle(): assert "handle" not in make_issue("文字", "low", "空文字", handle=None) # --------------------------------------------------------------------------- # check_zero_length # --------------------------------------------------------------------------- def test_zero_length_turns_items_into_issues(): client = FakeMcpClient(responses={"find_zero_length": ok_text({ "total": 2, "returned": 2, "offset": 0, "items": [ {"handle": "A1", "type": "LINE", "layer": "0", "length": 0.0}, {"handle": "A2", "type": "ARC", "layer": "0", "length": 0.0001}, ], })}) ctx = make_ctx(client=client) issues = check_zero_length(ctx, WINDOW, tolerance=0.001, limit=200, log=QUIET) assert [i["handle"] for i in issues] == ["A1", "A2"] assert all(i["category"] == "几何" and i["severity"] == "medium" for i in issues) def test_zero_length_skips_items_without_handle(): client = FakeMcpClient(responses={"find_zero_length": ok_text( {"items": [{"type": "LINE"}, {"handle": "B1"}]} )}) ctx = make_ctx(client=client) assert [i["handle"] for i in check_zero_length(ctx, WINDOW, tolerance=0.001, limit=200, log=QUIET)] == ["B1"] def test_zero_length_passes_window_and_params(): client = FakeMcpClient(responses={"find_zero_length": ok_text({"items": []})}) ctx = make_ctx(client=client) check_zero_length(ctx, WINDOW, tolerance=0.5, limit=77, log=QUIET) assert client.arguments_of("find_zero_length") == { "window": WINDOW, "tolerance": 0.5, "limit": 77, } def test_zero_length_swallows_tool_error(): logs = [] client = FakeMcpClient(responses={"find_zero_length": McpToolError("炸了")}) ctx = make_ctx(client=client) assert check_zero_length(ctx, WINDOW, tolerance=0.001, limit=200, log=logs.append) == [] assert any("find_zero_length 失败" in line for line in logs) def test_zero_length_tolerates_non_dict_envelope(): client = FakeMcpClient(responses={"find_zero_length": ok_text("oops")}) ctx = make_ctx(client=client) assert check_zero_length(ctx, WINDOW, tolerance=0.001, limit=200, log=QUIET) == [] # --------------------------------------------------------------------------- # check_duplicate_entities # --------------------------------------------------------------------------- def test_duplicate_group_becomes_single_issue_with_all_handles(): client = FakeMcpClient(responses={"find_duplicate_entities": ok_text({ "count": 1, "groups": [{"reason": "identical_geometry", "type": "LINE", "handles": ["B1", "B2", "B3"]}], })}) ctx = make_ctx(client=client) issues = check_duplicate_entities(ctx, WINDOW, tolerance=0.001, group_limit=50, log=QUIET) assert len(issues) == 1 # 一组 = 一个问题 assert issues[0]["handle"] == "B1" assert issues[0]["handles"] == ["B1", "B2", "B3"] assert issues[0]["severity"] == "low" def test_duplicate_skips_empty_groups(): client = FakeMcpClient(responses={"find_duplicate_entities": ok_text({ "groups": [{"handles": []}, {"handles": None}, {"handles": ["C1", "C2"]}], })}) ctx = make_ctx(client=client) issues = check_duplicate_entities(ctx, WINDOW, tolerance=0.001, group_limit=50, log=QUIET) assert [i["handle"] for i in issues] == ["C1"] def test_duplicate_passes_group_limit(): client = FakeMcpClient(responses={"find_duplicate_entities": ok_text({"groups": []})}) ctx = make_ctx(client=client) check_duplicate_entities(ctx, WINDOW, tolerance=0.2, group_limit=9, log=QUIET) assert client.arguments_of("find_duplicate_entities") == { "window": WINDOW, "tolerance": 0.2, "limit": 9, } def test_duplicate_swallows_tool_error(): logs = [] client = FakeMcpClient(responses={"find_duplicate_entities": McpError(-32603, "x")}) ctx = make_ctx(client=client) assert check_duplicate_entities(ctx, WINDOW, tolerance=0.001, group_limit=50, log=logs.append) == [] assert any("find_duplicate_entities 失败" in line for line in logs) # --------------------------------------------------------------------------- # derive_from_evidence # --------------------------------------------------------------------------- def test_derive_flags_empty_and_whitespace_texts(): ev = make_evidence(texts={"items": [ {"handle": "T1", "content": ""}, {"handle": "T2", "content": " "}, {"handle": "T3", "content": None}, {"handle": "T4", "content": "正常文字"}, ]}) issues = derive_from_evidence(ev) assert [i["handle"] for i in issues] == ["T1", "T2", "T3"] assert all(i["category"] == "文字" for i in issues) def test_derive_flags_unclosed_polyline(): ev = make_evidence(polylines={"items": [ {"handle": "P1", "vertex_count": 4, "closed": False}, {"handle": "P2", "vertex_count": 4, "closed": True}, {"handle": "P3", "vertex_count": 2, "closed": False}, {"handle": "P4", "closed": False}, ]}) issues = derive_from_evidence(ev) assert [i["handle"] for i in issues] == ["P1"] def test_derive_can_disable_unclosed_check(): ev = make_evidence(polylines={"items": [{"handle": "P1", "vertex_count": 4, "closed": False}]}) assert derive_from_evidence(ev, check_unclosed=False) == [] def test_derive_tolerates_missing_sources(): assert derive_from_evidence({}) == [] assert derive_from_evidence(make_evidence()) == [] # --------------------------------------------------------------------------- # run_deterministic_checks # --------------------------------------------------------------------------- def build_ctx(): client = FakeMcpClient(responses={ "find_zero_length": ok_text({"items": [{"handle": "Z1", "type": "LINE", "length": 0.0}]}), "find_duplicate_entities": ok_text({"groups": [ {"type": "LINE", "handles": ["D1", "D2"]}]}), }) return make_ctx(client=client) def test_run_combines_derived_and_mcp(): ctx = build_ctx() ev = make_evidence( texts={"items": [{"handle": "T1", "content": ""}]}, polylines={"items": [{"handle": "P1", "vertex_count": 3, "closed": False}]}, ) issues = run_deterministic_checks(ctx, ev, log=QUIET) by_handle = {i.get("handle"): i for i in issues} assert set(by_handle) == {"T1", "P1", "Z1", "D1"} assert by_handle["D1"]["handles"] == ["D1", "D2"] def test_run_passes_window_to_mcp_tools(): ctx = build_ctx() ev = make_evidence(bbox=(0.0, 0.0, 10.0, 10.0)) run_deterministic_checks(ctx, ev, log=QUIET) assert ctx.client.arguments_of("find_zero_length")["window"] == ev["window"] assert ctx.client.arguments_of("find_duplicate_entities")["window"] == ev["window"] def test_run_without_window_skips_mcp_but_keeps_derived(): ctx = build_ctx() ev = make_evidence() ev["window"] = None ev["texts"] = {"items": [{"handle": "T1", "content": ""}]} issues = run_deterministic_checks(ctx, ev, log=QUIET) assert [i["handle"] for i in issues] == ["T1"] assert ctx.client.calls == [] def test_run_respects_unclosed_switch(monkeypatch): monkeypatch.setattr(checks.config, "CHECK_UNCLOSED_POLYLINES", False) ctx = build_ctx() ev = make_evidence(polylines={"items": [{"handle": "P1", "vertex_count": 4, "closed": False}]}) assert [i["handle"] for i in run_deterministic_checks(ctx, ev, log=QUIET)] == ["Z1", "D1"]