From a766f9a9751e7c4cb29f55434a95a26c5f33e444 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 22 Sep 2026 22:37:43 +1200 Subject: [PATCH] Sync misc/qet-mcp: report_link_mismatch check, NEXT_REPORT/PREVIOUS_REPORT fixtures Brings misc/qet-mcp up to date with checkContinuity()'s new report_link_mismatch finding: updated qet_continuity's description, and two new tests reproducing #974 with the shipped 02going_arrow.elmt/ 01coming_arrow.elmt pair (no custom fixtures needed -- this element type already ships). Co-Authored-By: Claude Sonnet 5 --- misc/qet-mcp/qet_mcp.py | 27 +++++++++++----- misc/qet-mcp/test_qet_mcp.py | 60 ++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index 11b30094c..59ca80f71 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -1445,18 +1445,26 @@ def tool_continuity(binary: str, project: str, folio: int | None = None, elements_dir: str | None = None, timeout: int = 180) -> dict: """Run qet.checkContinuity() and get its findings back. - Two checks, against the live Terminal/Conductor object graph rather + Three checks, against the live Terminal/Conductor object graph rather than a heuristic read of the XML (that is qet_check's job, and the two are complementary, not redundant -- qet_check looks at labels and numbering conventions, this looks at the electrical graph itself): - unconnected_terminal (info -- routine, not necessarily a mistake) and + unconnected_terminal (info -- routine, not necessarily a mistake), potential_mismatch (error -- two conductors QElectroTech's own setConductorProperty() would always keep identical, found disagreeing, which only happens from hand-edited XML, a legacy file, or an external - tool). See qet.checkContinuity()'s own doc comment (qetscriptapi.cpp) - for what this deliberately does not check: pin electrical direction/ - power conflicts and No/Nc/Common contact shorts, since QElectroTech's - terminal data model does not carry the information either would need. + tool), and report_link_mismatch (warning -- a next_report/ + previous_report folio-jump pair whose conductors disagree on colour, + style, num, etc.; unlike potential_mismatch this one CAN happen through + ordinary use, since LinkElementCommand::isLinkable() never checks + conductor properties, only type and freedom -- see + qelectrotech/qelectrotech-source-mirror#974, which this check + reproduces exactly: one folio-link conductor drawn in two different + colours on either side of the link). See qet.checkContinuity()'s own + doc comment (qetscriptapi.cpp) for what this deliberately does not + check: pin electrical direction/power conflicts and No/Nc/Common + contact shorts, since QElectroTech's terminal data model does not + carry the information either would need. """ proj = Path(project).expanduser() if not proj.is_file(): @@ -2315,7 +2323,12 @@ TOOLS = [ "electrical potential disagreeing on num/colour/section/" "function/bus/cable, which QElectroTech's own edits never " "produce, so it means hand-edited XML, a legacy file, or an " - "external tool). Does NOT check pin electrical direction/power " + "external tool); and report_link_mismatch (warning -- a " + "next_report/previous_report folio-jump pair whose conductors " + "disagree, which CAN happen through ordinary use since linking " + "two report elements never checks or syncs conductor " + "properties -- reproduces qelectrotech/qelectrotech-source-" + "mirror#974). Does NOT check pin electrical direction/power " "conflicts or No/Nc/Common contact shorts -- QElectroTech's " "terminal data model carries neither. One QElectroTech " "launch; read-only.", diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 2c7c0fe3e..cfac553d0 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -54,6 +54,12 @@ needs_examples = unittest.skipUnless(have_binary and have_examples, COIL = "common://10_electric/10_allpole/310_relays_contactors_contacts/01_coils/bobine_ka_a_remanence.elmt" SLAVE = ("common://10_electric/10_allpole/310_relays_contactors_contacts/" "02_contacts_cross_referencing/15_protection_contacts/contact_relais_nf_esclave.elmt") +# The shipped "going/coming arrow" pair -- a folio-jump link, next_report on +# one folio linked to previous_report on the next, one terminal each. Same +# category of element as issue #974's custom "naechste_folie_rechts.elmt" / +# "vorherige_folie_links.elmt". +NEXT_REPORT = "common://10_electric/10_allpole/100_folio_referencing/02going_arrow.elmt" +PREVIOUS_REPORT = "common://10_electric/10_allpole/100_folio_referencing/01coming_arrow.elmt" TERMINAL = "common://10_electric/10_allpole/130_terminals_terminal_strips/borne_2.elmt" # No shipped element has masterType/slaveType "plc" -- these two minimal @@ -1946,6 +1952,60 @@ class Integration(unittest.TestCase): self.assertGreater(both["finding_count"], 0) self.assertEqual(only_1["finding_count"], 0, "folio 1 has no elements at all") + def test_continuity_finds_a_report_link_colour_mismatch(self): + """Reproduces qelectrotech/qelectrotech-source-mirror#974: a + folio-jump conductor drawn in two different colours on either side + of a next_report/previous_report link. LinkElementCommand never + synchronises conductor properties across the link (only type and + freedom are checked), so this is a real, reproducible gap, not + something only a hand-edited file could produce.""" + base = self.sb.new(folios=2) + ops = [{"op": "add_element", "id": "a", "folio": 0, "path": COIL, "x": 100, "y": 100}, + {"op": "add_element", "id": "next", "folio": 0, "path": NEXT_REPORT, "x": 300, "y": 100}, + {"op": "add_conductor", "folio": 0, "from": "$a", "from_terminal": 0, + "to": "$next", "to_terminal": 0}, + {"op": "set_conductor", "folio": 0, "element": "$a", "terminal": 0, + "property": "color", "value": "#0000ff"}, + {"op": "add_element", "id": "b", "folio": 1, "path": COIL, "x": 100, "y": 100}, + {"op": "add_element", "id": "prev", "folio": 1, "path": PREVIOUS_REPORT, "x": 300, "y": 100}, + {"op": "add_conductor", "folio": 1, "from": "$b", "from_terminal": 0, + "to": "$prev", "to_terminal": 0}, + {"op": "set_conductor", "folio": 1, "element": "$b", "terminal": 0, + "property": "color", "value": "#55aa00"}, + {"op": "link_elements", "folio": 0, "element": "$next", + "to_folio": 1, "to": "$prev"}] + r = self.ok(self.sb.edit(base, ops)) + result = m.tool_continuity(BINARY, r["output"], elements_dir=ELEMENTS) + mismatches = [f for f in result["findings"] if f["kind"] == "report_link_mismatch"] + self.assertEqual(len(mismatches), 1) + self.assertEqual(mismatches[0]["property"], "color") + self.assertEqual(sorted(mismatches[0]["values"]), ["#0000ff", "#55aa00"]) + self.assertEqual(mismatches[0]["severity"], "warning", + "unenforced by the app -- a real gap, but not proof of external " + "tampering the way potential_mismatch's \"error\" is") + self.assertEqual(result["warnings"], 1) + + def test_continuity_no_false_positive_when_report_link_colours_match(self): + base = self.sb.new(folios=2) + ops = [{"op": "add_element", "id": "a", "folio": 0, "path": COIL, "x": 100, "y": 100}, + {"op": "add_element", "id": "next", "folio": 0, "path": NEXT_REPORT, "x": 300, "y": 100}, + {"op": "add_conductor", "folio": 0, "from": "$a", "from_terminal": 0, + "to": "$next", "to_terminal": 0}, + {"op": "set_conductor", "folio": 0, "element": "$a", "terminal": 0, + "property": "color", "value": "#0000ff"}, + {"op": "add_element", "id": "b", "folio": 1, "path": COIL, "x": 100, "y": 100}, + {"op": "add_element", "id": "prev", "folio": 1, "path": PREVIOUS_REPORT, "x": 300, "y": 100}, + {"op": "add_conductor", "folio": 1, "from": "$b", "from_terminal": 0, + "to": "$prev", "to_terminal": 0}, + {"op": "set_conductor", "folio": 1, "element": "$b", "terminal": 0, + "property": "color", "value": "#0000ff"}, + {"op": "link_elements", "folio": 0, "element": "$next", + "to_folio": 1, "to": "$prev"}] + r = self.ok(self.sb.edit(base, ops)) + result = m.tool_continuity(BINARY, r["output"], elements_dir=ELEMENTS) + mismatches = [f for f in result["findings"] if f["kind"] == "report_link_mismatch"] + self.assertEqual(mismatches, []) + def test_auto_numbered_conductor_text_reaches_the_database(self): """ConductorCreator inserted the database row before refreshText() resolved the formula, so the wiring list showed 'W%sequ_1' instead