The last browser scripts refuse to measure a tree or dev server that is not this checkout
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

BUG-BROWSER-GATES-CAN-MEASURE-ANOTHER-WORKTREES-TREE, the remainder. A
static scan of every internal/ script that drives a browser at a local
target found eight with no identity guard (a9550858 and 59f497eb
guarded the rest):
  built tree  check-spa-tab-order -> requireThisTree
  dev server  long-frames-probe, mobile-sheet-probe, theme-flash-probe,
              plugin-gallery-smoke, share-roundtrip-smoke (--plugin-gallery),
              smoke-studio, legend-columns-probe (only when aimed at
              /plugin-designs/; its default target is the DX-life site
              it compares against on purpose)

A dev server has no built manifest, so the new
requireDevServesThisCheckout checks the source it serves. It fetches
three library files with ?raw and compares them byte for byte with
this checkout. Another tree's dev server answers differently or 403s,
and the gate exits 75 (NOT MEASURED). Vite answers a plain fetch with
the file's text and an import-style request with `export default "…"`;
both shapes are accepted, since the first version parsed only the
second and refused this checkout's own server.

Proven: against a dev server running from 319064a1's parent and a
built tree whose manifest differs by one byte, all eight exit 75 and
name the differing file (useMapInstance.js). Against this checkout,
check-spa-tab-order and smoke-studio PASS, and plugin-gallery-smoke
gets past the guard. A re-scan finds no unguarded script left.
This commit is contained in:
doxa-maps-88655c70 2026-09-25 11:10:32 +00:00
parent 92a81ed291
commit 4cc4a1284b
9 changed files with 87 additions and 0 deletions

View file

@ -168,3 +168,42 @@ export function requirePathInThisCheckout(file, repo, label) {
console.log(`${label}: note — ${file} is outside any checkout, so which build produced it cannot be checked.`) console.log(`${label}: note — ${file} is outside any checkout, so which build produced it cannot be checked.`)
return { ok: true, why: 'outside any checkout' } return { ok: true, why: 'outside any checkout' }
} }
/**
* The same guard for gates aimed at a VITE DEV SERVER, which has no built manifest to fingerprint.
*
* A dev server serves this repo's SOURCE, so identity is the source itself: ask it for a few of this
* checkout's library files with Vite's `?raw` query (the file's exact text, as an ES module string)
* and compare byte for byte. A dev server rooted in another worktree with different code answers
* differently, or 403s the path — either way it is NOT MEASURED (exit 75). One serving identical code
* is, by definition, measuring the same thing. Unreachable is left to the gate's own handling.
* Remote (non-loopback) targets are not checked, as in requireThisTree.
*/
const DEV_FINGERPRINT = ['library/layouts/VerticalTabsLayout.vue', 'library/composables/useMapInstance.js', 'library/composables/useShadowStyles.js']
export async function requireDevServesThisCheckout(base, repo, label, files = DEV_FINGERPRINT) {
let origin
try { origin = new URL(String(base)).origin } catch { return { ok: true, why: 'not a URL' } }
if (!/^(localhost|127\.|0\.0\.0\.0$|\[?::1\]?$)/.test(new URL(origin).hostname)) return { ok: true, why: 'remote target' }
const refuse = (why) => {
console.log(`${label}: NOT MEASURED — the dev server at ${origin} is not serving THIS checkout's source (${why}).`)
console.log(' Start `bun x vite --port <yours> --strictPort` in this worktree and pass that URL.')
process.exit(75)
}
for (const rel of files) {
let local
try { local = readFileSync(resolve(repo, rel), 'utf8') } catch { continue }
const r = await fetch(`${origin}/${rel}?raw`, { signal: AbortSignal.timeout(8000) }).catch(() => null)
if (!r) return { ok: false, why: 'unreachable' }
if (!r.ok) refuse(`${rel} → HTTP ${r.status}`)
// A plain fetch (not a module import) gets the file's TEXT served directly; an import-style
// request gets it wrapped as `export default "…"`. Accept both shapes — the first version only
// parsed the wrapped one and refused this very checkout's own dev server (measured 2026-09-25).
const body = await r.text()
const m = body.match(/^export default ("(?:[^"\\]|\\.)*")/s)
let served = body
if (m) { try { served = JSON.parse(m[1]) } catch { served = null } }
if (served !== local) refuse(`${rel} differs`)
}
return { ok: true, why: 'same source' }
}

View file

@ -32,8 +32,14 @@
* actual side by side. * actual side by side.
*/ */
import { chromium } from 'playwright' import { chromium } from 'playwright'
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..')
const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps' const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps'
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
await __treeGuard(BASE, __ROOT, 'SPA-TAB-ORDER')
// The Driver's sequence, verbatim from the open-work brief. Prefix-matched (case-insensitive, // The Driver's sequence, verbatim from the open-work brief. Prefix-matched (case-insensitive,
// whitespace-collapsed) so a row may carry its subtitle. // whitespace-collapsed) so a row may carry its subtitle.

