Gospel checks: one dev-assets gate, and cover the gap it leaves
The gospel-only dev-assets check is gone in favour of the shared one, which is better at the job: it tests the /__leaf/<bundle>/<profile> prefix — the one that actually broke — across all seven map bundles, and confirms each leaf hands its element the base it claims. The gospel version never looked at that prefix, so keeping both would have meant two implementations and the weaker one still green while the map was broken. What the shared gate samples is the POOL, the files every map shares. It cannot know this leaf also fetches gospel tables by name, so a table that stopped being staged would leave it green and this map fetching a 404. That is the only gap left, and this covers exactly it. The wanted list is parsed out of resources-map.vue rather than written down, which immediately earned itself: the hand-written list it replaces was already two short — pray-index and gospel-link-verification were being fetched and were not in it. A list maintained by hand is the field-list-goes-stale bug, and this bundle has produced that three times in one day. Poison-tested by unstaging a real table: FAIL, exit 1, then restored.
This commit is contained in:
parent
3ab1d7aad5
commit
0b54b0354e
4 changed files with 62 additions and 108 deletions
|
|
@ -36,6 +36,7 @@ const STATIC = [
|
|||
['check:stale-sweep-claim', 'no caveat outliving its truth'],
|
||||
['check:click-sheet', 'the country sheet resolves, links are absolute'],
|
||||
['check:snapshot-guard', 'no fetcher writes around the guard'],
|
||||
['check:gospel-tables-staged', 'every asset the map fetches is in the shipped tree'],
|
||||
]
|
||||
const BROWSER = [
|
||||
['check:leaves-mount', 'all five leaves actually mount'],
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-gospel-dev-assets — every asset the gospel map fetches must be reachable in the DEV
|
||||
* SERVER, not only in the built tree.
|
||||
*
|
||||
* WHY THIS EXISTS. Every other gate for this bundle measures public/ — the built tree — and the
|
||||
* Driver found the map broken where none of them look: `bun run dev`. Boundary polygons were
|
||||
* missing there on five maps at once, this one included, and every built-tree gate stayed green
|
||||
* throughout, because the thing that differs is how the leaf RESOLVES its asset base, not what
|
||||
* the build produces.
|
||||
*
|
||||
* MEASURED against the dev server, headless, with curl-equivalent fetches:
|
||||
* /assets/boundaries/world-adm0.geojson 404
|
||||
* /dx-maps/assets/boundaries/world-adm0.geojson 200, 1,911,385 bytes
|
||||
* The file is there; the base is wrong. A map with no boundaries draws no countries at all, and
|
||||
* a country is half of what this map is for.
|
||||
*
|
||||
* NO BROWSER, deliberately. Browsers on this box are rationed to one agent, and none is needed:
|
||||
* the question is whether a URL answers, which fetch settles. That also makes this gate cheap
|
||||
* enough to run on every change rather than when a slot happens to be free.
|
||||
*
|
||||
* ⚠ IT ASSERTS BYTES, NOT JUST STATUS. A dev server that answers 200 with an HTML fallback for a
|
||||
* missing .geojson is the SPA-shell trap this repo has already been caught by twice, so each
|
||||
* asset must come back as the KIND of thing it claims to be.
|
||||
*
|
||||
* ⚠ WHAT THIS DOES *NOT* PROVE, said plainly so nobody reads a green line as the all-clear.
|
||||
* It proves the assets ARE SERVED at /<tree>/assets in dev. It does NOT prove the leaf ASKS for
|
||||
* that path — and the reported defect is exactly that: resolveAssetsBase falls back to './assets'
|
||||
* in dev, so the map fetches /assets/… and gets a 404 while the file sits at /<tree>/assets/…
|
||||
* untouched. That fix is in library/ and belongs to studio. This gate is the other half: once the
|
||||
* base is right, it says whether there is anything at the end of it, and it will keep saying so
|
||||
* when a table is renamed or a boundary file is dropped from the staged pool.
|
||||
*
|
||||
* Usage: node internal/check-gospel-dev-assets.mjs [--base http://127.0.0.1:5399]
|
||||
* Start the server yourself first: bun x vite --port 5399 --strictPort
|
||||
*/
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const argBase = process.argv.indexOf('--base')
|
||||
const BASE = (argBase > -1 && process.argv[argBase + 1]) || process.env.DEV_BASE || 'http://127.0.0.1:5399'
|
||||
|
||||
/* Exactly what resources-map.vue fetches, by the same paths, so this cannot drift into testing
|
||||
files the map does not actually want. */
|
||||
const ASSETS = [
|
||||
{ path: 'boundaries/world-adm0.geojson', kind: 'json', min: 100000 },
|
||||
{ path: 'tables/people-group-pins.json', kind: 'json', min: 100000 },
|
||||
{ path: 'tables/gospel-by-country.json', kind: 'json', min: 1000 },
|
||||
{ path: 'tables/doxa-people-groups.json', kind: 'json', min: 1000 },
|
||||
{ path: 'tables/country-languages.json', kind: 'json', min: 1000 },
|
||||
]
|
||||
|
||||
let reachable = false
|
||||
try {
|
||||
const r = await fetch(`${BASE}/${TREE_NAME}/assets/${ASSETS[0].path}`, { signal: AbortSignal.timeout(20000) })
|
||||
reachable = r.ok
|
||||
} catch { /* precondition below */ }
|
||||
if (!reachable) {
|
||||
console.log(`DEV-ASSETS: SKIP — no dev server answering at ${BASE}.`)
|
||||
console.log(' Start one on your own port first: bun x vite --port 5399 --strictPort')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const fails = []
|
||||
for (const a of ASSETS) {
|
||||
const url = `${BASE}/${TREE_NAME}/assets/${a.path}`
|
||||
let res, body
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(60000) })
|
||||
body = await res.text()
|
||||
} catch (e) {
|
||||
fails.push(`${a.path} — ${String(e.message).slice(0, 60)}`)
|
||||
continue
|
||||
}
|
||||
if (!res.ok) { fails.push(`${a.path} — HTTP ${res.status}`); continue }
|
||||
/* The shell trap: 200 with an HTML page instead of the asset. */
|
||||
if (/^\s*<!doctype html|^\s*<html/i.test(body)) {
|
||||
fails.push(`${a.path} — 200 but the body is HTML, not ${a.kind}; the dev server fell back to a page`)
|
||||
continue
|
||||
}
|
||||
if (body.length < a.min) {
|
||||
fails.push(`${a.path} — 200 but only ${body.length} bytes, expected at least ${a.min}`)
|
||||
continue
|
||||
}
|
||||
try { JSON.parse(body) } catch { fails.push(`${a.path} — 200 but does not parse as JSON`); continue }
|
||||
console.log(` ok ${a.path.padEnd(38)} ${String(body.length).padStart(9)} bytes`)
|
||||
}
|
||||
|
||||
/* THE CONTROL. If a bogus path also answers 200, every line above means nothing — the server is
|
||||
answering everything and the gate cannot tell a served asset from a missing one. */
|
||||
let controlOk = false
|
||||
try {
|
||||
const c = await fetch(`${BASE}/${TREE_NAME}/assets/tables/zzz-not-a-real-table.json`, { signal: AbortSignal.timeout(20000) })
|
||||
controlOk = !c.ok
|
||||
} catch { controlOk = true }
|
||||
if (!controlOk) {
|
||||
console.error('DEV-ASSETS: NOT MEASURED — a bogus asset path also answers 200, so this server')
|
||||
console.error(' cannot distinguish a present asset from a missing one.')
|
||||
process.exitCode = 2
|
||||
} else if (fails.length) {
|
||||
console.error(`DEV-ASSETS: FAIL — ${fails.length} asset(s) the map fetches are not usable in the dev server:`)
|
||||
for (const f of fails) console.error(` ${f}`)
|
||||
console.error(` The map resolves its base through resolveAssetsBase; in dev that must land on`)
|
||||
console.error(` /${TREE_NAME}/assets, not /assets. A map with no boundaries draws no countries.`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`DEV-ASSETS: PASS — all ${ASSETS.length} assets served under /${TREE_NAME}/assets, and a bogus path 404s.`)
|
||||
}
|
||||
60
internal/check-gospel-tables-staged.mjs
Normal file
60
internal/check-gospel-tables-staged.mjs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-gospel-tables-staged — every table the gospel map fetches is actually in the shipped tree.
|
||||
*
|
||||
* WHY THIS IS NOT check-dev-assets. That gate owns the question "does the asset pool resolve at
|
||||
* every prefix a dev page can produce", across all seven map bundles, and it does it better than
|
||||
* the gospel-only version this replaces — it tests the /__leaf/<bundle>/<profile> prefix, which
|
||||
* is the one that actually broke, and which mine never looked at. One implementation.
|
||||
*
|
||||
* What it samples, though, is the POOL: world-adm0 and country-connectivity, the files common to
|
||||
* every map. It cannot know that this leaf also fetches four gospel tables by name, so a table
|
||||
* that stopped being staged would leave that gate green and this map fetching a 404. That is the
|
||||
* only gap left once the duplicate is gone, so this covers exactly it and nothing else.
|
||||
*
|
||||
* Read from the SOURCE OF TRUTH rather than a second list: the fetch() calls in resources-map.vue
|
||||
* are parsed out of the file, so adding a table to the leaf adds it here with no edit. A list
|
||||
* maintained by hand is the field-list-goes-stale bug, which this bundle has now produced three
|
||||
* times in one day.
|
||||
*
|
||||
* Headless, filesystem only. No server, no browser.
|
||||
*/
|
||||
import { readFileSync, existsSync, statSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const LEAF = resolve(ROOT, 'plugin-designs/gospel-resources/resources-map.vue')
|
||||
const POOL = resolve(ROOT, 'public', TREE_NAME, 'assets')
|
||||
|
||||
const src = readFileSync(LEAF, 'utf8')
|
||||
/* `${assetsBase.value}/tables/x.json` and `${assetsBase.value}/boundaries/y.geojson` */
|
||||
const wanted = [...new Set(
|
||||
[...src.matchAll(/\$\{assetsBase\.value\}\/([A-Za-z0-9_\-./]+)/g)].map((m) => m[1]),
|
||||
)]
|
||||
|
||||
if (!wanted.length) {
|
||||
console.error('GOSPEL-TABLES-STAGED: NOT MEASURED — no asset fetches found in resources-map.vue.')
|
||||
console.error(' The leaf changed shape; this check would pass vacuously, which is worse than failing.')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const missing = []
|
||||
for (const rel of wanted) {
|
||||
const p = resolve(POOL, rel)
|
||||
if (!existsSync(p)) { missing.push(`${rel} — not staged`); continue }
|
||||
const bytes = statSync(p).size
|
||||
/* A staged-but-empty file is the same outage with a friendlier face. */
|
||||
if (bytes < 200) { missing.push(`${rel} — staged but only ${bytes} bytes`); continue }
|
||||
console.log(` ok ${rel.padEnd(42)} ${String(bytes).padStart(9)} bytes`)
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
console.error(`GOSPEL-TABLES-STAGED: FAIL — ${missing.length} of ${wanted.length} asset(s) the map fetches are not in the tree:`)
|
||||
for (const m of missing) console.error(` ${m}`)
|
||||
console.error(` Looked under public/${TREE_NAME}/assets. Run \`bun run tree\` after building this bundle.`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`GOSPEL-TABLES-STAGED: PASS — all ${wanted.length} asset(s) resources-map.vue fetches are staged.`)
|
||||
}
|
||||
|
|
@ -40,7 +40,6 @@
|
|||
"check:csv-shape": "node internal/check-csv-export-shape.mjs",
|
||||
"check:csv-shape:self-test": "node internal/check-csv-export-shape.mjs --self-test",
|
||||
"check:deck-click": "node internal/check-deck-click.mjs",
|
||||
"check:dev-assets": "node internal/check-gospel-dev-assets.mjs",
|
||||
"check:dep-names": "node internal/check-dependency-names.mjs",
|
||||
"check:descriptions": "node internal/check-profile-descriptions.mjs",
|
||||
"check:dev-assets": "node internal/check-dev-assets.mjs",
|
||||
|
|
@ -81,6 +80,7 @@
|
|||
"check:gallery-embed:live-cors": "node internal/check-research-gallery-embed.mjs --live-cors",
|
||||
"check:gallery-embed:maplibre": "node internal/check-research-gallery-embed.mjs --provider maplibre",
|
||||
"check:gospel-all": "node internal/check-gospel-all.mjs",
|
||||
"check:gospel-tables-staged": "node internal/check-gospel-tables-staged.mjs",
|
||||
"check:heartbeat": "node internal/check-heartbeat-threshold.mjs",
|
||||
"check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test",
|
||||
"check:host-locale-contract": "node internal/check-host-locale-contract.mjs",
|
||||
|
|
|
|||
Loading…
Reference in a new issue