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', '')