Assert the packet variant survives the trip from the menu to the renderer

This is the gate for the defect that cost the most: the packet dropdown
was built into a component the profile people open does not mount, so it
was landed, served, and invisible. Nothing caught it because every check
looked inside ONE file. The legend EMITS a string and the plugin PARSES
it, and those live in different files -- the only assertion that could
have failed is one that reads both and compares.

So it reads what the menu emits, then asserts the consumer can handle
every one of those kinds; that the kind is split on ':' rather than
matched against the two current literals, because matching literals is how
the next variant silently becomes a no-op; and that the parsed value
actually travels -- call site, signature, and the forward into printPacket
-- since asserting the split alone would pass while the value was dropped
on the floor. It also checks the five strings the menu renders exist,
because a missing key prints the raw key and the explainer is the only
place a reader meets what a UPG 100 list is.

Mutation-testing found a hole in my own assertion. The check for the call
site matched `printPrayerPacket(zoneId,\s*variant`, which the DECLARATION
-- async function printPrayerPacket(zoneId, variant = 'full') -- also
matches. A mutant that deleted the argument at the call site left the
declaration behind and the assertion stayed green: true for the wrong
reason, the same family as a tautology but reached through ambiguity.
Anchored on `await`, which only a call has.

Three mutants now fail with their own messages: the legend emitting a kind
the plugin cannot parse, the call site dropping the variant, and
printPrayerPacket holding it without forwarding it.
This commit is contained in:
Doxa Maps 2026-09-28 19:01:32 +00:00
parent f172312cab
commit afaaa85ef5

View file

@ -111,6 +111,52 @@ for (const fn of ['onLegendExport', 'makeSelectionPacket', 'makeSelectionPoster'
`${fn}() sets a busy state without a finally — a throw would leave the surface permanently busy and the control dead`)
}
// ── THE VARIANT REACHES THE RENDERER, ACROSS THE FILE BOUNDARY ─────────────────────────────────
// ★ THIS IS THE GATE FOR THE DEFECT THAT COST THE MOST. The packet ▾ was built into PosterWall,
// which the profile people actually open does not mount, so the feature was landed, served and
// invisible. Nothing caught it because every check looked INSIDE one file. The legend EMITS a
// string and the plugin PARSES it, and those two live in different files — so the only assertion
// that could have failed is one that reads BOTH and compares.
{
const LEGEND = read('library/components/legends/SemanticTreeLegend.vue')
// What the menu emits…
const emitted = [...LEGEND.matchAll(/emitExport\('([a-z:]+)'\)/g)].map((m) => m[1])
ok(emitted.includes('packet:full') && emitted.includes('packet:lightweight'),
`the legend no longer offers both packets — it emits: ${emitted.join(', ') || '(none)'}`)
ok(emitted.includes('poster'), 'the legend no longer offers the poster')
// …and what the plugin understands. A bare 'packet' must keep working, so the consumer splits on
// ':' rather than matching the two new literals — matching literals is how the next variant
// silently becomes a no-op.
ok(/String\(kind\)\.split\(':'\)/.test(VUE),
'onLegendExport no longer splits the kind — a packet:<variant> emit would fall through as an unknown kind and print nothing')
for (const kind of emitted) {
const [base] = kind.split(':')
ok(base === 'poster' || base === 'packet',
`the legend emits '${kind}', whose base '${base}' is neither poster nor packet — the plugin's handler has no branch for it`)
}
// The variant must actually travel to the renderer. Asserting the split alone would pass while
// the parsed value was dropped on the floor.
// ⚠ ANCHOR ON `await`, WHICH ONLY A CALL HAS. The first version of this matched
// `printPrayerPacket(zoneId,\s*variant`, and the DECLARATION — `async function
// printPrayerPacket(zoneId, variant = 'full')` — matches that too. So when a mutant deleted the
// ARGUMENT at the call site, the declaration alone kept the assertion green: true for the wrong
// reason. Same family as a tautology, reached through ambiguity rather than derivation.
ok(/await printPrayerPacket\(zoneId,\s*variant/.test(VUE),
'the parsed variant is not passed to printPrayerPacket — the menu would offer two packets and print one')
ok(/async function printPrayerPacket\([^)]*variant/.test(VUE),
'printPrayerPacket does not accept a variant')
ok(/printPacket\(zone,\s*\{[\s\S]{0,120}variant/.test(VUE),
'printPrayerPacket does not forward the variant to printPacket — the choice would be parsed, held, and never used')
// ⚠ AND THE STRINGS THE MENU RENDERS MUST EXIST. A missing key prints the raw key, which reads as
// a broken translation rather than a missing string — and the explainer is the only place a
// reader meets what a UPG 100 list IS.
const EN_LEGEND = JSON.parse(read('library/i18n/locales/en/legend.json'))
for (const k of ['packet', 'packetExplain', 'packetAria', 'packetFull', 'packetLightweight']) {
ok(EN_LEGEND.export && EN_LEGEND.export[k],
`legend.json export.${k} is missing — the menu would render the raw key`)
}
}
if (fails.length) {
console.error('SELECTION-KINDS: FAIL')
for (const f of fails) console.error(` ✗ ${f}`)