The golden path told a new author to fetch a token from doxa.life/api/maps/token. Measured: that token is URL-restricted to doxa.life, so a tile request without that Referer returns 403 and every basemap in their smoke test renders blank while the pins draw fine. That is exactly the symptom the Driver reported as loading the pins but not the underlying map, and it is why tokenUrl was emptied in the build config the day before. The first command in the hand-off doc reproduced a known bug. It now reads the token from the build config, which is the source that works: same tile request, config token 200, endpoint token 403. That is a pk. public key baked deliberately, protected by URL restrictions rather than secrecy, and check:public-hygiene allows pk. while failing the build on sk. and tk. -- so it is safe to read and never a secret to guard. The doc shows the command, not the key. The aggregator had the same bug and a worse symptom: it fetched from the same endpoint and printed "token present", which meant a string had been obtained, not that it worked. So a run could report a present token while every map gate measured a blank basemap. It now prefers the config token, falls back to the endpoint, and validates whichever it gets with one tile request. Verified in all three states: the config token reports "present and serves tiles", a fabricated token reports "tiles 401 - basemaps will be blank", and the old endpoint token reports "tiles 403 - URL-restricted?". The banner reports what was measured rather than what was fetched.
203 lines
12 KiB
JavaScript
203 lines
12 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 { 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' },
|
|
{ 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}`)
|
|
} 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).')
|