doxa-maps/internal/check-gospel-engine-contract.mjs
Doxa Maps 8419d68b52
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source 91ea64f8
2026-09-26 18:43:40 +00:00

160 lines
10 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 'playwright'
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, AND WHY THE SELF-TEST LIVES HERE ════════════════════════════════════════
* The six runtime measurements are the behavioural evidence, but they cannot be made to fail on
* demand: with the fallback deleted, planting a substitute engine on the PAGE proves nothing —
* the leaf correctly ignores it, so the arms pass and a --poison that expected a failure was
* asserting the wrong thing (measured: it reported "this check cannot see the defect", about
* itself). The defect is a line of SOURCE, so the falsifiable arm reads the source. */
const LEAF_SRC = resolve(HERE, '../plugin-designs/gospel-resources/resources-map.vue')
let src = readFileSync(LEAF_SRC, 'utf8')
if (POISON) src = src.replace('const gl = engineGlobal(grProvider)\n', 'const gl = engineGlobal(grProvider) || window.mapboxgl\n')
/* COMMENTS STRIPPED FIRST. The fix's own comment QUOTES the deleted line — "this used to read
`engineGlobal(grProvider) || window.mapboxgl`" — and the first version of this arm read that
quotation as the defect, failing the very commit that removed it. A gate that cannot tell code
from prose about code will keep flagging the explanation forever. */
const code = src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:'"\\])\/\/[^\n]*/g, '$1')
const BAD = /engineGlobal\([^)]*\)\s*\|\|/
if (BAD.test(code)) {
fails.push('resources-map.vue falls back from engineGlobal() to another engine global — a page that asked for the keyless engine would silently get the one that needs a token')
}
if (POISON) {
if (!fails.length) { console.error('ENGINE-CONTRACT --poison: the reintroduced `|| window.mapboxgl` was NOT caught — this check cannot see the defect it exists for.'); process.exit(1) }
console.log(`ENGINE-CONTRACT --poison: caught it — ${fails[0]}`)
console.log('ENGINE-CONTRACT: SELF-TEST PASS — the static arm fails as designed. The six runtime measurements are not re-run here; they 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 — the source keeps no cross-engine fallback, and 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)