From 4cc4a1284b6b74772c66f4930313fa45ea1c45c6 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:10:32 +0000 Subject: [PATCH] The last browser scripts refuse to measure a tree or dev server that is not this checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/_serves-this-tree.mjs | 39 ++++++++++++++++++++++++++++++ internal/check-spa-tab-order.mjs | 6 +++++ internal/legend-columns-probe.mjs | 6 +++++ internal/long-frames-probe.mjs | 6 +++++ internal/mobile-sheet-probe.mjs | 6 +++++ internal/plugin-gallery-smoke.mjs | 6 +++++ internal/share-roundtrip-smoke.mjs | 6 +++++ internal/smoke-studio.mjs | 6 +++++ internal/theme-flash-probe.mjs | 6 +++++ 9 files changed, 87 insertions(+) diff --git a/internal/_serves-this-tree.mjs b/internal/_serves-this-tree.mjs index ebc13b188..34133c4eb 100644 --- a/internal/_serves-this-tree.mjs +++ b/internal/_serves-this-tree.mjs @@ -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.`) 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 --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' } +} diff --git a/internal/check-spa-tab-order.mjs b/internal/check-spa-tab-order.mjs index 2bb4a5703..aef0c7a91 100644 --- a/internal/check-spa-tab-order.mjs +++ b/internal/check-spa-tab-order.mjs @@ -32,8 +32,14 @@ * actual side by side. */ 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' +// 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, // whitespace-collapsed) so a row may carry its subtitle. diff --git a/internal/legend-columns-probe.mjs b/internal/legend-columns-probe.mjs index cce81b783..03f1eeecc 100644 --- a/internal/legend-columns-probe.mjs +++ b/internal/legend-columns-probe.mjs @@ -14,8 +14,14 @@ * Plugin gallery dev server: --url http://localhost:5174/plugin-designs/doxa-simple-map/index.html */ 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 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 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()) diff --git a/internal/long-frames-probe.mjs b/internal/long-frames-probe.mjs index 3f6da977f..84c67bbca 100644 --- a/internal/long-frames-probe.mjs +++ b/internal/long-frames-probe.mjs @@ -56,12 +56,18 @@ import { writeFileSync, mkdirSync } from 'node:fs' import { dirname } from 'node:path' 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 args = process.argv.slice(2) 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 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 TAGS = argAll('--tag') const GREP = argOf('--grep', '') diff --git a/internal/mobile-sheet-probe.mjs b/internal/mobile-sheet-probe.mjs index 54f13cc57..3e87b3a95 100644 --- a/internal/mobile-sheet-probe.mjs +++ b/internal/mobile-sheet-probe.mjs @@ -61,11 +61,17 @@ import { writeFileSync, mkdirSync } from 'node:fs' import { dirname, join } from 'node:path' 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 args = process.argv.slice(2) 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')) +// 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 NTH = Number(argOf('--nth', 0)) const INSTANCE = argOf('--instance', '') diff --git a/internal/plugin-gallery-smoke.mjs b/internal/plugin-gallery-smoke.mjs index 4a1314ada..ac0509dbb 100644 --- a/internal/plugin-gallery-smoke.mjs +++ b/internal/plugin-gallery-smoke.mjs @@ -30,6 +30,10 @@ */ import { writeFileSync } from 'node:fs' 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 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 // blames the user for not starting a server they had started. 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. const EXPECT_CANVAS = process.env.EXPECT_CANVAS === '1' diff --git a/internal/share-roundtrip-smoke.mjs b/internal/share-roundtrip-smoke.mjs index d9e18c064..b43a1ff86 100644 --- a/internal/share-roundtrip-smoke.mjs +++ b/internal/share-roundtrip-smoke.mjs @@ -44,6 +44,10 @@ import { readFileSync, writeFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' 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' const TOOL = 'share-smoke' @@ -63,6 +67,8 @@ const GREP = argOf('--grep', '') const GALLERY = has('--plugin-gallery') ? String((() => { const v = args[args.indexOf('--plugin-gallery') + 1]; return v && !v.startsWith('--') ? v : 'http://localhost:5174' })()).replace(/\/+$/, '') : 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) { console.error(`[${TOOL}] usage: --url [--url ...] [--tag ] | --plugin-gallery [base] (see header)`) diff --git a/internal/smoke-studio.mjs b/internal/smoke-studio.mjs index 7a21ffb95..250c27a1d 100644 --- a/internal/smoke-studio.mjs +++ b/internal/smoke-studio.mjs @@ -47,12 +47,18 @@ * 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 { 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 i = process.argv.indexOf(flag) return i > -1 && process.argv[i + 1] ? process.argv[i + 1] : dflt } 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) // Deep walker — SHADOW DOM DOES NOT PIERCE. A flat querySelectorAll lies about this component, and a diff --git a/internal/theme-flash-probe.mjs b/internal/theme-flash-probe.mjs index 4aaadf52f..50e3ea144 100644 --- a/internal/theme-flash-probe.mjs +++ b/internal/theme-flash-probe.mjs @@ -119,11 +119,17 @@ import { writeFileSync, mkdirSync, readFileSync, rmSync, mkdtempSync } from 'node:fs' import { dirname, join } from 'node:path' 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' const args = process.argv.slice(2) 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(/\/+$/, '') +// 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 TARGET = argOf('--target', '') const TAG = argOf('--tag', '')