doxa-maps/internal/check-gospel-engine-contract.mjs
Doxa Maps fbc517c94a A browser gate refuses to launch outside the browser slot; check:browser-gates-guarded keeps it that way
Only the browser seat runs browsers, and only inside internal/browser-slot.sh.
That rule was remembered, not enforced: an agent ran a gate it took for a
headless source check and it launched Chromium outside the slot.
internal/lib/playwright-guarded.mjs is now the one way a repo file reaches
playwright: its launch(), launchPersistentContext(), launchServer(), connect()
and connectOverCDP() exit 75 (NOT MEASURED) with a message naming
browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it
for the command it runs, so a bash -c grandchild inherits it. All 72 files that
imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates;
plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2,
research-map-plus-grid 2, research-dashboard 1): a mechanical import swap.
check:browser-gates-guarded (gates:pre) fails a direct import;
bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate
moves to internal/lib/browser-gates.mjs so the runner and the check share it.
2026-09-28 08:20:21 +00:00

152 lines
9.3 KiB
JavaScript

/**
* check-gospel-engine-contract.mjs — the map must use the engine it was ASKED for, or say nothing
* at all. Never a different one.
*
* THE DEFECT THIS IS WRITTEN AGAINST (regions' C3 read + upg100's addendum, 2026-09-26).
* resources-map resolved its engine as `engineGlobal(provider) || window.mapboxgl`. engineGlobal
* already defaults to mapbox, so that fallback could only fire for provider 'maplibre' — and then
* it returned MAPBOX. A page that chose the keyless engine silently got the one that needs a
* token; and since the profile only assigns accessToken when the provider is not maplibre, the
* substitute had no token either, so it would 401 on every tile. Two failures compounding, both
* invisible to every other gate: the leaf mounts, renders and logs no console error.
*
* SIX MEASUREMENTS: two providers x three load states.
* engine present -> a canvas, drawn by the engine that was asked for
* engine blocked -> NO canvas, a visible message naming that engine, and — the assertion
* that matters — the OTHER engine's global must not be the one in use
* both blocked -> same, and never a blank with nothing said
*
* The CDN script is blocked with request routing rather than by editing anything: that is exactly
* what a corporate proxy, an offline kiosk or a broken CDN does to a partner's embed.
*
* Self-test: `--poison` restores the old `|| window.mapboxgl` behaviour in the page (by defining
* window.mapboxgl before the bundle runs while maplibre is blocked) and requires the maplibre arm
* to FAIL — if it passes, this check cannot see the defect it exists for.
*/
import { chromium } from './lib/playwright-guarded.mjs'
import { serveReaped } from './_serve-reaped.mjs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const PUBLIC = resolve(HERE, '../public')
const PORT = Number(process.env.PORT || 5395)
const BASE = process.env.BASE_URL || `http://127.0.0.1:${PORT}`
const POISON = process.argv.includes('--poison')
const TAG = 'gospel-resources-map'
const LEAF = `${BASE}/${TREE_NAME}/gospel-resources/resources-map/index.html`
const SCRIPT = { mapbox: /mapbox-gl.*\.js/i, maplibre: /maplibre-gl.*\.js/i }
let srv = null
if (!process.env.BASE_URL) srv = serveReaped(PUBLIC, PORT)
const run = async (browser, provider, block) => {
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } })
const warns = [], errors = []
page.on('console', (m) => { if (m.type() === 'warning' || m.type() === 'error') warns.push(m.text().slice(0, 200)) })
page.on('pageerror', (e) => errors.push(String(e).slice(0, 200)))
for (const b of block) await page.route(SCRIPT[b], (r) => r.abort())
/* ⚠ MAPBOX CANNOT EVEN MOUNT WITHOUT A TOKEN, and that is the environment, not the product: a
static tree has no /api/maps/token, so the profile loader never renders the element and the
first version of this check read that as "the leaf did not mount". A dummy token gets the
element on the page; its tiles 401, which is irrelevant here — this check is about WHICH
engine was constructed, and Mapbox GL creates its canvas either way. */
if (provider === 'mapbox') await page.addInitScript(() => { window.MAP_TOKEN = 'pk.test.not-a-real-token' })
await page.goto(`${LEAF}?provider=${provider}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
/* 20s, not 11: this leaf fetches ~4 MB of tables before the map exists, and at 11s three of
six runs were still showing the profile loader — the gate was measuring its own impatience. */
await page.waitForTimeout(20000)
const r = await page.evaluate((tag) => {
const sr = document.querySelector(tag)?.shadowRoot
const txt = sr ? (sr.textContent || '').replace(/\s+/g, ' ').trim() : ''
return {
mounted: !!sr,
canvas: !!sr?.querySelector('canvas'),
text: txt.slice(0, 600),
/* The leaf's own error line. Reading the whole shadow text instead caught the stylesheet —
".profile-loader .profile-error" — and scored a rendered message as a blank. */
errText: (sr?.querySelector('.gm-err')?.textContent || '').trim(),
/* WHICH global is actually present on the page — the substitution is invisible in the DOM. */
hasMapbox: !!window.mapboxgl,
hasMaplibre: !!window.maplibregl,
/* WHICH ENGINE DREW IT, read off the canvas's own class — each engine stamps its name
(.mapboxgl-canvas / .maplibregl-canvas). Read from window.__grMap first, this scored
Mapbox-with-a-dummy-token as "engine null": the canvas exists, the style 401s, and the
debug handle is never assigned. The canvas cannot lie about who created it. */
engineOfMap: (() => {
const c = sr?.querySelector('canvas')
const cls = c ? c.className || '' : ''
if (/maplibregl/.test(cls)) return 'maplibre'
if (/mapboxgl/.test(cls)) return 'mapbox'
const m = window.__grMap
if (m && window.maplibregl && m instanceof window.maplibregl.Map) return 'maplibre'
if (m && window.mapboxgl && m instanceof window.mapboxgl.Map) return 'mapbox'
return c ? 'unknown' : null
})(),
}
}, TAG)
await page.close()
return { ...r, warns, errors }
}
const fails = []
/* ⚠ THE STATIC ARM LIVES ELSEWHERE NOW, DELIBERATELY. It used to read this leaf's source for a
* cross-engine fallback, and check:no-silent-engine-substitution does that for EVERY plugin. Two
* gates owning one source-level rule means this leaf was checked twice while the others were
* checked once, and the copy that drifts is always the narrower one. What stays here is the part
* no source read can do: the six RUNTIME measurements below.
*
* ⚠ SO --poison HAS NOTHING TO FALSIFY HERE. With the fallback deleted, planting a substitute
* engine on the PAGE proves nothing (the leaf correctly ignores it), which is measured: an earlier
* runtime poison reported "this check cannot see the defect" about itself. The falsifiable arm went
* with the static check, and this one says so rather than pretending.
*/
if (POISON) {
console.log('ENGINE-CONTRACT --poison: the falsifiable source arm is check:no-silent-engine, run with --self-test; it owns that rule tree-wide and sits in gates:post as of main 23976fb0.')
console.log('ENGINE-CONTRACT: the six runtime measurements cannot be forced to fail without reintroducing the defect in a built bundle.')
process.exit(0)
}
let browser
try {
browser = await chromium.launch()
for (const provider of ['mapbox', 'maplibre']) {
/* 1 — the engine is there. It must be the one asked for. */
const ok = await run(browser, provider, [])
if (!ok.mounted) fails.push(`${provider}/present: the leaf did not mount — errors: ${ok.errors.join(' | ') || 'none'}; warns: ${ok.warns.slice(0,2).join(' | ')}`)
else if (!ok.canvas) fails.push(`${provider}/present: no canvas — the map did not draw`)
else if (ok.engineOfMap !== provider && ok.engineOfMap !== 'unknown') fails.push(`${provider}/present: the map instance came from ${ok.engineOfMap}`)
else console.log(` ok ${provider}/present`.padEnd(30) + `canvas, engine=${ok.engineOfMap}`)
/* 2 — the engine is blocked. Visible failure, and NEVER the other engine. */
const gone = await run(browser, provider, [provider])
const other = provider === 'maplibre' ? 'mapbox' : 'maplibre'
if (!gone.mounted) fails.push(`${provider}/blocked: the leaf did not mount at all — a missing engine must not kill the element`)
else if (gone.canvas) fails.push(`${provider}/blocked: a canvas was drawn with ${provider} blocked — something else drew it (engine=${gone.engineOfMap})`)
else if (gone.engineOfMap === other) fails.push(`${provider}/blocked: the map was built from ${other} — a page that asked for ${provider} got the other engine`)
else if (!/did not load|engine/i.test(gone.errText)) fails.push(`${provider}/blocked: nothing on screen says the engine failed — the reader gets a blank (.gm-err reads "${gone.errText}")`)
else if (!gone.warns.some((w) => /engine global never loaded/.test(w))) fails.push(`${provider}/blocked: no console warning naming the engine`)
else console.log(` ok ${provider}/blocked`.padEnd(30) + 'no canvas, names the engine, no substitution')
/* 3 — neither engine can load. */
const both = await run(browser, provider, ['mapbox', 'maplibre'])
if (!both.mounted) fails.push(`${provider}/both-blocked: the leaf did not mount`)
else if (both.canvas) fails.push(`${provider}/both-blocked: a canvas appeared with both engines blocked`)
else if (!/did not load|engine/i.test(both.errText)) fails.push(`${provider}/both-blocked: a blank with nothing said`)
else console.log(` ok ${provider}/both-blocked`.padEnd(30) + 'no canvas, message shown')
}
if (fails.length) {
console.error(`ENGINE-CONTRACT: FAIL — ${fails.length} of 6 measurement(s):`)
for (const f of fails) console.error(` ${f}`)
process.exit(1)
}
console.log('ENGINE-CONTRACT: PASS — 2 providers x 3 load states: the engine asked for or a named failure, never a substitute.')
} finally {
try { await browser?.close() } catch (_) {}
try { srv?.close?.() } catch (_) {}
}
process.exit(0)