Files
cad-agent/tests/test_evidence_model.py
T
gjm 6143bb9ab2 feat(review): PDF/图片离线源适配(C1)—— 无需 CAD 的整条审查链路
支持直接审查 PDF 与位图图纸,全链路离线(不连 CAD、不连 8080 端口)。
渲染在内存内完成,复用既有墨迹热力图 + 自适应瓦片规划。

- 新增 dwg_review/sources.py
  - detect_source / plan_file / collect 统一入口
  - PdfReader:pypdfium2 惰性导入,页序即图框序,处理 /Rotate 与 crop 边距
  - ImageReader:世界单位 = 源像素,snap_box 整数对齐保证窗口与像素严格一致
  - ink_grid:纯 PIL 灰度阈值 → 占用网格(不引入 numpy)
- ReviewContext.create(connect_cad=False):离线时 client/tools 均为 None
- 全链路 ctx.tools is None 守卫:review 只读子循环 / inspect_region / annotator /
  checks 的窗口类检查 / 绘图区最大化
- checklist.build_system_prompt(allow_tools=False, offline=True):离线提示词
- __main__:位置参数 source,按扩展名自动判定源类型并决定是否连 CAD
- config 新增 8 个常量(PDF_MAX_PAGES / 文本最小字数 / 各源缩放上限 / 墨迹阈值等)
- tiling.content_predicate 与 heatmap.cell_box 提升为公开函数

验证:
- pytest 457 passed
- verify_pdf_source.py:PDF 23/23、图片 9/9(不需 CAD),含实测瓦片字高像素与
  旋转映射自检(文本区墨迹 0.311 vs 忽略 /Rotate 反向对照 0.145)
- 真实 LLM 端到端:20 页 HVAC 图纸前 3 页跑通,结论具体可核验且未编造 handle

已知局限:页边线在墨迹网格里会把整页连成单簇,瓦片常退化为整页象限(CAD 侧
靠 drop_oversized 丢图框块,离线侧暂无等价手段),留待后续打磨。
2026-10-16 23:07:23 +08:00

116 lines
3.2 KiB
Python

"""`dwg_review.evidence_model` 单元测试。"""
from dwg_review import evidence_model as em
def test_make_image_defaults():
assert em.make_image("QUJD") == {"b64": "QUJD", "mime": "image/png", "label": None}
def test_make_image_custom():
assert em.make_image("QUJD", "image/jpeg", "tile-1") == {
"b64": "QUJD",
"mime": "image/jpeg",
"label": "tile-1",
}
def test_make_image_empty_mime_falls_back():
assert em.make_image("x", "")["mime"] == "image/png"
def test_empty_envelope_default():
assert em.empty_envelope() == {"total": 0, "items": []}
def test_empty_envelope_total():
env = em.empty_envelope(7)
assert env == {"total": 7, "items": []}
# items 必须是**新列表**(调用方可能各自 append,不能共享同一对象)
assert em.empty_envelope()["items"] is not em.empty_envelope()["items"]
def test_empty_envelope_int_coercion():
assert em.empty_envelope("3")["total"] == 3
def test_source_type_of_default_cad():
assert em.source_type_of({}) == em.SOURCE_CAD
assert em.source_type_of(None) == em.SOURCE_CAD
assert em.source_type_of({"source_type": ""}) == em.SOURCE_CAD
def test_source_type_of_explicit():
assert em.source_type_of({"source_type": "pdf"}) == "pdf"
assert em.source_type_of({"source_type": "image"}) == "image"
def test_evidence_images_new_form():
ev = {
"images": [
em.make_image("A", label="overall"),
em.make_image("B", label="tile-1"),
]
}
out = em.evidence_images(ev)
assert [i["b64"] for i in out] == ["A", "B"]
assert [i["label"] for i in out] == ["overall", "tile-1"]
def test_evidence_images_filters_empty_b64():
ev = {"images": [{"b64": "", "mime": "image/png"}, {"b64": "A"}]}
out = em.evidence_images(ev)
assert len(out) == 1
assert out[0]["b64"] == "A"
def test_evidence_images_legacy_fallback():
ev = {"image_b64": "LEG", "image_mime": "image/jpeg"}
assert em.evidence_images(ev) == [
{"b64": "LEG", "mime": "image/jpeg", "label": None, "window": None}
]
def test_evidence_images_legacy_default_mime():
assert em.evidence_images({"image_b64": "LEG"})[0]["mime"] == "image/png"
def test_evidence_images_empty():
assert em.evidence_images({}) == []
assert em.evidence_images(None) == []
assert em.evidence_images({"images": []}) == []
def test_evidence_images_prefers_new_over_legacy():
ev = {"images": [em.make_image("NEW")], "image_b64": "OLD"}
assert [i["b64"] for i in em.evidence_images(ev)] == ["NEW"]
def test_primary_image_prefers_overall():
ev = {
"images": [
em.make_image("A", label="tile-1"),
em.make_image("B", label="overall"),
]
}
assert em.primary_image(ev)["b64"] == "B"
def test_primary_image_first_when_no_overall():
ev = {"images": [em.make_image("A", label="tile-1"), em.make_image("B")]}
assert em.primary_image(ev)["b64"] == "A"
def test_primary_image_none_when_empty():
assert em.primary_image({}) is None
def test_image_labels():
ev = {
"images": [
em.make_image("A", label="overall"),
em.make_image("B", label="tile-1"),
]
}
assert em.image_labels(ev) == ["overall", "tile-1"]