View file

@ -14,8 +14,14 @@
* Plugin gallery dev server: --url http://localhost:5174/plugin-designs/doxa-simple-map/index.html * Plugin gallery dev server: --url http://localhost:5174/plugin-designs/doxa-simple-map/index.html
*/ */
import { launch, TELEMETRY } from './lib/browser.mjs' import { launch, TELEMETRY } from './lib/browser.mjs'
import { requireDevServesThisCheckout } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const args = process.argv.slice(2); const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d } const args = process.argv.slice(2); const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const URL = argOf('--url', 'http://localhost:3001/pray'), TAG = argOf('--tag', 'doxa-map'), W = +argOf('--width', 1440), H = +argOf('--height', 900), ROWS = +argOf('--rows', 8) const URL = argOf('--url', 'http://localhost:3001/pray'), TAG = argOf('--tag', 'doxa-map'), W = +argOf('--width', 1440), H = +argOf('--height', 900), ROWS = +argOf('--rows', 8)
// Guard ONLY when pointed at OUR dev gallery (/plugin-designs/…). The default target is the DX-life
// site (a different app this probe compares against on purpose), which has no source of ours to match.
if (/\/plugin-designs\//.test(URL)) await requireDevServesThisCheckout(URL, __rs(__dn(__f2p(import.meta.url)), '..'), 'LEGEND-COLUMNS')
const browser = await launch('firefox', { label: 'legend-columns' }) const browser = await launch('firefox', { label: 'legend-columns' })
const ctx = await browser.newContext({ viewport: { width: W, height: H }, hasTouch: true, isMobile: W < 600 }); const page = await ctx.newPage() const ctx = await browser.newContext({ viewport: { width: W, height: H }, hasTouch: true, isMobile: W < 600 }); const page = await ctx.newPage()
await page.route(TELEMETRY, r => r.abort()) await page.route(TELEMETRY, r => r.abort())

View file

@ -56,12 +56,18 @@
import { writeFileSync, mkdirSync } from 'node:fs' import { writeFileSync, mkdirSync } from 'node:fs'
import { dirname } from 'node:path' import { dirname } from 'node:path'
import { launch, DEEP_FIND_SRC, deepEvaluate, TELEMETRY } from './lib/browser.mjs' import { launch, DEEP_FIND_SRC, deepEvaluate, TELEMETRY } from './lib/browser.mjs'
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..')
const TOOL = 'long-frames-probe' const TOOL = 'long-frames-probe'
const args = process.argv.slice(2) const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d } const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const argAll = (k) => args.flatMap((a, i) => a === k && args[i + 1] ? [args[i + 1]] : []) const argAll = (k) => args.flatMap((a, i) => a === k && args[i + 1] ? [args[i + 1]] : [])
const BASE = String(argOf('--base', 'http://localhost:5174')).replace(/\/+$/, '') const BASE = String(argOf('--base', 'http://localhost:5174')).replace(/\/+$/, '')
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
await __devGuard(BASE, __ROOT, 'LONG-FRAMES')
const URLS = argAll('--url') const URLS = argAll('--url')
const TAGS = argAll('--tag') const TAGS = argAll('--tag')
const GREP = argOf('--grep', '') const GREP = argOf('--grep', '')

View file

@ -61,11 +61,17 @@
import { writeFileSync, mkdirSync } from 'node:fs' import { writeFileSync, mkdirSync } from 'node:fs'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import { launch, TELEMETRY } from './lib/browser.mjs' import { launch, TELEMETRY } from './lib/browser.mjs'
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..')
const TOOL = 'mobile-sheet-probe' const TOOL = 'mobile-sheet-probe'
const args = process.argv.slice(2) const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d } const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const URL = String(argOf('--url', 'http://localhost:5174/plugin-designs/doxa-simple-map/index.html')) const URL = String(argOf('--url', 'http://localhost:5174/plugin-designs/doxa-simple-map/index.html'))
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
await __devGuard(URL, __ROOT, 'MOBILE-SHEET')
const TAG = argOf('--tag', 'doxa-map') const TAG = argOf('--tag', 'doxa-map')
const NTH = Number(argOf('--nth', 0)) const NTH = Number(argOf('--nth', 0))
const INSTANCE = argOf('--instance', '') const INSTANCE = argOf('--instance', '')

View file

