From fd5cd1b058dd549b5f5c6d707edeaebcc21c1165 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:50:47 +0800 Subject: [PATCH] test(acceptance): run loader ownership once through stdio MCP (#6354) Keep every competing-loader source on the shared stdio session and compare three renderer outcomes with the compiled CLI. Retire the Early, Middle, and Late shards, and seed their historical duration onto the consolidated file. Co-authored-by: Cursor Agent Co-authored-by: morluto --- docs/testing.md | 21 ++-- .../javascriptLiteralLoaderOwnership.test.ts | 29 +++++ .../javascriptLiteralLoaderOwnership.ts | 108 ++++++++++++------ ...ascriptLiteralLoaderOwnershipEarly.test.ts | 29 ----- ...vascriptLiteralLoaderOwnershipLate.test.ts | 18 --- ...scriptLiteralLoaderOwnershipMiddle.test.ts | 18 --- .../filesystem/ciTestSequencer.test.ts | 48 ++++++-- tests/ciTestSequencer.ts | 50 +++++--- 8 files changed, 188 insertions(+), 133 deletions(-) create mode 100644 tests/acceptance/applications/javascriptLiteralLoaderOwnership.test.ts delete mode 100644 tests/acceptance/applications/javascriptLiteralLoaderOwnershipEarly.test.ts delete mode 100644 tests/acceptance/applications/javascriptLiteralLoaderOwnershipLate.test.ts delete mode 100644 tests/acceptance/applications/javascriptLiteralLoaderOwnershipMiddle.test.ts diff --git a/docs/testing.md b/docs/testing.md index a148f37fcf..92aee89458 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1493,14 +1493,19 @@ fallback weight for new files, rather than dividing only by file count. Each file keeps its full duration on one worker. Extra workers overlap other files in the same sequence group, and `process-boundary` runs after that group. History published before the competing-loader split still names only -`javascriptLiteralPercentPaths.test.ts`. Until that artifact records one of -the three new files, the sequencer assigns that recorded duration by the -pre-split case counts: 54/192 to the remaining file and 46/192 to each new -file. Every discovered file still belongs to exactly one shard; worker limits -and project ordering remain unchanged. Fast qualification keeps eight built -shards without coverage. The merge job rejects successful reports with missing, -unexpected, or duplicate portable files. Coverage instrumentation and aggregate -thresholds remain enforced in full qualification. +`javascriptLiteralPercentPaths.test.ts`. Until an artifact records +`javascriptLiteralLoaderOwnership.test.ts`, the sequencer assigns that +recorded duration by the pre-split case counts: 54/192 stays with the +percent-path file and 138/192 goes to the consolidated loader file. History +that names the retired Early, Middle, and Late shards sums those durations +onto the consolidated file instead, and scales a partial set as three equal +shares. That weight is the old dual-surface cost. The demoted file runs every +loader once through the shared stdio MCP session, so the first schedule +overestimates it. Every discovered file still belongs to exactly one shard; +worker limits and project ordering remain unchanged. Fast qualification keeps +eight built shards without coverage. The merge job rejects successful reports +with missing, unexpected, or duplicate portable files. Coverage instrumentation +and aggregate thresholds remain enforced in full qualification. Missing or unusable history falls back to Vitest's default assignment. Only timing data is reused; live provider results are never cached. Turbo outputs are restored by task hash, with trusted default-branch runs as cache writers and PRs diff --git a/tests/acceptance/applications/javascriptLiteralLoaderOwnership.test.ts b/tests/acceptance/applications/javascriptLiteralLoaderOwnership.test.ts new file mode 100644 index 0000000000..4f225f0c13 --- /dev/null +++ b/tests/acceptance/applications/javascriptLiteralLoaderOwnership.test.ts @@ -0,0 +1,29 @@ +import { expect, it } from "vitest"; + +import { cliTest } from "../../support/cli/cliFixture.js"; +import { + assertCompetingLoaderCliParity, + assertCompetingLoaderOwnership, + cliParityLoaders, + competingLoaderNames, +} from "./javascriptLiteralLoaderOwnership.js"; + +it("keeps compiled CLI parity inside the competing-loader matrix", () => { + expect(new Set(cliParityLoaders).size).toBe(cliParityLoaders.length); + for (const loader of cliParityLoaders) + expect(competingLoaderNames).toContain(loader); +}); + +cliTest.for(competingLoaderNames.map((loader) => ({ loader })))( + "preserves competing loader ownership for $loader through stdio MCP", + async ({ loader }, { cli }) => { + await assertCompetingLoaderOwnership(loader, cli); + }, +); + +cliTest.for(cliParityLoaders.map((loader) => ({ loader })))( + "matches compiled CLI analysis for competing loader $loader", + async ({ loader }, { cli }) => { + await assertCompetingLoaderCliParity(loader, cli); + }, +); diff --git a/tests/acceptance/applications/javascriptLiteralLoaderOwnership.ts b/tests/acceptance/applications/javascriptLiteralLoaderOwnership.ts index 7f9e6ae520..a672fe7092 100644 --- a/tests/acceptance/applications/javascriptLiteralLoaderOwnership.ts +++ b/tests/acceptance/applications/javascriptLiteralLoaderOwnership.ts @@ -244,44 +244,27 @@ export const competingLoaderNames = [ "single percent main", ]; -const groupSize = Math.ceil(competingLoaderNames.length / 3); -/** Three disjoint groups so Vitest can run the matrix on separate workers. */ -export const competingLoaderGroups = [ - competingLoaderNames.slice(0, groupSize), - competingLoaderNames.slice(groupSize, groupSize * 2), - competingLoaderNames.slice(groupSize * 2), +/** + * Compiled CLI parity for this stateless matrix. These three loaders are the + * distinct renderer outcomes: unresolved, the first application page, and the + * percent-encoded second page. Both surfaces call + * analyzeJavaScriptApplication; parity compares those normalized results. + * Every loader, including these, still runs through the shared stdio session. + */ +export const cliParityLoaders = [ + "module.require", + "earlier reassigned module alias", + "single percent main", ] as const; -/** One public CLI or MCP observation for a competing loader fixture. */ -export const assertCompetingLoaderOwnership = async ( - surface: "CLI" | "stdio MCP", - loader: string, - cli: TestCli, -): Promise => { +type CompetingLoaderGraph = Awaited< + ReturnType +>["graph"]; + +/** Write one competing-loader application and return its directory. */ +const writeCompetingLoaderFixture = async (loader: string): Promise => { const root = await createTestTempDirectory("rea-loader-owner-"); const single = loader === "single percent main"; - const inert = [ - "native array assignment local loader", - "exports assignment local loader", - "rest assignment local loader", - "excluded assignment local loader", - "inactive default assignment local loader", - "overwritten assignment local loader", - "local assigned loader", - "overwritten local loader", - "invalid absolute ESM", - "invalid absolute dynamic import", - "builtin alias", - "electron alias", - "electron main alias", - "inactive fallback", - "blocked alias", - "local module", - "projected module", - "resolve-only", - "missing wildcard alias", - "earlier reassigned module alias", - ].includes(loader); const absolute = loader === "absolute main"; const main = fixtureMainPath(loader); const imports = fixtureImports(loader); @@ -307,7 +290,38 @@ export const assertCompetingLoaderOwnership = async ( "shared/index.html": "source-relative decoy", "index.html": "artifact-root decoy", }); - const { graph } = await analyzeJavaScriptDirectory(surface, cli, root); + return root; +}; + +/** Ownership facts for one competing loader, independent of transport. */ +const expectCompetingLoaderGraph = ( + loader: string, + graph: CompetingLoaderGraph, +): void => { + const single = loader === "single percent main"; + const main = fixtureMainPath(loader); + const inert = [ + "native array assignment local loader", + "exports assignment local loader", + "rest assignment local loader", + "excluded assignment local loader", + "inactive default assignment local loader", + "overwritten assignment local loader", + "local assigned loader", + "overwritten local loader", + "invalid absolute ESM", + "invalid absolute dynamic import", + "builtin alias", + "electron alias", + "electron main alias", + "inactive fallback", + "blocked alias", + "local module", + "projected module", + "resolve-only", + "missing wildcard alias", + "earlier reassigned module alias", + ].includes(loader); if (loader.startsWith("Windows ")) expect(graph.edges).toEqual( expect.arrayContaining([ @@ -399,3 +413,27 @@ export const assertCompetingLoaderOwnership = async ( .map(({ properties }) => properties.resolved_path), ).toEqual(target === null ? [] : [target]); }; + +/** Observe one competing loader through the shared stdio MCP session. */ +export const assertCompetingLoaderOwnership = async ( + loader: string, + cli: TestCli, +): Promise => { + const root = await writeCompetingLoaderFixture(loader); + const { graph } = await analyzeJavaScriptDirectory("stdio MCP", cli, root); + expectCompetingLoaderGraph(loader, graph); +}; + +/** + * Compare one compiled CLI analysis with the shared stdio MCP analysis of the + * same fixture. Ownership assertions stay on the MCP matrix. + */ +export const assertCompetingLoaderCliParity = async ( + loader: string, + cli: TestCli, +): Promise => { + const root = await writeCompetingLoaderFixture(loader); + const mcp = await analyzeJavaScriptDirectory("stdio MCP", cli, root); + const compiled = await analyzeJavaScriptDirectory("CLI", cli, root); + expect(compiled).toEqual(mcp); +}; diff --git a/tests/acceptance/applications/javascriptLiteralLoaderOwnershipEarly.test.ts b/tests/acceptance/applications/javascriptLiteralLoaderOwnershipEarly.test.ts deleted file mode 100644 index fe51424ee7..0000000000 --- a/tests/acceptance/applications/javascriptLiteralLoaderOwnershipEarly.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, it } from "vitest"; - -import { cliTest } from "../../support/cli/cliFixture.js"; -import { - assertCompetingLoaderOwnership, - competingLoaderGroups, - competingLoaderNames, -} from "./javascriptLiteralLoaderOwnership.js"; - -const loaders = competingLoaderGroups[0]; - -it("partitions every competing loader across the three ownership files", () => { - const assigned = competingLoaderGroups.flat(); - expect(assigned).toEqual(competingLoaderNames); - expect(new Set(assigned).size).toBe(competingLoaderNames.length); - for (const group of competingLoaderGroups) - expect(group.length).toBeGreaterThan(0); -}); - -cliTest.for( - (["CLI", "stdio MCP"] as const).flatMap((surface) => - loaders.map((loader) => ({ surface, loader })), - ), -)( - "preserves competing loader ownership for $loader through $surface", - async ({ surface, loader }, { cli }) => { - await assertCompetingLoaderOwnership(surface, loader, cli); - }, -); diff --git a/tests/acceptance/applications/javascriptLiteralLoaderOwnershipLate.test.ts b/tests/acceptance/applications/javascriptLiteralLoaderOwnershipLate.test.ts deleted file mode 100644 index 2f503d5e2a..0000000000 --- a/tests/acceptance/applications/javascriptLiteralLoaderOwnershipLate.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { cliTest } from "../../support/cli/cliFixture.js"; -import { - assertCompetingLoaderOwnership, - competingLoaderGroups, -} from "./javascriptLiteralLoaderOwnership.js"; - -const loaders = competingLoaderGroups[2]; - -cliTest.for( - (["CLI", "stdio MCP"] as const).flatMap((surface) => - loaders.map((loader) => ({ surface, loader })), - ), -)( - "preserves competing loader ownership for $loader through $surface", - async ({ surface, loader }, { cli }) => { - await assertCompetingLoaderOwnership(surface, loader, cli); - }, -); diff --git a/tests/acceptance/applications/javascriptLiteralLoaderOwnershipMiddle.test.ts b/tests/acceptance/applications/javascriptLiteralLoaderOwnershipMiddle.test.ts deleted file mode 100644 index 7ebdee179f..0000000000 --- a/tests/acceptance/applications/javascriptLiteralLoaderOwnershipMiddle.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { cliTest } from "../../support/cli/cliFixture.js"; -import { - assertCompetingLoaderOwnership, - competingLoaderGroups, -} from "./javascriptLiteralLoaderOwnership.js"; - -const loaders = competingLoaderGroups[1]; - -cliTest.for( - (["CLI", "stdio MCP"] as const).flatMap((surface) => - loaders.map((loader) => ({ surface, loader })), - ), -)( - "preserves competing loader ownership for $loader through $surface", - async ({ surface, loader }, { cli }) => { - await assertCompetingLoaderOwnership(surface, loader, cli); - }, -); diff --git a/tests/boundary/filesystem/ciTestSequencer.test.ts b/tests/boundary/filesystem/ciTestSequencer.test.ts index dc0945779a..7705d181d5 100644 --- a/tests/boundary/filesystem/ciTestSequencer.test.ts +++ b/tests/boundary/filesystem/ciTestSequencer.test.ts @@ -11,7 +11,7 @@ import { parseTestDurations, readTestDurations, scheduleWall, - seedSplitLoaderDurations, + seedLoaderOwnershipDurations, } from "../../ciTestSequencer.js"; describe("CI test history", () => { @@ -86,38 +86,68 @@ describe("CI test history", () => { }); }); -describe("split loader history", () => { +describe("loader ownership history", () => { const source = "tests/acceptance/applications/javascriptLiteralPercentPaths.test.ts"; + const consolidated = + "tests/acceptance/applications/javascriptLiteralLoaderOwnership.test.ts"; const parts = [ "tests/acceptance/applications/javascriptLiteralLoaderOwnershipEarly.test.ts", "tests/acceptance/applications/javascriptLiteralLoaderOwnershipMiddle.test.ts", "tests/acceptance/applications/javascriptLiteralLoaderOwnershipLate.test.ts", ]; - it("divides a pre-split duration across the four current files", () => { - const seeded = seedSplitLoaderDurations( + it("assigns the pre-split loader share to the consolidated file", () => { + const seeded = seedLoaderOwnershipDurations( new Map([ [source, 192_000], ["tests/other.test.ts", 10], ]), ); expect(seeded.get(source)).toBe(54_000); - for (const path of parts) expect(seeded.get(path)).toBe(46_000); + expect(seeded.get(consolidated)).toBe(138_000); + for (const path of parts) expect(seeded.has(path)).toBe(false); expect(seeded.get("tests/other.test.ts")).toBe(10); expect( [...seeded.values()].reduce((sum, duration) => sum + duration, 0), ).toBe(192_000 + 10); }); - it("keeps published durations once any split file is in history", () => { - const recorded = new Map([ + it("sums retired shard durations onto the consolidated file", () => { + const seeded = seedLoaderOwnershipDurations( + new Map([ + [source, 20_000], + [parts[0]!, 30_000], + [parts[1]!, 40_000], + [parts[2]!, 50_000], + ["tests/other.test.ts", 10], + ]), + ); + expect(seeded.get(source)).toBe(20_000); + expect(seeded.get(consolidated)).toBe(120_000); + expect(seeded.get("tests/other.test.ts")).toBe(10); + }); + + it("scales a partial retired shard as three equal shares", () => { + const seeded = seedLoaderOwnershipDurations( + new Map([ + [source, 20_000], + [parts[0]!, 30_000], + ]), + ); + expect(seeded.get(source)).toBe(20_000); + expect(seeded.get(consolidated)).toBe(90_000); + }); + + it("keeps a published consolidated duration", () => { + const recorded = new Map([ [source, 20_000], [parts[0]!, 30_000], + [consolidated, 12_000], ]); - expect(seedSplitLoaderDurations(recorded)).toEqual(recorded); + expect(seedLoaderOwnershipDurations(recorded)).toEqual(recorded); expect( - seedSplitLoaderDurations(new Map([["tests/other.test.ts", 10]])), + seedLoaderOwnershipDurations(new Map([["tests/other.test.ts", 10]])), ).toEqual(new Map([["tests/other.test.ts", 10]])); }); }); diff --git a/tests/ciTestSequencer.ts b/tests/ciTestSequencer.ts index cb56f53d01..47d5eec9aa 100644 --- a/tests/ciTestSequencer.ts +++ b/tests/ciTestSequencer.ts @@ -62,8 +62,10 @@ export async function readTestDurations( /** * Case counts from the competing-loader file before it was split: 54 cases - * stayed in the original file and 46 moved into each of the three new files. - * History that still names only the original path is that combined cost. + * stayed in the percent-path file and 46 ran in each of the three dual-surface + * shards. History that still names only the original path is that combined + * cost. The shards are retired; one file now runs every loader through stdio + * MCP plus a few compiled CLI parity cases. */ const LOADER_HISTORY_SOURCE = "tests/acceptance/applications/javascriptLiteralPercentPaths.test.ts"; @@ -72,6 +74,8 @@ const LOADER_HISTORY_PARTS = [ "tests/acceptance/applications/javascriptLiteralLoaderOwnershipMiddle.test.ts", "tests/acceptance/applications/javascriptLiteralLoaderOwnershipLate.test.ts", ] as const; +const LOADER_HISTORY_CONSOLIDATED = + "tests/acceptance/applications/javascriptLiteralLoaderOwnership.test.ts"; const LOADER_HISTORY_REMAINDER_CASES = 54; const LOADER_HISTORY_PART_CASES = 46; const LOADER_HISTORY_CASES = @@ -79,28 +83,42 @@ const LOADER_HISTORY_CASES = LOADER_HISTORY_PART_CASES * LOADER_HISTORY_PARTS.length; /** - * Give the split loader files a share of the pre-split duration. Once history - * records any of the new paths, their published durations stand. + * Charge the consolidated loader file until its own duration is published. + * A pre-split percent-path duration keeps 54/192 and gives 138/192 to the + * consolidated file. Recorded Early/Middle/Late durations are summed instead, + * and a partial set is scaled as three equal shares. That weight is the old + * dual-surface cost, so the first schedule overestimates the demoted file. + * A published consolidated duration stands, including beside retired shards. */ -export function seedSplitLoaderDurations( +export function seedLoaderOwnershipDurations( durations: ReadonlyMap, ): Map { + if (durations.has(LOADER_HISTORY_CONSOLIDATED)) return new Map(durations); + const recordedParts = LOADER_HISTORY_PARTS.flatMap((path) => { + const duration = durations.get(path); + return duration === undefined ? [] : [duration]; + }); + if (recordedParts.length > 0) { + const seeded = new Map(durations); + const sum = recordedParts.reduce((total, duration) => total + duration, 0); + seeded.set( + LOADER_HISTORY_CONSOLIDATED, + (sum * LOADER_HISTORY_PARTS.length) / recordedParts.length, + ); + return seeded; + } const combined = durations.get(LOADER_HISTORY_SOURCE); - if ( - combined === undefined || - LOADER_HISTORY_PARTS.some((path) => durations.has(path)) - ) - return new Map(durations); + if (combined === undefined) return new Map(durations); const seeded = new Map(durations); + const loaderCases = LOADER_HISTORY_CASES - LOADER_HISTORY_REMAINDER_CASES; seeded.set( LOADER_HISTORY_SOURCE, (combined * LOADER_HISTORY_REMAINDER_CASES) / LOADER_HISTORY_CASES, ); - for (const path of LOADER_HISTORY_PARTS) - seeded.set( - path, - (combined * LOADER_HISTORY_PART_CASES) / LOADER_HISTORY_CASES, - ); + seeded.set( + LOADER_HISTORY_CONSOLIDATED, + (combined * loaderCases) / LOADER_HISTORY_CASES, + ); return seeded; } @@ -247,7 +265,7 @@ export class CiTestSequencer extends BaseSequencer { const root = this.ctx.config.root; const recorded = await readTestDurations(historyPath, root); if (!recorded) return super.shard(files); - const durations = seedSplitLoaderDurations(recorded); + const durations = seedLoaderOwnershipDurations(recorded); const weighted = files.map((item) => { const path = relative(root, resolve(root, item.moduleId))