Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
156 lines
6.0 KiB
Python
156 lines
6.0 KiB
Python
"""Six P&ID focuses; all silent on a drawing with no P&ID content."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from engineering.pid.drawlines import draw_line
|
|
from engineering.pid.insert import place_symbol
|
|
from engineering.plan_spec import ALL_CRITIQUE_FOCUSES
|
|
from engineering.preflight import pid_plan_warnings, preflight_drawing
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
PID_FOCUSES = (
|
|
"pid_dangling_line",
|
|
"pid_duplicate_tag",
|
|
"pid_incompatible_connection",
|
|
"pid_untagged_instrument",
|
|
"pid_illegal_tag",
|
|
"pid_unconnected_equipment",
|
|
)
|
|
|
|
|
|
def test_focuses_are_in_the_closed_enum():
|
|
for focus in PID_FOCUSES:
|
|
assert focus in ALL_CRITIQUE_FOCUSES
|
|
|
|
|
|
async def _clean(backend):
|
|
pump = await place_symbol(backend, "centrifugal_pump", 100, 100, tag="P-101")
|
|
cv = await place_symbol(backend, "globe", 160, 106, tag="FCV-101", actuator="diaphragm")
|
|
fic = await place_symbol(
|
|
backend, "instrument", 160, 140, tag="FIC-101", type="dcs", location="primary"
|
|
)
|
|
await draw_line(
|
|
backend,
|
|
{"handle": pump["handle"], "port": "discharge"},
|
|
{"handle": cv["handle"], "port": "in"},
|
|
)
|
|
await draw_line(backend, {"handle": cv["handle"], "port": "out"}, {"x": 220, "y": 106})
|
|
await draw_line(
|
|
backend,
|
|
{"handle": fic["handle"]},
|
|
{"handle": cv["handle"], "port": "signal"},
|
|
line_class="electric",
|
|
)
|
|
return pump, cv, fic
|
|
|
|
|
|
async def test_clean_pid_has_no_pid_issues_except_the_free_end(backend):
|
|
await _clean(backend)
|
|
issues = await backend.drawing_critique(list(PID_FOCUSES))
|
|
focuses = [i.focus for i in issues]
|
|
assert focuses == ["pid_dangling_line"], "the line to a free point is the only finding"
|
|
assert issues[0].severity == "error" and "hint" in issues[0].detail
|
|
|
|
|
|
async def test_dangling_carries_the_nearest_port_hint(backend):
|
|
pump, cv, _ = await _clean(backend)
|
|
await backend.entity_move(cv["handle"], 0.0, 1.0)
|
|
issues = await backend.drawing_critique(["pid_dangling_line"])
|
|
hinted = [i for i in issues if i.detail.get("nearest")]
|
|
assert hinted and hinted[0].detail["nearest"]["node"] == cv["handle"]
|
|
assert "pid_line_draw" in hinted[0].detail["hint"]
|
|
|
|
|
|
async def test_duplicate_and_illegal_and_untagged(backend):
|
|
await place_symbol(backend, "instrument", 0, 0, tag="FIC-101")
|
|
await place_symbol(backend, "instrument", 30, 0, tag="FIC-101")
|
|
await place_symbol(backend, "instrument", 60, 0, tag="FCI-102")
|
|
await place_symbol(backend, "instrument", 90, 0)
|
|
await place_symbol(backend, "gate", 120, 0, tag="FIC-101")
|
|
issues = await backend.drawing_critique(
|
|
["pid_duplicate_tag", "pid_illegal_tag", "pid_untagged_instrument"]
|
|
)
|
|
by_focus = {}
|
|
for issue in issues:
|
|
by_focus.setdefault(issue.focus, []).append(issue)
|
|
assert len(by_focus["pid_duplicate_tag"]) == 1
|
|
assert len(by_focus["pid_duplicate_tag"][0].handles) == 3
|
|
assert by_focus["pid_duplicate_tag"][0].severity == "error"
|
|
assert len(by_focus["pid_illegal_tag"]) == 1
|
|
assert by_focus["pid_illegal_tag"][0].severity == "warning"
|
|
assert len(by_focus["pid_untagged_instrument"]) == 1
|
|
|
|
|
|
async def test_incompatible_connection(backend):
|
|
pump = await place_symbol(backend, "centrifugal_pump", 100, 100, tag="P-101")
|
|
fic = await place_symbol(backend, "instrument", 100, 160, tag="FIC-1")
|
|
await draw_line(
|
|
backend,
|
|
{"handle": fic["handle"]},
|
|
{"handle": pump["handle"], "port": "discharge"},
|
|
line_class="electric",
|
|
)
|
|
issues = await backend.drawing_critique(["pid_incompatible_connection"])
|
|
assert len(issues) == 1 and issues[0].severity == "error"
|
|
assert issues[0].detail == {
|
|
"expected": "signal",
|
|
"actual": "process",
|
|
"port": "discharge",
|
|
"line_class": "electric",
|
|
"hint": issues[0].detail["hint"],
|
|
}
|
|
|
|
|
|
async def test_unconnected_equipment_is_informational(backend):
|
|
await place_symbol(backend, "vertical_vessel", 0, 0, tag="V-1")
|
|
issues = await backend.drawing_critique(["pid_unconnected_equipment"])
|
|
assert len(issues) == 1 and issues[0].severity == "info"
|
|
|
|
|
|
async def test_mechanical_drawing_is_untouched_by_pid_focuses(backend):
|
|
from engineering.layers import ensure_engineering_layers
|
|
|
|
await ensure_engineering_layers(backend)
|
|
await backend.entity_create_circle(0, 0, 20, layer="GEOMETRY")
|
|
await backend.entity_create_line(-30, 0, 30, 0, layer="CENTER")
|
|
assert await backend.drawing_critique(list(PID_FOCUSES)) == []
|
|
|
|
|
|
async def test_refine_reports_pid_issues_as_manual_and_moves_nothing(backend):
|
|
# The refine loop is `engineering.refiner.refine_drawing` (what the
|
|
# `drawing_refine` tool calls) — the backend has no `drawing_refine` method.
|
|
from engineering.refiner import refine_drawing
|
|
|
|
await _clean(backend)
|
|
before = await backend.entity_count()
|
|
result = (
|
|
await refine_drawing(
|
|
backend, max_rounds=1, min_score=100.0, focus=list(PID_FOCUSES), dry_run=True
|
|
)
|
|
).to_dict()
|
|
actions = [a for r in result["rounds"] for a in r["actions"]]
|
|
assert actions, "the free-end dangling line must reach the refiner as an action"
|
|
assert all(a["status"] == "manual_required" for a in actions)
|
|
assert await backend.entity_count() == before
|
|
|
|
|
|
def test_preflight_flags_a_pid_intent_on_the_wrong_layer_set():
|
|
result = preflight_drawing(
|
|
"P&ID for the feed section",
|
|
{"units": "mm", "part_type": "pid", "dimensions": {}, "tolerance_policy": "none"},
|
|
layer_set_id="mech",
|
|
)
|
|
codes = [c.code for c in result.conflicts]
|
|
assert "LAYER_SET_INTENT_MISMATCH" in codes
|
|
ok = preflight_drawing(
|
|
"P&ID for the feed section",
|
|
{"units": "mm", "part_type": "pid", "dimensions": {}, "tolerance_policy": "none"},
|
|
layer_set_id="pid",
|
|
)
|
|
assert "LAYER_SET_INTENT_MISMATCH" not in [c.code for c in ok.conflicts]
|
|
assert pid_plan_warnings("Bearing housing", "mech") == []
|
|
assert pid_plan_warnings("piping and instrumentation diagram", "mech")
|