Some checks are pending
build-gate / build (push) Waiting to run
verify-shell-scroll.mjs was written and negative-tested, then left with no package.json entry and no importer, so nothing could ever run it again. It also defaulted to localhost:8899 -- an ad-hoc port from an earlier session -- so it failed with ERR_CONNECTION_REFUSED for anyone else and read as a broken gate rather than a missing server. Now check:shell-scroll, defaulting to BASE_URL or 127.0.0.1:4173 like every other browser gate, and listed in verify-browser-all's GATES array. check:no-orphan-gates fails any gate-shaped file in internal/ that nothing can invoke. The repo carded this once already; it recurs because writing a gate and registering one are separate acts and only the first feels like work. Not every unreferenced file is a lost gate, so the rule is not "everything must be wired": theme-flash-probe is a dated diagnostic for two fixed bugs, worth keeping as a reproduction recipe. It now carries an @one-shot tag saying so, which makes the author state which kind of thing they wrote. Two self-referential defects in the first version of that gate, both from reading evidence out of documentation: - it cited theme-flash-probe.mjs in its own header as an example, then grepped for importers, found itself, and declared the orphan reachable. The gate written to catch orphans made an orphan pass by mentioning it. A gate must never be its own witness, so it now excludes its own file. - it matched @one-shot anywhere in prose, including its own paragraph explaining the tag, and reported "1 tagged" when nothing had been tagged. The tag must now be a marker at line start. Negative-tested: removing check:shell-scroll from package.json fails by name. The scroll fix itself is NOT done. contain:layout and .vtl-host overflow-y are confirmed in the built bundle, and a real wheel gesture still moves the grid 0px with 2586px mounted and unreachable. The gate is what caught that; the diff looked correct.
154 lines
9.3 KiB
JavaScript
154 lines
9.3 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) })
|
|
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 # python http.server 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.
|
|
let token = process.env.MAPBOX_TOKEN || ''
|
|
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 */ }
|
|
}
|
|
|
|
console.log(`BROWSER-ALL: ${GATES.length} gate(s) over ${bundleCount} bundle(s) at ${BASE}` +
|
|
`${token ? ' (token present)' : ' (NO TOKEN — map gates may report blank)'}\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).')
|