Some checks are pending
build-gate / build (push) Waiting to run
Checkpointing thirteen files of agent work that were sitting uncommitted while three agents wrote to a live tree the Driver was watching through hot reload. Uncommitted work is work at risk, and a tree being edited under an observer oscillates between states that look like regressions and are really just partial saves. The second capability-5 shell now mounts its tabs as real elements too, so no iframe remains in either shell. It was the last holdout: eight tabs, all iframe mode, each pointing at a hardcoded built-tree leaf that does not exist under the dev server. The facet rail keeps its shape when a filter narrows it. Selecting one place took the rail from sixty-six rows to twenty-one — forty-six rows deleted, none dimmed — so the row a user was about to click vanished under the cursor, which is what "the buttons went dead" actually was. Rows whose count falls to zero are now re-admitted at their original slot and depth, dimmed and inert, which also restores the component's own promise to lose nothing against the tree it replaced. Expanding a branch and filtering by it are now separate, visible affordances: the whole row filters, and expand is a real button with its own hit area and label. Rows that were never clickable are unmistakably inert rather than silently ignored. Three more facet axes reach the grid — affinity bloc, engagement status and primary language — each verified to share a field with an existing prop rather than being mapped on name resemblance. Two derived facets remain unmapped on purpose: they have no matching row field, and mapping them would filter confidently to the wrong rows.
153 lines
9 KiB
JavaScript
153 lines
9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* smoke.mjs — boot the built site in a real browser and assert it actually works.
|
|
*
|
|
* WHY THIS EXISTS
|
|
* Every other gate checks artefacts: that files exist, parse, and carry the right
|
|
* paths. All of them passed while the app was incapable of drawing a single map,
|
|
* because the shell had no way to obtain a Mapbox token on any host but this one.
|
|
* Nothing that inspects files can catch that. It only shows up when a browser runs
|
|
* the page.
|
|
*
|
|
* WHAT IT ASSERTS
|
|
* on the SHELL (/):
|
|
* 1. it renders (has a <title>),
|
|
* 2. it resolves a token from the endpoint the build writes,
|
|
* 3. no uncaught page errors,
|
|
* 4. no failed same-origin requests — this is what caught a 404 on every single
|
|
* page load, from a <script src> for a file that is deliberately never deployed.
|
|
* on a GENERATED PROFILE PAGE (picked from the manifest, never hardcoded):
|
|
* 5. a bundle imports and registers its custom element,
|
|
* 6. an element actually mounts with a profile-config,
|
|
* 7. its own #status node reports no complaint,
|
|
* 8. no uncaught errors and no failed same-origin requests there either.
|
|
*
|
|
* ★ 5 and 6 are asserted on the PROFILE PAGE ONLY, and that is deliberate (2026-09-22). They used
|
|
* to be asserted on the shell too, from when the root page embedded a live preview. The gallery is
|
|
* now a navigation CATALOG that embeds nothing, so those copies reported a product decision as a
|
|
* failure. The shell still REPORTS both counts in its log line, so a change of behaviour is
|
|
* visible; it just no longer fails on the intended one.
|
|
*
|
|
* It checks TWO surfaces, because they resolve a token by different routes and one
|
|
* passing says nothing about the other: the shell at / fetches the endpoint itself,
|
|
* while a generated profile page walks the six-rung ladder. A generated page is also
|
|
* what a partner iframes, so it is the surface an outside site actually depends on.
|
|
*
|
|
* Requests to Mapbox and the people-group API are IGNORED: this runs offline, and
|
|
* their failure says nothing about whether the site is correctly built.
|
|
*
|
|
* Usage: bun run smoke (serves public/ on a free port, then drives it)
|
|
*/
|
|
import { chromium } from 'playwright'
|
|
import { createServer } from 'node:http'
|
|
import { readFile, stat } from 'node:fs/promises'
|
|
import { join, extname, resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'public')
|
|
const TYPES = { '.html':'text/html', '.js':'text/javascript', '.mjs':'text/javascript',
|
|
'.json':'application/json', '.css':'text/css', '.webp':'image/webp', '.png':'image/png',
|
|
'.svg':'image/svg+xml', '.geojson':'application/json', '.csv':'text/csv' }
|
|
|
|
const server = createServer(async (req, res) => {
|
|
try {
|
|
let p = join(ROOT, decodeURIComponent(req.url.split('?')[0]))
|
|
if ((await stat(p)).isDirectory()) p = join(p, 'index.html')
|
|
res.writeHead(200, { 'content-type': TYPES[extname(p)] || 'application/octet-stream' })
|
|
res.end(await readFile(p))
|
|
} catch { res.writeHead(404).end('not found') }
|
|
})
|
|
await new Promise(r => server.listen(0, '127.0.0.1', r))
|
|
const BASE = `http://127.0.0.1:${server.address().port}`
|
|
|
|
const EXTERNAL = ['api.mapbox.com', 'events.mapbox.com', 'pray.doxa.life', 'esm.sh']
|
|
const browser = await chromium.launch()
|
|
const page = await browser.newPage()
|
|
const pageErrors = [], failed = []
|
|
page.on('pageerror', e => pageErrors.push(e.message.split('\n')[0]))
|
|
page.on('requestfailed', r => {
|
|
if (!EXTERNAL.some(h => r.url().includes(h))) failed.push(`${r.failure()?.errorText} ${r.url().replace(BASE, '')}`)
|
|
})
|
|
|
|
const fail = []
|
|
try {
|
|
await page.goto(BASE + '/', { waitUntil: 'networkidle', timeout: 60000 })
|
|
const title = await page.title()
|
|
if (!title.trim()) fail.push('shell rendered no <title>')
|
|
|
|
const token = await page.evaluate(async () => {
|
|
try { const r = await fetch('./api/maps/token.json'); return r.ok ? ((await r.json()).token || '') : `HTTP ${r.status}` }
|
|
catch (e) { return 'ERR ' + e.message }
|
|
})
|
|
if (!String(token).startsWith('pk.') && !String(token).startsWith('tk.'))
|
|
fail.push(`shell could not resolve a token from ./api/maps/token.json (got: ${token}) — every basemap would be blank`)
|
|
|
|
// ── THE SHELL IS A CATALOG, NOT A HOST — these two counts are REPORTED, NOT ASSERTED ────────
|
|
//
|
|
// ★ CORRECTED 2026-09-22. This block used to fail when the shell mounted no element. That was
|
|
// right when the root page embedded a live preview; it is wrong now. It was first relaxed when
|
|
// the root was the GALLERY (a capability-sorted NAVIGATION CATALOG whose four "profile-config"
|
|
// occurrences were all escaped snippet text), and it stays relaxed now that the root is the
|
|
// PLUGIN STUDIO, for the stronger reason spelled out below: <plugin-studio> mounts its panes ON
|
|
// ACTIVATION, and it carries no profile-config attribute of its own here, so a load-time count of
|
|
// [profile-config] is 0 BY DESIGN on this page. Asserting it would fail the build on correct
|
|
// behaviour — the classic case of a gate measuring the wrong surface.
|
|
//
|
|
// THE COVERAGE IS NOT LOST, which is the only reason this is safe to relax: the generated-profile
|
|
// block below asserts the SAME TWO THINGS ("mounted no element" / "imported no bundle that
|
|
// registered its element") against a real leaf page picked from the manifest — and that surface
|
|
// passes (mounted=1). 41 of the built leaf pages mount with a profile-config at load (corrected
|
|
// 2026-09-22 — an earlier note said 21, which came from sampling only the first 30 leaves). So these
|
|
// were duplicate assertions aimed at the one surface that by design satisfies neither.
|
|
//
|
|
// WHY NOT RE-AIM THEM AT plugin-studio INSTEAD (my first instinct, and it was wrong): the cap-5
|
|
// studio leaf has exactly ONE [profile-config] element at load — its own <plugin-studio> host —
|
|
// and ZERO nested elements or live iframes. It mounts panes ON ACTIVATION, which is the lazy
|
|
// design that keeps the shell fast. A load-time assertion cannot see behaviour that starts on a
|
|
// click. Verified in a browser before changing this.
|
|
//
|
|
// KEPT AS OUTPUT so a regression is still visible: if the shell ever starts mounting again, or a
|
|
// future catalog page is expected to, these numbers say so in the log line below.
|
|
const registered = await page.evaluate(() =>
|
|
[...document.querySelectorAll('[profile-config]')].map(e => e.tagName.toLowerCase())
|
|
.filter(t => !!customElements.get(t)))
|
|
|
|
const mounted = await page.evaluate(() => document.querySelectorAll('[profile-config]').length)
|
|
|
|
if (pageErrors.length) fail.push(`uncaught page errors: ${pageErrors.slice(0, 3).join(' | ')}`)
|
|
if (failed.length) fail.push(`failed same-origin requests: ${failed.slice(0, 3).join(' | ')}`)
|
|
|
|
console.log(`[smoke] shell title=${JSON.stringify(title)} token=${String(token).slice(0, 6)}… registered=[${registered}] mounted=${mounted}`)
|
|
|
|
// A generated profile page — the surface a partner iframes. Picked from the
|
|
// manifest rather than hardcoded, so renaming or removing a bundle cannot leave
|
|
// this silently checking a page that no longer exists.
|
|
const manifest = JSON.parse(await readFile(join(ROOT, 'doxa-maps', 'manifest.json'), 'utf8'))
|
|
const first = manifest.bundles.find(b => b.profiles?.length)
|
|
if (!first) fail.push('manifest declares no bundle with a profile — nothing to iframe')
|
|
else {
|
|
const path = `/doxa-maps/${first.bundle}/${first.profiles[0].profile}/`
|
|
const p2 = await browser.newPage()
|
|
const err2 = [], failed2 = []
|
|
p2.on('pageerror', e => err2.push(e.message.split('\n')[0]))
|
|
p2.on('requestfailed', r => { if (!EXTERNAL.some(h => r.url().includes(h))) failed2.push(`${r.failure()?.errorText} ${r.url().replace(BASE, '')}`) })
|
|
await p2.goto(BASE + path, { waitUntil: 'networkidle', timeout: 60000 })
|
|
const mounted2 = await p2.evaluate(() => document.querySelectorAll('[profile-config]').length)
|
|
const defined2 = await p2.evaluate(() => [...document.querySelectorAll('[profile-config]')]
|
|
.map(e => e.tagName.toLowerCase()).filter(t => !!customElements.get(t)))
|
|
// These pages surface their own failure in #status — an empty one means no complaint.
|
|
const status = await p2.evaluate(() => document.getElementById('status')?.textContent.trim() || '')
|
|
if (!mounted2) fail.push(`${path} mounted no element`)
|
|
if (!defined2.length) fail.push(`${path} imported no bundle that registered its element`)
|
|
if (status) fail.push(`${path} reported: ${status.slice(0, 90)}`)
|
|
if (err2.length) fail.push(`${path} uncaught errors: ${err2.slice(0, 2).join(' | ')}`)
|
|
if (failed2.length) fail.push(`${path} failed same-origin requests: ${failed2.slice(0, 2).join(' | ')}`)
|
|
console.log(`[smoke] profile ${path} defined=[${defined2}] mounted=${mounted2}`)
|
|
await p2.close()
|
|
}
|
|
} finally {
|
|
await browser.close(); server.close()
|
|
}
|
|
|
|
if (fail.length) { console.error('\n[smoke] FAILED:'); for (const f of fail) console.error(' ✗ ' + f); process.exit(1) }
|
|
console.log('[smoke] PASS — the built site boots, resolves a token, and mounts a map')
|