The geocoder check failed a contract it should have been reading
rega: "0 search bars on both engines". The product is right and the check was wrong, twice, and both mistakes are mine. UNDER MAPLIBRE, NO SEARCH BAR IS THE CONTRACT. research-map.vue says `hasGeocoder = mapProvider === 'mapbox'`, and research-map.instances.json says it in words: "Under maplibre the search bar is deliberately ABSENT — GeocoderComponent's maplibre path still geocodes through Mapbox, so shipping it would bill the very key this instance exists to avoid." The previous revision had just been taught that a bar-less leaf is a hollow pass, and applied that lesson to the one engine where absence is the point. An absent bar is in fact the STRONGEST form of this gate's own assertion: a search that cannot be typed into cannot send anyone's IP to Mapbox. The rule is conditional now rather than hardcoded, because a keyless geocoder may ship one day and this gate should not need rewriting when it does: no bar under maplibre is recorded and passes; a bar under maplibre must work and still make zero Mapbox requests. Either way the zero is what is asserted. THE MAPBOX RUN WAS NEVER GIVEN A TOKEN, so it could not have rendered a bar either. The leaf bakes BAKED_TOKEN = "" and nothing else supplies one, so mapbox-gl cannot construct a Map, `map` stays null, and the bar's `v-if="appReady && map && hasGeocoder"` never fires. Note the mechanism, because it is not the obvious one: an explicit ?provider=mapbox does NOT fall back to maplibre — explicitMapProvider reads the URL first — so the engine is Mapbox, it simply has no key. A gate that fails a product for a key the gate did not provide is measuring its own environment. It takes MAPBOX_TOKEN from the env, or `token` from the build config — the same ladder check:experimental-shelf uses — and injects it as window.MAP_TOKEN the way check:auto-engine and check:a11y-button-names do. Only for the mapbox run: a token present during the maplibre run is the very variable this gate holds at zero. With no token the mapbox half is NOT MEASURED, reported through the SKIP line run-parallel reads, so the wave prints it as unmeasured rather than counting it as a pass. The maplibre half is measured either way, keyless being the whole point of that engine. Also separates "the bar was removed" from "the map never constructed" by probing for a canvas, since those are different bugs with one symptom — and the second is how this check misread itself. The token is never printed, only its presence.
This commit is contained in:
parent
c377e19925
commit
f42daf2802
1 changed files with 94 additions and 22 deletions
|
|
@ -25,13 +25,45 @@
|
|||
* look identical: the bars appear, suggestions work, nothing errors. Only the request log tells the
|
||||
* truth, which is why this counts requests rather than looking at the page.
|
||||
*
|
||||
* ASSERTIONS, on the built tree:
|
||||
* maplibre ZERO requests to api.mapbox.com.
|
||||
* both whatever search bars this leaf renders still produce suggestions — a zero bought by
|
||||
* breaking search is not a fix. A leaf with NO bar now FAILS (it used to pass hollow);
|
||||
* the default leaf is doxa-research-map/research-map, which mounts one (--leaf to change).
|
||||
* ★ CORRECTED 2026-09-26, after rega reported "0 search bars on both engines" and it was the CHECK
|
||||
* that was wrong, not the product. Two separate mistakes, both of them mine:
|
||||
*
|
||||
* Exit: 0 pass or SKIP (no built tree, no browser, no data) · 1 fail.
|
||||
* 1. UNDER MAPLIBRE, NO SEARCH BAR IS THE CONTRACT — not a hollow pass and not a failure.
|
||||
* research-map.vue: `hasGeocoder = mapProvider === 'mapbox'`, and research-map.instances.json
|
||||
* says it in words: "Under maplibre the search bar is deliberately ABSENT — GeocoderComponent's
|
||||
* maplibre path still geocodes through Mapbox, so shipping it would bill the very key this
|
||||
* instance exists to avoid". The previous revision had just been taught that a bar-less leaf is
|
||||
* a hollow pass, and applied that lesson to the one engine where ABSENCE IS THE POINT. An
|
||||
* absent bar is in fact the STRONGEST form of this gate's own assertion: a search that cannot
|
||||
* be typed into cannot send anyone's IP to Mapbox.
|
||||
* So the rule is now conditional rather than hardcoded, because a keyless geocoder may ship
|
||||
* one day and this gate should not have to be rewritten when it does: if maplibre renders NO
|
||||
* bar, that is recorded and passes; if it renders one, it must WORK and still make zero
|
||||
* Mapbox requests. Either way the zero is what is asserted.
|
||||
*
|
||||
* 2. THE MAPBOX RUN WAS NEVER GIVEN A TOKEN, so it could not have rendered a bar either. The leaf
|
||||
* bakes BAKED_TOKEN = "" and nothing else supplies one, so mapbox-gl cannot construct a Map,
|
||||
* `map` stays null, and the bar's `v-if="appReady && map && hasGeocoder"` never fires. Note the
|
||||
* mechanism: an explicit ?provider=mapbox does NOT fall back to maplibre (explicitMapProvider
|
||||
* reads the URL first) — the engine is Mapbox, it simply has no key. A gate that fails a
|
||||
* product for a key the gate did not provide is measuring its own environment.
|
||||
* It now takes MAPBOX_TOKEN from the env (or vite.doxa-maps-build-config.json `token`, the
|
||||
* same ladder check:experimental-shelf uses) and injects it as window.MAP_TOKEN before
|
||||
* navigation, the way check:auto-engine and check:a11y-button-names do. With no token the
|
||||
* mapbox half is NOT MEASURED — reported as a SKIP line so run-parallel prints ⊘ rather than
|
||||
* counting it as a pass, because an always-taken skip is indistinguishable from an unwired
|
||||
* gate unless the runner says so.
|
||||
*
|
||||
* ASSERTIONS, on the built tree:
|
||||
* maplibre ZERO requests to api.mapbox.com. (Measured with or without a token.)
|
||||
* If a bar is present it must produce suggestions; if it is absent, that is the
|
||||
* documented contract and is recorded as such.
|
||||
* mapbox the bar is PRESENT and produces suggestions. Requires a token; without one this
|
||||
* half is skipped, never failed. Mapbox traffic is expected here and not asserted on.
|
||||
*
|
||||
* NEVER PRINTS THE TOKEN. Only its presence is reported.
|
||||
*
|
||||
* Exit: 0 pass or SKIP (no built tree, no browser, no data, no token) · 1 fail.
|
||||
*/
|
||||
import { createServer } from 'node:http'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
|
@ -50,6 +82,12 @@ let chromium
|
|||
try { ({ chromium } = await import('playwright')) } catch { skip('playwright is not installed.') }
|
||||
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
||||
// The token ladder every other browser gate here uses: env first, then the build config. Presence
|
||||
// is reported; the value never is.
|
||||
const TOKEN = (() => {
|
||||
if (process.env.MAPBOX_TOKEN) return process.env.MAPBOX_TOKEN
|
||||
try { return JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8')).token || '' } catch { return '' }
|
||||
})()
|
||||
// ★ A LEAF THAT MOUNTS THE SEARCH BAR, or the suggestion half proves nothing (2026-09-26: the
|
||||
// research-dashboard leaf renders no bar, so every run passed with "search bars: 0" — a hollow PASS).
|
||||
// Default: the research map, which mounts the library GeocoderComponent. --leaf <bundle>/<profile>.
|
||||
|
|
@ -78,10 +116,17 @@ const browser = await chromium.launch({ args: ['--enable-unsafe-swiftshader', '-
|
|||
const fails = []
|
||||
const lines = []
|
||||
try {
|
||||
for (const engine of ['maplibre', 'mapbox']) {
|
||||
// The mapbox half needs a key. Measure maplibre either way — it is the headline claim and it is
|
||||
// fully measurable keyless, which is the whole point of the engine.
|
||||
const engines = TOKEN ? ['maplibre', 'mapbox'] : ['maplibre']
|
||||
for (const engine of engines) {
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
|
||||
const mapboxCalls = []
|
||||
page.on('request', (r) => { const u = r.url(); if (u.includes('api.mapbox.com')) mapboxCalls.push(u) })
|
||||
// ★ ONLY under mapbox. Injecting a token into the maplibre run would change what is being
|
||||
// measured: resolveMapProvider's `auto` rung reads a token as "use Mapbox", and even with an
|
||||
// explicit ?provider=maplibre a present token is a variable this gate exists to hold at zero.
|
||||
if (engine === 'mapbox') await page.addInitScript((tk) => { window.MAP_TOKEN = tk }, TOKEN)
|
||||
await page.goto(`${LEAF}?provider=${engine}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
|
||||
await page.waitForTimeout(20000)
|
||||
|
||||
|
|
@ -96,8 +141,11 @@ try {
|
|||
if (el.shadowRoot) walk(el.shadowRoot)
|
||||
}
|
||||
}
|
||||
walk(document)
|
||||
if (!hits.length) return { bars: 0, suggestions: 0 }
|
||||
// Did an engine actually construct a map? A canvas is the one artefact both engines share,
|
||||
// and it tells "the bar was removed" apart from "there was never a map to hang it on".
|
||||
let mapReady = false
|
||||
for (const root of seen) { if (root.querySelector && root.querySelector('canvas.mapboxgl-canvas, canvas.maplibregl-canvas, .mapboxgl-map canvas, .maplibregl-map canvas')) { mapReady = true; break } }
|
||||
if (!hits.length) return { bars: 0, suggestions: 0, mapReady }
|
||||
const input = hits[0]
|
||||
input.focus()
|
||||
input.value = 'Afar'
|
||||
|
|
@ -106,23 +154,40 @@ try {
|
|||
await sleep(500)
|
||||
const root = input.getRootNode()
|
||||
const n = root.querySelectorAll('.mapboxgl-ctrl-geocoder .suggestions li, .maplibregl-ctrl-geocoder .suggestions li, .suggestions li').length
|
||||
if (n) return { bars: hits.length, suggestions: n }
|
||||
if (n) return { bars: hits.length, suggestions: n, mapReady }
|
||||
}
|
||||
return { bars: hits.length, suggestions: 0 }
|
||||
return { bars: hits.length, suggestions: 0, mapReady }
|
||||
})
|
||||
await page.close()
|
||||
|
||||
lines.push(` · ${engine.padEnd(9)} api.mapbox.com requests: ${mapboxCalls.length} · search bars: ${search.bars} · suggestions for "Afar": ${search.suggestions}`)
|
||||
lines.push(` · ${engine.padEnd(9)} api.mapbox.com requests: ${mapboxCalls.length} · search bars: ${search.bars} · suggestions for "Afar": ${search.suggestions}${engine === 'mapbox' ? ' · token: supplied' : ''}`)
|
||||
|
||||
if (engine === 'maplibre' && mapboxCalls.length) {
|
||||
fails.push(`under maplibre the dashboard made ${mapboxCalls.length} request(s) to api.mapbox.com:`)
|
||||
for (const u of [...new Set(mapboxCalls)].slice(0, 8)) fails.push(` ${u.slice(0, 150)}`)
|
||||
}
|
||||
// NO BAR IS A FAILURE, not a pass: this gate exists to prove search still works with zero Mapbox calls.
|
||||
if (!search.bars) {
|
||||
fails.push(`under ${engine} the leaf rendered NO search bar, so search was never exercised — point --leaf at a leaf that mounts one.`)
|
||||
} else if (!search.suggestions) {
|
||||
fails.push(`under ${engine} the search bar rendered but typing produced no suggestions — zero Mapbox traffic bought by a broken search is not a fix.`)
|
||||
if (engine === 'maplibre') {
|
||||
// THE HEADLINE. One request sends the visitor's IP to Mapbox and breaks the keyless promise,
|
||||
// whatever the page looks like afterwards.
|
||||
if (mapboxCalls.length) {
|
||||
fails.push(`under maplibre the leaf made ${mapboxCalls.length} request(s) to api.mapbox.com:`)
|
||||
for (const u of [...new Set(mapboxCalls)].slice(0, 8)) fails.push(` ${u.slice(0, 150)}`)
|
||||
}
|
||||
// A bar is OPTIONAL here and its absence is the documented contract (see the header). What is
|
||||
// NOT optional is that a bar, if present, works — otherwise a future keyless geocoder could
|
||||
// ship broken and this gate would read it as a clean zero.
|
||||
if (!search.bars) {
|
||||
lines.push(' (no search bar under maplibre — the documented contract: research-map.vue hasGeocoder = provider === \'mapbox\', because GeocoderComponent\'s maplibre path still geocodes through Mapbox. An absent bar is the strongest form of the zero above.)')
|
||||
} else if (!search.suggestions) {
|
||||
fails.push('under maplibre a search bar rendered but typing produced no suggestions — if a keyless geocoder now ships it has to work, not merely be quiet.')
|
||||
}
|
||||
} else {
|
||||
// MAPBOX: the bar is the feature this profile was built around, and it must still work.
|
||||
// `mapReady` separates "the bar was removed" from "the map never constructed", which are
|
||||
// different bugs with the same symptom — and the second one is how this gate misread itself.
|
||||
if (!search.bars) {
|
||||
fails.push(search.mapReady
|
||||
? 'under mapbox the map constructed but rendered NO search bar — hasGeocoder or the GeocoderComponent mount is broken.'
|
||||
: 'under mapbox NO map constructed (so the bar\'s v-if could never fire). The token was supplied, so this is the product or the leaf, not a missing key.')
|
||||
} else if (!search.suggestions) {
|
||||
fails.push('under mapbox the search bar rendered but typing produced no suggestions.')
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -138,4 +203,11 @@ if (fails.length) {
|
|||
for (const f of fails) console.log(` ✗ ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`${TAG}: PASS — MapLibre calls Mapbox zero times and still searches; Mapbox is unchanged.`)
|
||||
if (!TOKEN) {
|
||||
// Half-measured is not a pass. run-parallel reads a `TAG: SKIP` line and prints ⊘, so the wave
|
||||
// cannot report this as one of N gates that graded something. Exit 0: nothing FAILED.
|
||||
console.log(`${TAG}: SKIP — maplibre measured and clean; the mapbox half needs a key and none was supplied.`)
|
||||
console.log(' Set MAPBOX_TOKEN in the environment, or `token` in vite.doxa-maps-build-config.json, to measure it.')
|
||||
process.exit(0)
|
||||
}
|
||||
console.log(`${TAG}: PASS — MapLibre calls Mapbox zero times, and Mapbox still searches.`)
|
||||
|
|
|
|||
Loading…
Reference in a new issue