Files
cad-agent/tests/test_annotator.py
T
gjm daccc734bf test(review): 把 5 处既有瑕疵锁成 *_known_gap 用例
让既有瑕疵可见,而不是被当成正确行为;改 1~3(确定性检查前置/结论结构化/
证据交叉校验)时可据此判断行为是否被改变。

- bbox_to_window 含非数值 → ValueError(上游只判长度)
- summarize / extract_issues 硬下标 f['frame'] → KeyError
- annotate 的 created 无去重(同 handle 产生重复批注)
- extract_issues 单行 ```json[...]``` 围栏剥离失败 → []
- result_json 文本 'null' → None(与「无 text 返回 default」语义易混)
2026-10-14 19:09:56 +08:00

167 lines
6.2 KiB
Python

"""`dwg_review.annotator` 单元测试。"""
import pytest
from dwg_review.annotator import _pick_point, annotate, clear
from dwg_review.mcp_client import McpError, McpToolError
from tests.fakes import FakeMcpClient, make_ctx, ok_text
# ---------------------------------------------------------------------------
# _pick_point
# ---------------------------------------------------------------------------
def test_pick_point_from_bbox_center():
assert _pick_point({"bbox": [0, 0, 10, 20]}) == [5.0, 10.0, 0.0]
def test_pick_point_from_position():
assert _pick_point({"position": [1, 2, 3]}) == [1.0, 2.0, 3.0]
def test_pick_point_from_position_without_z():
assert _pick_point({"position": [1, 2]}) == [1.0, 2.0, 0.0]
def test_pick_point_prefers_bbox_over_position():
assert _pick_point({"bbox": [0, 0, 10, 10], "position": [99, 99]}) == [5.0, 5.0, 0.0]
def test_pick_point_returns_none_when_unlocatable():
assert _pick_point({}) is None
assert _pick_point({"bbox": [0, 0, 1]}) is None
assert _pick_point("not-a-dict") is None
# ---------------------------------------------------------------------------
# annotate
# ---------------------------------------------------------------------------
def build_client(detail=None, created=None, fail_leader=False):
responses = {"get_entity_detail": detail if detail is not None else ok_text({"bbox": [0, 0, 10, 10]})}
if fail_leader:
responses["add_leader_annotation"] = McpToolError("引线失败")
else:
responses["add_leader_annotation"] = ok_text(
created if created is not None else {"created": [{"handle": "NEW1"}]}
)
responses["highlight_entities"] = ok_text({})
return FakeMcpClient(responses=responses)
def test_annotate_dry_run_does_not_call_cad():
logs = []
ctx = make_ctx(client=FakeMcpClient())
created, highlighted = annotate(
ctx, [{"handle": "A", "severity": "high", "message": "x"}], dry_run=True, log=logs.append
)
assert created == [] and highlighted == []
assert ctx.client.calls == []
assert any("[dry-run]" in line for line in logs)
def test_annotate_skips_issues_without_handle():
ctx = make_ctx(client=FakeMcpClient())
created, highlighted = annotate(
ctx, [{"severity": "high", "message": "无 handle"}], log=lambda *a: None
)
assert created == [] and highlighted == []
assert ctx.client.calls == []
def test_annotate_creates_leader_and_highlights():
ctx = make_ctx(client=build_client())
created, highlighted = annotate(
ctx, [{"handle": "A", "severity": "high", "message": "重叠"}], log=lambda *a: None
)
assert created == ["NEW1"]
assert highlighted == ["A"]
names = ctx.client.called_names()
assert "get_entity_detail" in names
assert "add_leader_annotation" in names
assert "highlight_entities" in names
def test_annotate_leader_payload_shape():
ctx = make_ctx(client=build_client())
annotate(ctx, [{"handle": "A", "severity": "high", "message": "m"}], log=lambda *a: None)
args = ctx.client.arguments_of("add_leader_annotation")
assert args["layer"] == "AI_REVIEW"
assert len(args["points"]) == 2
assert args["points"][0] == [5.0, 5.0, 0.0] # 实体定位点
assert args["text_position"] == args["points"][1]
assert args["text"].startswith("[high]")
def test_annotate_skips_when_unlocatable():
logs = []
ctx = make_ctx(client=build_client(detail=ok_text({})))
created, highlighted = annotate(
ctx, [{"handle": "A", "message": "x"}], log=logs.append
)
assert created == [] and highlighted == []
assert "add_leader_annotation" not in ctx.client.called_names()
assert any("无法定位坐标" in line for line in logs)
def test_annotate_tool_error_does_not_highlight():
logs = []
ctx = make_ctx(client=build_client(fail_leader=True))
created, highlighted = annotate(
ctx, [{"handle": "A", "message": "x"}], log=logs.append
)
assert created == [] and highlighted == []
assert "highlight_entities" not in ctx.client.called_names()
assert any("批注失败" in line for line in logs)
def test_annotate_handles_multiple_created_entities():
client = build_client(created={"created": [{"handle": "N1"}, {"handle": "N2"}]})
ctx = make_ctx(client=client)
created, _ = annotate(ctx, [{"handle": "A", "message": "x"}], log=lambda *a: None)
assert created == ["N1", "N2"]
def test_annotate_created_not_deduped_known_gap():
"""已知瑕疵:同一 handle 出现两次会创建两条批注(created 无去重,仅 highlighted 去重)。"""
ctx = make_ctx(client=build_client())
created, highlighted = annotate(
ctx,
[
{"handle": "A", "severity": "high", "message": "第一次"},
{"handle": "A", "severity": "high", "message": "第二次"},
],
log=lambda *a: None,
)
assert created == ["NEW1", "NEW1"] # 重复批注(期望去重)
assert highlighted == ["A"] # 高亮已去重
def test_annotate_propagates_protocol_error_on_detail():
ctx = make_ctx(client=FakeMcpClient(responses={"get_entity_detail": McpError(-32602, "bad")}))
# annotate 只兜 add_leader/highlight 的错;detail 的协议错误会向上冒泡(现状行为)
with pytest.raises(McpError):
annotate(ctx, [{"handle": "A", "message": "x"}], log=lambda *a: None)
# ---------------------------------------------------------------------------
# clear
# ---------------------------------------------------------------------------
def test_clear_deletes_and_restores():
ctx = make_ctx(client=FakeMcpClient())
clear(ctx, handles=["N1"], highlighted=["A"], log=lambda *a: None)
assert ctx.client.arguments_of("delete_entities") == {"handles": ["N1"]}
assert ctx.client.arguments_of("highlight_entities") == {"handles": ["A"], "restore": True}
def test_clear_noop_without_arguments():
ctx = make_ctx(client=FakeMcpClient())
clear(ctx, log=lambda *a: None)
assert ctx.client.calls == []
def test_clear_swallows_tool_errors():
logs = []
client = FakeMcpClient(responses={"delete_entities": McpToolError("删不掉")})
ctx = make_ctx(client=client)
clear(ctx, handles=["N1"], log=logs.append)
assert any("撤销失败" in line for line in logs)