Let the map gate start its own server, so it will actually run
Registering a gate is not enough to make it run. verify:browser-all is invoked NOWHERE in this repo; build:unlocked runs only `bun run verify`, and verify-all.mjs skips anything marked browser:true unless --full is passed. That is precisely how two gates rotted here before — "both had rotted unnoticed precisely because the build chain has never run them". A gate whose precondition is "remember to start a server first" has a precondition nobody remembers. So the precondition is gone. If nothing answers, the gate spawns serve-strict on an ephemeral port — ephemeral so two gates running at once cannot collide on 4173 — and shuts it down afterwards. `bun run verify:gospel-map` now works from a clean shell with no setup. An explicitly named BASE_URL or argv is honoured and never auto-served: a caller naming a host means it, and quietly pointing at a different tree would be worse than skipping. Verified — a dead explicit URL SKIPs and starts nothing. ★ THE FAILURE PATH LEAKED A PROCESS AND THE PASS PATH DID NOT. process.exit(1) inside the try terminates before the finally block's cleanup runs, so a failing run left its spawned server alive: measured 0 strays after a pass, 1 after a fail. A gate that leaks only when it fails leaks exactly when someone is re-running it to see what broke. The exit code is now set, not thrown, and the process exits after cleanup. Re-measured from a clean baseline: 0 strays after pass, after fail, and after restore. One scare worth recording: a stray server DID show up mid-test and it was the orphan from the pre-fix run, six minutes older than the code being tested. Checking its start time rather than its existence is what told them apart.
This commit is contained in:
parent
3d3586b5f9
commit
ff8a318e51
1 changed files with 67 additions and 16 deletions
|
|
@ -26,34 +26,77 @@
|
|||
* eBible/PNG.Bible/Door43/JESUS Film may appear. A link to an excluded publisher would be
|
||||
* a doctrinal failure, not a broken link, so it fails this gate loudly.
|
||||
*
|
||||
* USAGE: needs a server. `bun run serve` (serve-strict on 4173), then
|
||||
* node internal/verify-gospel-map.mjs [baseUrl]
|
||||
* Override with BASE_URL. Skips loudly (exit 0) if the tree is not built, the way CI sees it.
|
||||
* ★ IT STARTS ITS OWN SERVER. Browser gates in this repo rot because they need a server
|
||||
* someone else remembered to start: verify:browser-all is invoked NOWHERE, build:unlocked
|
||||
* runs only `bun run verify`, and verify-all.mjs skips anything marked browser:true unless
|
||||
* --full is passed. The repo has already lost two gates that way — "both had rotted
|
||||
* unnoticed precisely because the build chain has never run them". A gate with a manual
|
||||
* precondition is a gate that will not run, so this one removes the precondition: if
|
||||
* nothing answers, it spawns serve-strict on an ephemeral port and shuts it down after.
|
||||
*
|
||||
* An explicit BASE_URL or argv[2] is honoured as-is and never auto-served — a caller
|
||||
* naming a host means it, and silently pointing at a different tree would be worse than
|
||||
* failing.
|
||||
*
|
||||
* USAGE: node internal/verify-gospel-map.mjs [baseUrl] (no setup needed)
|
||||
* Skips loudly (exit 0) only if the tree is not built, the way CI sees it.
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const BASE = (process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173').replace(/\/+$/, '')
|
||||
const PAGE = `${BASE}/doxa-maps/gospel-resources/resources-map/index.html`
|
||||
const GL = ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox']
|
||||
// The screened publishers. Anything else is a doctrine failure, not a typo.
|
||||
const ALLOWED = /^(https:\/\/)?([a-z0-9-]+\.)*(ebible\.org|png\.bible|alkitab\.pw|git\.door43\.org|jesusfilm\.org|arc\.gt)(\/|$)/i
|
||||
|
||||
const res = await fetch(PAGE).catch(() => null)
|
||||
if (!res || !res.ok) {
|
||||
console.log(`GOSPEL-MAP: SKIP — ${PAGE} not served (${res ? res.status : 'no server'}).`)
|
||||
console.log(' Start one with: bun run serve (serve-strict, production parity)')
|
||||
process.exit(0)
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '..')
|
||||
const EXPLICIT = !!(process.env.BASE_URL || process.argv[2])
|
||||
|
||||
let base = BASE
|
||||
let server = null
|
||||
const alive = async (b) => {
|
||||
const r = await fetch(`${b}/doxa-maps/gospel-resources/resources-map/index.html`).catch(() => null)
|
||||
return !!(r && r.ok)
|
||||
}
|
||||
|
||||
if (!(await alive(base))) {
|
||||
if (EXPLICIT) {
|
||||
console.log(`GOSPEL-MAP: SKIP — nothing served at ${base} (you named it explicitly, so none was started).`)
|
||||
process.exit(0)
|
||||
}
|
||||
if (!existsSync(resolve(REPO, 'public/doxa-maps/gospel-resources/resources-map/index.html'))) {
|
||||
console.log('GOSPEL-MAP: SKIP — no built tree. Run `bun run tree` (cheap) or a full build first.')
|
||||
process.exit(0)
|
||||
}
|
||||
// Ephemeral port so two gates running at once cannot collide on 4173.
|
||||
const port = 5100 + Math.floor(Math.random() * 800)
|
||||
base = `http://127.0.0.1:${port}`
|
||||
server = spawn(process.execPath, [resolve(HERE, 'serve-strict.mjs'), '--port', String(port), '--directory', resolve(REPO, 'public')], { stdio: 'ignore', detached: false })
|
||||
const until = Date.now() + 15000
|
||||
while (Date.now() < until && !(await alive(base))) await new Promise((r) => setTimeout(r, 250))
|
||||
if (!(await alive(base))) {
|
||||
try { server.kill() } catch (_) {}
|
||||
console.error('GOSPEL-MAP: FAIL — could not start serve-strict for the check.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`GOSPEL-MAP: started serve-strict on ${port} for this run.`)
|
||||
}
|
||||
const PAGE_URL = `${base}/doxa-maps/gospel-resources/resources-map/index.html`
|
||||
|
||||
const fails = []
|
||||
let linkCount = 0
|
||||
let exitCode = 0
|
||||
const browser = await chromium.launch({ headless: true, args: GL })
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
|
||||
const pageErrors = []
|
||||
page.on('pageerror', (e) => pageErrors.push(String(e)))
|
||||
|
||||
try {
|
||||
await page.goto(PAGE, { waitUntil: 'networkidle', timeout: 60000 })
|
||||
await page.goto(PAGE_URL, { waitUntil: 'networkidle', timeout: 60000 })
|
||||
// 1 — mounted. A stale-SRI leaf never executes, and that reads as "did not mount".
|
||||
await page.waitForFunction(() => typeof window.__grState === 'function', { timeout: 45000 })
|
||||
// ★ idle FIRST: counting before first paint returns 0 and looks like a dead layer.
|
||||
|
|
@ -91,7 +134,7 @@ try {
|
|||
// jw.org URL planted in the served data did NOT fail this gate until the scan covered
|
||||
// the whole index. The doctrine boundary is the one thing here that must never be
|
||||
// spot-checked: a single off-list link is a doctrinal failure, not a broken link.
|
||||
const idxRes = await fetch(`${BASE}/doxa-maps/assets/tables/people-group-pins.json`)
|
||||
const idxRes = await fetch(`${base}/doxa-maps/assets/tables/people-group-pins.json`)
|
||||
if (!idxRes.ok) fails.push(`pin index not served (${idxRes.status})`)
|
||||
else {
|
||||
const idx = await idxRes.json()
|
||||
|
|
@ -162,12 +205,20 @@ try {
|
|||
if (fails.length) {
|
||||
console.error(`GOSPEL-MAP: FAIL — ${fails.length} problem(s):`)
|
||||
for (const f of fails) console.error(` ${f}`)
|
||||
process.exit(1)
|
||||
// ★ NOT process.exit() here. Calling it inside the try terminates the process before
|
||||
// the finally block's cleanup runs, so a FAILING run left its spawned server alive
|
||||
// while a passing run did not. Measured: 0 strays after a pass, 1 after a fail — a
|
||||
// gate that leaks only when it fails leaks exactly when someone is re-running it.
|
||||
exitCode = 1
|
||||
} else {
|
||||
console.log(`GOSPEL-MAP: PASS — ${st.pins} pins rendered; "${opened.pg}" (${opened.lang}) opens with`)
|
||||
console.log(` a named language; all ${linkCount} links in the index are on screened publishers;`)
|
||||
console.log(` the table link carries iso=${opened.iso}; the gap filter`)
|
||||
console.log(` cuts ${filtered.before} features to ${filtered.after}, all tier 0.`)
|
||||
}
|
||||
console.log(`GOSPEL-MAP: PASS — ${st.pins} pins rendered; "${opened.pg}" (${opened.lang}) opens with`)
|
||||
console.log(` a named language; all ${linkCount} links in the index are on screened publishers;`)
|
||||
console.log(` the table link carries iso=${opened.iso}; the gap filter`)
|
||||
console.log(` cuts ${filtered.before} features to ${filtered.after}, all tier 0.`)
|
||||
} finally {
|
||||
await browser.close()
|
||||
// Never leave a stray server behind, on any exit path.
|
||||
if (server) { try { server.kill('SIGTERM') } catch (_) {} }
|
||||
}
|
||||
process.exit(exitCode)
|
||||
|
|
|
|||
Loading…
Reference in a new issue