diff --git a/internal/_serves-this-tree.mjs b/internal/_serves-this-tree.mjs new file mode 100644 index 000000000..2db4c8b7b --- /dev/null +++ b/internal/_serves-this-tree.mjs @@ -0,0 +1,48 @@ +/** + * _serves-this-tree.mjs — a browser gate must prove the server it talks to is THIS build. + * + * THE FAILURE THIS EXISTS FOR, MEASURED. Browser gates here default to + * http://127.0.0.1:4173 and adopt whatever answers 200 on it. On this machine that port was + * held by an unrelated process belonging to a different worktree, and the gospel-resources + * gate spent three runs reporting on a build that was not the one on disk — first a PASS, + * then a mount timeout that looked exactly like a regression in code that had nothing to do + * with it. A gate that can report on the wrong tree is worse than no gate: it produces + * confident findings about code nobody changed, and hides real ones. + * + * "Answers 200" and "is serving this checkout" are different questions. This asks the second. + * + * THE FINGERPRINT is public/doxa-maps/manifest.json, which generate-tree rewrites on every + * build and which names every bundle and profile in the tree. If the bytes served differ + * from the bytes on disk, it is a different build — a different worktree, or a stale server + * someone forgot to restart, both of which produce the same lie. + * + * Deliberately NOT fatal on its own: the caller decides whether to start its own server + * instead, or to skip. What it must never do is quietly continue. + */ +import { readFileSync } from 'node:fs' +import { createHash } from 'node:crypto' +import { resolve } from 'node:path' + +const MANIFEST = 'doxa-maps/manifest.json' + +/** + * @param {string} base e.g. http://127.0.0.1:4173 (a trailing /doxa-maps/ is tolerated) + * @param {string} repo absolute path to the repo root + * @returns {Promise<{ok:boolean, why:string}>} + */ +export async function servesThisTree(base, repo) { + let local + try { + local = createHash('sha256').update(readFileSync(resolve(repo, 'public', MANIFEST))).digest('hex') + } catch { + return { ok: false, why: `no local public/${MANIFEST} to compare against — build the tree first` } + } + const root = String(base).replace(/\/+$/, '').replace(/\/doxa-maps$/, '') + const r = await fetch(`${root}/${MANIFEST}`).catch(() => null) + if (!r || !r.ok) return { ok: false, why: `nothing serving ${MANIFEST} at ${root}` } + const served = createHash('sha256').update(Buffer.from(await r.arrayBuffer())).digest('hex') + if (served !== local) { + return { ok: false, why: `${root} serves a DIFFERENT build than public/ — another worktree, or a server that needs restarting` } + } + return { ok: true, why: 'same build' } +} diff --git a/internal/check-a11y-button-names.mjs b/internal/check-a11y-button-names.mjs index 0fffe18d8..c765453cd 100644 --- a/internal/check-a11y-button-names.mjs +++ b/internal/check-a11y-button-names.mjs @@ -34,6 +34,7 @@ import { readFileSync, existsSync } from 'node:fs' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { chromium } from 'playwright' +import { servesThisTree } from './_serves-this-tree.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') @@ -45,6 +46,20 @@ if (!existsSync(MANIFEST)) { process.exit(0) } +// ★ The server has to be serving THIS checkout, not merely answering. 4173 on this machine +// was held by an unrelated process from another worktree, and a gate that adopts it reports +// a confident result about a build nobody here changed. A pass earned against the wrong tree +// is worse than no pass. +{ + const tree = await servesThisTree(BASE, ROOT) + if (!tree.ok) { + console.error('A11Y-BUTTON-NAMES: NOT MEASURED — the server is not serving this checkout.') + console.error(` ${tree.why}`) + console.error(' Point BASE at your own server (BASE=http://127.0.0.1:PORT/doxa-maps/).') + process.exit(2) + } +} + let tk = '' try { const r = await fetch('https://doxa.life/api/maps/token', { signal: AbortSignal.timeout(8000) }) diff --git a/internal/verify-browser-all.mjs b/internal/verify-browser-all.mjs index 5c6cd8ef9..5ee868b21 100644 --- a/internal/verify-browser-all.mjs +++ b/internal/verify-browser-all.mjs @@ -53,6 +53,7 @@ * 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' @@ -120,6 +121,20 @@ try { 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') diff --git a/internal/verify-gospel-map.mjs b/internal/verify-gospel-map.mjs index b34af39cc..7d0f2b111 100644 --- a/internal/verify-gospel-map.mjs +++ b/internal/verify-gospel-map.mjs @@ -42,9 +42,9 @@ * Skips loudly (exit 0) only if the tree is not built, the way CI sees it. */ import { chromium } from 'playwright' +import { servesThisTree } from './_serves-this-tree.mjs' import { spawn } from 'node:child_process' import { existsSync, readFileSync } from 'node:fs' -import { createHash } from 'node:crypto' import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' @@ -67,22 +67,12 @@ let server = null // regression in code that had nothing to do with it. // The fingerprint is the built bundle itself: if the bytes served differ from the bytes on // disk, it is a different tree and must not be used. -const BUNDLE_PATH = 'doxa-maps/gospel-resources/gospel-resources.js' -const localBundle = (() => { - try { return createHash('sha256').update(readFileSync(resolve(REPO, 'public', BUNDLE_PATH))).digest('hex') } catch { return null } -})() -const servesThisTree = async (b) => { - if (!localBundle) return false - const r = await fetch(`${b}/${BUNDLE_PATH}`).catch(() => null) - if (!r || !r.ok) return false - const served = createHash('sha256').update(Buffer.from(await r.arrayBuffer())).digest('hex') - return served === localBundle -} const alive = async (b) => { const r = await fetch(`${b}/doxa-maps/gospel-resources/resources-map/index.html`).catch(() => null) if (!(r && r.ok)) return false - if (await servesThisTree(b)) return true - console.log(`GOSPEL-MAP: something answers at ${b} but serves a DIFFERENT build than public/ — not adopting it.`) + const tree = await servesThisTree(b, REPO) + if (tree.ok) return true + console.log(`GOSPEL-MAP: not adopting ${b} — ${tree.why}`) return false }