The gospel-resources gate was found adopting whatever answered on the default port, which on this machine was an unrelated process from a different worktree. It reported on a build that was not the one on disk: first a PASS, then a mount timeout that read exactly like a regression in code nobody had touched. "Answers 200" and "is serving this checkout" are different questions, and only the second one is the gate's job. That fix was local to one gate. The same default sits under the rest of them, so it is now one shared helper, internal/_serves-this-tree.mjs, fingerprinting public/doxa-maps/manifest.json — which generate-tree rewrites on every build and which names every bundle and profile in the tree. Different bytes means a different build: another worktree, or a server someone forgot to restart. Both produce the same lie. Wired into verify-browser-all, which hands ten gates their base URL and would have passed the wrong tree to all of them, and into check-a11y-button-names. Both now refuse with a NOT MEASURED exit rather than producing a confident wrong answer. The gospel gate keeps its behaviour of starting its own server instead. Verified in both directions: against the foreign 4173 each prints "serves a DIFFERENT build than public/" and exits 2; against this tree the aggregator proceeds to its ten gates and the gospel gate passes. ONE REPORTED NUMBER WAS WRONG BECAUSE OF THIS. The a11y result recorded earlier in this flight, "810 buttons, 810 with an accessible name", was measured against the foreign tree. Re-run against this checkout it is 825 buttons, 825 with an accessible name, still passing across 21 leaf pages. The conclusion held; the evidence had not been about this build. Audited the rest of this flight's browser evidence rather than assuming: the 41 probe scripts in _qa-cycle all target port 5399, which is served by serve-strict --directory <this worktree>/public, and the manifest served there is byte-identical to the local one. Those results stand.
222 lines
14 KiB
JavaScript
222 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* verify-browser-all.mjs — run every BROWSER gate over the built tree, in one command.
|
|
*
|
|
* WHY THIS EXISTS. library/scripts/research/verify-all.mjs already solved this for the DATA guards,
|
|
* and its header states the principle better than I can paraphrase it: "Scattered, they are worth
|
|
* much less than they look: nobody knows the list, so nobody runs them, and a guard nobody runs is
|
|
* documentation." That file has ZERO references to playwright or public/doxa-maps — it is the data
|
|
* half. The BROWSER half had no equivalent.
|
|
*
|
|
* ★ MEASURED 2026-09-22: all seven browser gates were ORPHANS — each had a package.json script, and
|
|
* NOT ONE was invoked by any other script. They were reachable only by someone who already knew the
|
|
* name to type, which is precisely the person who does not need the list. This repo has already
|
|
* been bitten by exactly this: the Gate-1 acceptance test existed, worked, and was invoked by
|
|
* nothing, so it "could never report anything" (IDEA-SIX-INTERNAL-SCRIPTS-ARE-UNREACHABLE). An
|
|
* orphan gate is worse than a missing one, because it LOOKS like coverage on the board.
|
|
*
|
|
* ★ EVERY GATE LISTED HERE WAS RUN BEFORE IT WAS LISTED. That rule is inherited deliberately from
|
|
* the same card: "do not mass-wire ... Adding scripts for programs I have not executed would put
|
|
* unvalidated entries in the hand-off surface." Results on 2026-09-22, against the built tree:
|
|
* check:nav-reachable PASS 2 cap-5 shells reach their rail at 414px
|
|
* check:a11y-button-names PASS 332/332 buttons have an accessible name across 19 leaves
|
|
* verify:gate1 PASS 13/13 acceptance assertions against 2107 LIVE records
|
|
* verify:5-caps PASS all 5 capability levels mount, render, error-free
|
|
* smoke:embeds PASS 43/43 leaf pages mount cleanly
|
|
* verify:map-paint PASS 11/11 map leaves paint a real canvas under SwiftShader
|
|
*
|
|
* ★ I EXCLUDED smoke:share HERE AND THAT WAS WRONG — corrected 2026-09-22, two ticks later.
|
|
* The original note read: "it takes required arguments (--url / --plugin-gallery) and its gallery
|
|
* mode reads /__plugin-gallery/registry.json, which only the VITE DEV SERVER serves". Every clause
|
|
* is true and the CONCLUSION did not follow. I had tested exactly two invocations — bare (exit 2,
|
|
* usage) and --plugin-gallery (needs vite) — and never tried --url, which the script's own header
|
|
* documents FIRST. It runs perfectly against the static tree: measured 8/8 pass, each Share button
|
|
* reopening the correct custom element with a live canvas.
|
|
*
|
|
* That is the same shape as AP-a-plausible-disposition-recorded-without-checking-it, found in my
|
|
* own work: a not-applicable verdict is more durable than a wrong bug report, because it closes the
|
|
* question. It cost this suite its highest-consequence surface for two ticks — a share URL is the
|
|
* one artefact a partner pastes into an email, and it fails SILENTLY.
|
|
*
|
|
* It now runs as `smoke:share-all`, a script carrying its own 8 --url flags plus --browser chromium
|
|
* (Firefox headless has no WebGL on this box, so the default engine stops — see
|
|
* BUG-SHARE-SMOKE-FIREFOX-DEFAULT-FALSE-FAILURES). The 8 leaves are the set derived by grepping
|
|
* plugin-designs for ShareButton, not a guess.
|
|
*
|
|
* Still not covered here: `--plugin-gallery` mode, which genuinely does need the vite dev server.
|
|
*
|
|
* PRECONDITIONS (asserted, not assumed): a built tree, and a static server on the BASE port. Both
|
|
* are checked up front so a missing server reports as a SETUP problem rather than six mystery
|
|
* failures.
|
|
*
|
|
* Exit 0 = every gate passed. Exit 1 = at least one failed (each named, with its output tail).
|
|
* Exit 2 = preconditions unmet (no tree / no server) — distinguishable from a real failure.
|
|
*/
|
|
import { spawnSync } from 'node:child_process'
|
|
import { servesThisTree } from './_serves-this-tree.mjs'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173'
|
|
|
|
// Each entry was executed on 2026-09-22 with the result recorded above. `why` is what the gate
|
|
// actually protects, so a failure here tells a reader what broke without opening the file.
|
|
const GATES = [
|
|
{ script: 'verify:5-caps', why: 'all 5 capability levels mount and render' },
|
|
{ script: 'smoke:embeds', why: 'every shipped leaf page mounts its element' },
|
|
{ script: 'verify:map-paint', why: 'every map leaf paints a real canvas' },
|
|
// verify:map-paint proves a canvas painted; this proves the gospel map's PINS are on it
|
|
// and answer a tap. It also scans every link in the index against the screened-publisher
|
|
// list, which is a doctrine boundary rather than a rendering one.
|
|
{ script: 'verify:gospel-map', why: 'the gospel map renders its pins, answers a tap, and links only screened publishers' },
|
|
{ script: 'verify:gate1', why: 'the Gate-1 chain filters live data end to end' },
|
|
{ script: 'check:a11y-button-names', why: 'every visible button has an accessible name' },
|
|
{ script: 'check:nav-reachable', why: 'a phone can open the cap-5 tab rail' },
|
|
{ script: 'smoke:share-all', why: 'every Share button emits a URL that reopens the right element' },
|
|
// ★ The only gate here that measures ONE component in TWO hosts. It exists because a popover can
|
|
// be perfectly anchored standalone and 256px adrift inside the cap-5 shell, and no single-host
|
|
// gate above can tell those apart — the defect lives in the host's ancestor chain, not the
|
|
// component. See internal/check-fixed-anchoring.mjs.
|
|
{ script: 'check:fixed-anchoring', why: 'the filter dropdown stays anchored in BOTH hosts, not just standalone' },
|
|
// Same family as the gate above, and found the same week: the cap-5 shell's pane geometry breaks a
|
|
// plugin that is perfect standalone. Here .vtl-host was inset:0 with overflow:visible, so 2586px of
|
|
// mounted grid was unreachable and a real wheel gesture moved 0px. Asserts with a WHEEL, not with
|
|
// scrollHeight — scrollHeight > innerHeight was the FALSE PASS that closed this bug twice.
|
|
{ script: 'check:shell-scroll', why: 'a wheel gesture in the cap-5 shell actually reaches the grid below the dashboard' },
|
|
]
|
|
|
|
// ── PRECONDITION 1: a built tree ────────────────────────────────────────────
|
|
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
|
|
if (!existsSync(MANIFEST)) {
|
|
console.error('BROWSER-ALL: SETUP — no public/doxa-maps/manifest.json. Run `bun run build` first.')
|
|
process.exit(2)
|
|
}
|
|
let bundleCount = 0
|
|
try { bundleCount = (JSON.parse(readFileSync(MANIFEST, 'utf8')).bundles || []).length } catch { /* reported below */ }
|
|
if (bundleCount < 2) {
|
|
console.error(`BROWSER-ALL: SETUP — manifest lists ${bundleCount} bundle(s); the tree looks empty or truncated.`)
|
|
process.exit(2)
|
|
}
|
|
|
|
// ── PRECONDITION 2: a server on BASE ────────────────────────────────────────
|
|
// Every gate here fetches over HTTP. Without this check a missing server yields six identical
|
|
// timeouts and looks like six broken gates.
|
|
try {
|
|
const r = await fetch(`${BASE}/doxa-maps/manifest.json`, { signal: AbortSignal.timeout(5000) })
|
|
// ★ 503 IS A DIFFERENT DIAGNOSIS FROM "no server", and saying so is the whole point of the code
|
|
// below. serve-strict.mjs returns 503 while public/doxa-maps/manifest.json is absent, because a
|
|
// build rm -rf's that tree at its START — so a gate running then reads an EMPTY tree and reports
|
|
// a confident wrong answer. Measured: verify-shell-scroll said "could not find the
|
|
// dashboard-plus-grid tab" twice in ten minutes during a swarm rebuild, which reads exactly like
|
|
// a real regression and cost three ticks. Without this branch the aggregator blamed a missing
|
|
// server, sending the reader to start one that was already running.
|
|
if (r.status === 503) {
|
|
console.error('BROWSER-ALL: NOT MEASURED — the built tree is mid-build.')
|
|
console.error(` ${BASE} answered 503: public/doxa-maps/manifest.json is absent, which is what a`)
|
|
console.error(' build looks like from the outside (clean rm -rf\'s the tree before rebuilding it).')
|
|
console.error(' Anything measured now is a PHANTOM. Wait for the build and re-run; do NOT record')
|
|
console.error(' this as a product finding. Check with: bun run check:settled')
|
|
process.exit(2)
|
|
}
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
|
// ★ ANSWERING IS NOT THE SAME QUESTION AS SERVING THIS TREE. This used to stop at "something
|
|
// replied", and on this machine 4173 was held by an unrelated process from a DIFFERENT
|
|
// worktree. The gospel-resources gate adopted it and spent three runs reporting on a build
|
|
// that was not the one on disk — a PASS, then a mount timeout that read exactly like a
|
|
// regression in untouched code. Every gate below would inherit the same lie, so the
|
|
// aggregator refuses rather than hand six gates the wrong tree.
|
|
const tree = await servesThisTree(BASE, ROOT)
|
|
if (!tree.ok) {
|
|
console.error('BROWSER-ALL: NOT MEASURED — the server is not serving this checkout.')
|
|
console.error(` ${tree.why}`)
|
|
console.error(' Anything measured against it is about someone else\'s build. Point BASE_URL at')
|
|
console.error(' your own server, or stop the foreign one and run: bun run serve')
|
|
process.exit(2)
|
|
}
|
|
} catch (e) {
|
|
console.error(`BROWSER-ALL: SETUP — nothing serving ${BASE} (${String(e.message).slice(0, 40)}).`)
|
|
console.error(' Start one first: bun run serve # serve-strict.mjs on 4173, --directory public')
|
|
console.error(' Or point elsewhere: BASE_URL=http://host:port bun run verify:browser-all')
|
|
process.exit(2)
|
|
}
|
|
|
|
// A token is optional: the map gates need one to paint, the chrome gates do not. Fetch once and
|
|
// share it, rather than letting six gates each pay for the round trip.
|
|
// ★ "TOKEN PRESENT" USED TO MEAN "A STRING WAS OBTAINED", NOT "A TOKEN THAT WORKS", and the two
|
|
// differ. The only source here was https://doxa.life/api/maps/token, which serves a token
|
|
// URL-RESTRICTED to doxa.life: measured 2026-09-23, a tile request without that Referer returns 403,
|
|
// so every basemap renders blank while the pins draw fine — the exact symptom the Driver reported as
|
|
// "loading the pins, but not the actual underlying map". This banner still said "(token present)".
|
|
//
|
|
// Order matters: the BUILD CONFIG token is tried first because it is the one that works from a local
|
|
// gate (measured: config token 200, endpoint token 403, same request). It is a `pk.` public key baked
|
|
// deliberately (Driver ruling 2026-09-21), protected by URL restrictions rather than secrecy;
|
|
// check:public-hygiene allows pk. and fails the build on sk./tk.. The endpoint stays as a fallback
|
|
// because a future config may legitimately carry no baked token.
|
|
//
|
|
// The token is then VALIDATED with one tile request, so the banner reports what was measured rather
|
|
// than what was fetched. A gate that computes a value and reports it without checking it is the
|
|
// defect family this repo keeps finding; the banner is no different.
|
|
let token = process.env.MAPBOX_TOKEN || ''
|
|
if (!token) {
|
|
try {
|
|
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
|
|
if (typeof cfg.token === 'string' && cfg.token.trim()) token = cfg.token.trim()
|
|
} catch { /* fall through to the endpoint */ }
|
|
}
|
|
if (!token) {
|
|
try {
|
|
const r = await fetch('https://doxa.life/api/maps/token', { signal: AbortSignal.timeout(8000) })
|
|
if (r.ok) token = (await r.json()).token || ''
|
|
} catch { /* the chrome gates still run; the map gates will say so themselves */ }
|
|
}
|
|
let tokenNote = 'NO TOKEN — map gates may report blank'
|
|
if (token) {
|
|
try {
|
|
const t = await fetch(
|
|
`https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/1/1/1.vector.pbf?access_token=${token}`,
|
|
{ signal: AbortSignal.timeout(8000) })
|
|
tokenNote = t.ok
|
|
? 'token present and serves tiles'
|
|
: `token present but tiles ${t.status} — URL-restricted? basemaps will be blank`
|
|
} catch {
|
|
tokenNote = 'token present, tile check unreachable (offline?)'
|
|
}
|
|
}
|
|
|
|
console.log(`BROWSER-ALL: ${GATES.length} gate(s) over ${bundleCount} bundle(s) at ${BASE}` +
|
|
` (${tokenNote})\n`)
|
|
|
|
const results = []
|
|
for (const g of GATES) {
|
|
process.stdout.write(` … ${g.script.padEnd(24)} ${g.why}\n`)
|
|
const t0 = Date.now()
|
|
const r = spawnSync('bun', ['run', g.script], {
|
|
cwd: ROOT, encoding: 'utf8', timeout: 900000,
|
|
env: { ...process.env, MAPBOX_TOKEN: token, BASE_URL: BASE },
|
|
})
|
|
const secs = Math.round((Date.now() - t0) / 1000)
|
|
const out = ((r.stdout || '') + (r.stderr || '')).trim()
|
|
const ok = r.status === 0
|
|
results.push({ ...g, ok, secs, tail: out.split('\n').filter(Boolean).slice(-3).join('\n') })
|
|
console.log(` ${ok ? '✓' : '✗'} ${g.script.padEnd(24)} ${ok ? 'PASS' : 'FAIL'} (${secs}s)`)
|
|
}
|
|
|
|
const failed = results.filter(r => !r.ok)
|
|
console.log(`\nBROWSER-ALL: ${results.length - failed.length}/${results.length} gate(s) passed.`)
|
|
|
|
if (failed.length) {
|
|
console.error('\nFAILED:')
|
|
for (const f of failed) {
|
|
console.error(` ✗ ${f.script} — ${f.why}`)
|
|
for (const line of f.tail.split('\n')) console.error(` ${line}`)
|
|
}
|
|
console.error('\n Re-run one on its own to see its full output: bun run <script>')
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('BROWSER-ALL: PASS — every browser gate over the built tree is green.')
|
|
console.log(' NOT covered here: smoke:share (needs the vite dev server, not a static tree) and the')
|
|
console.log(' DATA guards (`bun run verify` / `verify:full`, which own the corpus side).')
|