mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
Merge remote-tracking branch 'origin/main' into codex/audit-continuation-20261020-refresh20
This commit is contained in:
commit
6389e50337
9 files changed
+779
-468
No files matched your search
@@ -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
@@ -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 }) => {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user