qet-mcp: tests for what a second mutation audit found untested

tools/qet-mcp-audit/mutate.py over the code changed since #1095
(_build_script, the diff helpers, _parse_script_output, tool_items and
the wire-end keys of #1118), tests needing no QElectroTech only: master
f530b7636's suite noticed 531 of 578 planted bugs (92 %). With these
tests, 568 (98 %). The 10 left are equivalent: rsplit("/", 1) vs 2, a
1e-6 tolerance compared with < or <=, a one-letter orientation sliced
[:1] or [:2], branches that only touch parts without terminals, and the
placeholder terminal 0 a conductor uuid overwrites.

- Every argument kind refuses what it cannot take, before any launch:
  malformed indices, bool, points and nodes, search_and_replace with an
  unknown kind or conductor field or an empty element_info field,
  "conductor" given with "element" or "terminal" alone, an op that is not
  an object -- and the same kinds accept what they should.
- _parse_script_output reports capabilities (none logged = unknown, not
  "nothing missing"), notes on the right op, save and stopped_early;
  ignores a line without the marker even when it would parse, and a line
  of a kind it does not know.
- A uuid wire end in a column of terminals (same x) keys like the
  numbered one: the match is on x, y and orientation together.

Tests only; 264/264.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-29 12:09:08 +13:00
parent f530b7636e
commit fdef38eba8
+131
View File
@@ -458,6 +458,80 @@ class EditValidation(unittest.TestCase):
need = json.loads(re.search(r"var need = (\[.*?\]);", script).group(1))
self.assertFalse({"shapeIndex", "textIndex", "imageIndex"} & set(need))
def test_malformed_arguments_are_refused(self):
"""Each argument kind rejects what it cannot take, before any launch.
Found by the mutation audit: forcing any of these checks off went
unnoticed."""
f = {"op": "add_folio", "id": "f"}
cases = [
("indices", {"op": "group_terminals", "strip": 0, "indices": []}),
("indices", {"op": "group_terminals", "strip": 0, "indices": [1, "2"]}),
("indices", {"op": "group_terminals", "strip": 0, "indices": [True]}),
("indices", {"op": "bridge_terminals", "strip": 0, "indices": 3}),
("true or false", {"op": "add_polygon", "folio": 0, "closed": 1,
"points": [{"x": 0, "y": 0}, {"x": 1, "y": 1}]}),
("at least 2", {"op": "add_polygon", "folio": 0, "closed": True,
"points": [{"x": 0, "y": 0}]}),
("at least 2", {"op": "add_polygon", "folio": 0, "closed": True, "points": "xy"}),
("entries must be", {"op": "add_polygon", "folio": 0, "closed": True,
"points": [{"x": 0, "y": 0}, {"x": "1", "y": 1}]}),
("entries must be", {"op": "add_polygon", "folio": 0, "closed": True,
"points": [{"x": 0, "y": 0}, {"x": True, "y": 1}]}),
("entries must be", {"op": "add_polygon", "folio": 0, "closed": True,
"points": [{"x": 0, "y": 0}, [1, 1]]}),
("at least 2 nodes", {"op": "add_path", "folio": 0, "closed": False,
"nodes": [{"x": 0, "y": 0}]}),
("corner, smooth or symmetric", {"op": "add_path", "folio": 0, "closed": False,
"nodes": [{"x": 0, "y": 0},
{"x": 1, "y": 1, "kind": "sharp"}]}),
("inHandle", {"op": "add_path", "folio": 0, "closed": False,
"nodes": [{"x": 0, "y": 0}, {"x": 1, "y": 1, "inHandle": [0, 0]}]}),
("outHandle", {"op": "add_path", "folio": 0, "closed": False,
"nodes": [{"x": 0, "y": 0}, {"x": 1, "y": 1, "outHandle": {"x": 0}}]}),
("unknown kind", {"op": "search_and_replace", "kind": "folio", "field": "x",
"pattern": "a", "replacement": "b", "regex": False,
"case_sensitive": False}),
("unknown conductor field", {"op": "search_and_replace", "kind": "conductor",
"field": "label", "pattern": "a", "replacement": "b",
"regex": False, "case_sensitive": False}),
("non-empty", {"op": "search_and_replace", "kind": "element_info", "field": "",
"pattern": "a", "replacement": "b", "regex": False,
"case_sensitive": False}),
("not both", {"op": "delete_conductor", "folio": 0,
"conductor": "{11111111-2222-4333-8444-555555555555}",
"element": "{11111111-2222-4333-8444-555555555555}"}),
("not both", {"op": "delete_conductor", "folio": 0,
"conductor": "{11111111-2222-4333-8444-555555555555}", "terminal": 0}),
("not an object", "add_folio"),
]
for message, op in cases:
with self.subTest(op=op):
with self.assertRaisesRegex(ValueError, message):
self.build([f, op])
def test_valid_shapes_of_those_arguments_pass(self):
"""The other side: the same kinds accept what they should, so the
refusals above are not everything failing."""
s = self.build([
{"op": "add_polygon", "folio": 0, "closed": False,
"points": [{"x": 0, "y": 0}, {"x": 1.5, "y": -2}]},
{"op": "add_path", "folio": 0, "closed": True,
"nodes": [{"x": 0, "y": 0, "kind": "smooth", "inHandle": {"x": 1, "y": 1}},
{"x": 5, "y": 5, "outHandle": {"x": 2, "y": 2}}]},
{"op": "add_polygon", "folio": 0, "closed": True,
"points": [{"x": 0, "y": 0, "kind": "anything"}, {"x": 1, "y": 1}]},
{"op": "search_and_replace", "kind": "conductor", "field": "num", "pattern": "a",
"replacement": "b", "regex": True, "case_sensitive": False},
{"op": "search_and_replace", "kind": "text", "field": "", "pattern": "a",
"replacement": "b", "regex": False, "case_sensitive": True}])
self.assertIn("qet.addPolygon(0, [{", s)
self.assertIn("qet.searchAndReplace(\"conductor\", \"num\", \"a\", \"b\", true, false)", s)
self.assertIn("qet.searchAndReplace(\"text\", \"\", \"a\", \"b\", false, true)", s)
def test_no_id_stores_nothing(self):
s = self.build([{"op": "add_folio"}])
self.assertNotIn("R[", s.split("if (missing.length === 0) {")[1])
def test_a_string_index_that_is_not_a_uuid_is_refused(self):
for bad in ("3", "{nope}", "11111111-1111"):
with self.subTest(index=bad):
@@ -566,6 +640,37 @@ class ResultParsing(unittest.TestCase):
self.assertEqual(parsed["operations"], [])
def test_capabilities_notes_and_save(self):
"""Every field the script reports reaches the result, attached where
it belongs. Found by the mutation audit: only the binary tests read
these back."""
text = "\n".join([
self.line(kind="capabilities", missing=["addFolio"]),
self.line(kind="op", index=0, op="a", id=None, result=True),
self.line(kind="op", index=1, op="b", id=None, result=False),
self.line(kind="op_note", index=1, note="why"),
self.line(kind="save", result=True, stopped_early=True),
self.line(kind="something_newer", result=False)]) # not a save
parsed = m._parse_script_output(text)
self.assertEqual(set(parsed), {"missing_methods", "operations", "saved", "stopped_early"})
self.assertEqual(parsed["missing_methods"], ["addFolio"])
self.assertTrue(parsed["saved"])
self.assertTrue(parsed["stopped_early"])
self.assertNotIn("note", parsed["operations"][0])
self.assertEqual(parsed["operations"][1]["note"], "why")
# no capabilities line: unknown, not "nothing missing"
self.assertIsNone(m._parse_script_output(self.line(kind="save", result=False))["missing_methods"])
self.assertEqual(m._parse_script_output(self.line(kind="capabilities", missing=None))
["missing_methods"], [])
def test_a_line_without_the_marker_is_ignored(self):
"""Even one that would parse if read from where the marker would be."""
almost = "x" * (len(m._MARKER) - 1) + json.dumps({"kind": "save", "result": True})
parsed = m._parse_script_output(almost)
self.assertIsNone(parsed["saved"])
self.assertFalse(parsed["stopped_early"])
class TerminalOrder(unittest.TestCase):
"""QElectroTech indexes terminals top-to-bottom then left-to-right, not in
file order. Getting it wrong wires the wrong end of a coil with no error;
@@ -962,6 +1067,32 @@ class ReadToolContracts(unittest.TestCase):
self.assertEqual(numbered, "1:{E}@0,-4,2--{E}@6,0,1")
self.assertEqual(by_uuid, numbered)
def test_conductor_key_same_in_both_forms_in_a_column(self):
"""Terminals one above the other share their x: a uuid end must be
matched on x, y and orientation together, not on any one of them.
Found by the mutation audit: an "or" in place of "and" went
unnoticed while every test's terminals differed in every coordinate."""
root = ET.fromstring(
'<project><collection><category name="import"><category name="x">'
'<element name="C.elmt"><definition><description>'
'<terminal uuid="{T1}" x="0" y="0" orientation="n"/>'
'<terminal uuid="{T2}" x="0" y="20" orientation="n"/>'
'<terminal uuid="{T3}" x="30" y="20" orientation="n"/>'
'</description></definition></element>'
'</category></category></collection>'
'<diagram><elements><element uuid="{E}" type="embed://import/x/C.elmt">'
'<terminals><terminal id="1" x="0" y="4" orientation="0"/>'
'<terminal id="2" x="0" y="24" orientation="0"/>'
'<terminal id="3" x="30" y="24" orientation="0"/></terminals>'
'</element></elements><conductors>'
'<conductor terminal1="2" terminal2="3"/>'
'<conductor element1="{E}" terminal1="{T2}" element2="{E}" terminal2="{T3}"/>'
'</conductors></diagram></project>')
numbered, by_uuid = [m._conductor_row(i, c, ix)["key"]
for i, c, ix in m._conductors(root)]
self.assertEqual(numbered, "1:{E}@0,24,0--{E}@30,24,0")
self.assertEqual(by_uuid, numbered)
def test_conductor_row_without_an_index(self):
c = ET.fromstring('<conductor terminal1="7" terminal2="8"/>')
self.assertEqual(m._conductor_row(3, c)["key"], "3:#7--#8")