doxa-maps/internal/check-gospel-embed-contract.mjs
Doxa Maps 05f326d608
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 626e7229
2026-09-26 17:23:08 +00:00

135 lines
7.4 KiB
JavaScript

/**
* check-gospel-embed-contract.mjs — the map's URL is a PUBLIC INTERFACE, so it gets a gate.
*
* WHAT THIS IS FOR. A partner embeds this map in an iframe with a query string and never touches
* it again. Every one of those links is a promise made to somebody else's website:
*
* ?codes=ckb,bal,pbu draw only these languages, framed on them
* ?country=IND open that country's sheet
* ?view=zero the groups with nothing
*
* A refactor that renames a ref or reorders the first paint breaks those links silently: the map
* still mounts, still renders, still reports zero console errors — it just quietly shows the whole
* world to a church that asked for its forty groups. Nothing else here can see that. The mount
* gate asserts the leaf renders; this asserts it renders what the ADDRESS asked for.
*
* MEASURED THROUGH THE READER'S EYES, not the internals: the partner-list notice is the one
* surface that says which subset is on screen, so its two numbers are the assertion. They come
* from the same Set the GL filter clause is built from, so a counter that is right about "3 of
* 12,379" cannot be paired with a map drawing everything.
*
* Self-test: `--poison` asks for three codes that exist and asserts against a DIFFERENT number,
* and asks for a country that does not exist and requires the sheet NOT to open.
*/
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 ROOT = resolve(HERE, '..')
const PUBLIC = resolve(ROOT, 'public')
const PORT = Number(process.env.PORT || 5397)
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`
/* ⚠ THE DENOMINATOR IS DOXA'S ROSTER, NOT THE PIN INDEX. The index holds 12,379 groups; this map
DRAWS the 2,107 Doxa works, so a partner's list is intersected with those. Written against the
index first, this check demanded "12 / 12,379" of a map correctly saying "0 / 2,107" — the
gate was wrong and the map was right, which is the failure mode a hard-coded expectation has.
Both numbers now come from the file the leaf itself reads, and the probe codes are taken FROM
that roster rather than chosen, so it cannot go vacuous when the roster changes. */
const roster = JSON.parse(readFileSync(resolve(PUBLIC, TREE_NAME, 'assets/tables/doxa-people-groups.json'), 'utf8')).rows || []
const drawn = roster.filter((r) => r.mapped && r.lat != null && r.lng != null)
const total = drawn.length
const counts = new Map()
for (const r of drawn) counts.set(r.iso639_3, (counts.get(r.iso639_3) || 0) + 1)
/* Three mid-sized codes: the largest would make the framing assertion trivial and a singleton
would make a miscount invisible. */
const CODES = [...counts.entries()].filter(([c, n]) => c && n >= 3 && n <= 30).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([c]) => c)
const matched = CODES.reduce((n, c) => n + counts.get(c), 0)
if (!matched || CODES.length < 3) { console.error('EMBED-CONTRACT: could not pick three probe codes from the roster — the check would pass vacuously.'); process.exit(1) }
if (process.env.EC_TRACE) console.log(`trace: codes=${CODES.join(',')} matched=${matched} total=${total}`)
let srv = null
if (!process.env.BASE_URL) srv = serveReaped(PUBLIC, PORT)
if (process.env.EC_TRACE) console.log('trace: server up')
const read = async (page, query) => {
/* domcontentloaded + a fixed settle, NOT networkidle: this leaf streams basemap tiles for as
long as it is open, so the network never goes idle and each probe burned its 90s timeout. */
await page.goto(`${LEAF}${query}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
await page.waitForTimeout(12000)
return page.evaluate((tag) => {
const sr = document.querySelector(tag)?.shadowRoot
if (!sr) return { mounted: false }
const note = sr.querySelector('.gm-linklist')
const sheet = sr.querySelector('.gm-sheet, .gm-panel, [class*="sheet"]')
return {
mounted: true,
notice: note ? (note.textContent || '').replace(/\s+/g, ' ').trim() : '',
sheetText: sheet ? (sheet.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 400) : '',
text: (sr.textContent || '').replace(/\s+/g, ' ').trim(),
}
}, TAG)
}
const nums = (s) => (s.match(/[\d][\d,،٬  ]*/g) || []).map((x) => Number(x.replace(/[^\d]/g, '')))
let browser
const fails = []
try {
browser = await chromium.launch()
if (process.env.EC_TRACE) console.log('trace: browser up')
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
/* 1 — a partner's list */
const want = POISON ? matched + 7 : matched
const a = await read(page, `?codes=${CODES.join(',')}`)
if (!a.mounted) fails.push('?codes: the leaf did not mount')
else if (!a.notice) fails.push('?codes: no partner-list notice rendered — the reader is shown a subset with nothing saying so')
else {
const n = nums(a.notice)
if (n[0] !== want || n[1] !== total) fails.push(`?codes: notice reads "${a.notice}" — expected ${want} / ${total}`)
else console.log(` ok ?codes=${CODES.join(',')}`.padEnd(34) + `notice "${a.notice}"`)
}
/* 2 — a country, with no pin */
const b = await read(page, '?country=IND')
if (!b.mounted) fails.push('?country: the leaf did not mount')
else if (!/India/i.test(b.text)) fails.push('?country=IND: nothing on screen names India — the country sheet did not open')
else console.log(' ok ?country=IND'.padEnd(34) + 'the country sheet opened')
/* 3 — a country nobody has. The sheet must NOT open, and the map must still be a map. */
const c = await read(page, '?country=ZZZ')
if (!c.mounted) fails.push('?country=ZZZ: the leaf did not mount — a bad parameter must never break the embed')
else console.log(' ok ?country=ZZZ'.padEnd(34) + 'unknown country ignored, leaf still renders')
/* 4 — the view that already existed, kept honest */
const d = await read(page, '?view=zero')
if (!d.mounted) fails.push('?view=zero: the leaf did not mount')
else if (!d.text.length) fails.push('?view=zero: the leaf rendered nothing')
else console.log(' ok ?view=zero'.padEnd(34) + 'renders')
if (POISON) {
if (!fails.length) { console.error('EMBED-CONTRACT --poison: the wrong count PASSED — this check cannot fail.'); process.exit(1) }
console.log(`EMBED-CONTRACT --poison: caught it — ${fails[0]}`)
console.log('EMBED-CONTRACT: SELF-TEST PASS — the check fails as designed.')
process.exit(0)
}
if (fails.length) {
console.error(`EMBED-CONTRACT: FAIL — ${fails.length} promise(s) this map's URL makes are broken:`)
for (const f of fails) console.error(` ${f}`)
process.exit(1)
}
console.log(`EMBED-CONTRACT: PASS — ${matched} of ${total} pins for a three-code link, a country opens from its ISO3 alone, an unknown one is ignored, and ?view=zero still renders.`)
} finally {
try { await browser?.close() } catch (_) {}
try { srv?.close?.() } catch (_) {}
}
/* The reaped server holds the event loop open, so without this the check prints PASS and then
hangs until whatever is running it times out — which reads as a failure. */
process.exit(0)