From 6b1a722c35318048bc853d4eb635e897fa18324f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:26:40 +0000 Subject: [PATCH 1/2] test(spec,lint): cheaper slow-file sweeps and re-measured test budgets - lint non-record-object-entry: one sweep of the authoring-rule table per distinct stack, read by both the throws case and the count case; the control stack is swept once per collection. - spec ui/app: the compiler-API case reads the ES2022 lib and no ambient @types, the inputs the NavigationArea type actually has. - spec bscm manifest: the #12588 pair shares one generation. - Five declared budgets re-measured over refresh PR #25960's window, and five undeclared rows declare their ruled class at their CI reading. Claude-Session: https://claude.ai/code/session_01LpCAbWQuprShJmC2GJXTp7 Co-authored-by: Claude --- packages/lint/src/lazy-deps.test.ts | 1 + .../lint/src/non-record-object-entry.test.ts | 84 ++++++++++++------- packages/lint/src/runtime-lazy-deps.test.ts | 1 + .../build-schemas-check-mode.anchor.test.ts | 2 +- .../build-schemas-check-mode.manifest.test.ts | 17 +++- ...ld-schemas-check-mode.retired-keys.test.ts | 2 +- .../scripts/openapi-self-consistency.test.ts | 1 + .../scripts/pure-schema-construction.test.ts | 1 + .../root-entry-type-nameability.pin.test.ts | 1 + .../spec/scripts/upgrade-guide-docs.test.ts | 2 +- packages/spec/src/eager-entry-import.test.ts | 2 +- packages/spec/src/ui/app.test.ts | 7 ++ 12 files changed, 84 insertions(+), 37 deletions(-) diff --git a/packages/lint/src/lazy-deps.test.ts b/packages/lint/src/lazy-deps.test.ts index fb1dec22892..7876949e16f 100644 --- a/packages/lint/src/lazy-deps.test.ts +++ b/packages/lint/src/lazy-deps.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — whole-package import: the in-process case imports the whole @objectstack/lint barrel cold and loads each lazy dep on its trigger, and the two built-dist probes each spawn node once to cold-load the dist and every lazy dep (typescript among them); slowest CI reading 13.91 s (run 40200279306, refresh PR #25960's window (hourly runs 40200279306 to 40272142430)) x 1.25, rounded up // // Boot-path contract: importing @objectstack/lint must NOT load its heavy, // gate-only dependencies. The package sits on the kernel boot path, while each diff --git a/packages/lint/src/non-record-object-entry.test.ts b/packages/lint/src/non-record-object-entry.test.ts index 56fa0a6b3a2..6a5bc6e77c8 100644 --- a/packages/lint/src/non-record-object-entry.test.ts +++ b/packages/lint/src/non-record-object-entry.test.ts @@ -87,10 +87,50 @@ const NON_RECORD_ENTRIES: readonly (readonly [string, unknown])[] = [ ]; const stackWith = (junk: unknown): AnyRec => ({ objects: [junk, VALID_OBJECT] }); -const validOnly: AnyRec = { objects: [VALID_OBJECT] }; +const validOnly = (): AnyRec => ({ objects: [VALID_OBJECT] }); -const findingCount = (stack: AnyRec): number => - AUTHORING_RULES.reduce((n, rule) => n + rule.run(stack, {}).length, 0); +/** What one rule did with one stack: the message it threw, or how many findings it returned. */ +type RuleOutcome = { readonly rule: string } & ({ readonly threw: string } | { readonly findings: number }); + +/** + * One sweep per distinct stack. Every rule is pure, `(stack) => Finding[]`, so + * running the table once over a stack and recording what each rule did answers + * both questions a case asks of that stack — did any rule throw, and how many + * findings did the rest return. The "throws" case and the "count" case of one + * stack read the same sweep, and a control stack shared by every junk shape is + * swept once. Each rule still gets a stack of its own, built fresh. + */ +const sweeps = new Map(); +const sweep = (key: string, build: () => AnyRec): readonly RuleOutcome[] => { + let outcomes = sweeps.get(key); + if (!outcomes) { + outcomes = AUTHORING_RULES.map((rule): RuleOutcome => { + try { + return { rule: rule.name, findings: rule.run(build(), {}).length }; + } catch (e) { + return { rule: rule.name, threw: (e as Error).message }; + } + }); + sweeps.set(key, outcomes); + } + return outcomes; +}; + +/** `rule: message` for every rule that threw. */ +const throwsOf = (outcomes: readonly RuleOutcome[]): string[] => + outcomes.flatMap((o) => ('threw' in o ? [`${o.rule}: ${o.threw}`] : [])); + +/** + * The finding count over every rule not in `skip`. A rule outside `skip` that + * threw fails the count with its own message rather than counting as zero, as + * the count did when it ran the rules itself. + */ +const countOf = (outcomes: readonly RuleOutcome[], skip: readonly string[] = []): number => + outcomes.reduce((n, o) => { + if (skip.includes(o.rule)) return n; + if ('threw' in o) throw new Error(`${o.rule}: ${o.threw}`); + return n + o.findings; + }, 0); describe('a non-record entry in `stack.objects` (#15552)', () => { /** @@ -103,23 +143,17 @@ describe('a non-record entry in `stack.objects` (#15552)', () => { expect(AUTHORING_RULES.length).toBeGreaterThanOrEqual(42); }); - describe.each(NON_RECORD_ENTRIES)('with %s beside a valid object', (_label, junk) => { + describe.each(NON_RECORD_ENTRIES)('with %s beside a valid object', (label, junk) => { + const withJunk = (): readonly RuleOutcome[] => sweep(`objects · ${label}`, () => stackWith(junk)); + it('no authoring rule throws', () => { - const threw: string[] = []; - for (const rule of AUTHORING_RULES) { - try { - rule.run(stackWith(junk), {}); - } catch (e) { - threw.push(`${rule.name}: ${(e as Error).message}`); - } - } - expect(threw).toEqual([]); + expect(throwsOf(withJunk())).toEqual([]); }); it('still judges the valid object standing beside it', () => { - const baseline = findingCount(validOnly); + const baseline = countOf(sweep('objects · valid only', validOnly)); expect(baseline).toBeGreaterThan(0); - expect(findingCount(stackWith(junk))).toBe(baseline); + expect(countOf(withJunk())).toBe(baseline); }); }); @@ -575,19 +609,12 @@ describe('a non-record entry in any other stack collection (#15636)', () => { describe.each(NON_RECORD_ENTRIES)('with %s', (shape, junk) => { const key = `${collection.label} · ${shape}`; + const withJunk = (): readonly RuleOutcome[] => sweep(key, () => collection.stack(members(junk))); it('throws out of no rule but the ones still filed as broken', () => { - const threw: string[] = []; - const detail: string[] = []; - for (const rule of AUTHORING_RULES) { - try { - rule.run(collection.stack(members(junk)), {}); - } catch (e) { - threw.push(rule.name); - detail.push(`${rule.name}: ${(e as Error).message}`); - } - } - expect(threw.sort(), detail.join(' | ')).toEqual([...(RESIDUAL_THROWS[key] ?? [])].sort()); + const outcomes = withJunk(); + const threw = outcomes.flatMap((o) => ('threw' in o ? [o.rule] : [])); + expect(threw.sort(), throwsOf(outcomes).join(' | ')).toEqual([...(RESIDUAL_THROWS[key] ?? [])].sort()); }); it('invents no finding about the entry no author wrote', () => { @@ -596,9 +623,8 @@ describe('a non-record entry in any other stack collection (#15636)', () => { // folding that into the population count would let a crash read as // "reported nothing", which is the confusion this file exists to end. const skip = RESIDUAL_THROWS[key] ?? []; - const count = (stack: AnyRec): number => - AUTHORING_RULES.reduce((n, rule) => (skip.includes(rule.name) ? n : n + rule.run(stack, {}).length), 0); - expect(count(collection.stack(members(junk))) - count(control())).toBe(RESIDUAL_INVENTED[key] ?? 0); + const controlOutcomes = sweep(`${collection.label} · control`, control); + expect(countOf(withJunk(), skip) - countOf(controlOutcomes, skip)).toBe(RESIDUAL_INVENTED[key] ?? 0); }); }); }); diff --git a/packages/lint/src/runtime-lazy-deps.test.ts b/packages/lint/src/runtime-lazy-deps.test.ts index 2cbe3abb70c..8f3928676ed 100644 --- a/packages/lint/src/runtime-lazy-deps.test.ts +++ b/packages/lint/src/runtime-lazy-deps.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — whole-package import: the in-process cases import the @objectstack/lint/runtime entry cold, which reaches the whole gate graph, and the built-dist probes each spawn node once to cold-load it; slowest CI reading 13.05 s (run 40200279306, refresh PR #25960's window (hourly runs 40200279306 to 40272142430)) x 1.25, rounded up // // The kernel boot-path contract for `@objectstack/lint/runtime` (#4463). // diff --git a/packages/spec/scripts/build-schemas-check-mode.anchor.test.ts b/packages/spec/scripts/build-schemas-check-mode.anchor.test.ts index a65944ef981..c6109cb83d2 100644 --- a/packages/spec/scripts/build-schemas-check-mode.anchor.test.ts +++ b/packages/spec/scripts/build-schemas-check-mode.anchor.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 140 — spawns the real producer once per single-cause fixture: every case runs the real schema generator (build-schemas.ts --check) in its own sandbox git tree, one fixture per cause; measured 2026-10-23 on its first CI reading after #25372 split build-schemas-check-mode.test.ts (run 39963105499, Test Core, head dce4c6406): 104.13 s x 1.25, rounded up; one reading only, its wall tracks the run's load, so re-measure once the window has more +// test-budget: 190 — spawns the real producer once per single-cause fixture: every case runs the real schema generator (build-schemas.ts --check) in its own sandbox git tree, one fixture per cause, and one generator run is about 7 s of JSON Schema emission and spec module load before any assertion; re-measured 2026-10-26 over the 10 executed readings in refresh PR #25960's window (hourly runs 40200279306 to 40272142430), slowest 144.97 s (run 40204927516) x 1.25, rounded up // // The in-tree anchor: it carries the deletion gate offline (#5235), and only // --update-base moves it (#5358). diff --git a/packages/spec/scripts/build-schemas-check-mode.manifest.test.ts b/packages/spec/scripts/build-schemas-check-mode.manifest.test.ts index 58d5a00ee3c..eb011a9cb6b 100644 --- a/packages/spec/scripts/build-schemas-check-mode.manifest.test.ts +++ b/packages/spec/scripts/build-schemas-check-mode.manifest.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 180 — spawns the real producer once per single-cause fixture: every case runs the real schema generator (build-schemas.ts --check) in its own sandbox git tree, one fixture per cause; measured 2026-10-23 on its first CI reading after #25372 split build-schemas-check-mode.test.ts (run 39963105499, Test Core, head dce4c6406): 140.73 s x 1.25, rounded up; one reading only, its wall tracks the run's load, so re-measure once the window has more +// test-budget: 290 — spawns the real producer once per single-cause fixture: every case runs the real schema generator (build-schemas.ts --check) in its own sandbox git tree, one fixture per cause, and one generator run is about 7 s of JSON Schema emission and spec module load before any assertion; re-measured 2026-10-26 over the 10 executed readings in refresh PR #25960's window (hourly runs 40200279306 to 40272142430), slowest 226.64 s (run 40204927516) x 1.25, rounded up // // The manifest ratchet and its neighbours: a check reports, it does not write (#4711); // an export that never published must be declared (#16431); the output clean spares a @@ -557,7 +557,7 @@ describe('build-schemas.ts — the output clean spares a sibling generator (#537 describe('build-schemas.ts — the bundle counts the definitions it carries (#12588)', () => { const OUT = () => path.join(sandbox, 'json-schema'); - beforeEach(() => { + beforeAll(() => { // A current, self-consistent tree, so the run exits 0 and these assertions // are about the bundle rather than about some ratchet upstream of it. seedManifest((s) => s); @@ -566,11 +566,20 @@ describe('build-schemas.ts — the bundle counts the definitions it carries (#12 seedSurfaceBase(tip, (k) => k); }); + /** + * One generation for the block. Both cases read the same fixture and only + * READ what the run printed and wrote, so the first case to need it runs the + * generator and the other reads that run, the way + * `openapi-self-consistency.test.ts` shares its shipped run. + */ + let generation: ReturnType | undefined; + const generated = (): ReturnType => (generation ??= run([])); + it( 'writes x-schema-count equal to its own $defs size, and to the files on disk', { timeout: SPAWN_TIMEOUT_MS }, () => { - const { status, output } = run([]); + const { status, output } = generated(); expect(status).toBe(0); const bundle = JSON.parse( @@ -605,7 +614,7 @@ describe('build-schemas.ts — the bundle counts the definitions it carries (#12 'accounts for every emit the definition count does not include', { timeout: SPAWN_TIMEOUT_MS }, () => { - const { status, output } = run([]); + const { status, output } = generated(); expect(status).toBe(0); const bundle = JSON.parse( diff --git a/packages/spec/scripts/build-schemas-check-mode.retired-keys.test.ts b/packages/spec/scripts/build-schemas-check-mode.retired-keys.test.ts index 335e1352391..3bd36b9bdbe 100644 --- a/packages/spec/scripts/build-schemas-check-mode.retired-keys.test.ts +++ b/packages/spec/scripts/build-schemas-check-mode.retired-keys.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 190 — spawns the real producer once per single-cause fixture: every case runs the real schema generator (build-schemas.ts --check) in its own sandbox git tree, one fixture per cause; measured 2026-10-23 on its first CI reading after #25372 split build-schemas-check-mode.test.ts (run 39963105499, Test Core, head dce4c6406): 147.5 s x 1.25, rounded up; one reading only, its wall tracks the run's load, so re-measure once the window has more +// test-budget: 310 — spawns the real producer once per single-cause fixture: every case runs the real schema generator (build-schemas.ts --check) in its own sandbox git tree, one fixture per cause, and one generator run is about 7 s of JSON Schema emission and spec module load before any assertion; re-measured 2026-10-26 over the 10 executed readings in refresh PR #25960's window (hourly runs 40200279306 to 40272142430), slowest 241.05 s (run 40204927516) x 1.25, rounded up // // RETIRED_KEYS_BY_MAJOR rows, judged by exact key: check (b) matches the retired key, // not its leaf (#4659); a nested retirement row is judged, not ignored (#17969). diff --git a/packages/spec/scripts/openapi-self-consistency.test.ts b/packages/spec/scripts/openapi-self-consistency.test.ts index 98992a2afb5..7bb806a6c79 100644 --- a/packages/spec/scripts/openapi-self-consistency.test.ts +++ b/packages/spec/scripts/openapi-self-consistency.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: the subprocess group runs the real build-openapi.ts once per distinct fixture, the shipped source and each mutation, and cases that share a fixture read its one run; slowest CI reading 10.48 s (run 40204927516, refresh PR #25960's window (hourly runs 40200279306 to 40272142430)) x 1.25, rounded up // // Pins the self-consistency gate on the generated OpenAPI document (#5168). // diff --git a/packages/spec/scripts/pure-schema-construction.test.ts b/packages/spec/scripts/pure-schema-construction.test.ts index b741f8a654b..4b3ea02fa2b 100644 --- a/packages/spec/scripts/pure-schema-construction.test.ts +++ b/packages/spec/scripts/pure-schema-construction.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: each build case runs the real tsup build (in-process, with the package's own main options and its esbuild plugin) once over the entry or fixture it pins, and the ./shared case bundles two consumers of that output; slowest CI reading 11.86 s (run 40214410982, refresh PR #25960's window (hourly runs 40200279306 to 40272142430)) x 1.25, rounded up // // The `pure-schema-construction` build plugin (`tsup.config.ts`) marks real // calls only (#20686). Its earlier line-prefix rule also rewrote a marked name diff --git a/packages/spec/scripts/root-entry-type-nameability.pin.test.ts b/packages/spec/scripts/root-entry-type-nameability.pin.test.ts index 54fda77e40d..f1ab06ba780 100644 --- a/packages/spec/scripts/root-entry-type-nameability.pin.test.ts +++ b/packages/spec/scripts/root-entry-type-nameability.pin.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — compiles the package type graph once: the consumer program is one tsc run over the BUILT root entry's declarations, and the canary is one more tsc run over a one-line fixture; slowest CI reading 12.92 s (run 40214410982, refresh PR #25960's window (hourly runs 40200279306 to 40272142430)) x 1.25, rounded up /** * Root-entry nameability pin (#11350) — the consumer shape, verbatim. diff --git a/packages/spec/scripts/upgrade-guide-docs.test.ts b/packages/spec/scripts/upgrade-guide-docs.test.ts index 961f15337ca..15fca5bf1ea 100644 --- a/packages/spec/scripts/upgrade-guide-docs.test.ts +++ b/packages/spec/scripts/upgrade-guide-docs.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 30 — spawns the real producer once per single-cause fixture: the last block spawns the real build-upgrade-guide.ts once per mode or usage error it pins (twice for the determinism pin), and the rest are in-process; slowest CI reading 19.7 s (hourly run 38798542648) x 1.25, rounded up +// test-budget: 40 — spawns the real producer once per single-cause fixture: the last block spawns the real build-upgrade-guide.ts once per mode or usage error it pins (twice for the determinism pin), and the rest are in-process; re-measured 2026-10-26 over the 8 executed readings in refresh PR #25960's window (hourly runs 40200279306 to 40272142430), slowest 30.07 s (run 40214410982) x 1.25, rounded up // // Pins the protocol upgrade guide's public address (#22449 B′, condition 2): // one docs-site page per protocol major plus an index, generated at docs build diff --git a/packages/spec/src/eager-entry-import.test.ts b/packages/spec/src/eager-entry-import.test.ts index 982873e6251..6801df1de5b 100644 --- a/packages/spec/src/eager-entry-import.test.ts +++ b/packages/spec/src/eager-entry-import.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 30 — whole-package import: every published entry of the package is imported first in its own fresh tsx child process, eagerly, plus two unflagged controls; slowest CI reading 19.9 s (hourly run 38657693085) x 1.25, rounded up +// test-budget: 60 — whole-package import: every published entry of the package is imported first in its own fresh tsx child process, eagerly, plus two unflagged controls; re-measured 2026-10-26 over the 8 executed readings in refresh PR #25960's window (hourly runs 40200279306 to 40272142430), slowest 41.61 s (run 40266979145) x 1.25, rounded up /** * Every published entry of `@objectstack/spec` imports cleanly as a process's diff --git a/packages/spec/src/ui/app.test.ts b/packages/spec/src/ui/app.test.ts index 2b185dd573a..d47b2dcc852 100644 --- a/packages/spec/src/ui/app.test.ts +++ b/packages/spec/src/ui/app.test.ts @@ -1595,7 +1595,14 @@ describe('unknown keys are rejected, not stripped', () => { // without a build. it('the retired gates are gone from the TYPE, not only from the parse (compiler API)', () => { const entry = resolve(dirname(fileURLToPath(import.meta.url)), 'app.zod.ts'); + // The program reads what the type needs and nothing else: the ES2022 lib + // rather than the ES5 default's DOM declarations, and no ambient `@types` + // packages. Neither is an input of `app.zod.ts`, and guard #2 below still + // reads the full member list off the resolved type. const program = ts.createProgram([entry], { + target: ts.ScriptTarget.ES2022, + lib: ['lib.es2022.d.ts'], + types: [], module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, strict: true, From ea742f51b6ca36041f03acc7e4165cffa28f15c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:34:49 +0000 Subject: [PATCH 2/2] test(spec): declare ui/app.test.ts's re-measured budget The compiler-API case's cheaper program brings the file down about 12% locally, which still leaves the window's 11.90 s CI reading near the 10 s line. Declare 20 s from that reading (x 1.25, rounded up). Claude-Session: https://claude.ai/code/session_01LpCAbWQuprShJmC2GJXTp7 Co-authored-by: Claude --- packages/spec/src/ui/app.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/spec/src/ui/app.test.ts b/packages/spec/src/ui/app.test.ts index d47b2dcc852..5dd52a847a5 100644 --- a/packages/spec/src/ui/app.test.ts +++ b/packages/spec/src/ui/app.test.ts @@ -1,3 +1,4 @@ +// test-budget: 20 — compiles the package type graph once: the compiler-API case builds one ts program over app.zod.ts and the spec type graph it imports, and that one program is most of the file's time (the other cases are in-process parses); slowest CI reading 11.90 s (run 40204927516, refresh PR #25960's window (hourly runs 40200279306 to 40272142430)) x 1.25, rounded up import { describe, it, expect } from 'vitest'; // Loaded here, at module top, not inside the compiler-API case below: a // clocked window measures behaviour, never loading, and `typescript` alone is