From 602e007a0d2f2dac40219647c443b34da33932b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Vila=C3=A7a=20de=20Jesus?= Date: Sun, 25 Oct 2026 11:28:58 -0300 Subject: [PATCH 1/3] chore: start #604 From a0759e6e34af960f9f6f969132ab71addfd07bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Vila=C3=A7a=20de=20Jesus?= Date: Sun, 25 Oct 2026 11:32:36 -0300 Subject: [PATCH 2/3] fix: test: budget of test:changed follows the selection scope --- AGENTS.md | 2 +- package.json | 4 +-- packages/ui/tests/test-graph.test.ts | 46 +++++++++++++++++++--------- scripts/test-changed.sh | 9 ++++++ scripts/test-changed.ts | 30 ++++++++++++++---- scripts/test-graph.ts | 22 ++++++++++--- scripts/test-run.ts | 4 ++- 7 files changed, 87 insertions(+), 30 deletions(-) create mode 100755 scripts/test-changed.sh diff --git a/AGENTS.md b/AGENTS.md index cc29aad5..273e431d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ Unit tests run on `bun test` (repo root, `bunfig.toml`); component and visual te | Command | What it runs | Budget | Measured | |---|---|---|---| -| `bun run test` = `test:changed` | what the diff affects: `bun test --changed` (unit); the component tests whose imports reach a changed file (`scripts/test-graph.ts`: a static import graph with the barrel flattened, because Vitest's own `--changed` selects every file in browser mode); the screenshots of those components; the docs routes mapped by `scripts/route-impact.ts`. Prints what it selected and why. Every component test runs when a shared file changed (styles, tokens, theme, stores, primitives, a test setup file). The site part needs a fresh `site/dist` (a build is ~75 s, over the budget) and says so when it skips | 60 s | 28 s for a one-component edit with a fresh `dist` (unit 0.4, components 4.6, screenshots 6.7, routes 16.6; load ~15) | +| `bun run test` = `test:changed` | what the diff affects: `bun test --changed` (unit); the component tests whose imports reach a changed file (`scripts/test-graph.ts`: a static import graph with the barrel flattened, because Vitest's own `--changed` selects every file in browser mode); the screenshots of those components; the docs routes mapped by `scripts/route-impact.ts`. Prints what it selected and why. Every component test runs when a shared file or a test-runtime dependency changed (styles, tokens, theme, stores, primitives, a test setup file, `package.json`, `bun.lock`): the run prints `widened to all components because ` and the budget widens with it. The site part needs a fresh `site/dist` (a build is ~75 s, over the budget) and says so when it skips | 60 s narrow; 240 s (`landShared`) when widened to all components (`scripts/test-changed.sh` reads it from `--print-budget`) | 28 s for a one-component edit with a fresh `dist` (unit 0.4, components 4.6, screenshots 6.7, routes 16.6; load ~15). Widened by the happy-dom bump (#588): 95 s at load 10.5 for the full component set (~2190 tests), which is why the narrow 60 s does not apply to it | | `bun scripts/test-changed.ts --land` | the same plus the package and site builds and 5 canary routes (home, recipes, app-shell, templates/app, theme). This is the gate `issue-land.sh` runs; `validate` runs on the push. The package builds first (the unit tests that read `dist` need it), then the site build runs beside the unit and component tests (the component tests render from `src`). **When a shared file changed** (styles, tokens, theme, stores, primitives, a test setup file, the site layout, or a file the route map cannot place) it runs every component test and a **thinned route sweep** and **no screenshots**, under the larger budget; `--print-budget` says which one applies. The thinned sweep (`E2E_GALLERY=representatives`) keeps 6 gallery items, 3 atlas terms and the fewest docs pages that still render every component folder and subpath (`docsRepresentatives` in `scripts/route-impact.ts`: 81 of 160), plus every page the diff itself changed (`E2E_KEEP`); `test:full` audits all of them. The ~880 social cards are cached by the sha256 of their HTML and the browser version in `$TMPDIR/ca-og-cards` (shared by every worktree, `CA_OG_CACHE_DIR` overrides, cards unused for 30 days are pruned) and rendered four at a time | 180 s; 240 s when a shared file changed | 104 s for a one-component edit (builds 17 + 58, routes 17; load ~15). Shared case at load 7 (components ALL 210 files): 193.5 s with a warm card cache (package 11 + site build 94 beside the tests + routes 88) and 224.8 s with a cold one (site build 125); it was 382.5 s before #598 (unit 8 + components 80 + package 12 + site 134 + routes 148) | | `bun run validate` | themes, token lint, Biome, unit tests, both type checks, build, size | 90 s | 51 s at load 11 | | `bun run test:full` | unit, every component test, every route (layout, axe, dist-html, every project including WebKit), fixtures. Release gate, part 1 of 2 | 300 s | components 109 s (load 25) + routes 168 s (load 29) + unit 40 s + fixtures 7 s: over budget on a busy machine, not measured on a quiet one | diff --git a/package.json b/package.json index 7f797f17..d048633e 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "lint:fix": "biome check --write .", "validate": "bun scripts/test-run.ts --budget=validate bun scripts/validate.ts", "postinstall": "node scripts/fix-ts-symlink.js && node scripts/install-hooks.js && bunx playwright install chromium webkit", - "test": "bun scripts/test-run.ts --budget=changed bun scripts/test-changed.ts", - "test:changed": "bun scripts/test-run.ts --budget=changed bun scripts/test-changed.ts", + "test": "sh scripts/test-changed.sh", + "test:changed": "sh scripts/test-changed.sh", "test:full": "bun scripts/test-run.ts --budget=full sh -c 'bun run test:unit && bun run test:browser && bun run test:fixtures && bun run test:e2e'", "test:unit": "bun run --cwd packages/ui test:unit && bun run --cwd site test:unit", "test:browser": "bun run --cwd packages/ui test:browser", diff --git a/packages/ui/tests/test-graph.test.ts b/packages/ui/tests/test-graph.test.ts index 905fd75f..b75fb3b0 100644 --- a/packages/ui/tests/test-graph.test.ts +++ b/packages/ui/tests/test-graph.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from "bun:test"; -import { affected, buildGraph, isSharedComponent, landBudget } from "../../../scripts/test-graph"; +import { + affected, + buildGraph, + isSharedComponent, + resolveBudget, + widenedBecause, +} from "../../../scripts/test-graph"; // scripts/test-graph.ts selects component tests by a static import graph, because Vitest's `--changed` // selects every file in browser mode. Too wide a selection defeats it; too narrow hides a regression. @@ -70,13 +76,21 @@ describe("test-graph", () => { }); }); -// `issue-land.sh` picks the land budget from this: a shared change runs every component test and every route -// (360 s, no screenshots), anything else the affected ones plus the canaries (180 s). -describe("landBudget", () => { - it("a component edit is under the 180 s budget", () => { - expect(landBudget([`${dir}/Avatar/Avatar.svelte`])).toBe("land"); - expect(landBudget([`${dir}/Avatar/Avatar.svelte.test.ts`, "AGENTS.md"])).toBe("land"); - expect(landBudget([])).toBe("land"); +// The budget follows the scope of the selection. A dependency bump (happy-dom, #588) correctly selects every +// component test, and the full set alone is ~80 s (95 s at load 10.5), over the 60 s `changed` budget that +// assumed a narrow selection. Widened to all components: `landShared`; narrow: `changed` (dev) or `land`. +describe("resolveBudget", () => { + const narrow = [`${dir}/Avatar/Avatar.svelte`]; + + it("a narrow selection keeps its budget and has no widening reason", () => { + expect(resolveBudget(narrow, false)).toBe("changed"); + expect(resolveBudget([`${dir}/Avatar/Avatar.svelte.test.ts`, "AGENTS.md"], false)).toBe( + "changed" + ); + expect(resolveBudget([], false)).toBe("changed"); + expect(resolveBudget(narrow, true)).toBe("land"); + expect(resolveBudget([], true)).toBe("land"); + expect(widenedBecause(narrow)).toBeUndefined(); }); it.each([ @@ -86,16 +100,18 @@ describe("landBudget", () => { "packages/ui/src/lib/primitives/presence.svelte.ts", "packages/ui/tests/setup.browser.ts", "packages/ui/vite.config.ts", + "packages/ui/package.json", "site/src/layouts/Base.astro", - ])("%s is shared: the 360 s budget", (file) => { - expect(landBudget([file])).toBe("landShared"); + ])("%s widens to all components: landShared, dev run and land gate", (file) => { + for (const land of [false, true]) + expect(resolveBudget([...narrow, file], land)).toBe("landShared"); + expect(widenedBecause([...narrow, file])).toBe(file); }); - it("one shared file among ordinary ones is enough, and a file the route map does not know is shared", () => { - expect(landBudget([`${dir}/Avatar/Avatar.svelte`, "packages/ui/src/lib/styles/base.css"])).toBe( - "landShared" - ); - expect(landBudget(["site/public/some-new-asset.svg"])).toBe("landShared"); + it("a file the route map does not know widens too, and the reason names it", () => { + const file = "site/public/some-new-asset.svg"; + expect(resolveBudget([file], false)).toBe("landShared"); + expect(widenedBecause([...narrow, file])).toBe(file); }); it("isSharedComponent only flags what every component test renders through", () => { diff --git a/scripts/test-changed.sh b/scripts/test-changed.sh new file mode 100755 index 00000000..271875fe --- /dev/null +++ b/scripts/test-changed.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# `bun run test` / `test:changed`: the budget follows the selection's scope. A narrow selection keeps the 60 s +# `changed` budget; one widened to every component test (a shared file, a test-runtime dependency like happy-dom) +# runs under `landShared`, and test-changed.ts says why. The budget is read before the clock starts. +# sh scripts/test-changed.sh [test-changed args] +set -e +cd "$(dirname "$0")/.." +BUDGET=$(bun scripts/test-changed.ts --print-budget "$@") +exec bun scripts/test-run.ts --budget="$BUDGET" bun scripts/test-changed.ts "$@" diff --git a/scripts/test-changed.ts b/scripts/test-changed.ts index 9633926f..0831840d 100755 --- a/scripts/test-changed.ts +++ b/scripts/test-changed.ts @@ -8,7 +8,10 @@ * A SHARED change (a shared component file, or a route-wide one: styles, tokens, theme, primitives, the * site layout, a test setup file, or a file the route map cannot place) runs every component test and, when * it reaches the site, every route, and NO screenshots: those run at release (`test:vrt`) and on demand. - * `--print-budget` prints the budget this selection runs under (`land`, 180 s, or `landShared`, 240 s). + * `--print-budget` prints the budget this selection runs under: `changed` (60 s) for a narrow dev run, `land` + * (180 s) for a narrow land run, `landShared` (240 s) for either when the selection widens to every component + * test (a shared file or a test-runtime dependency such as happy-dom); the run then prints + * "widened to all components because ". * * unit `bun test --changed ` (module graph); * components the component tests whose imports reach a changed file (scripts/test-graph.ts; Vitest's @@ -34,7 +37,14 @@ import { existsSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; import { distStale } from "./dist-fresh.ts"; import { lockReachesRuntime, ownRoutes, ROOT, select } from "./route-impact.ts"; -import { affected, buildGraph, isSharedComponent, landBudget } from "./test-graph.ts"; +import { + affected, + buildGraph, + isSharedComponent, + resolveBudget, + widenedBecause, +} from "./test-graph.ts"; +import { BUDGETS } from "./test-run.ts"; if (!import.meta.main) throw new Error("scripts/test-changed.ts is a command, not a module"); @@ -88,6 +98,13 @@ const changed = [ ]), ].filter((f) => f !== "bun.lock" || !lockIsDevOnly()); const short = base.slice(0, 8); +// The budget follows the selection's scope (scripts/test-graph.ts `resolveBudget`): scripts/test-changed.sh and +// `issue-land.sh` read it from here before the clock starts. +const budget = resolveBudget(changed, land); +if (process.argv.includes("--print-budget")) { + console.log(budget); + process.exit(0); +} if (!changed.length && !land) { console.log(`test:changed: no changes against origin/main (${short}), nothing to run`); process.exit(0); @@ -103,10 +120,11 @@ const visualRun = sel.all || sel.routes.some((r) => VISUAL_ROUTES.has(r)); const routeRun = sweepAll || routes.length > 0; // Shared: everything that can reach every test runs, and screenshots are left to `test:vrt`. const shared = sel.all || sharedFile !== undefined; -if (process.argv.includes("--print-budget")) { - console.log(land ? landBudget(changed) : "changed"); - process.exit(0); -} +const widened = widenedBecause(changed); +if (widened !== undefined) + console.log( + `test:changed: widened to all components because ${widened}, so the ${budget} budget applies (${BUDGETS[budget]} s)` + ); console.log( `test:changed${land ? " --land" : ""}: ${changed.length} changed file(s) against ${short}` diff --git a/scripts/test-graph.ts b/scripts/test-graph.ts index ad95bc0a..b3839534 100644 --- a/scripts/test-graph.ts +++ b/scripts/test-graph.ts @@ -206,10 +206,22 @@ export const isSharedComponent = (file: string) => !isTestFile(file) && SHARED_COMPONENT.some((re) => re.test(file)); /** - * The budget `issue-land.sh` runs the land gate under: a SHARED change (a shared component file, or one the - * route map says reaches every page or does not know) runs every component test and every route and gets - * `landShared`; any other change runs what it affects plus the canaries under `land`. + * Why a selection widens to EVERY component test: the first shared file, or the file the route map says reaches + * every page or does not know. `undefined` when the selection is narrow. */ -export function landBudget(changed: string[]): "land" | "landShared" { - return changed.some(isSharedComponent) || select(changed).all ? "landShared" : "land"; +export function widenedBecause(changed: string[]): string | undefined { + const shared = changed.find(isSharedComponent); + if (shared !== undefined) return shared; + const sel = select(changed); + return sel.all ? sel.reason.split(":")[0] : undefined; +} + +/** + * The budget a run gets, from the scope of its selection (the budget assumed a narrow one: a dependency bump + * selects every component test, ~80 s alone, 95 s at load 10.5 in #588). Widened to all components: `landShared` + * (dev run and land gate alike). Narrow: `changed` (60 s) for a dev run, `land` (180 s) with the canaries. + */ +export function resolveBudget(changed: string[], land: boolean): "changed" | "land" | "landShared" { + if (widenedBecause(changed) !== undefined) return "landShared"; + return land ? "land" : "changed"; } diff --git a/scripts/test-run.ts b/scripts/test-run.ts index caad8b0e..cd82fb9c 100755 --- a/scripts/test-run.ts +++ b/scripts/test-run.ts @@ -194,7 +194,7 @@ export function releaseLock(pid: number) { /** Wall-time budget in seconds of each command. Raise one only in its own commit, with the measured reason. */ export const BUDGETS = { - /** `bun run test` / `test:changed`: what the diff against origin/main affects */ + /** `bun run test` / `test:changed` with a NARROW selection (one that widens to every component test runs under `landShared`, scripts/test-graph.ts `resolveBudget`) */ changed: 60, /** `bun run validate`: build, types, lint, unit tests, size */ validate: 90, @@ -209,6 +209,8 @@ export const BUDGETS = { * 1044 (#598). Measured at load 7 on 12 cores, a shared change (components ALL 210 files): 224.8 s with a cold card * cache (package 11 s, then site build 125 s beside the tests, then routes 88 s) and 193.5 s with a warm one (site * build 94 s). The full sweep stays at release (`test:full`). Do not raise it again: make the run faster. + * A dev run (`bun run test`) widened to every component test gets it too: the full component set alone is ~80 s + * (95 s at load 10.5 on the happy-dom bump #588), over the 60 s `changed` budget that assumed a narrow selection. */ landShared: 240, /** `bun run test:full`: unit, every component test, every route, fixtures (release gate, part 1) */ From db1441b0043fe4c2efe51281d0cd6d63dbf14b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Vila=C3=A7a=20de=20Jesus?= Date: Sun, 25 Oct 2026 11:32:46 -0300 Subject: [PATCH 3/3] chore: regenerate lockfile and manifest after rebase --- bun.lock | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 02ec446f..fb32ae52 100644 --- a/bun.lock +++ b/bun.lock @@ -134,7 +134,7 @@ "@astrojs/check": "0.9.10", "@axe-core/playwright": "4.13.0", "@playwright/test": "1.64.0", - "happy-dom": "21.0.1", + "happy-dom": "21.0.4", "typescript": "6.0.3", "wrangler": "4.149.0", }, @@ -1697,8 +1697,6 @@ "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "site/happy-dom": ["happy-dom@21.0.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-pRBs/u9fhrncyByOl58DMex0R2uAj6ZPKWIjV5j5yNGZBqLw3P6BTRhlc0DD5b/Ht4/6JYTMuE2LJhUWRC6SGw=="], - "speedline-core/@types/node": ["@types/node@25.9.9", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-b4e2xxj/yMeT2hNlD6x7zNFiQK2Dlmkb9CQ6v3/1G2RezKN4YS2lcEv8lzGm1mhs4f2lM4yNAMI/zUDBZBBt7w=="], "svelte/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -1743,8 +1741,6 @@ "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "site/happy-dom/ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], - "svelte-check/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "@astrojs/check/yargs/cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],