rea/tests/boundary/filesystem/ciChangeScope.test.ts

1247 lines
38 KiB
TypeScript

import {
chmod,
cp,
mkdir,
readFile,
readdir,
rename,
writeFile,
} from "node:fs/promises";
import { dirname, join, matchesGlob } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, beforeAll, expect, it } from "vitest";
import { parse } from "yaml";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
createTestWorkspace,
removeTestWorkspace,
} from "../../support/workspace/workspaceFixture.js";
import {
executeWorkflowFixture as execFileAsync,
workflowGit as git,
} from "../../support/workflowGit.js";
async function commit(directory: string) {
await git(directory, ["add", "."]);
await git(directory, ["commit", "-m", "fix: fixture change"]);
return git(directory, ["rev-parse", "HEAD"]);
}
async function change(
directory: string,
paths: readonly string[],
content = "Changed fixture content\n",
) {
for (const path of paths) {
const file = join(directory, path);
await mkdir(dirname(file), { recursive: true });
await writeFile(file, content);
}
return commit(directory);
}
let seed: { directory: string; base: string } | undefined;
let seedWorkspaceRoot: string | undefined;
beforeAll(async () => {
const workspace = await createTestWorkspace("rea-ci-scope-seed-");
seedWorkspaceRoot = workspace.root;
const directory = await workspace.mkdir("repository");
await git(directory, ["init", "--initial-branch=main"]);
const base = await change(
directory,
["src/provider.ts", "README.md"],
"Initial fixture content\n",
);
seed = { directory, base };
});
afterAll(async () => {
if (seedWorkspaceRoot !== undefined)
await removeTestWorkspace(seedWorkspaceRoot);
});
async function fixture() {
const initial = seed;
if (initial === undefined)
throw new Error("CI scope seed was not initialized");
const directory = await createTestTempDirectory("rea-ci-scope-");
// Copy the pristine object database, refs and index; each case owns its mutations.
await cp(initial.directory, directory, { recursive: true });
return { directory, base: initial.base };
}
async function classify(
directory: string,
base: string,
head: string,
event = "pull_request",
full = false,
skipFull = false,
) {
const output = join(directory, "scope-output");
await writeFile(output, "");
const result = await execFileAsync(
process.execPath,
[fileURLToPath(new URL("../../../scripts/ci/plan.mjs", import.meta.url))],
{
cwd: directory,
env: {
...process.env,
BASE_SHA: base,
HEAD_SHA: head,
GITHUB_EVENT_NAME: event,
CI_FULL: String(full),
CI_SKIP_FULL: String(skipFull),
GITHUB_OUTPUT: output,
},
},
);
return z.record(z.string(), z.unknown()).parse(JSON.parse(result.stdout));
}
it("reuses only scheduled qualification and preserves every explicit full request", async () => {
const { directory, base } = await fixture();
const reused = await classify(directory, base, base, "schedule", false, true);
expect(reused.tier).toBe("reused");
const { CI_FLAGS } = await loadScopes();
for (const flag of CI_FLAGS) expect(reused[flag]).toBe(false);
for (const event of ["workflow_dispatch", "pull_request", "schedule"])
expect(
(await classify(directory, base, base, event, true, true)).tier,
).toBe("full");
expect(
(await classify(directory, base, base, "push", false, true)).tier,
).toBe("fast");
});
it("keeps every translated README and authored guide on the documentation lane", async () => {
const { directory, base } = await fixture();
const readmes = (await readdir(new URL("../../../", import.meta.url))).filter(
(name) => /^README(?:_.*)?\.md$/u.test(name),
);
const translated = (
await readdir(new URL("../../../docs/readme/", import.meta.url))
)
.filter((name) => /^README_.*\.md$/u.test(name))
.map((name) => `docs/readme/${name}`);
expect(readmes).toContain("README.md");
expect(translated.length).toBeGreaterThan(0);
const head = await change(directory, [
...readmes,
...translated,
"docs/guide.md",
"AGENTS.md",
"CONTRIBUTING.md",
]);
expect(await classify(directory, base, head)).toMatchObject({
code: false,
package: false,
docs: true,
});
});
it.each([
"job-timings.mjs",
"job-timings-batch.mjs",
"test-report.mjs",
"test-timing-reporter.mjs",
"test-history.mjs",
])(
"keeps portable CI reporter %s on static checks and portable tests",
async (name) => {
const { directory, base } = await fixture();
const head = await change(directory, [`scripts/ci/${name}`]);
const plan = await classify(directory, base, head);
const { CI_FLAGS } = await loadScopes();
expect(plan.code).toBe(true);
for (const flag of CI_FLAGS)
if (flag !== "code") expect(plan[flag]).toBe(false);
},
);
it("preserves a provider lane when a portable CI reporter changes with provider source", async () => {
const { directory, base } = await fixture();
const head = await change(directory, [
"scripts/ci/test-report.mjs",
"src/evm/Example.ts",
]);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
evm: true,
browser: false,
package: false,
});
});
it("keeps unknown CI scripts on complete qualification", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["scripts/ci/new-qualification.mjs"]);
const plan = await classify(directory, base, head);
const { CI_FLAGS } = await loadScopes();
for (const flag of CI_FLAGS) expect(plan[flag]).toBe(true);
});
it("emits an observational owner proposal without changing ordinary or full portable execution", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["src/evm/Example.ts"]);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
testSelection: { mode: "candidate", owners: ["evm"] },
});
expect(
await classify(directory, base, head, "pull_request", true),
).toMatchObject({
code: true,
testSelection: { mode: "full", owners: [] },
});
const output = await readFile(join(directory, "scope-output"), "utf8");
expect(output).toContain('test_selection={"version":1,"mode":"full"');
});
it("does not charge a documentation PR for unrelated implementation updates on main", async () => {
const { directory, base } = await fixture();
await git(directory, ["switch", "-c", "docs/change"]);
const head = await change(directory, ["docs/readme/README_zh-TW.md"]);
await git(directory, ["switch", "main"]);
const advancedBase = await change(directory, ["src/anotherProvider.ts"]);
expect(advancedBase).not.toBe(base);
expect(await classify(directory, advancedBase, head)).toMatchObject({
code: false,
package: false,
});
});
it.each([
"src/gdb/GdbSessionManager.ts",
"src/rizin/RizinDebugSessionManager.ts",
"src/cutter/CutterBridgeClient.ts",
"src/objdump/ObjdumpCommand.ts",
"tests/boundary/providers/GdbSessionManagerCoverage.test.ts",
"tests/boundary/providers/GdbCommandQueue.test.ts",
"tests/boundary/providers/RizinDebugSessionCoverage.part3.test.ts",
"tests/boundary/providers/RizinCommandTimeout.test.ts",
"tests/boundary/providers/RizinTimeoutEvidence.test.ts",
"tests/boundary/providers/GdbSessionReviewRegressions.test.ts",
"tests/boundary/providers/gdbTestHelpers.ts",
"tests/boundary/providers/RizinDebugSessionReviewRegressions.test.ts",
"tests/boundary/mcp/gdbCancellationMcp.test.ts",
"tests/boundary/mcp/rizinDebuggerCancellationMcp.test.ts",
"tests/composition/reverseEngineeringAdmission.test.ts",
"tests/support/reverseEngineeringMcpHarness.ts",
"tests/support/reverseEngineeringCompositionEdgeCases.ts",
"tests/support/reverseEngineeringCompositionFixtures.ts",
"tests/support/reverseEngineeringService.ts",
"tests/support/ownedRizinCommandEvidenceTests.ts",
"tests/support/reverseDebuggerMcpTestHelpers.ts",
"tests/boundary/cli/gdbStartupCleanupCli.test.ts",
"vitest.reverse-tools.config.ts",
"vitest.reverse-tools-semantics.config.ts",
"stryker.config.mjs",
"stryker.semantics.config.mjs",
"tsconfig.reverse-tools-mutation.json",
"scripts/prepare-reverse-tools-mutation.mjs",
"scripts/lib/reverse-tools-mutation-targets.mjs",
])(
"selects the reverse-tool coverage and mutation gate for %s",
async (path) => {
const { directory, base } = await fixture();
const head = await change(directory, [path]);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
reverse_tools: true,
});
},
);
const reverseToolsVitestConfig = await readFile(
new URL("../../../vitest.reverse-tools.config.ts", import.meta.url),
"utf8",
);
const reverseToolsVitestInputs = [
...reverseToolsVitestConfig.matchAll(/"((?:\.\/)?(?:src|tests)\/[^"]+)"/gu),
].flatMap((match) =>
match[1] === undefined ? [] : [match[1].replace(/^\.\//u, "")],
);
const reverseToolsMutationConfig = JSON.parse(
await readFile(
new URL("../../../tsconfig.reverse-tools-mutation.json", import.meta.url),
"utf8",
),
) as { include: string[] };
const reverseToolsInputs = [
...new Set([
...reverseToolsVitestInputs,
...reverseToolsMutationConfig.include.map((path) =>
path.replace(/^\.\//u, ""),
),
]),
];
it.each(reverseToolsInputs)(
"selects the reverse-tool quality gate for configured input %s",
async (path) => {
const { directory, base } = await fixture();
const head = await change(directory, [path]);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
reverse_tools: true,
});
},
);
it("requires the reverse-tool gate when its scope is selected", async () => {
const workflow = parse(
await readFile(
new URL("../../../.github/workflows/ci.yml", import.meta.url),
"utf8",
),
);
expect(workflow.jobs["reverse-tools-quality"].if).toBe(
"needs.changes.outputs.reverse_tools == 'true'",
);
expect(workflow.jobs.required.needs).toContain("reverse-tools-quality");
const { CI_FLAGS } = await loadScopes();
const outputs = {
...Object.fromEntries(CI_FLAGS.map((flag) => [flag, "false"])),
tier: "fast",
reverse_tools: "true",
};
const command = new URL("../../../scripts/ci/required.mjs", import.meta.url)
.pathname;
await expect(
execFileAsync(process.execPath, [command], {
env: {
...process.env,
CI_JOBS: JSON.stringify({
changes: { result: "success", outputs },
static: { result: "success" },
test: { result: "success" },
"reverse-tools-quality": { result: "skipped" },
}),
},
}),
).rejects.toMatchObject({ code: 1 });
await expect(
execFileAsync(process.execPath, [command], {
env: {
...process.env,
CI_JOBS: JSON.stringify({
changes: { result: "success", outputs },
static: { result: "success" },
test: { result: "success" },
"reverse-tools-quality": { result: "failure" },
}),
},
}),
).rejects.toMatchObject({ code: 1 });
await expect(
execFileAsync(process.execPath, [command], {
env: {
...process.env,
CI_JOBS: JSON.stringify({
changes: { result: "success", outputs },
static: { result: "success" },
test: { result: "success" },
"reverse-tools-quality": { result: "success" },
}),
},
}),
).resolves.toMatchObject({
stdout: expect.stringContaining("Every selected CI lane passed"),
});
});
it.each([
"src/README.md",
"README.md.ts",
"bridge/ghidra/provider.py",
"tests/fixture.test.ts",
".github/workflows/ci.yml",
"unknown-file",
])(
"retains complete checks when documentation is mixed with %s",
async (path) => {
const { directory, base } = await fixture();
const head = await change(directory, ["README.md", path]);
expect(await classify(directory, base, head)).toMatchObject({ code: true });
},
);
it("retains complete checks when implementation is renamed into documentation", async () => {
const { directory, base } = await fixture();
await mkdir(join(directory, "docs"));
await rename(
join(directory, "src/provider.ts"),
join(directory, "docs/provider.md"),
);
const head = await commit(directory);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
runtime: true,
});
});
it("keeps main pushes scoped when a successful ancestor baseline is available", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["docs/readme/README_ja.md"]);
expect(await classify(directory, base, head, "push")).toMatchObject({
tier: "fast",
code: false,
package: false,
docs: true,
matrix: {
include: [
{ os: "ubuntu-latest", platform: "linux", architecture: "x64" },
],
},
});
});
it("includes changes from earlier cancelled pushes since the last successful baseline", async () => {
const { directory, base } = await fixture();
await change(directory, ["src/evm/client.ts"]);
const head = await change(directory, ["README.md"]);
expect(await classify(directory, base, head, "push")).toMatchObject({
tier: "fast",
code: true,
evm: true,
runtime: true,
package: false,
});
});
it.each(["", "not-a-commit"])(
"fails closed to all lanes for a missing or invalid push baseline (%s)",
async (base) => {
const { directory } = await fixture();
const head = await change(directory, ["README.md"]);
const plan = await classify(directory, base, head, "push");
expect(plan).toMatchObject({
tier: "fast",
code: true,
package: true,
evm: true,
browser: true,
reasons: expect.arrayContaining([expect.stringContaining("baseline")]),
});
const { CI_FLAGS } = await loadScopes();
for (const flag of CI_FLAGS) expect(plan[flag]).toBe(true);
},
);
it("selects only a trusted successful ancestor from recent CI push runs", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["src/evm/client.ts"]);
const bin = join(directory, "fake-bin");
await mkdir(bin);
const gh = join(bin, "gh");
await writeFile(gh, '#!/bin/sh\ncat "$GH_FIXTURE_RESPONSE"\n');
await chmod(gh, 0o755);
const response = join(directory, "runs.json");
await writeFile(
response,
JSON.stringify({
workflow_runs: [
{
event: "push",
conclusion: "success",
head_branch: "main",
head_repository: { full_name: "untrusted/fork" },
head_sha: base,
},
{
event: "push",
conclusion: "success",
head_branch: "main",
head_repository: { full_name: "owner/repo" },
head_sha: base,
},
],
}),
);
const output = join(directory, "baseline-output");
const result = await execFileAsync(
process.execPath,
[new URL("../../../scripts/ci/baseline.mjs", import.meta.url).pathname],
{
cwd: directory,
env: {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
GH_FIXTURE_RESPONSE: response,
CI_REPOSITORY: "owner/repo",
CI_DEFAULT_BRANCH: "main",
HEAD_SHA: head,
GITHUB_OUTPUT: output,
},
},
);
expect(JSON.parse(result.stdout)).toEqual({ base, reason: "" });
expect(await readFile(output, "utf8")).toBe(`base=${base}\nreason=\n`);
});
it("returns an empty baseline when GitHub lookup fails", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["src/evm/client.ts"]);
const bin = join(directory, "fake-bin");
await mkdir(bin);
const gh = join(bin, "gh");
await writeFile(gh, "#!/bin/sh\nexit 1\n");
await chmod(gh, 0o755);
const result = await execFileAsync(
process.execPath,
[new URL("../../../scripts/ci/baseline.mjs", import.meta.url).pathname],
{
cwd: directory,
env: {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
CI_REPOSITORY: "owner/repo",
CI_DEFAULT_BRANCH: "main",
HEAD_SHA: head,
},
},
);
expect(JSON.parse(result.stdout)).toMatchObject({
base: "",
reason: expect.stringContaining("Could not read"),
});
expect(head).not.toBe(base);
});
it("fails closed when the successful push baseline is not an ancestor", async () => {
const { directory, base } = await fixture();
await git(directory, ["switch", "-c", "unrelated"]);
const unrelated = await change(directory, ["other.txt"]);
await git(directory, ["switch", "main"]);
const head = await change(directory, ["README.md"]);
expect(await classify(directory, unrelated, head, "push")).toMatchObject({
tier: "fast",
code: true,
package: true,
evm: true,
browser: true,
});
expect(head).not.toBe(base);
});
it("fails classification rather than treating an invalid Git comparison as a scope", async () => {
const { directory, base } = await fixture();
await expect(classify(directory, "missing-ref", base)).rejects.toMatchObject({
code: 1,
});
expect(await readFile(join(directory, "scope-output"), "utf8")).toBe("");
});
it("retains native Inspector verification on every documented package host", async () => {
const stepSchema = z.object({
run: z.string().optional(),
if: z.string().optional(),
});
const steps = z.array(stepSchema);
const workflow = z
.object({
jobs: z.object({
"package-e2e": z.object({
steps,
}),
"windows-curated": z.object({ steps }),
}),
})
.parse(
parse(
await readFile(
new URL("../../../.github/workflows/ci.yml", import.meta.url),
"utf8",
),
),
);
const packageLane = workflow.jobs["package-e2e"];
const packageInspector = stepSchema.parse(
packageLane.steps.find((step) => step.run === "npm run verify:inspector"),
);
expect(packageInspector.if).toBeUndefined();
expect(
(await classify((await fixture()).directory, "", "", "workflow_dispatch"))
.matrix,
).toEqual({
include: [
{ os: "ubuntu-latest", platform: "linux", architecture: "x64" },
{ os: "ubuntu-24.04-arm", platform: "linux", architecture: "arm64" },
{ os: "macos-15", platform: "darwin", architecture: "arm64" },
{ os: "macos-15-intel", platform: "darwin", architecture: "x64" },
],
});
const windowsInspector = stepSchema.parse(
workflow.jobs["windows-curated"].steps.find(
(step) => step.run === "npm run verify:inspector",
),
);
expect(windowsInspector.if).toBeUndefined();
});
it.each([
[
"src/evm/client.ts",
{
code: true,
runtime: true,
evm: true,
browser: false,
package: false,
sqlite: false,
},
],
[
"scripts/verify-solidity-metadata.mjs",
{ code: true, evm: true, browser: false, package: false },
],
[
"src/browser/execution/evaluate.ts",
{
code: true,
runtime: true,
browser: true,
web_runtime: true,
evm: false,
package: false,
},
],
[
"tests/module/example.test.ts",
{ code: true, runtime: false, browser: false, package: false },
],
["website/src/index.ts", { code: false, website: true, package: false }],
[
"website/public/examples/notes-electron/preload.js",
{ code: true, website: true, package: false },
],
[
"website/locales/zh-CN/messages.po",
{ code: false, website: true, package: false },
],
["scripts/website_i18n.py", { code: false, website: true, package: false }],
[
"docs/readme/README_ja.md",
{ code: false, docs: true, website: true, package: false },
],
[
".github/workflows/website-pages.yml",
{ code: false, website: true, package: false },
],
[
"src/process/ProviderProcess.ts",
{ code: true, package: true, evm: true, browser: true },
],
[
"package-lock.json",
{ code: true, package: true, evm: true, browser: true },
],
[
".github/workflows/real-evm-interface.yml",
{ code: false, evm: true, browser: false, package: false },
],
["unknown-input", { code: true, package: true, evm: true, browser: true }],
[
"src/application/Example.ts",
{
code: true,
runtime: true,
package: false,
apple: false,
evm: false,
browser: false,
},
],
[
"bridge/ghidra/Example.java",
{ code: true, runtime: true, package: false, apple: false },
],
[
"src/native/AppleExample.ts",
{ code: true, runtime: true, apple: true, package: false, evm: false },
],
[
"scripts/verify/apple/example.mjs",
{ code: true, apple: true, package: false, runtime: false },
],
[
"src/windows/Example.ts",
{ code: true, runtime: true, package: true, apple: false, evm: false },
],
[
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
{ code: true, runtime: true, package: true, javascript_recovery: true },
],
["install.sh", { code: true, runtime: true, package: true, evm: false }],
[
".agents/skills/reverse-engineer-anything/SKILL.md",
{ code: true, runtime: true, docs: true, package: false, evm: false },
],
[
"scripts/verify-real-ghidra.mjs",
{ code: true, runtime: false, package: false, apple: false, evm: false },
],
[
"scripts/verify-claude-client.mjs",
{ code: true, runtime: false, package: false, browser: false },
],
[
"tests/boundary/process/darwinProcessRunTokenReader.test.ts",
{ code: true, apple: true, package: false, runtime: false },
],
[
"tests/boundary/cli/nativePlistCli.test.ts",
{ code: true, apple: true, package: false, runtime: false },
],
[
"tests/boundary/filesystem/windowsNpxRegistration.test.ts",
{ code: true, package: true, runtime: true, apple: false },
],
[
"src/process/ProcessOwnership.test.ts",
{ code: true, package: true, apple: false, evm: false, browser: false },
],
[
"src/sqlite/SqliteDatabaseProvider.ts",
{ code: true, sqlite: true, runtime: true, package: false },
],
[
"src/application/sqlite/SqliteDatabaseService.test.ts",
{ code: true, sqlite: true, runtime: false },
],
[
"tests/boundary/sqlite/sqliteDatabaseProvider.test.ts",
{ code: true, sqlite: true, runtime: false },
],
[
"tests/fixtures/sqlite/database.ts",
{ code: true, sqlite: true, runtime: false },
],
["tests/acceptance/sqliteDatabaseCli.test.ts", { code: true, sqlite: true }],
[
"tests/boundary/mcp/serverLifecycleMcp.test.ts",
{ code: true, sqlite: true },
],
["src/process/OwnedCommand.ts", { code: true, sqlite: true, runtime: true }],
["src/server/createServer.ts", { code: true, sqlite: true, runtime: true }],
[
"src/contracts/toolContracts.ts",
{ code: true, sqlite: true, runtime: true },
],
[
"src/domain/analysisErrorProjection.ts",
{ code: true, sqlite: true, runtime: true },
],
["src/domain/javascript/example.ts", { code: true, sqlite: false }],
])("selects the appropriate lanes for %s", async (path, expected) => {
const { directory, base } = await fixture();
const head = await change(directory, [path]);
expect(await classify(directory, base, head)).toMatchObject(expected);
});
it("scopes package script edits while retaining broad dependency checks", async () => {
const { directory } = await fixture();
const before = {
name: "fixture",
scripts: { "verify:evm": "node scripts/verify-evm-interface.mjs" },
};
const base = await change(
directory,
["package.json"],
JSON.stringify(before),
);
const after = {
...before,
scripts: {
"verify:evm": "node scripts/verify-evm-interface.mjs --verbose",
},
};
const head = await change(directory, ["package.json"], JSON.stringify(after));
expect(await classify(directory, base, head)).toMatchObject({
code: true,
evm: true,
browser: false,
package: false,
});
const dependency = await change(
directory,
["package.json"],
JSON.stringify({ ...after, dependencies: { example: "1.0.0" } }),
);
expect(await classify(directory, head, dependency)).toMatchObject({
code: true,
browser: true,
package: true,
});
});
it.each(["test:sqlite", "test:sqlite:run"])(
"routes %s script changes to the SQLite runtime matrix",
async (script) => {
const { directory } = await fixture();
const base = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { [script]: "vitest run" } }),
);
const head = await change(
directory,
["package.json"],
JSON.stringify({
scripts: { [script]: "vitest run --reporter=default" },
}),
);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
sqlite: true,
evm: false,
package: false,
});
},
);
it("keeps unrelated test script changes off the SQLite runtime matrix", async () => {
const { directory } = await fixture();
const base = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { "test:domain": "vitest run src/domain" } }),
);
const head = await change(
directory,
["package.json"],
JSON.stringify({
scripts: { "test:domain": "vitest run src/domain --reporter=default" },
}),
);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
sqlite: false,
evm: false,
});
});
it.each([
["browser", "real-browser"],
["binary_layout", "real-binary-layout"],
["evm", "real-evm-interface"],
["javascript_recovery", "real-javascript-recovery"],
["recorded_crash", "real-recorded-crash"],
["web_network", "real-web-network-captures"],
["web_runtime", "real-web-runtime"],
["web_source_map", "real-web-source-map"],
["nativeaot", "nativeaot-fixtures"],
["website", "website-check"],
])(
"routes %s through the selector without a duplicate PR trigger",
async (scope, name) => {
const workflow = z
.object({
on: z
.object({ workflow_call: z.object({}).passthrough().nullable() })
.passthrough(),
})
.parse(
parse(
await readFile(
new URL(`../../../.github/workflows/${name}.yml`, import.meta.url),
"utf8",
),
),
);
expect(workflow.on).not.toHaveProperty("pull_request");
const ci = z
.object({
jobs: z.record(
z.string(),
z.object({
uses: z.string().optional(),
if: z.string().optional(),
}),
),
})
.parse(
parse(
await readFile(
new URL("../../../.github/workflows/ci.yml", import.meta.url),
"utf8",
),
),
);
expect(ci.jobs[scope]).toMatchObject({
uses: `./.github/workflows/${name}.yml`,
if: `needs.changes.outputs.${scope} == 'true'`,
});
},
);
it("rejects skipped selected work while allowing unselected lanes to skip", async () => {
const command = fileURLToPath(
new URL("../../../scripts/ci/required.mjs", import.meta.url),
);
const { CI_FLAGS } = await loadScopes();
const jobs = {
changes: {
result: "success",
outputs: {
...Object.fromEntries(CI_FLAGS.map((flag) => [flag, "false"])),
tier: "fast",
evm: "true",
},
},
static: { result: "success" },
test: { result: "success" },
evm: { result: "success" },
browser: { result: "skipped" },
};
const run = (result: string) =>
execFileAsync(process.execPath, [command], {
env: {
...process.env,
CI_JOBS: JSON.stringify({ ...jobs, evm: { result } }),
},
});
await expect(run("success")).resolves.toMatchObject({ stderr: "" });
for (const result of ["failure", "cancelled", "skipped"]) {
await expect(run(result)).rejects.toMatchObject({ code: 1 });
}
});
it.each([
[
".github/workflows/sqlite-runtime.yml",
{ workflows: true, code: true, package: false, website: false },
],
[
".github/workflows/real-termux.yml",
{ workflows: true, code: false, package: false, website: false },
],
[
"src/domain/javascript/example.test.ts",
{ code: true, runtime: false, javascript_recovery: false, package: false },
],
[
"src/process/capture/example.test.ts",
{ code: true, runtime: false, browser: false, package: false },
],
[
".nvmrc",
{ code: true, package: true, browser: true, evm: true, sqlite: true },
],
[
"tsconfig.build.json",
{ code: true, package: true, browser: true, evm: true },
],
["docs/.vitepress/config.ts", { code: false, docs: true, package: false }],
["CHANGELOG.md", { code: false, docs: true, package: false }],
["SECURITY.md", { code: false, docs: true, package: false }],
[".github/pull_request_template.md", { code: false, docs: true }],
[
".github/ISSUE_TEMPLATE/bug.yml",
{ code: false, docs: true, workflows: false },
],
["native/windows/README.md", { code: false, docs: true, package: false }],
[
"src/contracts/evm/tools.ts",
{ code: true, docs: true, evm: true, package: false },
],
["scripts/unknown-website-helper.mjs", { code: true, package: true }],
[
".github/workflows/release.yml",
{ workflows: true, code: true, package: false, website: false },
],
[
".github/workflows/real-ghidra-windows.yml",
{ workflows: true, code: true, package: false, website: false },
],
[
"tests/conformance/c/fixture.c",
{ code: true, fixtures: true, readiness: true, package: false },
],
[
"tests/conformance/swift/fixture.swift",
{ code: true, fixtures: true, readiness: true, package: false },
],
[
"tests/conformance/readiness/javascript-cli/package.json",
{ code: true, fixtures: false, readiness: true, package: false },
],
[
"tests/conformance/ghidra/no-return.c",
{ code: true, fixtures: false, readiness: false, package: false },
],
])("preserves scope boundaries for %s", async (path, expected) => {
const { directory, base } = await fixture();
const head = await change(directory, [path]);
expect(await classify(directory, base, head)).toMatchObject(expected);
});
it("unions provider scopes across mixed changes and deletions", async () => {
const { directory } = await fixture();
const base = await change(directory, ["src/evm/client.ts"]);
await git(directory, ["rm", "src/evm/client.ts"]);
const head = await change(directory, ["src/browser/execution/evaluate.ts"]);
expect(await classify(directory, base, head)).toMatchObject({
evm: true,
browser: true,
runtime: true,
package: false,
});
});
it("fails without emitting a plan when package metadata is malformed", async () => {
const { directory } = await fixture();
const base = await change(directory, ["package.json"], '{"scripts":{}}');
const head = await change(directory, ["package.json"], "{broken");
await expect(classify(directory, base, head)).rejects.toMatchObject({
code: 1,
});
expect(await readFile(join(directory, "scope-output"), "utf8")).toBe("");
});
it.each([
[
"verify:keyed-archive",
"npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
{ apple: true, package: false, evm: false, browser: false },
],
[
"verify:inspector",
"npm run build:cached && node scripts/verify/inspector/runtime-observation.mjs",
{ package: true, apple: false, evm: false, browser: false },
],
[
"verify:windows-native",
"node scripts/verify-windows-native.mjs",
{ package: true, apple: false, evm: false, browser: false },
],
])(
"routes the %s script to its glob-owned lane",
async (script, command, expected) => {
const { directory } = await fixture();
const base = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { [script]: command } }),
);
const head = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { [script]: `${command} --verbose` } }),
);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
...expected,
});
},
);
it("retains a broad fallback when a provider script adds shared shell work", async () => {
const { directory } = await fixture();
const before = {
scripts: { "verify:evm": "node scripts/verify-evm-interface.mjs" },
};
const base = await change(
directory,
["package.json"],
JSON.stringify(before),
);
const after = {
scripts: {
"verify:evm": "node scripts/verify-evm-interface.mjs && npm run compile",
},
};
const head = await change(directory, ["package.json"], JSON.stringify(after));
expect(await classify(directory, base, head)).toMatchObject({
evm: true,
browser: true,
package: true,
});
});
it.each(["workflow_dispatch", "schedule"])(
"retains full selection for %s",
async (event) => {
const { directory, base } = await fixture();
expect(await classify(directory, base, base, event)).toMatchObject({
tier: "full",
code: true,
package: true,
sqlite: true,
evm: true,
browser: true,
});
},
);
it("honors ci:full for successive PR revisions", async () => {
const { directory, base } = await fixture();
const first = await change(directory, ["README.md"]);
const second = await change(directory, ["docs/readme/README_ja.md"]);
for (const head of [first, second]) {
expect(
await classify(directory, base, head, "pull_request", true),
).toMatchObject({
tier: "full",
code: true,
package: true,
sqlite: true,
evm: true,
browser: true,
});
}
});
it.each(["compile:termux", "build:termux", "build:termux:run"])(
"does not expand %s edits into unrelated providers",
async (script) => {
const { directory } = await fixture();
const base = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { [script]: "npm run compile:termux" } }),
);
const head = await change(
directory,
["package.json"],
JSON.stringify({
scripts: {
[script]:
"node scripts/run-exclusive.mjs artifacts npm run compile:termux",
},
}),
);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
workflows: true,
runtime: false,
package: false,
browser: false,
evm: false,
});
},
);
const INTEL_HOST = {
os: "macos-15-intel",
platform: "darwin",
architecture: "x64",
};
it("keeps macOS x64 package verification only for full qualification", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["src/windows/Example.ts"]);
const matrix = z.object({ include: z.array(z.unknown()) });
const hosts = async (event: string, full: boolean) =>
matrix.parse((await classify(directory, base, head, event, full)).matrix)
.include;
const pullRequest = await hosts("pull_request", false);
expect(pullRequest).toHaveLength(3);
expect(pullRequest).not.toContainEqual(INTEL_HOST);
expect(await hosts("pull_request", true)).toContainEqual(INTEL_HOST);
expect(await hosts("push", false)).not.toContainEqual(INTEL_HOST);
for (const event of ["schedule", "workflow_dispatch"])
expect(await hosts(event, false)).toContainEqual(INTEL_HOST);
});
const workflowFile = async (name: string): Promise<unknown> =>
parse(
await readFile(
new URL(`../../../.github/workflows/${name}`, import.meta.url),
"utf8",
),
);
const scopesSchema = z.object({
CI_FLAGS: z.array(z.string()),
SCOPES: z.record(z.string(), z.array(z.string())),
MANUAL_VERIFIERS: z.array(z.string()),
});
const loadScopes = async () =>
scopesSchema.parse(
await import(
new URL("../../../scripts/ci/scopes.mjs", import.meta.url).href
),
);
const stepRuns = z.object({
on: z.unknown(),
jobs: z.record(
z.string(),
z.object({
steps: z.array(z.object({ run: z.string().optional() })).optional(),
}),
),
});
it("owns every explicit native-host test path with the lane that runs it", async () => {
const { SCOPES } = await loadScopes();
const ci = stepRuns.parse(await workflowFile("ci.yml"));
const listed = (job: string) =>
(ci.jobs[job]?.steps ?? [])
.flatMap((step) => step.run?.split(/\s+/) ?? [])
.filter((token) => token.endsWith(".test.ts"));
const unowned = (job: string, scope: string) =>
listed(job).filter(
(path) =>
!(SCOPES[scope] ?? []).some((pattern) => matchesGlob(path, pattern)),
);
expect(listed("apple-artifacts").length).toBeGreaterThan(0);
expect(listed("windows-curated").length).toBeGreaterThan(0);
expect(unowned("apple-artifacts", "apple")).toEqual([]);
expect(unowned("windows-curated", "package")).toEqual([]);
});
it("never runs a manual-only verifier from an automatic workflow", async () => {
const { MANUAL_VERIFIERS } = await loadScopes();
const scripts = z
.object({ scripts: z.record(z.string(), z.string()) })
.parse(
JSON.parse(
await readFile(
new URL("../../../package.json", import.meta.url),
"utf8",
),
),
).scripts;
const directory = new URL("../../../.github/workflows/", import.meta.url);
const commands: string[] = [];
for (const name of await readdir(directory)) {
const workflow = stepRuns.parse(await workflowFile(name));
const triggers = JSON.stringify(workflow.on);
if (
name !== "ci.yml" &&
!triggers.includes("workflow_call") &&
!triggers.includes("pull_request")
)
continue;
for (const job of Object.values(workflow.jobs))
for (const step of job.steps ?? []) if (step.run) commands.push(step.run);
}
const files = new Set<string>();
const seen = new Set<string>();
while (commands.length) {
const command = commands.pop() ?? "";
for (const [, file] of command.matchAll(/\b(scripts\/[\w./-]+\.mjs)\b/g))
if (file) files.add(file);
for (const [, script] of command.matchAll(/npm run ([\w:.-]+)/g)) {
if (!script || seen.has(script)) continue;
seen.add(script);
const body = scripts[script];
if (body) commands.push(body);
}
}
expect(files.size).toBeGreaterThan(0);
expect(
[...files].filter((file) =>
MANUAL_VERIFIERS.some((pattern) => matchesGlob(file, pattern)),
),
).toEqual([]);
});