1247 lines
38 KiB
TypeScript
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([]);
|
|
});
|