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:
parent
f172312cab
commit
afaaa85ef5
1 changed files with 46 additions and 0 deletions
|
|
@ -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}`)
|
||||
|
|
|
|||
Loading…
Reference in a new issue