doxa-maps/internal/build-packet-fixtures.mjs
doxa-maps-88655c70 2f96f04b49
Some checks are pending
build-gate / build (push) Waiting to run
packets: fixtures at the PAGE BOUNDARY, which nothing had ever rendered
check:packets-all reported "every generated packet renders, in every locale
present" and passed on 11 packets. All 11 were the SAME ZONE (laos) in 11
locales. It proved one zone's layout eleven times.

The answer is not more zones -- 28 zones x 11 locales is a network fetch per
zone and ~100MB of artefacts. It is more SHAPES, chosen where the layout can
actually break, built from committed records with no network.

★ THE SHAPE THAT MATTERS IS THE PAGE BOUNDARY. packet-render.js:76 sets
PER_PAGE = 20 and paginates `for (start = 0; start < groups.length; start +=
PER_PAGE)`. laos has 95 groups and tibet 43 -- both mid-range remainder cases,
so the boundary ITSELF had never been rendered. Three fixtures, each a slice of
the committed tibet records, so they are byte-deterministic and need no API:
  exactly 20 -- one full page; an off-by-one yields a second, empty one
  21         -- two pages, the second holding a single card
  1          -- the minimum a zone can have

MUTATION-TESTED, and this is the argument for the whole change. Flipping the
page loop to `start <= groups.length` -- an off-by-one that only shows when the
count is a multiple of PER_PAGE:

  fixture-edge-exactly-20   FAIL   <- the only one of 15
  fixture-edge-21-groups    pass
  fixture-edge-one-group    pass
  fixture-tibet             pass
  laos x 11 locales         pass

A real pagination bug would have shipped completely green under the old suite.
The failure is specific too: "20 people cards should lay out as 4 sheets but the
packet has 5" and "list pages disagree on the zone total: 20 vs 21".

⚠ EACH BUILD OVERWROTE THE LAST, which is why this is a script and not a line of
shell. packet.mjs names its output for the ZONE, not the fixture, so four
fixture builds against one zone leave ONE file -- and check:packets-all would go
on seeing a single shape while reporting success. Each output is renamed to its
fixture's own name so all of them coexist and every one is evaluated.

packet:ci now builds all four. check:packets-all goes from 11 packets in 1 zone
to 15 across 5 shapes; it still reports "5 of 28" and names what it does not
cover.
2026-09-24 23:53:07 +00:00

82 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
/**
* build-packet-fixtures — build every committed packet fixture, so check:packets-all has more
* than one SHAPE to evaluate.
*
* THE GAP THIS CLOSES. check:packets-all reported "every generated packet renders, in every locale
* present" and passed on 11 packets — all of them the SAME ZONE (laos) in 11 locales, against 28
* poster zones. The suite proved one zone's layout eleven times. Generating all 28 x 11 is a
* network fetch per zone and ~100MB of artefacts, so the answer is not more zones: it is more
* SHAPES, chosen where the layout can actually break, built from committed records with no network.
*
* ★ THE SHAPES ARE THE PAGE BOUNDARY. packet-render.js:76 sets PER_PAGE = 20, and pagination is
* `for (start = 0; start < groups.length; start += PER_PAGE)`. laos has 95 groups and tibet 43 —
* both mid-range, both remainder cases, so the boundary itself had NEVER been rendered:
* - exactly 20 : one full page. An off-by-one here yields 2 list pages, the second empty.
* - 21 : two pages, the second holding a SINGLE card.
* - 1 : the minimum a zone can have.
* Each is derived by slicing the committed tibet fixture, so they are byte-deterministic and need
* no API.
*
* ⚠ EACH BUILD OVERWRITES THE PREVIOUS ONE, which is the reason this script exists rather than a
* line of shell. packet.mjs names its output for the ZONE, not the fixture, so four fixture builds
* against one zone leave one file — and check:packets-all would go on seeing a single shape while
* reporting success. Each output is renamed to its fixture's own name so all of them coexist and
* every one gets evaluated.
*
* Usage: node internal/build-packet-fixtures.mjs
*/
import { existsSync, renameSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
const FIXTURES = resolve(REPO, 'library/scripts/posters/fixtures')
const OUT = resolve(REPO, 'library/scripts/posters/out/packets')
// Every fixture is built against this zone: the records come from the fixture, and the zone only
// supplies page 1's map. Keeping it constant means a shape difference is the ONLY variable.
const ZONE = 'asia-china-tibet-autonomous-region'
if (!existsSync(FIXTURES)) {
console.log(`PACKET-FIXTURES: SKIP — no fixtures directory at ${FIXTURES}`)
process.exit(0)
}
const fixtures = readdirSync(FIXTURES).filter((f) => /^records-.*\.json$/.test(f)).sort()
if (!fixtures.length) {
console.log('PACKET-FIXTURES: SKIP — no records-*.json fixtures committed')
process.exit(0)
}
console.log(`PACKET-FIXTURES: building ${fixtures.length} fixture(s)`)
const built = []
const failed = []
for (const fx of fixtures) {
const name = fx.replace(/^records-/, '').replace(/\.json$/, '')
const res = spawnSync(process.execPath, [join(REPO, 'library/scripts/posters/packet.mjs')], {
cwd: REPO,
encoding: 'utf8',
env: { ...process.env, PACKET_RECORDS: join(FIXTURES, fx), ONLY: ZONE },
})
const produced = join(OUT, `${ZONE}.html`)
if (res.status !== 0 || !existsSync(produced)) {
failed.push(`${fx}: ${(res.stderr || res.stdout || '').trim().split('\n').slice(-1)[0] || 'no output'}`)
console.log(` ✗ ${fx}`)
continue
}
// Rename BEFORE the next build, or the next one overwrites it — see the warning above.
const dest = join(OUT, `fixture-${name}.html`)
renameSync(produced, dest)
built.push(`fixture-${name}.html`)
console.log(` ✓ ${fx} -> fixture-${name}.html`)
}
if (failed.length) {
console.error(`\nPACKET-FIXTURES: FAIL — ${failed.length} fixture(s) did not build`)
for (const f of failed) console.error(' - ' + f)
process.exit(1)
}
console.log(`\nPACKET-FIXTURES: ${built.length} packet(s) built. Run check:packets-all to evaluate them.`)