Merge remote-tracking branch 'origin/main' into codex/audit-continuation-20261020-refresh20

This commit is contained in:
morluto committed 2026-10-21 03:57:03 +08:00
commit 6389e50337
9 files changed
+779 -468

No files matched your search

+5 -3
View File
@@ -302,9 +302,11 @@ jobs:
# every shard's diagnostics and coverage. Passing PRs run the full suite.
fail-fast: ${{ github.event_name == 'pull_request' && needs.changes.outputs.tier == 'fast' }}
matrix:
# Fast feedback spends the same six test jobs on one source group and
# five built groups; full qualification retains six complete shards.
shard: ${{ fromJSON(needs.changes.outputs.tier == 'fast' && '["1/5","2/5","3/5","4/5","5/5"]' || '["1/6","2/6","3/6","4/6","5/6","6/6"]') }}
# Fast feedback uses one source group and eight built groups so the
# slowest built shard job stays inside a ten-minute execution budget.
# Classification, build, and report merge sit outside that job.
# Full qualification retains six complete shards with coverage.
shard: ${{ fromJSON(needs.changes.outputs.tier == 'fast' && '["1/8","2/8","3/8","4/8","5/8","6/8","7/8","8/8"]' || '["1/6","2/6","3/6","4/6","5/6","6/6"]') }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+43 -17
View File
@@ -471,8 +471,8 @@ partial diagnostics and preflights do not establish complete mutation outcomes
or qualify the release revision for mutation coverage.
Fast CI starts the source-only projects after classification, independently of
build and static checks, and runs five built-dependent shards after the build.
The six test jobs still select every portable test. Full qualification keeps six
build and static checks, and runs eight built-dependent shards after the build.
Those nine test jobs still select every portable test. Full qualification keeps six
complete Vitest shards with coverage. Neither tier retries tests. Canonical
project ownership lives in `scripts/lib/test-projects.mjs`, shared by Vitest,
CI group execution, and development-test preparation. A test importing compiled
@@ -533,11 +533,35 @@ JSON artifact members without executing artifact content. Reports and limits
are retained in `ci-health-diagnostics`; its ten-minute job limit covers the
existing four-minute timing collection plus the bounded diagnostic stages.
Shard-count reduction remains deferred: the available successful timing sample
covers an older inventory, and file phase weights are not job wall time. The
one-source/five-built split retains six test jobs; it makes no hosted speedup
claim. Current complete timings and completed candidate hosted runs are needed
before enabling a smaller adaptive matrix.
Fast built shards target under ten minutes of uncontended shard-job execution.
That figure is the `test-shard` job. The job waits for classification and the
build, and the portable-test merge runs after the slowest shard. On 2026-10-20,
run `39451548945` measured those surrounding jobs with runner queue excluded:
classification 15 seconds, build 49 seconds, and the portable-test merge 29
seconds. Shard 1/5 of that run spent 22 seconds before built Vitest and 3
seconds after it, including `npm ci` at 9 seconds. `node_modules` is about 674
MiB, so publishing it as a shared artifact would transfer more data than that
install saves. The npm download cache stays the setup cache.
File timings from that run put the built suite at 7944 seconds of module time.
The sequencer keeps each file's full duration on one worker. Extra workers
overlap other files in the same sequence group, and `process-boundary` runs
after that group. Scheduling the unsplit inventory onto five shards peaks at
820 seconds of tests. That run's slowest built shard job lasted 925 seconds
(shard 4/5), and shard 1/5 spent 874 seconds in built Vitest, so hosted job
time sits above the file schedule. Six shards of the same inventory peak at
684 seconds of tests. Eight shards, after the 578-second loader file is split
by case count (54 cases stay in that file and 46 move into each of three
files), peak at 513 seconds of tests. With the measured 25-second shard setup,
that shard job is about 538 seconds. Adding the measured classification, build,
and merge makes an uncontended portable critical path of about 631 seconds,
still before the gap between this file schedule and hosted Vitest. Runner queue
is separate and grows when more shard jobs compete for runners.
`process-boundary` stays serial because process-tree sampling shares the host.
Raising the worker cap from two to four on this four-core host cut a six-file
acceptance sample from 96–101 seconds to 71 seconds and did not remove the
single-file floor, so the cap stays two. Full qualification keeps six covered
shards; its longer instrumentation is outside this fast budget.
The separate `CI timings` workflow batches recent completed CI and Release runs
hourly or on manual dispatch. It uploads `ci-job-timings-batch` with raw API pages and
@@ -1130,7 +1154,7 @@ suite, without the source-test shards or native package
lanes. Classification compares the PR head with its merge base, so later
base-branch changes do not expand that scope. Static and portable-test aggregate jobs
remain present and fail if classification fails. Every code change retains the
complete six-shard Linux suite; individual test files are not selected by
complete portable Linux suite; individual test files are not selected by
imports or filenames.
Ordinary PRs also produce an **observational** test-selection plan. The candidate
@@ -1141,7 +1165,7 @@ tests. Shared inputs, unknown paths, missing owner tests, and removed tests fall
back to the complete inventory. Renames account for both paths. Main, scheduled,
manual, and `ci:full` runs always propose the full suite.
All six shards still execute the complete suite. The existing report merge
The source group and every built shard together still execute the complete suite. The existing report merge
compares the proposal with actual module outcomes and retains
`.cache/ci-test-selection/report.json` in `test-results`, including selected and
omitted paths, changed input paths, missed failures, unobserved files, scheduling weights, PR head and
@@ -1223,7 +1247,7 @@ push: changes from failed or cancelled intermediate runs remain in scope.
Missing, untrusted, or non-ancestor history selects the fast Linux x64
baseline: portable checks and the Linux runtime lane. The lookup is
bounded to the last 100 successful push runs; lookup failures broaden selection.
Every code change still selects all six portable shards, without coverage
Every code change still selects the source group and all eight fast built shards, without coverage
instrumentation in this fast tier. Ordinary PR matrices cancel remaining
shards after the first failure; passing PRs execute the complete suite. Main,
`ci:full`, scheduled, manual, and release qualification retain every shard's
@@ -1336,13 +1360,15 @@ producer; newer schedules that reuse qualification without producing timings do
not hide the last full run's artifact.
CI assigns discovered files by historical duration, with a deterministic
fallback weight for new files, rather than dividing only by file count.
`process-boundary` files are weighted at their full duration because that
project runs files serially. Other projects are weighted at half their file
duration, matching the two-worker cap, so a serial file does not look as cheap
as a file that overlaps with another. Every
discovered file still belongs to exactly one shard; worker limits and project
ordering remain unchanged. Fast qualification keeps five built shards without
coverage. The merge job rejects successful reports with missing,
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.
Missing or unusable history falls back to Vitest's default assignment. Only
@@ -0,0 +1,401 @@
import { expect } from "vitest";
import { aliasSelectionBudgetSource } from "../../fixtures/javascriptAliasSelectionBudget.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import type { TestCli } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
const fixtureImports = (loader: string) => {
switch (loader) {
case "pattern alias":
return { "#window/*": "./proxy/*.cjs" };
case "recursive alias":
return { "#entry": "#window", "#window": "./proxy/window.cjs" };
case "external-package alias":
return { "#entry": "fixture-window" };
case "builtin alias":
return { "#entry": "fs" };
case "electron alias":
return { "#entry": "electron" };
case "electron main alias":
return { "#entry": "electron/main" };
default:
return { "#entry": null };
}
};
const unprovenLoaderSources: Readonly<Record<string, string>> = {
"custom iterator assignment module loader":
'const values = { *[Symbol.iterator]() { yield module; } }; const holder = {}; [holder.loader] = values; holder.loader.require("../shared/window.cjs");',
"replaced iterator assignment module loader":
'const values = [{}]; values[Symbol.iterator] = function* () { yield module; }; const holder = {}; [holder.loader] = values; holder.loader.require("../shared/window.cjs");',
"custom iterator rest declaration module loader":
'const values = { *[Symbol.iterator]() { yield module; } }; const [...holder] = values; holder[0].require("../shared/window.cjs");',
"native array assignment local loader":
'const holder = { loader: module }; [holder.loader] = [{ require() {} }]; holder.loader.require("../shared/window.cjs");',
"logical rewrite assignment module loader":
'const holder = {}; holder.loader ??= module; holder.loader.require = replacement; require("../shared/window.cjs");',
"object rest declaration module loader":
'const { ...holder } = { loader: module }; holder.loader.require("../shared/window.cjs");',
"array rest declaration module loader":
'const [...holder] = [module]; holder[0].require("../shared/window.cjs");',
"object assignment module loader":
'const holder = {}; ({ loader: holder.loader } = { loader: module }); holder.loader.require("../shared/window.cjs");',
"array assignment module loader":
'const holder = {}; [holder.loader] = [module]; holder.loader.require("../shared/window.cjs");',
"nested assignment module loader":
'const holder = {}; ({ inner: { loader: holder.loader } } = { inner: { loader: module } }); holder.loader.require("../shared/window.cjs");',
"default assignment module loader":
'const holder = {}; ({ loader: holder.loader = module } = {}); holder.loader.require("../shared/window.cjs");',
"rest array assignment module loader":
'const holder = {}; let ignored; [ignored, ...holder.loaders] = [{}, module]; holder.loaders[0].require("../shared/window.cjs");',
"delegate assignment module loader":
'const holder = {}; ({ load: holder.load } = { load: module.require }); holder.load.call(module, "../shared/window.cjs");',
"rewrite assignment module loader":
'const holder = {}; ({ loader: holder.loader } = { loader: module }); holder.loader.require = replacement; require("../shared/window.cjs");',
"exports assignment local loader":
'const holder = {}; ({ exports: holder.loader } = module); holder.loader.require("../shared/window.cjs");',
"rest assignment local loader":
'const holder = {}; ({ ...holder.loader } = module); holder.loader.require("../shared/window.cjs");',
"excluded assignment local loader":
'const holder = {}; let ignored; ({ loader: ignored, ...holder.copy } = { loader: module }); holder.copy.loader.require("../shared/window.cjs");',
"inactive default assignment local loader":
'const holder = {}; ({ loader: holder.loader = module } = { loader: { require() {} } }); holder.loader.require("../shared/window.cjs");',
"overwritten assignment local loader":
'const holder = { loader: module }; ({ loader: holder.loader } = { loader: { require() {} } }); holder.loader.require("../shared/window.cjs");',
"bounded alias selection": aliasSelectionBudgetSource(),
"captured module delegate":
'const load = module.require; load.call(module, "../shared/window.cjs");',
"destructured module delegate":
'const { require: load } = module; load.apply(module, ["../shared/window.cjs"]);',
"selected local delegate slots":
'const table = { x: {}, y: {}, z: module }; const target = flag ? table.x : table.y; target.require = replacement; require("../shared/window.cjs");',
"local assigned loader":
'const holder = {}; holder.loader = { require() {} }; holder.loader.require("../shared/window.cjs");',
"overwritten local loader":
'const holder = { loader: module }; holder.loader = { require() {} }; holder.loader.require("../shared/window.cjs");',
"normalized absolute ESM": 'import "/discard%2F/../shared/window.cjs";',
"normalized absolute dynamic import":
'import("/discard%5C/../shared/window.cjs");',
"rewritten CommonJS delegate":
'module.require = replacement; require("../shared/window.cjs");',
"nested rewritten CommonJS delegate":
'const holder = { loader: module }; holder.loader.require = replacement; require("../shared/window.cjs");',
"assigned rewritten CommonJS delegate":
'const holder = {}; holder.loader = module; holder.loader.require = replacement; require("../shared/window.cjs");',
"assigned module loader":
'const holder = {}; holder.loader = module; holder.loader.require("../shared/window.cjs");',
"nested module loader":
'const holder = { loader: module }; holder.loader.require("../shared/window.cjs");',
"destructured module loader":
'const holder = { loader: module }; const { loader } = holder; loader.require("../shared/window.cjs");',
"array module loader":
'const holder = [module]; holder[0].require("../shared/window.cjs");',
"bound module loader":
'const load = module.require.bind(module); load("../shared/window.cjs");',
"bound module alias loader":
'const current = module; const load = current.require.bind(current); load("../shared/window.cjs");',
"computed bound module loader":
'const load = module["require"].bind(module); load("../shared/window.cjs");',
"fallback module alias":
'const { loader = module } = options; loader.require("../shared/window.cjs");',
"inactive fallback":
'const { loader = module } = { loader: {} }; loader.require("../shared/window.cjs");',
"absolute ESM": 'import "/shared/window.cjs";',
"invalid absolute ESM": 'import "/shared%2Fwindow.cjs";',
"invalid absolute dynamic import": 'import("/shared%5Cwindow.cjs");',
"absolute dynamic import": 'import("/shared/window.cjs");',
"absolute file URL": 'import "file:///shared/window.cjs";',
"dynamic file URL": 'import("file:///shared/window.cjs");',
"createRequire base":
'import { createRequire } from "node:module"; const require = createRequire(new URL("../other/nested/main.cjs", import.meta.url)); require("../shared/window.cjs");',
};
const uncheckedModuleLoaders = new Set([
"custom iterator assignment module loader",
"replaced iterator assignment module loader",
"custom iterator rest declaration module loader",
"logical rewrite assignment module loader",
"object rest declaration module loader",
"array rest declaration module loader",
"object assignment module loader",
"array assignment module loader",
"nested assignment module loader",
"default assignment module loader",
"rest array assignment module loader",
"delegate assignment module loader",
"rewrite assignment module loader",
"bounded alias selection",
"captured module delegate",
"destructured module delegate",
"rewritten CommonJS delegate",
"nested rewritten CommonJS delegate",
"assigned rewritten CommonJS delegate",
"assigned module loader",
"module.require",
"computed module.require",
"module alias",
"later reassigned module alias",
"conditional module alias",
"fallback module alias",
"bound module loader",
"bound module alias loader",
"computed bound module loader",
"nested module loader",
"destructured module loader",
"array module loader",
]);
const fixtureMainPath = (loader: string): string => {
switch (loader) {
case "absolute ESM":
case "normalized absolute ESM":
case "normalized absolute dynamic import":
case "invalid absolute ESM":
case "invalid absolute dynamic import":
case "absolute dynamic import":
case "absolute file URL":
case "dynamic file URL":
case "createRequire base":
return "main.mjs";
case "absolute main":
return "/opt/app/main.cjs";
case "Windows relative main":
return "sub\\main.cjs";
case "Windows drive-relative main":
return "C:main.cjs";
case "percent main":
case "single percent main":
return "main%2Fentry.cjs";
default:
return "main.cjs";
}
};
const fixtureMainInventoryPath = (loader: string, main: string): string => {
if (loader === "absolute main") return "opt/app/main.cjs";
if (loader === "Windows relative main") return "b/sub/main.cjs";
if (loader === "Windows drive-relative main") return "b/main.cjs";
return `b/${main}`;
};
const fixtureLoaderSource = (loader: string): string =>
unprovenLoaderSources[loader] ??
(loader === "resolve-only"
? 'require.resolve("../shared/window.cjs");'
: loader === "missing wildcard alias"
? 'require("#missing/*");'
: loader === "absolute require"
? 'require("/shared/window.cjs");'
: loader === "projected module"
? 'const { exports: api } = module; api.require = () => {}; api.require("../shared/window.cjs");'
: loader === "absolute main"
? 'require("../../shared/window.cjs");'
: loader === "later reassigned module alias"
? 'let current = module; current.require("../shared/window.cjs"); current = other;'
: loader === "earlier reassigned module alias"
? 'let current = module; current = { require() {} }; current.require("../shared/window.cjs");'
: loader === "conditional module alias"
? 'let current = module; if (flag) current = other; current.require("../shared/window.cjs");'
: loader === "module.require"
? 'module.require("../shared/window.cjs");'
: loader === "computed module.require"
? 'module["require"]("../shared/window.cjs");'
: loader === "module alias"
? 'const current = module; current.require("../shared/window.cjs");'
: loader === "local module"
? 'function local(module) { module.require("../shared/window.cjs"); }'
: loader === "pattern alias"
? 'require("#window/window");'
: loader.endsWith("alias")
? 'require("#entry");'
: 'require("../shared/window.cjs");');
const explicitCompetingLoaders = [
"module.require",
"computed module.require",
"module alias",
"later reassigned module alias",
"earlier reassigned module alias",
"conditional module alias",
"absolute main",
"Windows relative main",
"Windows drive-relative main",
"percent main",
"pattern alias",
"recursive alias",
"external-package alias",
"builtin alias",
"electron alias",
"electron main alias",
"blocked alias",
"local module",
"projected module",
"resolve-only",
"missing wildcard alias",
"absolute require",
] as const;
/** Every competing-loader case, in the order the original matrix ran. */
export const competingLoaderNames = [
...explicitCompetingLoaders,
...Object.keys(unprovenLoaderSources),
"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),
] 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<void> => {
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);
const load = fixtureLoaderSource(loader);
await writeFixtureFiles(root, {
"a/package.json": JSON.stringify({ main: "main.cjs" }),
"a/main.cjs":
single || absolute
? "module.exports = 1;"
: 'require("../shared/window.cjs");',
"b/package.json": JSON.stringify({ main, imports }),
[fixtureMainInventoryPath(loader, main)]: load,
"b/proxy/window.cjs": 'require("../../shared/window.cjs");',
"b/node_modules/fixture-window/package.json": JSON.stringify({
main: "index.cjs",
}),
"b/node_modules/fixture-window/index.cjs":
'require("../../../shared/window.cjs");',
"shared/window.cjs":
'const { BrowserWindow } = require("electron"); new BrowserWindow().loadFile("index.html");',
"a/index.html": "first application page",
"b/index.html": "second application page",
"shared/index.html": "source-relative decoy",
"index.html": "artifact-root decoy",
});
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
if (loader.startsWith("Windows "))
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "loads",
properties: expect.objectContaining({
declared_path: main,
resolution_status: "unavailable",
resolved_path: null,
}),
}),
]),
);
if (
["absolute require", "absolute ESM", "absolute dynamic import"].includes(
loader,
)
)
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
specifier: "/shared/window.cjs",
resolution_status: "unavailable",
resolved_path: null,
}),
}),
]),
);
if (loader === "createRequire base")
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
kind: "require",
specifier: null,
resolved_path: null,
}),
}),
]),
);
if (loader.endsWith("alias") && !inert && !loader.includes("module alias"))
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
specifier: loader === "pattern alias" ? "#window/window" : "#entry",
resolution_status: "unavailable",
}),
}),
]),
);
if (uncheckedModuleLoaders.has(loader))
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
kind: "require",
specifier: null,
resolved_path: null,
}),
}),
]),
);
const roles = graph.nodes.filter(({ kind }) => kind === "electron-renderer");
expect(roles).toHaveLength(1);
const role = roles[0];
const target = single ? "b/index.html" : inert ? "a/index.html" : null;
expect(role?.observations).toEqual(
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
resolved_path: target,
resolution_status: target === null ? "unavailable" : "resolved",
}),
}),
]),
);
expect(
graph.edges
.filter(
({ source_node_id, relation }) =>
source_node_id === role?.node_id && relation === "maps_to",
)
.map(({ properties }) => properties.resolved_path),
).toEqual(target === null ? [] : [target]);
};
@@ -0,0 +1,29 @@
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);
},
);
@@ -0,0 +1,18 @@
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);
},
);
@@ -0,0 +1,18 @@
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);
},
);
@@ -4,30 +4,10 @@ import { join } from "node:path";
import { createPackageWithOptions } from "@electron/asar";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { aliasSelectionBudgetSource } from "../../fixtures/javascriptAliasSelectionBudget.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
const fixtureImports = (loader: string) => {
switch (loader) {
case "pattern alias":
return { "#window/*": "./proxy/*.cjs" };
case "recursive alias":
return { "#entry": "#window", "#window": "./proxy/window.cjs" };
case "external-package alias":
return { "#entry": "fixture-window" };
case "builtin alias":
return { "#entry": "fs" };
case "electron alias":
return { "#entry": "electron" };
case "electron main alias":
return { "#entry": "electron/main" };
default:
return { "#entry": null };
}
};
cliTest.for(
(["CLI", "stdio MCP"] as const).flatMap((surface) =>
[
@@ -96,371 +76,6 @@ cliTest.for(
},
);
const unprovenLoaderSources: Readonly<Record<string, string>> = {
"custom iterator assignment module loader":
'const values = { *[Symbol.iterator]() { yield module; } }; const holder = {}; [holder.loader] = values; holder.loader.require("../shared/window.cjs");',
"replaced iterator assignment module loader":
'const values = [{}]; values[Symbol.iterator] = function* () { yield module; }; const holder = {}; [holder.loader] = values; holder.loader.require("../shared/window.cjs");',
"custom iterator rest declaration module loader":
'const values = { *[Symbol.iterator]() { yield module; } }; const [...holder] = values; holder[0].require("../shared/window.cjs");',
"native array assignment local loader":
'const holder = { loader: module }; [holder.loader] = [{ require() {} }]; holder.loader.require("../shared/window.cjs");',
"logical rewrite assignment module loader":
'const holder = {}; holder.loader ??= module; holder.loader.require = replacement; require("../shared/window.cjs");',
"object rest declaration module loader":
'const { ...holder } = { loader: module }; holder.loader.require("../shared/window.cjs");',
"array rest declaration module loader":
'const [...holder] = [module]; holder[0].require("../shared/window.cjs");',
"object assignment module loader":
'const holder = {}; ({ loader: holder.loader } = { loader: module }); holder.loader.require("../shared/window.cjs");',
"array assignment module loader":
'const holder = {}; [holder.loader] = [module]; holder.loader.require("../shared/window.cjs");',
"nested assignment module loader":
'const holder = {}; ({ inner: { loader: holder.loader } } = { inner: { loader: module } }); holder.loader.require("../shared/window.cjs");',
"default assignment module loader":
'const holder = {}; ({ loader: holder.loader = module } = {}); holder.loader.require("../shared/window.cjs");',
"rest array assignment module loader":
'const holder = {}; let ignored; [ignored, ...holder.loaders] = [{}, module]; holder.loaders[0].require("../shared/window.cjs");',
"delegate assignment module loader":
'const holder = {}; ({ load: holder.load } = { load: module.require }); holder.load.call(module, "../shared/window.cjs");',
"rewrite assignment module loader":
'const holder = {}; ({ loader: holder.loader } = { loader: module }); holder.loader.require = replacement; require("../shared/window.cjs");',
"exports assignment local loader":
'const holder = {}; ({ exports: holder.loader } = module); holder.loader.require("../shared/window.cjs");',
"rest assignment local loader":
'const holder = {}; ({ ...holder.loader } = module); holder.loader.require("../shared/window.cjs");',
"excluded assignment local loader":
'const holder = {}; let ignored; ({ loader: ignored, ...holder.copy } = { loader: module }); holder.copy.loader.require("../shared/window.cjs");',
"inactive default assignment local loader":
'const holder = {}; ({ loader: holder.loader = module } = { loader: { require() {} } }); holder.loader.require("../shared/window.cjs");',
"overwritten assignment local loader":
'const holder = { loader: module }; ({ loader: holder.loader } = { loader: { require() {} } }); holder.loader.require("../shared/window.cjs");',
"bounded alias selection": aliasSelectionBudgetSource(),
"captured module delegate":
'const load = module.require; load.call(module, "../shared/window.cjs");',
"destructured module delegate":
'const { require: load } = module; load.apply(module, ["../shared/window.cjs"]);',
"selected local delegate slots":
'const table = { x: {}, y: {}, z: module }; const target = flag ? table.x : table.y; target.require = replacement; require("../shared/window.cjs");',
"local assigned loader":
'const holder = {}; holder.loader = { require() {} }; holder.loader.require("../shared/window.cjs");',
"overwritten local loader":
'const holder = { loader: module }; holder.loader = { require() {} }; holder.loader.require("../shared/window.cjs");',
"normalized absolute ESM": 'import "/discard%2F/../shared/window.cjs";',
"normalized absolute dynamic import":
'import("/discard%5C/../shared/window.cjs");',
"rewritten CommonJS delegate":
'module.require = replacement; require("../shared/window.cjs");',
"nested rewritten CommonJS delegate":
'const holder = { loader: module }; holder.loader.require = replacement; require("../shared/window.cjs");',
"assigned rewritten CommonJS delegate":
'const holder = {}; holder.loader = module; holder.loader.require = replacement; require("../shared/window.cjs");',
"assigned module loader":
'const holder = {}; holder.loader = module; holder.loader.require("../shared/window.cjs");',
"nested module loader":
'const holder = { loader: module }; holder.loader.require("../shared/window.cjs");',
"destructured module loader":
'const holder = { loader: module }; const { loader } = holder; loader.require("../shared/window.cjs");',
"array module loader":
'const holder = [module]; holder[0].require("../shared/window.cjs");',
"bound module loader":
'const load = module.require.bind(module); load("../shared/window.cjs");',
"bound module alias loader":
'const current = module; const load = current.require.bind(current); load("../shared/window.cjs");',
"computed bound module loader":
'const load = module["require"].bind(module); load("../shared/window.cjs");',
"fallback module alias":
'const { loader = module } = options; loader.require("../shared/window.cjs");',
"inactive fallback":
'const { loader = module } = { loader: {} }; loader.require("../shared/window.cjs");',
"absolute ESM": 'import "/shared/window.cjs";',
"invalid absolute ESM": 'import "/shared%2Fwindow.cjs";',
"invalid absolute dynamic import": 'import("/shared%5Cwindow.cjs");',
"absolute dynamic import": 'import("/shared/window.cjs");',
"absolute file URL": 'import "file:///shared/window.cjs";',
"dynamic file URL": 'import("file:///shared/window.cjs");',
"createRequire base":
'import { createRequire } from "node:module"; const require = createRequire(new URL("../other/nested/main.cjs", import.meta.url)); require("../shared/window.cjs");',
};
const uncheckedModuleLoaders = new Set([
"custom iterator assignment module loader",
"replaced iterator assignment module loader",
"custom iterator rest declaration module loader",
"logical rewrite assignment module loader",
"object rest declaration module loader",
"array rest declaration module loader",
"object assignment module loader",
"array assignment module loader",
"nested assignment module loader",
"default assignment module loader",
"rest array assignment module loader",
"delegate assignment module loader",
"rewrite assignment module loader",
"bounded alias selection",
"captured module delegate",
"destructured module delegate",
"rewritten CommonJS delegate",
"nested rewritten CommonJS delegate",
"assigned rewritten CommonJS delegate",
"assigned module loader",
"module.require",
"computed module.require",
"module alias",
"later reassigned module alias",
"conditional module alias",
"fallback module alias",
"bound module loader",
"bound module alias loader",
"computed bound module loader",
"nested module loader",
"destructured module loader",
"array module loader",
]);
const fixtureMainPath = (loader: string): string => {
switch (loader) {
case "absolute ESM":
case "normalized absolute ESM":
case "normalized absolute dynamic import":
case "invalid absolute ESM":
case "invalid absolute dynamic import":
case "absolute dynamic import":
case "absolute file URL":
case "dynamic file URL":
case "createRequire base":
return "main.mjs";
case "absolute main":
return "/opt/app/main.cjs";
case "Windows relative main":
return "sub\\main.cjs";
case "Windows drive-relative main":
return "C:main.cjs";
case "percent main":
case "single percent main":
return "main%2Fentry.cjs";
default:
return "main.cjs";
}
};
const fixtureMainInventoryPath = (loader: string, main: string): string => {
if (loader === "absolute main") return "opt/app/main.cjs";
if (loader === "Windows relative main") return "b/sub/main.cjs";
if (loader === "Windows drive-relative main") return "b/main.cjs";
return `b/${main}`;
};
const fixtureLoaderSource = (loader: string): string =>
unprovenLoaderSources[loader] ??
(loader === "resolve-only"
? 'require.resolve("../shared/window.cjs");'
: loader === "missing wildcard alias"
? 'require("#missing/*");'
: loader === "absolute require"
? 'require("/shared/window.cjs");'
: loader === "projected module"
? 'const { exports: api } = module; api.require = () => {}; api.require("../shared/window.cjs");'
: loader === "absolute main"
? 'require("../../shared/window.cjs");'
: loader === "later reassigned module alias"
? 'let current = module; current.require("../shared/window.cjs"); current = other;'
: loader === "earlier reassigned module alias"
? 'let current = module; current = { require() {} }; current.require("../shared/window.cjs");'
: loader === "conditional module alias"
? 'let current = module; if (flag) current = other; current.require("../shared/window.cjs");'
: loader === "module.require"
? 'module.require("../shared/window.cjs");'
: loader === "computed module.require"
? 'module["require"]("../shared/window.cjs");'
: loader === "module alias"
? 'const current = module; current.require("../shared/window.cjs");'
: loader === "local module"
? 'function local(module) { module.require("../shared/window.cjs"); }'
: loader === "pattern alias"
? 'require("#window/window");'
: loader.endsWith("alias")
? 'require("#entry");'
: 'require("../shared/window.cjs");');
cliTest.for(
(["CLI", "stdio MCP"] as const).flatMap((surface) =>
[
"module.require",
"computed module.require",
"module alias",
"later reassigned module alias",
"earlier reassigned module alias",
"conditional module alias",
"absolute main",
"Windows relative main",
"Windows drive-relative main",
"percent main",
"pattern alias",
"recursive alias",
"external-package alias",
"builtin alias",
"electron alias",
"electron main alias",
"blocked alias",
"local module",
"projected module",
"resolve-only",
"missing wildcard alias",
"absolute require",
...Object.keys(unprovenLoaderSources),
"single percent main",
].map((loader) => ({ surface, loader })),
),
)(
"preserves competing loader ownership for $loader through $surface",
async ({ surface, loader }, { cli }) => {
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);
const load = fixtureLoaderSource(loader);
await writeFixtureFiles(root, {
"a/package.json": JSON.stringify({ main: "main.cjs" }),
"a/main.cjs":
single || absolute
? "module.exports = 1;"
: 'require("../shared/window.cjs");',
"b/package.json": JSON.stringify({ main, imports }),
[fixtureMainInventoryPath(loader, main)]: load,
"b/proxy/window.cjs": 'require("../../shared/window.cjs");',
"b/node_modules/fixture-window/package.json": JSON.stringify({
main: "index.cjs",
}),
"b/node_modules/fixture-window/index.cjs":
'require("../../../shared/window.cjs");',
"shared/window.cjs":
'const { BrowserWindow } = require("electron"); new BrowserWindow().loadFile("index.html");',
"a/index.html": "first application page",
"b/index.html": "second application page",
"shared/index.html": "source-relative decoy",
"index.html": "artifact-root decoy",
});
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
if (loader.startsWith("Windows "))
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "loads",
properties: expect.objectContaining({
declared_path: main,
resolution_status: "unavailable",
resolved_path: null,
}),
}),
]),
);
if (
["absolute require", "absolute ESM", "absolute dynamic import"].includes(
loader,
)
)
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
specifier: "/shared/window.cjs",
resolution_status: "unavailable",
resolved_path: null,
}),
}),
]),
);
if (loader === "createRequire base")
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
kind: "require",
specifier: null,
resolved_path: null,
}),
}),
]),
);
if (loader.endsWith("alias") && !inert && !loader.includes("module alias"))
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
specifier:
loader === "pattern alias" ? "#window/window" : "#entry",
resolution_status: "unavailable",
}),
}),
]),
);
if (uncheckedModuleLoaders.has(loader))
expect(graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
kind: "require",
specifier: null,
resolved_path: null,
}),
}),
]),
);
const roles = graph.nodes.filter(
({ kind }) => kind === "electron-renderer",
);
expect(roles).toHaveLength(1);
const role = roles[0];
const target = single ? "b/index.html" : inert ? "a/index.html" : null;
expect(role?.observations).toEqual(
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
resolved_path: target,
resolution_status: target === null ? "unavailable" : "resolved",
}),
}),
]),
);
expect(
graph.edges
.filter(
({ source_node_id, relation }) =>
source_node_id === role?.node_id && relation === "maps_to",
)
.map(({ properties }) => properties.resolved_path),
).toEqual(target === null ? [] : [target]);
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"propagates the application owner into an inventoried ASAR through %s",
async (surface, { cli }) => {
+106 -23
View File
@@ -8,10 +8,10 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
allocateByDuration,
estimatedShardCost,
parseTestDurations,
readTestDurations,
weightFileDurations,
scheduleWall,
seedSplitLoaderDurations,
} from "../../ciTestSequencer.js";
describe("CI test history", () => {
@@ -86,28 +86,43 @@ describe("CI test history", () => {
});
});
describe("split loader history", () => {
const source =
"tests/acceptance/applications/javascriptLiteralPercentPaths.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(
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("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([
[source, 20_000],
[parts[0]!, 30_000],
]);
expect(seedSplitLoaderDurations(recorded)).toEqual(recorded);
expect(
seedSplitLoaderDurations(new Map([["tests/other.test.ts", 10]])),
).toEqual(new Map([["tests/other.test.ts", 10]]));
});
});
describe("duration shard allocation", () => {
it("charges serial process-boundary files their full duration and other projects one worker share", () => {
expect(estimatedShardCost("process-boundary", 400)).toBe(400);
expect(estimatedShardCost("domain", 400)).toBe(200);
expect(estimatedShardCost("adapters", 400)).toBe(200);
});
it("fills unknown files from the raw median before serial and shared weighting", () => {
expect(
weightFileDurations([
{ project: "domain", duration: 400 },
{ project: "process-boundary", duration: undefined },
{ project: "adapters", duration: undefined },
]),
).toEqual([200, 400, 200]);
expect(
weightFileDurations([
{ project: "process-boundary", duration: undefined },
]),
).toEqual([undefined]);
});
it.prop([
fc.uniqueArray(fc.integer({ min: 0, max: 10_000 }), {
minLength: 1,
@@ -188,4 +203,72 @@ describe("duration shard allocation", () => {
expect(shards!.flat()).toHaveLength(items.length);
expect(new Set(shards!.flat()).size).toBe(items.length);
});
it("keeps each file whole and runs the serial group after the parallel group", () => {
const parallel = (id: string, duration: number) => ({
item: id,
key: id,
duration,
laneWidth: 2,
groupOrder: 0,
});
const serial = (id: string, duration: number) => ({
item: id,
key: id,
duration,
laneWidth: 1,
groupOrder: 1,
});
const paired = [
parallel("wide", 300),
parallel("peer", 300),
parallel("third", 300),
];
expect(scheduleWall(paired.slice(0, 1), 1)).toBe(300);
expect(scheduleWall(paired.slice(0, 2), 1)).toBe(300);
expect(scheduleWall(paired, 1)).toBe(600);
expect(
scheduleWall([parallel("ordinary", 100), serial("tail", 60)], 1),
).toBe(160);
expect(() =>
scheduleWall(
[parallel("wide", 10), { ...parallel("wide", 10), laneWidth: 1 }],
1,
),
).toThrow(RangeError);
const packed = [
parallel("ordinary", 100),
serial("tail-a", 60),
serial("tail-b", 60),
];
expect(allocateByDuration(packed, 2)).toEqual([
["ordinary"],
["tail-a", "tail-b"],
]);
const spread = [
parallel("wide", 300),
serial("tail-a", 100),
serial("tail-b", 100),
parallel("small", 40),
];
const shards = allocateByDuration(spread, 2);
expect(shards).toEqual([["wide"], ["tail-a", "tail-b", "small"]]);
expect(
scheduleWall(
spread.filter((entry) => entry.item === "wide"),
1,
),
).toBe(300);
expect(
Math.max(
...shards!.map((ids) =>
scheduleWall(
spread.filter((entry) => ids.includes(entry.item)),
1,
),
),
),
).toBe(300);
});
});
+159 -40
View File
@@ -60,32 +60,138 @@ export async function readTestDurations(
}
}
type TimedItem<T> = { item: T; key: string; duration: number | undefined };
/**
* 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.
*/
const LOADER_HISTORY_SOURCE =
"tests/acceptance/applications/javascriptLiteralPercentPaths.test.ts";
const LOADER_HISTORY_PARTS = [
"tests/acceptance/applications/javascriptLiteralLoaderOwnershipEarly.test.ts",
"tests/acceptance/applications/javascriptLiteralLoaderOwnershipMiddle.test.ts",
"tests/acceptance/applications/javascriptLiteralLoaderOwnershipLate.test.ts",
] as const;
const LOADER_HISTORY_REMAINDER_CASES = 54;
const LOADER_HISTORY_PART_CASES = 46;
const LOADER_HISTORY_CASES =
LOADER_HISTORY_REMAINDER_CASES +
LOADER_HISTORY_PART_CASES * LOADER_HISTORY_PARTS.length;
// process-boundary runs its files one at a time. Other projects overlap up to
// the runner worker cap, so equal file-duration sums are not equal wall time.
const PARALLEL_TEST_WORKERS = 2;
export function estimatedShardCost(
project: string,
durationMs: number,
): number {
if (project === "process-boundary") return durationMs;
return durationMs / PARALLEL_TEST_WORKERS;
/**
* Give the split loader files a share of the pre-split duration. Once history
* records any of the new paths, their published durations stand.
*/
export function seedSplitLoaderDurations(
durations: ReadonlyMap<string, number>,
): Map<string, number> {
const combined = durations.get(LOADER_HISTORY_SOURCE);
if (
combined === undefined ||
LOADER_HISTORY_PARTS.some((path) => durations.has(path))
)
return new Map(durations);
const seeded = new Map(durations);
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,
);
return seeded;
}
/** Weight raw durations, filling gaps from the raw median before worker sharing. */
export function weightFileDurations(
entries: readonly { project: string; duration: number | undefined }[],
): (number | undefined)[] {
const known = entries
.flatMap(({ duration }) => (duration === undefined ? [] : [duration]))
.sort((left, right) => left - right);
if (!known.length) return entries.map(() => undefined);
const lower = known[Math.floor((known.length - 1) / 2)]!;
const median = lower + (known[Math.floor(known.length / 2)]! - lower) / 2;
return entries.map(({ project, duration }) =>
estimatedShardCost(project, duration ?? median),
type TimedItem<T> = {
item: T;
key: string;
duration: number | undefined;
/**
* Workers for this file's sequence group. The file still occupies one worker
* for its full duration; the other lanes only overlap its peers.
*/
laneWidth?: number;
/**
* Vitest sequence group. Groups run from lowest to highest, so a later group
* adds its own makespan instead of overlapping an earlier one.
*/
groupOrder?: number;
};
type Phase = { laneWidth: number; lanes: number[] };
const laneWidthOf = <T>(entry: TimedItem<T>): number => {
const laneWidth = entry.laneWidth ?? 1;
if (!Number.isInteger(laneWidth) || laneWidth < 1)
throw new RangeError("laneWidth must be a positive integer");
return laneWidth;
};
const groupOrderOf = <T>(entry: TimedItem<T>): number => {
const groupOrder = entry.groupOrder ?? 0;
if (!Number.isInteger(groupOrder) || groupOrder < 0)
throw new RangeError("groupOrder must be a non-negative integer");
return groupOrder;
};
const durationOf = <T>(entry: TimedItem<T>, unknownMs: number): number => {
const duration = entry.duration ?? unknownMs;
if (!Number.isFinite(duration) || duration <= 0)
throw new RangeError("duration must be positive");
return duration;
};
const wallOf = (phases: ReadonlyMap<number, Phase>): number => {
let wall = 0;
for (const phase of phases.values())
wall += phase.lanes.reduce((max, lane) => Math.max(max, lane), 0);
return wall;
};
/** Place one whole file on the least-loaded lane of its sequence group. */
const place = <T>(
phases: ReadonlyMap<number, Phase>,
entry: TimedItem<T>,
duration: number,
): Map<number, Phase> => {
const groupOrder = groupOrderOf(entry);
const laneWidth = laneWidthOf(entry);
const existing = phases.get(groupOrder);
if (existing && existing.laneWidth !== laneWidth)
throw new RangeError("one sequence group must keep a single lane width");
const lanes = existing
? existing.lanes.slice()
: Array.from({ length: laneWidth }, () => 0);
let shortest = 0;
for (let index = 1; index < lanes.length; index += 1)
if (lanes[index]! < lanes[shortest]!) shortest = index;
lanes[shortest] = lanes[shortest]! + duration;
const next = new Map(phases);
next.set(groupOrder, { laneWidth, lanes });
return next;
};
/**
* Wall time of one shard. Each file keeps its full duration on one worker.
* Files overlap only inside a sequence group, up to that group's lane width,
* and later groups run after earlier ones.
*/
export function scheduleWall<T>(
items: readonly TimedItem<T>[],
unknownMs: number,
): number {
const ordered = items.toSorted(
(left, right) =>
durationOf(right, unknownMs) - durationOf(left, unknownMs) ||
(left.key < right.key ? -1 : left.key > right.key ? 1 : 0),
);
return wallOf(
ordered.reduce(
(phases, entry) => place(phases, entry, durationOf(entry, unknownMs)),
new Map<number, Phase>(),
),
);
}
@@ -104,19 +210,29 @@ export function allocateByDuration<T>(
const median = lower + (known[Math.floor(known.length / 2)]! - lower) / 2;
const ordered = items.toSorted(
(left, right) =>
(right.duration ?? median) - (left.duration ?? median) ||
durationOf(right, median) - durationOf(left, median) ||
(left.key < right.key ? -1 : left.key > right.key ? 1 : 0),
);
const shards = Array.from({ length: shardCount }, () => ({
duration: 0,
phases: new Map<number, Phase>(),
items: [] as T[],
}));
for (const entry of ordered) {
const lightest = shards.reduce((left, right) =>
right.duration < left.duration ? right : left,
);
lightest.items.push(entry.item);
lightest.duration += entry.duration ?? median;
const duration = durationOf(entry, median);
let chosen = shards[0]!;
let chosenPhases = place(chosen.phases, entry, duration);
let chosenWall = wallOf(chosenPhases);
for (const candidate of shards.slice(1)) {
const phases = place(candidate.phases, entry, duration);
const wall = wallOf(phases);
if (wall < chosenWall) {
chosen = candidate;
chosenPhases = phases;
chosenWall = wall;
}
}
chosen.phases = chosenPhases;
chosen.items.push(entry.item);
}
return shards.map(({ items }) => items);
}
@@ -129,26 +245,29 @@ export class CiTestSequencer extends BaseSequencer {
const shard = this.ctx.config.shard;
if (!historyPath || !shard) return super.shard(files);
const root = this.ctx.config.root;
const durations = await readTestDurations(historyPath, root);
if (!durations) return super.shard(files);
const recorded = await readTestDurations(historyPath, root);
if (!recorded) return super.shard(files);
const durations = seedSplitLoaderDurations(recorded);
const timed = files.map((item) => {
const weighted = files.map((item) => {
const path = relative(root, resolve(root, item.moduleId))
.split(sep)
.join("/");
const projectWorkers = item.project.config.maxWorkers;
const laneWidth =
typeof projectWorkers === "number" &&
Number.isInteger(projectWorkers) &&
projectWorkers >= 1
? projectWorkers
: 1;
return {
item,
key: path + "\0" + item.project.name + "\0" + item.pool,
project: item.project.name,
duration: durations.get(path),
laneWidth,
groupOrder: item.project.config.sequence.groupOrder,
};
});
const costs = weightFileDurations(timed);
const weighted = timed.map((entry, index) => ({
item: entry.item,
key: entry.key,
duration: costs[index],
}));
const shards = allocateByDuration(weighted, shard.count);
return shards ? shards[shard.index - 1]! : super.shard(files);
}