Two problems, one change. LOAD. Seven agents each running a wave put 7+ chrome-headless renders on one box; measured today it reached load 47 with 8GB of swap, and a browser gate that is swapping measures the swap, not the product. The wave runner now holds browser steps while 1-min load is at or above LOAD_MAX (12) and takes a coarse flock before each one, so they queue instead of dogpiling. Thresholds, cadence and the 20-minute cap are bundler-3's wait-for-quiet.sh verbatim, and BROWSER_GATE_LOCK selects the rendezvous, so every agent lands on one lock whichever entry point they use. Static gates are untouched: in gates:post that is 38 free, 4 queued. The load wait is taken ONCE per wave, not once per gate. Per-step it multiplied the same hold by the number of browser gates — an hour of a wave doing nothing, which is how a load fix becomes a reason to stop running gates. The flock is what serializes; the wait only decides when to join the queue. Browser steps are identified from the gate FILE (a playwright import), not a hand-kept list that goes stale the first time someone adds a gate — the one missing from such a list is exactly the one that rejoins the dogpile. `verify` is caught too: it imports no playwright itself but spawns three gates that render, and it already marks them browser:true, so that declaration is honoured rather than duplicated. COVERAGE. Yesterday's guard made check:provider-switch and check:packet-browser SKIP in every wave, because their default ports (4173, 4898) belong to other worktrees on this machine. Skipping was honest but a gate that skips every time is a gate nobody runs, so on a mismatch they now start a short-lived server over THIS checkout and measure that instead — the point was never the port, it was being able to prove which build answered. MEASURED: a served tree whose manifest differs by one key but has all the same pages -> "serves a DIFFERENT build", then "serving this checkout on :28384 instead", then a real verdict (16 -> 5 mapbox requests). packet-browser likewise, once it re-navigates to the new origin — a dynamic import across origins is refused by a plain static server, which cost one failed run to learn. gates:post 40/42, the only skips being the two that genuinely have no input (packets-all, zone-census); it was 4 before this. The lock path default is product-named. The shared file several shells export carries an internal code name, and check:no-codename refuses to let a repo we hand over carry one — it caught this commit's first draft.
157 lines
8.5 KiB
JavaScript
157 lines
8.5 KiB
JavaScript
// check-provider-switch — ?provider=maplibre must actually SWAP THE ENGINE.
|
|
//
|
|
// The bug this gates (BUG-UPG100-IGNORES-THE-PROVIDER-SWITCH): my-upg-100-list
|
|
// builds its map RAW with `new gl.Map` instead of going through useMapInstance, so
|
|
// the page-level switch — which resolves inside that composable — could not reach
|
|
// it. MEASURED at the time: five sibling leaves swapped cleanly while this one
|
|
// stayed on Mapbox and made 19 requests to api.mapbox.com.
|
|
//
|
|
// ★ IT COUNTS NETWORK REQUESTS BY HOST, not globals. `window.mapboxgl` can be
|
|
// present because a page preloads it, and a map can still be DRAWN by maplibre —
|
|
// or the reverse. What the switch is for is not loading a proprietary engine and
|
|
// not calling its tile servers, and only the request log shows that.
|
|
//
|
|
// Usage: node internal/check-provider-switch.mjs <leafUrl>
|
|
import { dirname, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { chromium } from 'playwright'
|
|
import { servesThisTree, serveThisTree } from './_serves-this-tree.mjs'
|
|
|
|
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
|
// ⚠ NOT named `URL` — that shadows the global URL constructor, so every
|
|
// `new URL(request.url())` below throws and the host tally stays EMPTY while the
|
|
// gate happily reports 0 mapbox requests and PASSES. Measured: it did exactly
|
|
// that, hiding 4 real requests to events.mapbox.com.
|
|
// ⚠ SKIP CLEANLY, IN THE FORM THE WAVE RUNNER RECOGNISES. This exited 2 on a missing
|
|
// argument, so it could never join a wave: run-parallel grades on the exit code and a
|
|
// bare `usage:` line would have turned every server-less build RED. It also had no
|
|
// default, so it only ever ran when somebody typed a URL — the same present-but-never-
|
|
// executed shape check:packets-all had. Default to the upg-100 leaf (the profile this
|
|
// gate was written for) and SKIP with a named verdict when nothing is served there.
|
|
const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
|
|
+ '/doxa-maps/my-upg-100-list/upg-100-list/index.html'
|
|
let LEAF = process.argv[2] || DEFAULT_LEAF
|
|
let OWN_SERVER = null
|
|
{
|
|
let reachable = false
|
|
try {
|
|
const res = await fetch(LEAF, { signal: AbortSignal.timeout(4000) })
|
|
reachable = res.ok
|
|
} catch { /* nothing served — that is a precondition, not a product failure */ }
|
|
if (!reachable) {
|
|
console.log(`PROVIDER-SWITCH: SKIP — nothing served at ${LEAF}.`)
|
|
console.log(' Serve the built tree and re-run, e.g.:')
|
|
console.log(' python3 -m http.server 4173 --directory public (or set BASE_URL)')
|
|
process.exit(0)
|
|
}
|
|
|
|
// ⚠ REACHABLE IS NOT THE SAME QUESTION AS MINE. Several worktrees of this repo run on one
|
|
// machine and the default port belongs to whichever started first — measured 2026-09-24:
|
|
// :4173 was another worktree's server, and an hour of probing reported on a build nobody
|
|
// here had changed. This gate's whole verdict is a request tally from a page; a tally taken
|
|
// from someone else's page is a confident statement about the wrong code.
|
|
// A mismatch is a PRECONDITION, not a product failure, so it SKIPs (exit 0) like the
|
|
// unreachable case above — but it says which, so nobody reads it as a pass.
|
|
// The helper wants the ORIGIN that serves public/, not the leaf page — passing the page URL
|
|
// makes it look for the manifest under the page's own path and report "nothing serving it",
|
|
// which would SKIP every run for the wrong reason. (Measured: it did, on both servers.)
|
|
const tree = await servesThisTree(new URL(LEAF).origin, REPO)
|
|
// Only a genuine MISMATCH skips. "No manifest to compare" (an unbuilt checkout) is a
|
|
// different precondition and the reachable check above already speaks for it — turning
|
|
// that into a skip here would silence the gate on exactly the builds it should run on.
|
|
if (/DIFFERENT build/.test(tree.why)) {
|
|
// Don't skip if we can simply serve our own tree — a gate that skips on every wave is a
|
|
// gate nobody runs, and on this box the default port belongs to another worktree.
|
|
const own = await serveThisTree(resolve(REPO, 'public'))
|
|
if (!own) {
|
|
console.log('PROVIDER-SWITCH: SKIP — the server is not serving THIS build, and I could not start one.')
|
|
console.log(` ${tree.why}`)
|
|
console.log(' Serve your own tree and re-run: bun run serve (then BASE_URL=http://127.0.0.1:PORT)')
|
|
process.exit(0)
|
|
}
|
|
OWN_SERVER = own
|
|
LEAF = own.origin + new URL(LEAF).pathname
|
|
console.log(` · ${tree.why}`)
|
|
console.log(` · serving this checkout on ${own.origin} instead, so the tally is about THIS build`)
|
|
}
|
|
}
|
|
|
|
async function run(provider) {
|
|
const b = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] })
|
|
const pg = await b.newPage({ viewport: { width: 1280, height: 900 } })
|
|
const hosts = new Map()
|
|
let unparsed = 0
|
|
pg.on('request', (r) => {
|
|
try {
|
|
const h = new URL(r.url()).hostname
|
|
if (!h) { unparsed += 1; return }
|
|
hosts.set(h, (hosts.get(h) || 0) + 1)
|
|
} catch { unparsed += 1 }
|
|
})
|
|
const errs = []
|
|
pg.on('pageerror', (e) => errs.push(String(e).slice(0, 120)))
|
|
const url = provider ? `${LEAF}${LEAF.includes('?') ? '&' : '?'}provider=${provider}` : LEAF
|
|
await pg.goto(url, { waitUntil: 'load', timeout: 45000 })
|
|
await pg.waitForTimeout(12000)
|
|
const globals = await pg.evaluate(() => ({
|
|
mapbox: typeof window.mapboxgl !== 'undefined',
|
|
maplibre: typeof window.maplibregl !== 'undefined',
|
|
}))
|
|
await b.close()
|
|
const count = (needle) => [...hosts.entries()]
|
|
.filter(([h]) => h.includes(needle)).reduce((a, [, n]) => a + n, 0)
|
|
// Report what could NOT be parsed rather than swallowing it: a silently empty
|
|
// tally is how this gate first passed while measuring nothing at all.
|
|
return { mapboxReq: count('mapbox.com'), globals, errs, unparsed, total: [...hosts.values()].reduce((a, n) => a + n, 0) }
|
|
}
|
|
|
|
const def = await run(null)
|
|
const ml = await run('maplibre')
|
|
|
|
// DERIVED from the leaf's own HTML, not a hardcoded number: count the distinct
|
|
// api.mapbox.com references the generated page carries, then allow a little slack
|
|
// because a stylesheet can be fetched more than once (the upg-100 leaf resolves 4
|
|
// distinct urls but issues 5 requests — one css is re-fetched). Hardcoding 5 would
|
|
// pass a leaf that legitimately preloads 4 and leaks 1 runtime call.
|
|
const pageHtml = await (await fetch(LEAF)).text()
|
|
const declared = new Set((pageHtml.match(/https:\/\/api\.mapbox\.com[^"')]+/g) || []))
|
|
const PRELOAD_ALLOWANCE = Number(process.env.PROVIDER_PRELOADS ?? (declared.size + 1))
|
|
|
|
console.log(` page preloads : ${declared.size} distinct api.mapbox.com url(s) in the leaf HTML`)
|
|
console.log(` default : ${def.mapboxReq} mapbox / ${def.total} total request(s) `
|
|
+ `[mapboxgl=${def.globals.mapbox} maplibregl=${def.globals.maplibre}]`)
|
|
console.log(` ?provider=maplibre: ${ml.mapboxReq} mapbox / ${ml.total} total request(s) `
|
|
+ `[mapboxgl=${ml.globals.mapbox} maplibregl=${ml.globals.maplibre}]`)
|
|
|
|
const fails = []
|
|
if (!ml.globals.maplibre) {
|
|
fails.push('?provider=maplibre did not load the maplibre engine at all')
|
|
}
|
|
// The RESIDUAL SET is the leaf page's own <link>/<script> preloads — mapbox-gl and
|
|
// the geocoder, referenced in the generated index.html header. Those fire before any
|
|
// Vue code runs, so no runtime switch can suppress them; removing them is a separate
|
|
// concern (the page template), not this bug. What the switch must achieve is that NO
|
|
// RUNTIME request goes to Mapbox: measured 19 -> 5 on the upg-100 leaf, and all 5 are
|
|
// in the HTML. Asserting "fewer than default" would pass a half-fix at 18.
|
|
if (ml.mapboxReq > PRELOAD_ALLOWANCE) {
|
|
fails.push(`?provider=maplibre made ${ml.mapboxReq} mapbox request(s), more than the `
|
|
+ `${PRELOAD_ALLOWANCE} page preloads — ${ml.mapboxReq - PRELOAD_ALLOWANCE} are RUNTIME `
|
|
+ 'calls the switch failed to redirect')
|
|
}
|
|
if (def.mapboxReq <= PRELOAD_ALLOWANCE) {
|
|
fails.push(`the default run made only ${def.mapboxReq} mapbox request(s) — the map is probably `
|
|
+ 'not rendering, so this comparison proves nothing')
|
|
}
|
|
if (!def.total) fails.push('no requests were recorded at all — the tally is broken, not the page')
|
|
if (ml.errs.length) fails.push(`${ml.errs.length} page error(s) under maplibre: ${ml.errs[0]}`)
|
|
|
|
OWN_SERVER?.stop()
|
|
|
|
if (fails.length) {
|
|
console.error(`\nPROVIDER-SWITCH: FAIL — ${fails.length} problem(s)`)
|
|
for (const f of fails) console.error(' - ' + f)
|
|
process.exit(1)
|
|
}
|
|
console.log(`\nPROVIDER-SWITCH: PASS — maplibre loads and mapbox traffic drops `
|
|
+ `${def.mapboxReq} -> ${ml.mapboxReq}.`)
|