@ -30,6 +30,10 @@
*/ */
import { writeFileSync } from 'node:fs' import { writeFileSync } from 'node:fs'
import { launch, deepEvaluate, TELEMETRY } from './lib/browser.mjs' import { launch, deepEvaluate, TELEMETRY } from './lib/browser.mjs'
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..')
const args = process.argv.slice(2) const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d } const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
@ -40,6 +44,8 @@ const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1]
// cannot reach its subject has not passed and has not failed: it has not run, and the message // cannot reach its subject has not passed and has not failed: it has not run, and the message
// blames the user for not starting a server they had started. // blames the user for not starting a server they had started.
const BASE = String(argOf('--url', process.env.DEV_URL || 'http://localhost:5173')).replace(/\/+$/, '') const BASE = String(argOf('--url', process.env.DEV_URL || 'http://localhost:5173')).replace(/\/+$/, '')
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
await __devGuard(BASE, __ROOT, 'PLUGIN-GALLERY-SMOKE')
// Paint needs a GPU this build box does not have; see the long note at the canvas assertion below. // Paint needs a GPU this build box does not have; see the long note at the canvas assertion below.
const EXPECT_CANVAS = process.env.EXPECT_CANVAS === '1' const EXPECT_CANVAS = process.env.EXPECT_CANVAS === '1'

View file

@ -44,6 +44,10 @@
import { readFileSync, writeFileSync } from 'node:fs' import { readFileSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..')
import { launch, deepEvaluate, TELEMETRY } from './lib/browser.mjs' import { launch, deepEvaluate, TELEMETRY } from './lib/browser.mjs'
const TOOL = 'share-smoke' const TOOL = 'share-smoke'
@ -63,6 +67,8 @@ const GREP = argOf('--grep', '')
const GALLERY = has('--plugin-gallery') const GALLERY = has('--plugin-gallery')
? String((() => { const v = args[args.indexOf('--plugin-gallery') + 1]; return v && !v.startsWith('--') ? v : 'http://localhost:5174' })()).replace(/\/+$/, '') ? String((() => { const v = args[args.indexOf('--plugin-gallery') + 1]; return v && !v.startsWith('--') ? v : 'http://localhost:5174' })()).replace(/\/+$/, '')
: null : null
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
if (GALLERY) await __devGuard(GALLERY, __ROOT, 'SHARE-ROUNDTRIP')
if (!URLS.length && !GALLERY) { if (!URLS.length && !GALLERY) {
console.error(`[${TOOL}] usage: --url <host page URL> [--url ...] [--tag <tag>] | --plugin-gallery [base] (see header)`) console.error(`[${TOOL}] usage: --url <host page URL> [--url ...] [--tag <tag>] | --plugin-gallery [base] (see header)`)

View file

@ -47,12 +47,18 @@
* removing tree-base makes it FAIL on the empty tab list. A gate that cannot fail is worse than none. * removing tree-base makes it FAIL on the empty tab list. A gate that cannot fail is worse than none.
*/ */
import { chromium } from 'playwright' import { chromium } from 'playwright'
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..')
const argOf = (flag, dflt) => { const argOf = (flag, dflt) => {
const i = process.argv.indexOf(flag) const i = process.argv.indexOf(flag)
return i > -1 && process.argv[i + 1] ? process.argv[i + 1] : dflt return i > -1 && process.argv[i + 1] ? process.argv[i + 1] : dflt
} }
const BASE = String(argOf('--url', 'http://localhost:5173')).replace(/\/+$/, '') const BASE = String(argOf('--url', 'http://localhost:5173')).replace(/\/+$/, '')
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
await __devGuard(BASE, __ROOT, 'SMOKE-STUDIO')
const BUDGET_MS = Number(process.env.SMOKE_BUDGET_MS || 20000) const BUDGET_MS = Number(process.env.SMOKE_BUDGET_MS || 20000)
// Deep walker — SHADOW DOM DOES NOT PIERCE. A flat querySelectorAll lies about this component, and a // Deep walker — SHADOW DOM DOES NOT PIERCE. A flat querySelectorAll lies about this component, and a

View file

@ -119,11 +119,17 @@
import { writeFileSync, mkdirSync, readFileSync, rmSync, mkdtempSync } from 'node:fs' import { writeFileSync, mkdirSync, readFileSync, rmSync, mkdtempSync } from 'node:fs'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..')
import { launch, DEEP_FIND_SRC, deepEvaluate, TELEMETRY } from './lib/browser.mjs' import { launch, DEEP_FIND_SRC, deepEvaluate, TELEMETRY } from './lib/browser.mjs'
const args = process.argv.slice(2) const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d } const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const BASE = String(argOf('--url', 'http://localhost:5174')).replace(/\/+$/, '') const BASE = String(argOf('--url', 'http://localhost:5174')).replace(/\/+$/, '')
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
await __devGuard(BASE, __ROOT, 'THEME-FLASH')
const GREP = argOf('--grep', '') const GREP = argOf('--grep', '')
const TARGET = argOf('--target', '') const TARGET = argOf('--target', '')
const TAG = argOf('--tag', '') const TAG = argOf('--tag', '')