openclaw/test/scripts/test-projects.test.ts

3092 lines
132 KiB
TypeScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { assert, beforeAll, describe, expect, it, vi } from "vitest";
import { listExtensionTestFilesForRoots } from "../../scripts/lib/extension-test-plan.mts";
import {
readRuntimeWorkerDescriptorReads as readDescriptorUses,
readTestSelectorSourceFacts as readSourceFacts,
} from "../../scripts/lib/test-selector-source-facts.mts";
import { resolveDefaultVitestNoOutputTimeoutMs } from "../../scripts/lib/vitest-process-env.mts";
import { resolveShardTimingKey } from "../../scripts/lib/vitest-shard-metadata.mts";
import { scriptModuleEntrypoints } from "../../scripts/script-module-runtime.test-support.mjs";
import {
applyDefaultVitestNoOutputTimeout,
applyFullExtensionsHeapBudget,
buildFullSuiteVitestRunPlans as planFullSuite,
buildVitestRunPlans as planRuns,
createVitestRunSpecs as runSpecs,
findUnmatchedExplicitTestTargets as unmatchedTargets,
hasImportGraphImpactOnTargets as impactsTargets,
isTestFileTarget,
orderFullSuiteSpecsForParallelRun,
parseTestProjectsArgs,
resolveChangedTestTargetPlanForArgs as changedPlanForArgs,
resolveChangedTestTargetPlan as changedPlan,
resolveChangedTargetArgs as changedArgs,
resolveAffectedTestsFromImportGraph as affectedTests,
resolveControlUiTestConsumers as uiConsumers,
resolveDependencyTestConsumers as dependencyConsumers,
writeVitestIncludeFile,
} from "../../scripts/test-projects.test-support.mts";
import {
resolveRuntimeWorkerArgv,
resolveRuntimeWorkerUrl,
} from "../../src/infra/runtime-worker-url.js";
import { withEnv } from "../../src/test-utils/env.js";
import { listGitTrackedFiles, toRepoPath } from "../../src/test-utils/repo-files.js";
import { requireNodeTool } from "../helpers/node-toolchain.js";
import { listVitestConfigTestFiles } from "../vitest-projects-config.test-support.js";
import { databaseWorkerCoreTestFiles } from "../vitest/vitest.database-worker-core-paths.mjs";
import { databaseWorkerExtensionTestFiles } from "../vitest/vitest.extension-database-workers-paths.mjs";
import {
gatewayDatabaseWorkerTestFiles,
isGatewayServerTestFile,
} from "../vitest/vitest.gateway-server-paths.mjs";
import { testRunnerFixtureEnv } from "./prepared-script-wrapper.test-support.js";
const repoPath = toRepoPath;
describe("Windows CI partitions", () => {
it("keeps explicit coverage disjoint without repeating small project setup", () => {
const { scripts } = JSON.parse(fs.readFileSync("package.json", "utf8")) as {
scripts: Record<string, string>;
};
const [first, second] = [1, 2].map((part) => {
const command = scripts[`test:windows:ci:${part}`];
assert(command);
const entrypoint = "scripts/test-projects.mts ";
expect(command).toContain(entrypoint);
const targets = command.slice(command.indexOf(entrypoint) + entrypoint.length).split(/\s+/u);
expect(targets.every((target) => isTestFileTarget(target))).toBe(true);
expect(unmatchedTargets(targets)).toEqual([]);
return {
targets,
configs: new Set(runSpecs(targets, { baseEnv: {} }).map((spec) => spec.config)),
};
});
assert(first && second);
const targets = [...first.targets, ...second.targets];
expect(new Set(targets).size).toBe(targets.length);
// Tooling and infra own shared fixtures. Other projects share setup within one part.
expect([...first.configs].filter((config) => second.configs.has(config))).toEqual([
configFile("tooling"),
configFile("infra"),
]);
});
});
function configFile(project: string) {
return `test/vitest/vitest.${project}.config.ts`;
}
function testFiles(root: string, names: string) {
return names.split(" ").map((name) => `${root}/${name}.test.ts`);
}
function selectedTestFiles(plans: readonly { includePatterns: readonly string[] | null }[]) {
return plans.flatMap((plan) => plan.includePatterns ?? []);
}
function withIncludeFile(patterns: string[], run: (file: string) => void, name = "include.json") {
withTinyFileTree({ [name]: JSON.stringify(patterns) }, (cwd) => run(path.join(cwd, name)));
}
function gitTree(cwd: string) {
const tree = spawnSync("git", ["write-tree"], { cwd, encoding: "utf8" });
expect(tree.status).toBe(0);
return tree.stdout.trim();
}
function withTinyGitRepo(files: Record<string, string>, test: (cwd: string) => void): void {
withTinyFileTree(files, (cwd) => {
const init = spawnSync("git", ["init"], { cwd, stdio: "ignore" });
expect(init.status).toBe(0);
const add = spawnSync("git", ["add", "."], { cwd, stdio: "ignore" });
expect(add.status).toBe(0);
test(cwd);
});
}
function withTinyFileTree(files: Record<string, string>, test: (cwd: string) => void): void {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-test-projects-"));
try {
for (const [file, source] of Object.entries(files)) {
const absolute = path.join(cwd, file);
fs.mkdirSync(path.dirname(absolute), { recursive: true });
fs.writeFileSync(absolute, source);
}
test(cwd);
} finally {
fs.rmSync(cwd, { recursive: true, force: true });
}
}
function expectChangedTargets(
changedPaths: string[],
targets: string[],
options?: Parameters<typeof changedPlan>[1],
): void {
expect(changedPlan(changedPaths, options), changedPaths.join(", ")).toEqual({
mode: "targets",
targets,
});
}
function runPlan(
config: string,
includePatterns: string[] | null = null,
forwardedArgs: string[] = [],
watchMode = false,
) {
return {
config: `test/vitest/vitest.${config}.config.ts`,
includePatterns,
forwardedArgs,
watchMode,
};
}
function expectRun(
actual: ReturnType<typeof planRuns>,
project: string,
expected: {
forwardedArgs?: string[];
includePatterns?: string[] | null;
watchMode?: boolean;
} = {},
): void {
expect(actual).toEqual([
runPlan(
project,
expected.includePatterns ?? null,
expected.forwardedArgs ?? [],
expected.watchMode ?? false,
),
]);
}
describe("scripts/test-projects changed-target routing", () => {
beforeAll(() => {
planRuns(["src/commands/onboard-non-interactive.test-helpers.ts"]);
unmatchedTargets([configFile("shared")], process.cwd());
});
it("bounds extensionless prefix probes while excluding deleted cached matches", () => {
const target = "src/selector/topic";
const rejectedFiles = [
"src/other/topic.test.ts",
"src/selector/topic-other.test.ts",
"src/selector/topic.helper.ts",
"src/selector/topic.child/nested.test.ts",
];
const files = Object.fromEntries(
[`${target}.test.ts`, `${target}.extra.spec.mts`, ...rejectedFiles].map((file) => [file, ""]),
);
withTinyGitRepo(files, (tempDir) => {
const cwd = fs.realpathSync(tempDir);
const candidatePaths = new Set(Object.keys(files).map((file) => path.join(cwd, file)));
const rejectedPaths = new Set(rejectedFiles.map((file) => path.join(cwd, file)));
const candidateProbes: string[] = [];
const originalExistsSync = fs.existsSync;
fs.existsSync = (file) => {
if (typeof file === "string" && candidatePaths.has(file)) {
candidateProbes.push(file);
}
return originalExistsSync(file);
};
try {
const parsed = [
parseTestProjectsArgs(["--changed", "origin/main"], cwd),
parseTestProjectsArgs(["--changed", "origin/main"], cwd),
];
for (const result of parsed) {
expect(result).toStrictEqual({
forwardedArgs: ["--changed", "origin/main"],
nonTargetArgs: ["--changed", "origin/main"],
targetArgs: [],
watchMode: false,
});
}
expect(candidateProbes).toHaveLength(0);
expectRun(planRuns([target], cwd), "unit", {
forwardedArgs: [`${target}.extra.spec.mts`, `${target}.test.ts`],
});
expect(unmatchedTargets([target], cwd)).toEqual([]);
fs.unlinkSync(path.join(cwd, `${target}.extra.spec.mts`));
expectRun(planRuns([target], cwd), "unit", {
forwardedArgs: [`${target}.test.ts`],
includePatterns: [`${target}.test.ts`],
});
expect(unmatchedTargets([target], cwd)).toEqual([]);
fs.unlinkSync(path.join(cwd, `${target}.test.ts`));
expect(unmatchedTargets([target], cwd)).toEqual([
{
target,
reason: "path-does-not-exist",
includePattern: `${target}{,.*}.{test,spec}.{js,jsx,ts,tsx,mjs,cjs,mts,cts}`,
},
]);
expect(candidateProbes.filter((file) => rejectedPaths.has(file))).toHaveLength(0);
} finally {
fs.existsSync = originalExistsSync;
}
});
});
it("skips deleted direct test files in changed mode", () => {
expect(
changedArgs(["--changed", "origin/main"], process.cwd(), () => [
"test/deleted-changed-target.test.ts",
]),
).toStrictEqual([]);
});
it("discovers conventional nested, dashed and basename script owners for .mts", () => {
const extension = "mts";
withTinyFileTree(
{
[`scripts/nested/fixture.${extension}`]: "",
"test/scripts/nested/fixture.test.ts": "",
"test/scripts/nested-fixture.test.ts": "",
"test/scripts/fixture.test.ts": "",
"src/scripts/nested/fixture.test.ts": "",
"src/scripts/nested-fixture.test.ts": "",
"src/scripts/fixture.test.ts": "",
"test/scripts/fixture.live.test.ts": "",
},
(cwd) => {
expectChangedTargets(
[`scripts/nested/fixture.${extension}`],
[
"test/scripts/nested/fixture.test.ts",
"test/scripts/nested-fixture.test.ts",
"test/scripts/fixture.test.ts",
"src/scripts/nested/fixture.test.ts",
"src/scripts/nested-fixture.test.ts",
"src/scripts/fixture.test.ts",
],
{ cwd },
);
expect(changedPlan([`scripts/fixture.${extension}`], { cwd }).targets).toEqual([
"test/scripts/fixture.test.ts",
"src/scripts/fixture.test.ts",
]);
expect(changedPlan([`scripts/unowned.${extension}`], { cwd }).targets).toEqual([
configFile("tooling"),
]);
},
);
});
it("selects compiler planner unit and workflow consumers for planner changes", () => {
expectChangedTargets(
["scripts/ci-check-plan.mts"],
testFiles("test/scripts", "ci-check-plan ci-workflow-planning"),
);
});
it("unions semantic workflow owners with bounded direct references", () => {
withTinyGitRepo(
{
".github/workflows/full-release-validation.yml": "name: FRV\n",
"test/scripts/full-release-validation-state.test.ts":
'const workflow = ".github/workflows/full-release-validation.yml";\n',
"test/scripts/unknown-frv-contract.test.ts":
'const workflow = ".github/workflows/full-release-validation.yml";\n',
"test/scripts/unknown-frv-contract.live.test.ts":
'const workflow = ".github/workflows/full-release-validation.yml";\n',
"test/scripts/test-projects.test.ts":
'const workflow = ".github/workflows/full-release-validation.yml";\n',
"test/scripts/workflow-substring.test.ts":
'const workflow = ".github/workflows/full-release-validation.yml.bak";\n',
},
(cwd) => {
const targets = changedPlan([".github/workflows/full-release-validation.yml"], {
cwd,
}).targets;
expect(targets).toContain("test/scripts/unknown-frv-contract.test.ts");
expect(
targets.filter(
(target) => target === "test/scripts/full-release-validation-state.test.ts",
),
).toHaveLength(1);
expect(targets).not.toContain("test/scripts/unknown-frv-contract.live.test.ts");
expect(targets).not.toContain("test/scripts/test-projects.test.ts");
expect(targets).not.toContain("test/scripts/workflow-substring.test.ts");
},
);
});
it("does not scan direct references for semantic non-YAML tooling", () => {
withTinyGitRepo(
{
"scripts/pr": "#!/bin/sh\n",
"test/scripts/direct-tooling-reference.test.ts": 'const target = "scripts/pr";\n',
},
(cwd) => {
const targets = changedPlan(["scripts/pr"], { cwd }).targets;
expect(targets).not.toContain("test/scripts/direct-tooling-reference.test.ts");
},
);
});
it("leaves actionlint validation external while retaining config consumers", () => {
for (const withConsumers of [false, true]) {
withTinyGitRepo(
{
".github/actionlint.yaml": "self-hosted-runner: {}\n",
".github/unknown-lint.yaml": "{}\n",
...(withConsumers
? {
"src/lint-config.ts":
'export const config = new URL("../.github/actionlint.yaml", import.meta.url);\n',
"test/scripts/lint-import.test.ts": 'import "../../src/lint-config.js";\n',
"test/scripts/lint-reader.test.ts": 'const config = ".github/actionlint.yaml";\n',
}
: {}),
},
(cwd) => {
expectChangedTargets(
[".github/actionlint.yaml"],
withConsumers ? testFiles("test/scripts", "lint-import lint-reader") : [],
{
cwd,
forceFullImportGraph: true,
resolveAliases: true,
runtimeOnly: true,
},
);
expect(changedPlan([".github/unknown-lint.yaml"], { cwd }).targets).toEqual([
configFile("tooling"),
]);
},
);
}
});
it.each<[string[], string[] | null]>([
[["src/selector/one.test.ts"], ["src/selector/one.test.ts"]],
[["./src/selector/one.test.ts"], ["src/selector/one.test.ts"]],
[["src/selector/missing.test.ts"], null],
[["src/selector/one.spec.ts"], null],
[["src/selector/*.test.ts"], null],
[["src/selector/one.test.ts", "src/selector/one.spec.ts"], null],
[testFiles("src/selector", "one missing"), null],
[["src/selector/one.live.test.ts"], null],
[["outside/one.test.ts"], null],
])("selects default-unit includes without losing CLI targets: %j", (targets, includePatterns) => {
withTinyFileTree(
{
"src/selector/one.test.ts": "",
"src/selector/one.spec.ts": "",
"src/selector/one.live.test.ts": "",
"outside/one.test.ts": "",
},
(cwd) => {
const filters = includePatterns ? ["-t", "case"] : [];
expectRun(planRuns([...targets, "--", ...filters], cwd), "unit", {
forwardedArgs: [...filters, ...targets],
includePatterns,
});
},
);
});
it("preserves absolute and watch default-unit target semantics", () => {
const target = "src/selector/one.test.ts";
withTinyFileTree({ [target]: "" }, (cwd) => {
const absolute = path.join(cwd, target);
expectRun(planRuns([absolute], cwd), "unit", { forwardedArgs: [absolute] });
expectRun(planRuns(["--watch", target], cwd), "unit", {
forwardedArgs: [target],
watchMode: true,
});
});
});
it("keeps inherited default-unit include files intersected with the CLI target", () => {
const target = "src/selector/one.test.ts";
withTinyFileTree({ [target]: "", "include.json": JSON.stringify([]) }, (cwd) => {
const includeFile = path.join(cwd, "include.json");
const [spec] = runSpecs([target], {
cwd,
baseEnv: { OPENCLAW_VITEST_INCLUDE_FILE: includeFile },
});
expect(spec).toMatchObject({
config: configFile("unit"),
includePatterns: null,
includeFilePath: null,
env: { OPENCLAW_VITEST_INCLUDE_FILE: includeFile },
});
expect(spec?.pnpmArgs.at(-1)).toBe(target);
expect(fs.readFileSync(includeFile, "utf8")).toBe("[]");
});
});
it.each(["test/scripts/run-node.test.ts", "extensions/matrix/src/matrix/client/storage.test.ts"])(
"keeps owned include selection independent of missing borrowed metadata: %s",
(target) => {
withTinyFileTree({}, (tempDir) => {
const borrowed = path.join(tempDir, "missing.json");
const files = Array.from({ length: 2 }, () => {
const [spec] = runSpecs([target], {
baseEnv: { OPENCLAW_VITEST_INCLUDE_FILE: borrowed },
});
expect(spec?.includePatterns).toEqual([target]);
assert(spec?.includeFilePath);
expect(path.dirname(spec.includeFilePath)).toBe(os.tmpdir());
expect(spec.env.OPENCLAW_VITEST_INCLUDE_FILE).toBe(spec.includeFilePath);
expect(spec.includeFilePath).not.toBe(borrowed);
return spec.includeFilePath;
});
expect(files[0]).not.toBe(files[1]);
expect(fs.existsSync(borrowed)).toBe(false);
});
},
);
it.each([
{ name: "Git inventory", withRepo: withTinyGitRepo },
{ name: "filesystem inventory", withRepo: withTinyFileTree },
])(
"attributes runtime-worker fixtures to their executing descriptor readers ($name)",
({ withRepo }) => {
const aggregator = "src/workers/entrypoints.test-support.ts";
const dep = "src/workers/fixtures/dep-a.ts";
const readerA = "src/workers/read-a.test.ts";
const readerAll = "src/workers/read-all.test.ts";
const readerB = "src/workers/read-b.test.ts";
const readerSingle = "src/workers/read-single.test.ts";
const readerSingleChild = "src/workers/read-single-child.test.ts";
const readerInline = "src/workers/read-inline.test.ts";
const readerManifest = "test/scripts/manifest.test.ts";
const expected = [readerA, readerAll];
withRepo(
{
[aggregator]: `export const group = {
a: { currentModuleUrl: import.meta.url, sourceWorkerName: "fixtures/a.test-support" },
b: { sourceWorkerName: "fixtures/b.test-support", currentModuleUrl: import.meta.url },
} as const;
export const single = {
currentModuleUrl: import.meta.url, sourceWorkerName: "fixtures/single.test-support",
child: { currentModuleUrl: import.meta.url, sourceWorkerName: "fixtures/b.test-support" },
} as const;`,
"src/workers/fixtures/a.test-support.ts": 'import "./dep-a.js";',
"src/workers/fixtures/b.test-support.ts": 'import "./dep-b.js";',
"src/workers/fixtures/single.test-support.ts": 'import "./dep-single.js";',
"src/workers/fixtures/inline.test-support.ts": 'import "./dep-inline.js";',
[dep]: "export {};",
"src/workers/fixtures/dep-b.ts": "export {};",
"src/workers/fixtures/dep-single.ts": "export {};",
"src/workers/fixtures/dep-inline.ts": "export {};",
[readerA]:
'import { group as workers } from "./entrypoints.test-support.js"; void workers.a;',
[readerB]: 'import { group } from "./entrypoints.test-support.js"; void group.b;',
[readerAll]:
'import { group } from "./entrypoints.test-support.js"; Object.values(group);',
[readerSingle]:
'import { single } from "./entrypoints.test-support.js"; void single.distWorkerPath;',
[readerSingleChild]:
'import { single } from "./entrypoints.test-support.js"; void single.child;',
[readerInline]: `const worker = {
currentModuleUrl: import.meta.url, sourceWorkerName: "fixtures/inline.test-support",
}; void worker;`,
"scripts/lib/manifest.mts":
'import { group } from "../../src/workers/entrypoints.test-support.js"; Object.values(group);',
[readerManifest]: 'import "../../scripts/lib/manifest.mjs";',
},
(cwd) => {
const options = { tooling: true, resolveAliases: true, runtimeOnly: true };
// The aggregator may already be visited when its executing consumer is reached.
expect(impactsTargets([dep], [aggregator, readerA], cwd, options)).toBe(true);
expect(impactsTargets([dep], [readerAll], cwd, options)).toBe(true);
for (const unrelated of [
readerB,
readerSingle,
readerSingleChild,
readerInline,
readerManifest,
]) {
expect(impactsTargets([dep], [unrelated], cwd, options), unrelated).toBe(false);
}
expectChangedTargets([dep], expected, { cwd, broad: true });
expect(affectedTests([dep], cwd, { ...options, forceFull: true })).toEqual(expected);
for (const [dependency, reader] of [
["dep-b", readerB],
["dep-single", readerSingle],
["dep-inline", readerInline],
] satisfies [string, string][]) {
expect(
affectedTests([`src/workers/fixtures/${dependency}.ts`], cwd, {
...options,
forceFull: true,
}),
).toEqual(
dependency === "dep-b"
? [readerAll, reader, readerSingleChild].toSorted((left, right) =>
left.localeCompare(right),
)
: [reader],
);
}
},
);
},
);
it.each<[string, boolean]>([
["tooling", false],
["tooling", true],
["extension-database-workers", false],
])(
"preserves owned files and inherited directory selection for %s (directory first: %s)",
(config, directoryFirst) => {
const worker = config === "extension-database-workers";
const owned = worker
? "extensions/matrix/src/matrix/client/storage.test.ts"
: "test/scripts/run-node.test.ts";
const selected = worker
? "extensions/matrix/src/matrix/sdk/idb-persistence.test.ts"
: "test/helpers/temp-dir.test.ts";
const directory = worker ? "extensions/matrix/src/matrix/sdk" : "test/helpers";
withIncludeFile([selected], (borrowed) => {
const args = directoryFirst ? [directory, owned] : [owned, directory];
const specs = runSpecs(args, {
baseEnv: { OPENCLAW_VITEST_INCLUDE_FILE: borrowed },
});
const spec = specs.find(
(entry) => entry.config === `test/vitest/vitest.${config}.config.ts`,
);
const expected = directoryFirst ? [selected, owned] : [owned, selected];
expect(worker ? spec?.includePatterns?.toSorted() : spec?.includePatterns).toEqual(
worker ? expected.toSorted() : expected,
);
expect(fs.readFileSync(borrowed, "utf8")).toBe(JSON.stringify([selected]));
});
},
);
it("preserves Git membership for large changed and explicit test inventories", () => {
withTinyGitRepo(
{
".gitignore": "src/runtime.ignored.test.ts\n",
"src/runtime.ts": "export const value = 1;\n",
"src/runtime.consumer.test.ts": 'import "./runtime.js";\n',
},
(cwd) => {
// Index-only padding reproduces a large checkout without thousands of fixture files.
const blob = spawnSync("git", ["hash-object", "-w", "--stdin"], {
cwd,
encoding: "utf8",
input: "",
});
expect(blob.status).toBe(0);
const padding = Array.from(
{ length: 6_000 },
(_, index) =>
`100644 ${blob.stdout.trim()}\tsrc/padding/${index}-${"x".repeat(190)}.ts\n`,
).join("");
const indexed = spawnSync("git", ["update-index", "--index-info"], {
cwd,
input: padding,
});
expect(indexed.status).toBe(0);
for (const root of ["extensions", "packages", "ui/src", "ui/config", "test"]) {
fs.mkdirSync(path.join(cwd, root), { recursive: true });
}
fs.writeFileSync(
path.join(cwd, "src/runtime.untracked.test.ts"),
'import "./runtime.js";\n',
);
fs.writeFileSync(path.join(cwd, "src/runtime.ignored.test.ts"), 'import "./runtime.js";\n');
expect(changedPlan(["src/runtime.ts"], { cwd }).targets).toEqual([
"src/runtime.consumer.test.ts",
]);
const includeFile = path.join(cwd, "include.json");
writeVitestIncludeFile(includeFile, ["src/**/*.test.ts"], { cwd });
expect(JSON.parse(fs.readFileSync(includeFile, "utf8")).toSorted()).toEqual(
testFiles("src", "runtime.consumer runtime.untracked"),
);
},
);
});
it("follows converging import frontiers without crossing test boundaries", () => {
withTinyGitRepo(
{
"src/value.ts": 'export * from "./left/bridge.js";\n',
"src/left/bridge.ts": 'export\n * from\n "../value.js";\n',
"src/right/bridge.ts": 'export * from "../value.js";\n',
"src/other/bridge.ts": "export const unrelated = 1;\n",
"src/combined.consumer.test.ts":
'import\u00a0"./left/bridge.js";\nimport(\n"./right/bridge.js"\n);\n',
"src/right.consumer.test.ts": 'import "./right/bridge.js";\n',
"src/other.consumer.test.ts": 'import "./other/bridge.js";\n',
"src/after-test.consumer.test.ts": 'import "./combined.consumer.test.js";\n',
"src/value.consumer.live.test.ts": 'import "./value.js";\n',
},
(cwd) => {
expect(changedPlan(["src/value.ts"], { cwd }).targets).toEqual(
testFiles("src", "combined.consumer right.consumer"),
);
},
);
});
it("bounds direct importer queries to one edge and retains edited tests", () => {
withTinyGitRepo(
{
"src/value.ts": "export const value = 1;\n",
"src/direct.test.ts": 'import "./value.js";\n',
"src/bridge.ts": 'export * from "./value.js";\n',
"src/indirect.test.ts": 'import "./bridge.js";\n',
"src/shared.test.ts": "export const fixture = 1;\n",
"src/shared-consumer.test.ts": 'import "./shared.test.js";\n',
},
(cwd) => {
for (const changed of ["src/value.ts", ["src/value.ts"]]) {
expect(affectedTests(changed, cwd, { direct: true })).toEqual(["src/direct.test.ts"]);
}
expect(affectedTests(["src/shared.test.ts"], cwd, { direct: true })).toEqual(
testFiles("src", "shared-consumer shared"),
);
expect(
impactsTargets(["src/value.ts"], ["src/direct.test.ts"], cwd, {
direct: true,
}),
).toBe(true);
expect(
impactsTargets(["src/value.ts"], ["src/indirect.test.ts"], cwd, {
direct: true,
}),
).toBe(false);
expect(impactsTargets(["src/value.ts"], ["src/indirect.test.ts"], cwd)).toBe(true);
},
);
});
it("retains configured Vitest setup, runner, and environment consumers", () => {
withTinyGitRepo(
{
[configFile("shared")]: `
import "./vitest.extension-config.ts";
const runnerPath = path.join(repoRoot, "test", "runner.ts",);
export const config = {
setupFiles: [resolveRepoRootPath("test/setup.ts")],
globalSetup: [resolveRepoRootPath("test/global-setup.ts")],
runner: runnerPath,
include: ["src/unrelated.test.ts"],
};`,
"test/vitest/vitest.extension-config.ts": `
export const createConfig = () => ({ setupFiles: ["test/extension-setup.ts"] });`,
[configFile("ui")]: `
import type { DOMWindow } from "jsdom";
import "./vitest.shared.config.ts";
export default { environment: "jsdom", setupFiles: ["ui/src/setup.ts"] };`,
"ui/vitest.config.ts": `
const nodeSetupFiles = ["./src/setup.ts"];
export default { environment: "jsdom", setupFiles: nodeSetupFiles };`,
"test/setup.ts": 'import "./helper.ts";',
"test/helper.ts": "export const helper = true;",
"test/runner.ts": 'import "./runner-helper.ts";',
"test/runner-helper.ts": "export const runnerHelper = true;",
"test/global-setup.ts": 'import "./global-helper.ts";',
"test/global-helper.ts": "export const globalHelper = true;",
"test/extension-setup.ts": 'import "./extension-helper.ts";',
"test/extension-helper.ts": "export const extensionHelper = true;",
"ui/src/setup.ts": 'import "./setup-helper.ts";',
"ui/src/setup-helper.ts": "export const uiSetup = true;",
"ui/src/component.test.ts": 'import "jsdom";',
"src/unrelated.test.ts": "export const unrelated = true;",
},
(cwd) => {
const options = { tooling: true, resolveAliases: true, runtimeOnly: true };
for (const helper of [
"test/helper.ts",
"test/runner-helper.ts",
"test/global-helper.ts",
"test/extension-helper.ts",
]) {
expect(impactsTargets([helper], [configFile("ui")], cwd, options), helper).toBe(true);
}
expect(
impactsTargets(["ui/src/setup-helper.ts"], ["ui/vitest.config.ts"], cwd, options),
).toBe(true);
expect(impactsTargets(["src/unrelated.test.ts"], [configFile("ui")], cwd, options)).toBe(
false,
);
const consumers = dependencyConsumers([{ root: ".", dependencies: ["jsdom"] }], cwd, {
runtimeOnly: true,
});
expect(consumers.unresolved).toEqual([]);
expect(consumers.sources.toSorted()).toEqual([
configFile("ui"),
"ui/src/component.test.ts",
"ui/vitest.config.ts",
]);
},
);
});
it("follows source aliases and package exports through shared test consumers for CI", () => {
withTinyGitRepo(
{
"tsconfig.json": JSON.stringify({
compilerOptions: {
paths: {
"openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"],
"@openclaw/library/*": ["./packages/library/src/*.ts"],
"@test-fixture/*": ["./test/fixtures/*/index.test.ts"],
},
},
}),
"packages/library/package.json": JSON.stringify({
name: "@openclaw/library",
exports: {
".": { default: "./src/index.ts" },
"./test-entry": { import: "./src/import.test.ts", require: "./src/require.test.ts" },
},
}),
"src/value.ts": "export const value = 1;\n",
"src/native-helper.js": 'export * from "./value.js";\n',
"src/script-consumer.test.ts": 'import "./native-helper.js";\n',
"src/plugin-sdk/public.ts": 'export * from "../value.js";\n',
"packages/library/src/index.ts": 'export * from "openclaw/plugin-sdk/public";\n',
"packages/library/src/other.ts": "export const unrelated = 1;\n",
"packages/library/src/import.test.ts": "export const importValue = 1;\n",
"packages/library/src/require.test.ts": "export const requireValue = 1;\n",
"test/fixtures/owner/index.test.ts": "export const value = 1;\n",
"extensions/consumer/conditional.test.ts": 'import "@openclaw/library/test-entry";\n',
"extensions/consumer/fixture.test.ts": 'import "@test-fixture/owner";\n',
"extensions/consumer/shared.test.ts": 'import "@openclaw/library";\n',
"extensions/consumer/downstream.test.ts": 'import "./shared.test.js";\n',
"extensions/consumer/direct.test.ts": 'require("openclaw/plugin-sdk/public");\n',
"extensions/consumer/type-only.test.ts":
'import type { value } from "openclaw/plugin-sdk/public";\n',
"extensions/consumer/other.test.ts": 'import "@openclaw/library/other";\n',
},
(cwd) => {
const expected = testFiles("extensions/consumer", "direct downstream shared");
expect(affectedTests(["src/value.ts"], cwd)).toEqual([]);
expect(affectedTests(["src/value.ts"], cwd, { resolveAliases: true })).toEqual([
...expected,
"extensions/consumer/type-only.test.ts",
]);
expect(
affectedTests(["src/value.ts"], cwd, {
resolveAliases: true,
runtimeOnly: true,
}),
).toEqual(expected);
expect(
affectedTests(["extensions/consumer/shared.test.ts"], cwd, {
resolveAliases: true,
forceFull: true,
}),
).toEqual(["extensions/consumer/downstream.test.ts"]);
expect(
affectedTests(["packages/library/src/other.ts"], cwd, {
resolveAliases: true,
}),
).toEqual(["extensions/consumer/other.test.ts"]);
for (const target of testFiles("packages/library/src", "import require")) {
expect(
affectedTests([target], cwd, {
resolveAliases: true,
forceFull: true,
}),
).toEqual(["extensions/consumer/conditional.test.ts"]);
}
expect(
affectedTests(["test/fixtures/owner/index.test.ts"], cwd, {
resolveAliases: true,
forceFull: true,
}),
).toEqual(["extensions/consumer/fixture.test.ts"]);
expect(affectedTests("src/value.ts", cwd, { tooling: true })).toEqual([
"src/script-consumer.test.ts",
]);
expect(
affectedTests(["src/value.ts"], cwd, {
tooling: true,
resolveAliases: true,
runtimeOnly: true,
}),
).toEqual([...expected, "src/script-consumer.test.ts"]);
fs.unlinkSync(path.join(cwd, "extensions/consumer/shared.test.ts"));
expect(
affectedTests(["extensions/consumer/shared.test.ts"], cwd, {
resolveAliases: true,
forceFull: true,
}),
).toEqual(["extensions/consumer/downstream.test.ts"]);
},
);
});
it("retains import consumers after a changed source is removed from the index", () => {
withTinyGitRepo({ "src/consumer.test.ts": 'export * from "./removed.js";\n' }, (cwd) => {
expect(
affectedTests(["src/removed.ts"], cwd, {
resolveAliases: true,
}),
).toEqual(["src/consumer.test.ts"]);
});
});
it("routes changed resolved dependencies through scoped bare and subpath consumers", () => {
withTinyGitRepo(
{
"src/root.ts": 'import "dependency/runtime"; import "second-dependency";\n',
"src/type-owner.ts":
'import type { Shape } from "types-only"; export type Callback = <T>(value: T) => T;\n',
"src/root.test.ts": 'import "./root.js";\n',
"extensions/consumer/source.ts": 'require("dependency");\n',
"extensions/consumer/source.test.ts": 'import "./source.js";\n',
"extensions/consumer/direct.test.ts": 'import("dependency/entry");\n',
"extensions/consumer/other.test.ts": 'import "dependency-other";\n',
"test/scripts/tool.test.ts":
'require.resolve("dependency"); import.meta.resolve("dependency/runtime");\n',
"test/scripts/generated-cli.test.ts": 'spawnSync("node_modules/.bin/opaque-cli", []);\n',
"ui/src/element-view.tsx":
'export const view = <><span></span><Lazy load={() => import("dependency")} /></>;\n',
"ui/src/element-view.test.ts": 'import "./element-view.js";\n',
"ui/src/text-view.tsx":
'export const view = <div> // inline text {import("dependency")} </div>;\n',
"ui/src/text-view.test.ts": 'import "./text-view.js";\n',
},
(cwd) => {
expect(
dependencyConsumers([{ root: "extensions/consumer", dependencies: ["dependency"] }], cwd)
.tests,
).toEqual(testFiles("extensions/consumer", "direct source"));
const root = dependencyConsumers([{ root: ".", dependencies: ["dependency"] }], cwd);
expect(root.sources.toSorted()).toEqual([
"extensions/consumer/direct.test.ts",
"extensions/consumer/source.ts",
"src/root.ts",
"test/scripts/tool.test.ts",
"ui/src/element-view.tsx",
"ui/src/text-view.tsx",
]);
expect(root.tests).toEqual([
"extensions/consumer/direct.test.ts",
"extensions/consumer/source.test.ts",
"src/root.test.ts",
"test/scripts/tool.test.ts",
"ui/src/element-view.test.ts",
"ui/src/text-view.test.ts",
]);
expect(root.unresolved).toEqual([]);
const mixed = dependencyConsumers(
[
{
root: ".",
dependencies: ["dependency", "second-dependency", "opaque-cli", "types-only"],
},
],
cwd,
{ runtimeOnly: true },
);
expect(mixed.tests).toEqual(root.tests);
expect(mixed.unresolved).toEqual([{ root: ".", dependency: "opaque-cli" }]);
expect(
dependencyConsumers([{ root: ".", dependencies: ["types-only"] }], cwd, {
runtimeOnly: true,
}),
).toEqual({
sources: [],
tests: [],
unresolved: [],
});
},
);
});
it("keeps root dependency changes behind nearer unchanged workspace bindings", () => {
withTinyGitRepo(
{
"src/root.test.ts": 'import "dependency";\n',
"ui/src/own.test.ts": 'import "dependency/subpath";\n',
"ui/src/shadowed.test.ts": 'import "workspace-only";\n',
"packages/own/src/value.ts": 'require("dependency");\n',
"packages/own/src/value.test.ts": 'import "./value.js";\n',
"src/package-consumer.test.ts": 'import "../packages/own/src/value.js";\n',
"extensions/own/value.test.ts": 'import "dependency";\n',
"extensions/undeclared/value.test.ts": 'import "dependency";\n',
},
(cwd) => {
const importerBindings = [".", "ui", "packages/own", "extensions/own"].map((root) => ({
root,
dependencies: root === "ui" ? ["dependency", "workspace-only"] : ["dependency"],
}));
expect(
dependencyConsumers([{ root: ".", dependencies: ["dependency"] }], cwd, {
importerBindings,
}).tests,
).toEqual(["extensions/undeclared/value.test.ts", "src/root.test.ts"]);
expect(
dependencyConsumers([{ root: "packages/own", dependencies: ["dependency"] }], cwd, {
importerBindings,
}).tests,
).toEqual(["packages/own/src/value.test.ts", "src/package-consumer.test.ts"]);
expect(
dependencyConsumers([{ root: "ui", dependencies: ["dependency"] }], cwd, {
importerBindings: importerBindings.filter(({ root }) => root !== "ui"),
}).tests,
).toEqual(["ui/src/own.test.ts"]);
expect(
dependencyConsumers([{ root: ".", dependencies: ["workspace-only"] }], cwd, {
importerBindings,
}).unresolved,
).toEqual([{ root: ".", dependency: "workspace-only" }]);
expect(
dependencyConsumers([{ root: "ui", dependencies: ["workspace-only"] }], cwd, {
importerBindings,
}).unresolved,
).toEqual([]);
},
);
});
it.each([false, true])(
"keeps broad-name consumers complete without synchronous source opens (narrow importer: %s)",
(includeNarrowImporter) => {
const files: Record<string, string> = {
"src/owner/value.ts": "export const value = 1;\n",
"src/owner/barrel.ts": 'export\n { value } from\n "./value.js";\n',
"src/owner/directory/index.ts": 'export * from "../barrel.js";\n',
"src/owner/consumer.test.ts": 'import(\n "./directory"\n);\n',
"src/owner/type.consumer.test.ts": 'import type {\n Value\n } from "./value.js";\n',
"src/owner/deleted.test.ts": 'import "./value.js";\n',
"src/owner/value.live.test.ts": 'import "./value.js";\n',
};
if (includeNarrowImporter) {
files["src/outside.consumer.test.ts"] = 'import "./owner/value.js";\n';
}
// A narrow path match must not hide consumers of a widely occurring basename.
for (let index = 0; index < 801; index += 1) {
files[`src/padding/${index}.ts`] = "// value\n";
}
withTinyGitRepo(files, (cwd) => {
fs.unlinkSync(path.join(cwd, "src/owner/deleted.test.ts"));
fs.writeFileSync(path.join(cwd, "src/owner/untracked.test.ts"), 'import "./value.js";\n');
const reads = vi.spyOn(fs, "readFileSync");
try {
const expectedTargets = [
...(includeNarrowImporter ? ["src/outside.consumer.test.ts"] : []),
"src/owner/consumer.test.ts",
"src/owner/type.consumer.test.ts",
];
expect(
changedPlan(["src/owner/value.ts"], { cwd }).targets,
"initial owner selection",
).toEqual(expectedTargets);
expect(
changedPlan(["src/owner/value.ts"], { cwd }).targets,
"cached owner selection",
).toEqual(expectedTargets);
expect(
reads.mock.calls.filter(([file]) => typeof file === "string" && file.startsWith(cwd)),
).toEqual([]);
} finally {
reads.mockRestore();
}
});
},
);
it.each([
{ name: "Git inventory", withRepo: withTinyGitRepo },
{ name: "filesystem inventory", withRepo: withTinyFileTree },
])(
"keeps tooling imports direct while preserving literal file references ($name)",
({ withRepo }) => {
const files: Record<string, string> = {
"tsconfig.json": JSON.stringify({
compilerOptions: { paths: { "@fixture/tool": ["./scripts/fixture-bridge.mts"] } },
}),
"scripts/fixture-source.mts": "export const value = 1;\n",
"scripts/fixture-data.json": '{"value":1}\n',
"scripts/no-direct-owner.mts": "export const other = 1;\n",
"scripts/fixture-bridge.mts":
'import data from "./fixture-data.json" with { type: "json" }; export { data }; export * from "./fixture-source.mjs"; export * from "./no-direct-owner.mjs";\n',
"test/scripts/alias.consumer.test.ts": 'import "@fixture/tool";\n',
"test/scripts/direct.consumer.test.ts": 'import "../../scripts/fixture-source.mjs";\n',
"test/scripts/type.consumer.test.ts":
'import type { Value } from "../../scripts/fixture-source.mjs";\n',
"test/scripts/transitive.consumer.test.ts": 'import "../../scripts/fixture-bridge.mjs";\n',
"test/scripts/literal.consumer.test.ts": 'const fixture = "scripts/fixture-source.mts";\n',
"test/scripts/substring.consumer.test.ts":
'const fixture = "scripts/fixture-source.mts.bak";\n',
};
for (let index = 0; index < 801; index += 1) {
files[`src/padding/${index}.ts`] = "// scripts/fixture-source\n";
}
withRepo(files, (cwd) => {
expect(changedPlan(["scripts/fixture-source.mts"], { cwd }).targets).toEqual(
testFiles("test/scripts", "direct.consumer type.consumer literal.consumer"),
);
const ciOptions = {
cwd,
forceFullImportGraph: true,
resolveAliases: true,
runtimeOnly: true,
};
expect(changedPlan(["scripts/fixture-source.mts"], ciOptions).targets).toEqual(
testFiles(
"test/scripts",
"alias.consumer direct.consumer transitive.consumer literal.consumer",
),
);
expect(changedPlan(["scripts/no-direct-owner.mts"], { cwd }).targets).toEqual([
configFile("tooling"),
]);
expect(changedPlan(["scripts/no-direct-owner.mts"], ciOptions).targets).toEqual(
testFiles("test/scripts", "alias.consumer transitive.consumer"),
);
expect(changedPlan(["scripts/fixture-data.json"], { cwd }).targets).toEqual([
configFile("tooling"),
]);
expect(changedPlan(["scripts/fixture-data.json"], ciOptions).targets).toEqual(
testFiles("test/scripts", "alias.consumer transitive.consumer"),
);
});
},
);
it.each([
{ name: "Git inventory", withRepo: withTinyGitRepo },
{ name: "filesystem inventory", withRepo: withTinyFileTree },
])("routes test helper and file-URL fixture consumers completely ($name)", ({ withRepo }) => {
const fixture = "test/scripts/fixtures/worker.mjs";
const helper = "test/scripts/runtime.test-support.ts";
const direct = "test/scripts/direct.consumer.test.ts";
const indirect = "test/scripts/indirect.consumer.test.ts";
const opaque = "test/scripts/opaque/index.mjs";
const explicitHelper = "test/scripts/source/input.test-support.ts";
const explicitConsumer = "test/scripts/outer/consumer.test.ts";
withRepo(
{
[fixture]: "export {};\n",
[helper]:
'export const worker = new URL("./fixtures/worker.mjs?generation=1#child", import.meta.url);\n',
"test/scripts/bridge.test-support.ts": 'export * from "./runtime.test-support.js";\n',
[direct]: 'import "./runtime.test-support.js";\n',
[indirect]: 'import "./bridge.test-support.js";\n',
"test/scripts/after-test.consumer.test.ts": 'import "./direct.consumer.test.js";\n',
"test/scripts/unrelated.consumer.test.ts": "export {};\n",
"test/scripts/live.consumer.live.test.ts": 'import "./runtime.test-support.js";\n',
[opaque]: "export {};\n",
"test/scripts/literal.opaque.consumer.test.ts": `const fixture = "${opaque}";\n`,
"test/scripts/relative.opaque.consumer.test.ts": 'import "./opaque/index.mjs";\n',
[explicitHelper]: "export const value = 1;\n",
"test/scripts/bridge/index.ts": 'export * from "../source/input.test-support.js";\n',
[explicitConsumer]: 'import "../bridge/index.js";\n',
},
(cwd) => {
const options = { cwd, broad: true, forceFullImportGraph: true };
for (const changed of [fixture, helper]) {
expectChangedTargets([changed], [direct, indirect], { cwd, broad: true });
expectChangedTargets(
[changed],
["test/scripts/after-test.consumer.test.ts", direct, indirect],
options,
);
}
// A literal reference alone cannot prove an opaque helper's complete frontier.
expectChangedTargets([opaque, direct], [opaque, direct], { cwd, broad: true });
expectChangedTargets(
[opaque, direct],
[
"test/scripts/relative.opaque.consumer.test.ts",
"test/scripts/literal.opaque.consumer.test.ts",
direct,
],
options,
);
expect(planRuns([explicitHelper], cwd)).toEqual([runPlan("tooling", [explicitConsumer])]);
},
);
});
it.each([false, true])(
"retains an unowned changed tooling test alongside its consumers (mixed input: %s)",
(mixedInput) => {
const changedTest = "test/e2e/qa-lab/runtime/changed-tooling.test.ts";
const reader = "test/scripts/tooling-reader.test.ts";
const otherTest = "src/independent.test.ts";
withTinyGitRepo(
{
[changedTest]: "export const value = 1;\n",
[reader]:
'import "../e2e/qa-lab/runtime/changed-tooling.test.js";\n' +
`const fixture = "${changedTest}";\n`,
[otherTest]: "export const independent = true;\n",
},
(cwd) => {
const inputs = mixedInput ? [changedTest, otherTest, changedTest] : [changedTest];
expectChangedTargets(inputs, [changedTest, reader, ...(mixedInput ? [otherTest] : [])], {
cwd,
});
},
);
},
);
it.each([
{
changedPath: "scripts/unowned-source.mts",
expectedTargets: ["test/scripts/tooling-reader.test.ts"],
},
{
changedPath: "test/scripts/owned.test.ts",
expectedTargets: ["test/scripts/owned.test.ts"],
},
{
changedPath: "test/e2e/qa-lab/runtime/changed-tooling.live.test.ts",
expectedTargets: ["test/scripts/tooling-reader.test.ts"],
},
])(
"preserves non-test, explicit-owner, and live selection for $changedPath",
({ changedPath, expectedTargets }) => {
withTinyGitRepo(
{
[changedPath]: "export const value = 1;\n",
"test/scripts/tooling-reader.test.ts": `const fixture = "${changedPath}";\n`,
},
(cwd) => {
expectChangedTargets([changedPath], expectedTargets, { cwd });
},
);
},
);
it.each([
{ count: 2, withRepo: withTinyGitRepo },
{ count: 13, withRepo: withTinyFileTree },
])("deduplicates import-graph owners of $count explicit source files", ({ count, withRepo }) => {
const files: Record<string, string> = {};
const imports: string[] = [];
const refs: string[] = [];
for (let index = 0; index < count; index += 1) {
files[`src/runtime-${index}.ts`] = `export const value${index} = ${index};\n`;
imports.push(`import { value${index} } from './runtime-${index}.js';`);
refs.push(`value${index}`);
}
files["src/runtime.consumer.test.ts"] = `${imports.join("\n")}\nvoid [${refs.join(", ")}];\n`;
withRepo(files, (cwd) => {
expectRun(
planRuns(
Array.from({ length: count }, (_, index) => `src/runtime-${index}.ts`),
cwd,
),
"unit",
{
forwardedArgs: ["src/runtime.consumer.test.ts"],
includePatterns: ["src/runtime.consumer.test.ts"],
},
);
});
});
it.each`
args | owner | watchMode | env | inheritedProjectShards
${["src/gateway/server.health.test.ts"]} | ${"gateway-server"} | ${false} | ${undefined} | ${undefined}
${["--watch", "src/gateway/server.health.test.ts"]} | ${"gateway"} | ${true} | ${undefined} | ${undefined}
${testFiles("src/gateway", "server.health call")} | ${"gateway"} | ${false} | ${undefined} | ${undefined}
${["--watch", "src/gateway/server.health.test.ts", "src/gateway/call.test.ts"]} | ${"gateway"} | ${true} | ${undefined} | ${undefined}
${["src/gateway/server.health.test.ts"]} | ${"gateway"} | ${false} | ${{ OPENCLAW_GATEWAY_PROJECT_SHARDS: "0" }} | ${undefined}
${["src/gateway/server.health.test.ts"]} | ${"gateway"} | ${false} | ${undefined} | ${"0"}
${["src/gateway/server.health.test.ts"]} | ${"gateway-server"} | ${false} | ${{ OPENCLAW_GATEWAY_PROJECT_SHARDS: "1" }} | ${"0"}
${[configFile("gateway-server")]} | ${"gateway-server"} | ${false} | ${{ OPENCLAW_GATEWAY_PROJECT_SHARDS: "0" }} | ${undefined}
`(
"keeps exact Gateway server files on their $owner owner for $args",
({
args,
owner,
watchMode,
env,
inheritedProjectShards,
}: {
args: string[];
owner: string;
watchMode: boolean;
env?: NodeJS.ProcessEnv;
inheritedProjectShards?: string;
}) => {
withEnv({ OPENCLAW_GATEWAY_PROJECT_SHARDS: inheritedProjectShards }, () => {
expectRun(
planRuns(args, process.cwd(), () => [], { env }),
owner,
{
includePatterns: args.some((arg) => arg.endsWith(".config.ts"))
? null
: args.filter((arg) => arg !== "--watch"),
watchMode,
},
);
});
},
);
it.each(
["src/gateway", "src/gateway/**/*.test.ts"].flatMap((target) =>
["alone", "worker-first", "aggregate-first"].map((order) => ({ target, order })),
),
)(
"keeps Gateway database consumers in the aggregate for $target ($order)",
({ target, order }) => {
const [workerFile] = gatewayDatabaseWorkerTestFiles;
assert(workerFile);
const targets =
order === "alone"
? [target]
: order === "worker-first"
? [workerFile, target]
: [target, workerFile];
const forwardedArgs = ["--reporter=dot", "--coverage"];
expect(planRuns([...targets, ...forwardedArgs])).toEqual([
runPlan(
"gateway",
targets.map((file) => (file === "src/gateway" ? "src/gateway/**/*.test.ts" : file)),
forwardedArgs,
),
runPlan(
"infra",
databaseWorkerCoreTestFiles.filter((file) => file.startsWith("src/gateway/")),
forwardedArgs,
),
]);
},
);
it.each(
[configFile("gateway"), "src/gateway/config-reload.telegram-policy.test.ts"].flatMap((target) =>
[true, false].map((workerFirst) => ({ target, workerFirst })),
),
)(
"coalesces Gateway worker config with $target (worker first: $workerFirst)",
({ target, workerFirst }) => {
const workerConfig = configFile("gateway-database-workers");
const targets = workerFirst ? [workerConfig, target] : [target, workerConfig];
expectRun(planRuns(targets), "gateway", {
includePatterns: target.endsWith(".config.ts")
? null
: workerFirst
? [...gatewayDatabaseWorkerTestFiles, target]
: [target, ...gatewayDatabaseWorkerTestFiles],
});
},
);
it.each([
["src/agents/**/*.test.ts", configFile("agents")],
["test/plugins", configFile("tooling")],
["test/scripts", configFile("tooling")],
["src/plugin-state", configFile("unit")],
])("preserves watch selection across database ownership for %s", (target, owner) => {
expect(planRuns(["--watch", target])).toEqual([
{
config: configFile("database-worker-watch"),
databaseWorkerWatchOwner: owner,
databaseWorkerWatchTests: databaseWorkerCoreTestFiles.filter((file) =>
file.startsWith(`${target.split("/**")[0]}/`),
),
forwardedArgs: [],
includePatterns: [target.endsWith(".test.ts") ? target : `${target}/**/*.test.ts`],
watchMode: true,
},
]);
});
it.each([
testFiles("src/agents/**", "* memory-* memory-*"),
[
"extensions/matrix",
"extensions/matrix/**/storage.test.ts",
"extensions/matrix/**/storage.test.ts",
],
testFiles("extensions/matrix/**", "storage * storage"),
testFiles("extensions/matrix/**", "storage storage storage"),
])("retains watch intersection for %s and %s", (directory, pattern, expected) => {
withIncludeFile([pattern], (includeFile) => {
const specs = runSpecs(["--watch", directory], {
baseEnv: { OPENCLAW_VITEST_INCLUDE_FILE: includeFile },
});
expect(specs).toHaveLength(1);
expect(specs[0]?.includePatterns).toContain(expected);
expect(
specs[0]?.includePatterns?.every(
(value) => value === expected || path.matchesGlob(value, expected),
),
).toBe(true);
expect(specs[0]?.watchMode).toBe(true);
});
});
it.each([
[configFile("plugins"), "src/plugins/doctor-contract-registry.load-paths.test.ts"],
[configFile("plugin-sdk"), "src/plugin-sdk/provider-auth.test.ts"],
[
configFile("unit-fast"),
"src/agents/embedded-agent-runner/run/model-setup.selected-model.test.ts",
],
[configFile("unit-fast"), "test/e2e/qa-lab/runtime/gateway-loopback-lan-access.test.ts"],
[configFile("unit-fast-isolated"), "src/state/openclaw-agent-execution-cleanup.test.ts"],
[
configFile("unit-fast-isolated"),
"src/auto-reply/reply/agent-runner-execution-auth-failures.test.ts",
],
])("preserves whole-owner watch coverage for %s with %s", (config, file) => {
const [plan] = planRuns(["--watch", config, file]);
expect(plan).toMatchObject({
config: configFile("database-worker-watch"),
databaseWorkerWatchOwner: config,
includePatterns: null,
watchMode: true,
});
expect(plan?.databaseWorkerWatchTests).toContain(file);
if (config.endsWith("vitest.plugin-sdk.config.ts")) {
expect(plan?.databaseWorkerWatchTests).toContain("src/plugin-sdk/memory-host-core.test.ts");
expect(plan?.databaseWorkerWatchTests).not.toContain(
"src/plugin-sdk/memory-host-events.test.ts",
);
expect(plan?.databaseWorkerWatchTests).not.toContain(
"src/plugin-sdk/session-store-runtime.maintenance.test.ts",
);
}
const [spec] = runSpecs(["--watch", config, file], { baseEnv: {} });
expect(spec?.includeFilePath).toBeNull();
expect(spec?.env.OPENCLAW_VITEST_DATABASE_WORKER_WATCH_OWNER).toBe(config);
expect(JSON.parse(spec?.env.OPENCLAW_VITEST_DATABASE_WORKER_WATCH_TESTS ?? "null")).toEqual(
plan?.databaseWorkerWatchTests,
);
});
describe("agent directory and glob inventory", () => {
const toolRoot = "src/agents/tools";
const failoverRoot = "src/agents/failover";
it.each([toolRoot, `${toolRoot}/**/*.test.ts`])(
"intersects inherited includes for %s without dropping an explicit file",
(target) => {
const selected = `${toolRoot}/computer-tool.schema.test.ts`;
const explicit = `${failoverRoot}/classify.legacy-provider-predicates.test.ts`;
withIncludeFile([selected], (includeFile) => {
const specs = runSpecs([target, explicit], {
baseEnv: { OPENCLAW_VITEST_INCLUDE_FILE: includeFile },
});
expect(selectedTestFiles(specs).toSorted()).toEqual([selected, explicit].toSorted());
});
},
);
it.each([toolRoot, `${toolRoot}/**/*.test.ts`])(
"keeps live and E2E opt-in outside ordinary selection for %s",
(target) => {
const ordinary = `${toolRoot}/example.test.ts`;
const live = `${toolRoot}/example.live.test.ts`;
const e2e = `${toolRoot}/example.e2e.test.ts`;
withTinyFileTree(
Object.fromEntries([ordinary, live, e2e].map((file) => [file, ""])),
(cwd) => {
const plans = planRuns([target], cwd);
expect(selectedTestFiles(plans)).toEqual([ordinary]);
const explicitPlans = planRuns([target, live, e2e], cwd);
expect(
explicitPlans.find((plan) => plan.config === configFile("e2e"))?.forwardedArgs,
).toEqual([e2e]);
expect(selectedTestFiles(explicitPlans)).toContain(live);
expectRun(planRuns([`${toolRoot}/*.e2e.test.ts`], cwd), "e2e", {
forwardedArgs: [`${toolRoot}/*.e2e.test.ts`],
});
},
);
},
);
});
it("routes changed Parallels process helpers to their owner tooling tests", () => {
expectRun(
planRuns(["--changed", "origin/main"], process.cwd(), () => [
"scripts/e2e/parallels/host-command.ts",
]),
"tooling",
{
includePatterns: testFiles(
"test/scripts",
"parallels-smoke-model parallels-npm-update-smoke",
),
},
);
});
it("unions semantic Docker owners for shared script and client inputs", () => {
const targets = [
"scripts/e2e/openai-image-auth-docker.sh",
"test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts",
];
expect(unmatchedTargets(targets)).toEqual([]);
expectChangedTargets(targets, [
...testFiles(
"test/scripts",
"docker-build-helper docker-e2e-plan openai-image-auth-docker-client",
),
"extensions/openai/image-generation-provider.test.ts",
"src/image-generation/openai-compatible-image-provider.test.ts",
]);
});
it.each([
["chunks the broad shell helper tooling shard after isolated targets", "test/scripts"],
["chunks broad shell helper globs after isolated targets", "test/scripts/*.test.ts"],
])("%s", (_title, target) => {
const plans = planRuns([target, "--reporter=dot"], process.cwd());
const pattern = target === "test/scripts" ? "test/scripts/**/*.test.ts" : target;
const expected = fs.globSync(pattern).map(repoPath).toSorted();
const selected = plans.flatMap(
(plan) =>
plan.includePatterns ?? plan.forwardedArgs.filter((arg) => arg.endsWith(".test.ts")),
);
expect(selected.toSorted()).toEqual(expected);
expect(new Set(selected).size).toBe(selected.length);
const requiredOwners: Record<string, string> = {
"test/scripts/ci-git-prerequisites.test.ts": configFile("unit-fast-isolated"),
"test/scripts/pr-ci-sweeper.reopen-timer.test.ts": configFile("unit-fast-fake-timers"),
"test/scripts/docker-build-helper.test.ts": configFile("tooling-docker"),
"test/scripts/openclaw-e2e-instance.test.ts": configFile("tooling-isolated"),
};
for (const plan of plans) {
const files =
plan.includePatterns ?? plan.forwardedArgs.filter((arg) => arg.endsWith(".test.ts"));
expect(plan.watchMode).toBe(false);
expect(plan.forwardedArgs).toContain("--reporter=dot");
if (plan.config === configFile("tooling")) {
expect(files.length).toBeLessThanOrEqual(60);
}
for (const file of files) {
const owner = file.endsWith(".e2e.test.ts") ? configFile("e2e") : requiredOwners[file];
if (owner) {
expect(plan.config, file).toBe(owner);
}
}
}
});
it("adds the CLI process project for broad CLI targets", () => {
const plans = planRuns(["src/cli"]);
expect(plans.map((plan) => plan.config)).toEqual(
expect.arrayContaining([
configFile("unit-fast"),
configFile("cli-process"),
configFile("cli"),
configFile("tooling-isolated"),
]),
);
const processPlan = plans.find((plan) => plan.config === configFile("cli-process"));
expect(processPlan?.includePatterns).toContain("src/cli/help-exit.process.test.ts");
expect(processPlan?.includePatterns).toContain("src/cli/update-dry-run-state.process.test.ts");
expect(
plans.find((plan) => plan.config === configFile("tooling-isolated"))?.includePatterns,
).toEqual(["src/cli/update-cli/update-command-legacy-finalize.test.ts"]);
});
it("prints wrapper help for --help without starting a broad local suite", () => {
const helpFlag = "--help";
const nodeExecPath = requireNodeTool("node");
withTinyFileTree({}, (tempDir) => {
const result = spawnSync(
nodeExecPath,
[
...resolveRuntimeWorkerArgv(
resolveRuntimeWorkerUrl(scriptModuleEntrypoints.testProjects),
nodeExecPath,
),
helpFlag,
],
{
encoding: "utf8",
// Keep the native help probe inside its invocation-owned temporary directory.
env: testRunnerFixtureEnv({ TMPDIR: tempDir, TMP: tempDir, TEMP: tempDir }),
timeout: 5_000,
},
);
expect(result.status).toBe(0);
expect(result.stdout).toContain("Usage: node scripts/test-projects.mts");
expect(result.stderr).not.toContain("[test] starting");
});
});
it.each([
{
target: "src/channels/plugins/contracts",
plans: ["surface", "config", "registry", "session"].map((owner) =>
runPlan(`contracts-channel-${owner}`),
),
},
{
target: "src/plugins/contracts",
plans: [
runPlan("contracts-plugin"),
runPlan(
"infra",
testFiles("src/plugins/contracts", "host-hook-state.identity host-hooks.contract"),
),
],
},
])("fans $target out to its contract and database owners", ({ target, plans }) => {
// Generic channels/plugins projects exclude contracts/** and would run zero tests.
expect(planRuns([target])).toEqual(plans);
});
it("adds the shared plugin-shape parity contract to a channel owner", () => {
const plan = changedPlan(["extensions/imessage/message-tool-api.ts"]);
expect(plan.mode).toBe("targets");
expect(plan.targets).toEqual(
expect.arrayContaining([
"extensions/imessage/src/message-tool-api.test.ts",
"src/channels/plugins/contracts/plugin-shape.contract.test.ts",
]),
);
});
it("fails safe for raw Git paths that explicit-path normalization would rewrite", () => {
for (const changedPath of [
" scripts/changed-lanes.mts",
String.raw`scripts\changed-lanes.mts`,
]) {
expect(
changedPlanForArgs(["--changed", "origin/main"], process.cwd(), () => [changedPath], {
broad: true,
}),
changedPath,
).toEqual({ mode: "broad", targets: [] });
}
});
it("keeps the broad changed run available for unknown root surfaces", async () => {
expect(
changedArgs(["--changed", "origin/main"], process.cwd(), () => ["unknown/file.txt"], {
env: { OPENCLAW_TEST_CHANGED_BROAD: "1" },
}),
).toBeNull();
const lanes = await import("../../scripts/changed-lanes.mts");
const changedPaths = vi
.spyOn(lanes, "listChangedPathsFromGit")
.mockReturnValue(["unknown/file.txt"]);
try {
const specs = runSpecs(["--changed", "origin/main"], {
baseEnv: { OPENCLAW_TEST_CHANGED_BROAD: "1" },
});
expect(specs.map(({ config, includePatterns }) => ({ config, includePatterns }))).toEqual([
{ config: configFile("unit"), includePatterns: null },
]);
} finally {
changedPaths.mockRestore();
}
});
it.each([false, true])("resolves standalone changed specs (watch=%s)", async (watch) => {
const lanes = await import("../../scripts/changed-lanes.mts");
const target = "test/scripts/run-node-lifecycle.test.ts";
const changedPaths = vi.spyOn(lanes, "listChangedPathsFromGit").mockReturnValue([target]);
try {
const specs = runSpecs([...(watch ? ["--watch"] : []), "--changed", "origin/main"], {
baseEnv: {},
});
expect(
specs.map(({ config, includePatterns, watchMode }) => ({
config,
includePatterns,
watchMode,
})),
).toEqual([
{
config: configFile("tooling"),
includePatterns: [target],
watchMode: watch,
},
]);
expect(changedPaths).toHaveBeenCalledTimes(1);
} finally {
changedPaths.mockRestore();
}
});
it("skips app-only changes because app tests are separate from Vitest lanes", () => {
expect(
planRuns(["--changed", "origin/main"], process.cwd(), () => [
"apps/macos/OpenClaw/AppDelegate.swift",
]),
).toStrictEqual([]);
});
it("keeps the top-level extensions watch target on its aggregate owner", () => {
expectRun(planRuns(["--watch", "extensions"]), "full-extensions", { watchMode: true });
});
it.each(["extension-telegram", "extension-matrix"])(
"preserves an externally scoped %s config target",
(project) => {
expectRun(
planRuns([configFile(project)], process.cwd(), () => [], {
env: { OPENCLAW_VITEST_INCLUDE_FILE: "ci-shard.json" },
}),
project,
);
},
);
it.each([
{
channel: "Telegram",
config: configFile("extension-telegram"),
directory: "extensions/telegram",
},
{
channel: "Matrix",
config: configFile("extension-matrix"),
directory: "extensions/matrix",
},
])("preserves an externally scoped $channel directory run spec", ({ config, directory }) => {
withIncludeFile(
[`${directory}/src/example.test.ts`],
(includeFile) => {
const [spec] = runSpecs([directory], {
baseEnv: { OPENCLAW_VITEST_INCLUDE_FILE: includeFile },
});
expect(spec).toMatchObject({
config,
env: { OPENCLAW_VITEST_INCLUDE_FILE: includeFile },
includeFilePath: null,
includePatterns: null,
});
},
"ci-shard.json",
);
});
it.each([
{
directory: "extensions/matrix/src/matrix/client",
selected: ["extensions/matrix/src/matrix/client/storage.test.ts"],
inherited: testFiles("extensions/matrix/src/matrix", "client/storage thread-bindings"),
},
{
directory: "extensions/matrix/src/matrix/sdk",
// Worker membership belongs to the database lane; this case protects the
// directory intersection as that membership grows.
selected: databaseWorkerExtensionTestFiles.filter((file) =>
file.startsWith("extensions/matrix/src/matrix/sdk/"),
),
inherited: ["extensions/matrix/**/*.test.ts"],
},
])(
"intersects $directory and inherited worker selections before emitting include files",
({ directory, selected, inherited }) => {
withIncludeFile(inherited, (includeFile) => {
const specs = runSpecs([directory], {
baseEnv: { OPENCLAW_VITEST_INCLUDE_FILE: includeFile },
});
const worker = specs.find(
(spec) => spec.config === configFile("extension-database-workers"),
);
expect(worker?.includePatterns).toEqual(selected);
expect(selectedTestFiles(specs)).not.toContain(
"extensions/matrix/src/matrix/thread-bindings.test.ts",
);
});
},
);
it("keeps a grouped Matrix config target unsplit beside its worker tests", () => {
const plans = planRuns(["extensions/matrix", configFile("extension-matrix")]);
expect(plans).toEqual([
runPlan(
"extension-database-workers",
databaseWorkerExtensionTestFiles.filter((file) => file.startsWith("extensions/matrix/")),
),
runPlan("extension-matrix"),
]);
});
it("keeps explicit Matrix files and watch runs unchunked", () => {
const testFile = listExtensionTestFilesForRoots(["extensions/matrix"])[0];
expect(testFile).toBeDefined();
expect(planRuns([testFile!])).toHaveLength(1);
const plans = planRuns(["--watch", "extensions/matrix"]);
expect(plans).toEqual([
{
config: configFile("database-worker-watch"),
databaseWorkerWatchOwner: configFile("extension-matrix"),
databaseWorkerWatchTests: databaseWorkerExtensionTestFiles.filter((file) =>
file.startsWith("extensions/matrix/"),
),
forwardedArgs: [],
includePatterns: expect.arrayContaining(["extensions/matrix/**/*.test.ts"]),
watchMode: true,
},
]);
});
it("keeps the opaque retention child owner beside additional fixture consumers", () => {
const helper = "src/plugins/runtime.retention.test-support.ts";
const owner = "src/plugins/runtime.retention.test.ts";
const direct = "src/other/direct.test.ts";
const indirect = "src/plugins/shared-consumer.test.ts";
withTinyGitRepo(
{
[helper]: "export const fixture = 1;\n",
[owner]: "export {};\n",
[direct]: 'import "../plugins/runtime.retention.test-support.js";\n',
"src/plugins/bridge.ts": 'export * from "./runtime.retention.test-support.js";\n',
[indirect]: 'import "./bridge.js";\n',
"src/plugins/unrelated.test.ts": "export {};\n",
},
(cwd) => {
const plan = changedPlan([helper], { cwd, boundedOwners: true });
expect(plan.ownerTargets).toEqual([owner]);
expect(plan.targets.toSorted()).toEqual([owner, direct, indirect].toSorted());
expect(plan.ownerAreas).toEqual(["src/plugins"]);
},
);
});
it("adds transitive owner tests and keeps direct cross-area readers without broad fallback", () => {
withTinyGitRepo(
{
"package.json": "{}",
"src/feature/value.ts": "export const value = 1;\n",
"src/feature/value.test.ts": "export const owner = true;\n",
"src/feature/consumer.test.ts": 'import "./value.js";\n',
"src/feature/bridge.ts": 'export * from "./value.js";\n',
"src/feature/indirect.test.ts": 'import "./bridge.js";\n',
"src/feature/unrelated.test.ts": "export const unrelated = true;\n",
"src/other/reader.test.ts": 'import "../feature/value.js";\n',
"src/other/indirect.test.ts": 'import "../feature/bridge.js";\n',
},
(cwd) => {
expect(
changedPlan(["package.json", "src/feature/value.ts"], {
cwd,
broad: true,
boundedOwners: true,
resolveAliases: true,
runtimeOnly: true,
combineSiblingWithImportGraph: true,
}),
).toEqual({
mode: "targets",
ownerTargets: ["src/feature/value.test.ts"],
ownerAreas: ["scripts", "src/scripts", "test/scripts", "src/feature"],
targets: testFiles("src", "feature/value feature/consumer feature/indirect other/reader"),
});
},
);
});
describe("Kova schema selection", () => {
const wrapper = "src/config/zod-schema.agent-defaults.ts";
const base = "src/config/zod-schema.agent-defaults-base.ts";
const sibling = "src/config/zod-schema.agent-defaults.test.ts";
const baseConsumer = "src/config/base-consumer.test.ts";
const wrapperConsumer = "src/config/wrapper-consumer.test.ts";
const unrelated = "src/config/unrelated.ts";
const unrelatedTest = "src/config/unrelated.test.ts";
const kova = "test/scripts/openclaw-performance-workflow.test.ts";
const changed = ["--changed", "origin/main"];
const normal = [baseConsumer, sibling, wrapperConsumer];
const files = {
[base]: "export const defaults = {};\n",
[wrapper]: 'export { defaults } from "./zod-schema.agent-defaults-base.js";\n',
[sibling]: 'import "./zod-schema.agent-defaults.js";\n',
[baseConsumer]: 'import "./zod-schema.agent-defaults-base.js";\n',
[wrapperConsumer]: 'import "./zod-schema.agent-defaults.js";\n',
[unrelated]: "export const unrelated = true;\n",
[unrelatedTest]: 'import "./unrelated.js";\n',
[kova]: "export {};\n",
};
it.each<[string, string[], string[], string[], boolean, boolean]>([
["changed wrapper", changed, [wrapper], [sibling, kova], false, false],
["explicit schemas", [base, wrapper, kova, base], [], [...normal, kova], false, false],
["CI wrapper", changed, [wrapper], [sibling, wrapperConsumer, kova], false, true],
["unrelated source", changed, [unrelated], [unrelatedTest], false, false],
["changed watch", ["--watch", ...changed], [base], normal, true, false],
["explicit watch", ["--watch", wrapper], [], [sibling], true, false],
["disabled watch", ["--watch", "false", wrapper], [], [sibling, kova], false, false],
["equals watch", ["--watch=false", wrapper], [], [sibling, kova], false, false],
["negated watch", ["--no-watch", wrapper], [], [sibling, kova], false, false],
])("preserves owner selection for %s", (_name, args, paths, expected, watchMode, ci) => {
withTinyGitRepo(files, (cwd) => {
const options = ci
? { combineSiblingWithImportGraph: true, forceFullImportGraph: true }
: {};
const plans = planRuns(args, cwd, () => paths, options);
expect(selectedTestFiles(plans).toSorted()).toEqual(expected.toSorted());
expect(plans.every((plan) => plan.watchMode === watchMode)).toBe(true);
expect(
plans.find((plan) => plan.config === configFile("tooling"))?.includePatterns,
).toEqual(expected.includes(kova) ? [kova] : undefined);
if (watchMode) {
expectRun(plans, "runtime-config", {
includePatterns: expect.arrayContaining(expected),
watchMode: true,
});
}
expect(unmatchedTargets(args, cwd)).toEqual([]);
});
});
it("does not admit unmatched watch sources through Kova", () => {
withTinyGitRepo({ [base]: "export {};\n", [kova]: "export {};\n" }, (cwd) => {
expect(unmatchedTargets([base], cwd)).toEqual([]);
expect(unmatchedTargets(["--watch", base], cwd)).toEqual([
expect.objectContaining({ target: base, reason: "target-matched-no-test-files" }),
]);
});
});
it("keeps skipped import-graph paths visible with mixed broad changes", () => {
withTinyGitRepo(files, (cwd) => {
const paths = [base, "unknown/file.txt"];
expect(changedPlan(paths, { cwd })).toEqual({
mode: "targets",
targets: [kova],
skippedBroadFallbackPaths: paths,
});
expect(changedPlan(paths, { cwd, broad: true })).toEqual({
mode: "broad",
targets: [],
});
});
});
});
it.each(["changed", "explicit"])(
"routes %s ui support files to the ui lane without dead include globs",
(mode) => {
const targets = ["ui/src/styles/base.css", "ui/src/test-helpers/dom.setup.ts"];
const plans = planRuns(
mode === "changed" ? ["--changed", "origin/main"] : targets,
process.cwd(),
() => targets,
);
expect(plans[0]).toEqual(runPlan("ui"));
expect(plans[1]?.config).toBe(configFile("ui-browser"));
expect(plans[1]?.includePatterns).toContain(
"ui/src/components/markdown-mermaid.runtime.browser.test.ts",
);
},
);
it.each(["relative", "trailing slash", "absolute", "dot segments"])(
"keeps an existing nested ui directory and explicit e2es scoped (%s)",
(spelling) => {
const directory = "ui/src/pages/new-session";
const isolated = [
`${directory}/draft-persistence.test.ts`,
`${directory}/new-session-attachment-lazy.test.ts`,
];
const unitFiles = [
`${directory}/view.test.ts`,
`${directory}/nested/child.test.ts`,
...isolated,
];
const e2es = testFiles(
"ui/src/e2e",
"new-session-page.workspace-validation.e2e new-session-page.projects-places.e2e new-session-page.device-dispatch.e2e",
);
withTinyFileTree(
Object.fromEntries(
[
...unitFiles,
...e2es,
"ui/src/pages/other/view.test.ts",
"ui/src/pages/workboard/view.test.ts",
"ui/src/components/other.browser.test.ts",
"ui/src/e2e/other.e2e.test.ts",
].map((file) => [file, ""]),
),
(cwd) => {
const target =
spelling === "absolute"
? path.join(cwd, directory)
: spelling === "trailing slash"
? `./${directory}/`
: spelling === "dot segments"
? "ui/src/pages/./new-session/../new-session"
: directory;
const plans = planRuns([target, ...e2es], cwd);
expect(plans).toEqual([
runPlan("ui", [`${directory}/**/*.test.ts`]),
runPlan("ui-isolated", isolated),
runPlan("ui-e2e", e2es),
]);
const includeFile = path.join(cwd, "include.json");
const filesByPlan = plans.map((plan) => {
writeVitestIncludeFile(includeFile, plan.includePatterns!, { cwd });
return JSON.parse(fs.readFileSync(includeFile, "utf8"));
});
// Shared UI intersects these files with its ownership exclusions.
expect(filesByPlan).toEqual([unitFiles.toSorted(), isolated, e2es]);
},
);
},
);
it("adds isolated and Chromium projects before timing budgets for broad ui targets", () => {
const plans = planRuns(["ui/src"]);
expect(plans.map((plan) => plan.config)).toEqual([
configFile("ui"),
configFile("ui-isolated"),
configFile("ui-browser"),
configFile("ui-timing"),
]);
expect(plans[1]?.includePatterns).toContain("ui/src/pages/chat/chat-pane.test.ts");
expect(plans[2]?.includePatterns).toContain(
"ui/src/components/markdown-mermaid.runtime.browser.test.ts",
);
expect(plans[3]?.includePatterns).toEqual(["ui/src/components/markdown.progress.node.test.ts"]);
});
it.each([
["ui/src/**/*.browser.test.ts"],
[
"ui/src/components/*.browser.test.ts",
"ui/src/pages/chat",
"ui/src/styles/cursor-policy.browser.test.ts",
],
])("retains both browser owners for mixed selectors %j", (...targets) => {
const plans = planRuns(targets);
const native = plans.find((plan) => plan.config === configFile("ui-browser"));
const node = plans.find((plan) => plan.config === configFile("ui"));
expect(native?.includePatterns).toContain(
"ui/src/components/markdown-mermaid.runtime.browser.test.ts",
);
expect(
node?.includePatterns === null ||
node?.includePatterns?.includes("ui/src/components/form-controls.browser.test.ts"),
).toBe(true);
expect(node?.includePatterns ?? []).not.toContain(
"ui/src/components/markdown-mermaid.runtime.browser.test.ts",
);
expect(native?.includePatterns).not.toContain(
"ui/src/components/form-controls.browser.test.ts",
);
if (targets.length === 1) {
// Browser screenshots live in directories named after their test files.
const matchingFiles = fs.globSync(targets[0]!).filter((file) => fs.statSync(file).isFile());
expect(selectedTestFiles(plans).toSorted()).toEqual(matchingFiles.toSorted());
}
});
it("rejects UI glob watch before freezing the current matching files", () => {
expect(() => planRuns(["--watch", "ui/src/components/markdown-*.browser.test.ts"])).toThrow(
"use a literal test path, directory, or dedicated UI suite",
);
});
it.each([
{
expandGlobs: true,
withRepo: withTinyGitRepo,
patterns: testFiles("src/gateway", "**/* server-* @(core|server-startup)"),
expected: testFiles("src/gateway", "core server-methods/ping server-startup"),
},
{
expandGlobs: false,
withRepo: withTinyFileTree,
patterns: ["src/gateway/**/*.test.ts"],
expected: ["src/gateway/**/*.test.ts"],
},
])(
"writes include files with expandGlobs=$expandGlobs",
({ expandGlobs, withRepo, patterns, expected }) => {
withRepo(
{
"src/gateway/core.test.ts": "",
"src/gateway/server-methods/ping.test.ts": "",
"src/gateway/server-startup.test.ts": "",
},
(cwd) => {
const includeFile = path.join(cwd, "include.json");
writeVitestIncludeFile(includeFile, patterns, { cwd, expandGlobs });
expect(JSON.parse(fs.readFileSync(includeFile, "utf8"))).toEqual(expected);
},
);
},
);
it("preflights targeted UI E2E specs with Playwright browser assets", () => {
const [spec] = runSpecs(["ui/src/pages/cron/run-transcript.e2e.test.ts"], {
baseEnv: {},
});
expect(spec?.config).toBe(configFile("ui-e2e"));
expect(spec?.preflightPnpmArgs).toEqual([
"exec",
"node",
"--import",
"tsx",
"scripts/ensure-playwright-chromium.mts",
]);
});
it("skips import-graph scans once a diff already needs broad fallback", () => {
const readFileSync = vi.spyOn(fs, "readFileSync");
const before = readFileSync.mock.calls.length;
const plan = changedPlan([".crabbox.yaml", "scripts/check.mts", "src/gateway/server.impl.ts"]);
const repoSourceReads = readFileSync.mock.calls
.slice(before)
.filter(([file]) => typeof file === "string" && repoPath(file).includes("/src/"));
readFileSync.mockRestore();
expect(plan).toEqual({
mode: "targets",
skippedBroadFallbackPaths: ["src/gateway/server.impl.ts"],
targets: testFiles("test/scripts", "package-acceptance-workflow check pr-gate-base"),
});
expect(repoSourceReads).toEqual([]);
});
it("routes prompt snapshot generator helper edits to the owner test", () => {
for (const target of [
"scripts/generate-prompt-snapshots.ts",
"test/helpers/agents/happy-path-prompt-snapshots.ts",
"test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md.diff",
]) {
expectChangedTargets([target], ["test/scripts/prompt-snapshots.test.ts"]);
}
});
it("routes package fixture assets to their owner test", () => {
const owner = "packages/ai/src/provider-transport-parity.test.ts";
const fixturePaths = [
"packages/ai/test/fixtures/provider-transport-parity/anthropic-success.snap.txt",
"packages/ai/test/fixtures/provider-transport-parity/anthropic-error.snap.txt",
];
expectChangedTargets(fixturePaths, [owner]);
expectRun(
planRuns(["--changed", "origin/main"], process.cwd(), () => fixturePaths),
"unit",
{ forwardedArgs: [owner], includePatterns: [owner] },
);
});
});
describe("changed export mock consumers", () => {
it.each`
name | before | after | selected
${"member access cannot invent an export"} | ${"export const old = 1; const api = { export: 0 }; api.export\nfunction added() {}"} | ${"export const old = 1; const api = { export: 0 }; api.export\nfunction added() {} export { added };"} | ${true}
${"a preceding period literal is not member access"} | ${"export const old = 1;"} | ${'export const old = 1; "."\nexport const added = 2;'} | ${true}
${"empty-string export alias"} | ${"export const old = 1; const value = 2;"} | ${'export const old = 1; const value = 2; export { value as "" };'} | ${true}
${"quoted closing brace is an export name"} | ${"export const old = 1;"} | ${'export const old = 1; export { "}" as added } from "./other.js";'} | ${true}
${"erased default becomes a runtime export"} | ${"export default interface API {}"} | ${"export default class API {}"} | ${true}
${"erased namespace gains a runtime member"} | ${"export namespace API { export type Value = string; }"} | ${"export namespace API { export const value = 1; }"} | ${true}
${"const enums retain mock coverage"} | ${"export const enum Old { Value = 1 }"} | ${"export const enum Old { Value = 1 } export const enum Added { Value = 2 }"} | ${true}
${"JSX text cannot hide a later export"} | ${"function component() { return <div>}</div>; } export const old = 1;"} | ${"function component() { return <div>}</div>; } export const old = 1; export const added = 2;"} | ${true}
${"Unicode names retain mock coverage"} | ${"export function café() {}"} | ${"export function cafè() {}"} | ${true}
${"bracket CommonJS exports retain mock coverage"} | ${'module["exports"] = { old: 1 };'} | ${'module["exports"] = { old: 1, added: 2 };'} | ${true}
${"CommonJS exports retain mock coverage"} | ${"exports.old = 1;"} | ${"exports.old = 1; exports.added = 2;"} | ${true}
${"implicit statement does not hide a new export"} | ${"export const old = 1\nconst hidden = 2, added = 3;"} | ${"export const old = 1\nconst hidden = 2, added = 3; export { added };"} | ${true}
${"namespace member does not hide a module export"} | ${"export namespace API { export const added = 1; }"} | ${"export namespace API { export const added = 1; } export const added = 2;"} | ${true}
${"generic initializer does not hide a new export"} | ${"class Handler {}; export const cache = new Map<string, Handler>();"} | ${"class Handler {}; export const cache = new Map<string, Handler>(); export { Handler };"} | ${true}
${"added function"} | ${"export const old = 1;"} | ${"export const old = 1; export function added() {}"} | ${true}
${"removed export"} | ${"export const old = 1;"} | ${"const old = 1;"} | ${true}
${"renamed re-export"} | ${'export { original as old } from "./value.js";'} | ${'export { original as renamed } from "./value.js";'} | ${true}
${"multiline export list"} | ${"const old = 1; export { old };"} | ${"const old = 1, added = 2; export {\n old,\n added\n };"} | ${true}
${"second variable"} | ${"export const old = 1;"} | ${"export const old = 1, added = 2;"} | ${true}
${"default export"} | ${"export const old = 1;"} | ${"export const old = 1; export default () => 2;"} | ${true}
${"wildcard source"} | ${'export * from "./one.js";'} | ${'export * from "./two.js";'} | ${true}
${"implementation only"} | ${"export function old() { return 1; }"} | ${"export function old() { return 2; }"} | ${false}
${"initializer only"} | ${"export const old = 1;"} | ${"export const old = call(2, 3);"} | ${false}
${"type only"} | ${"export const old = 1;"} | ${"export const old = 1; export type New = string; export interface Shape {}"} | ${false}
${"comments and strings"} | ${"export const old = 1;"} | ${`export const old = 1; // export const added = 2;\nconst text = 'export { added }';`} | ${false}
${"unknown bindings"} | ${"export const old = 1;"} | ${"export const { old, added } = value;"} | ${true}
`(
"selects all mock owners on $name",
({ before, after, selected }: { before: string; after: string; selected: boolean }) => {
const producer = "src/owner/runtime.ts";
const consumers = testFiles("test", "relative alias package shared");
withTinyGitRepo(
{
[producer]: before,
"src/owner/runtime.test.ts": "export {};",
"src/owner/other.ts": 'const value = 1; export { value as "}" };',
"tsconfig.json": JSON.stringify({
compilerOptions: { paths: { "@runtime/*": ["src/owner/*"] } },
}),
"package.json": JSON.stringify({
name: "example",
exports: { "./runtime": "./src/owner/runtime.ts" },
}),
[consumers[0]!]: 'vi.mock("../src/owner/runtime.js", () => ({ old: 1 }));',
[consumers[1]!]: 'vi.mock(import("@runtime/runtime"), () => ({ old: 1 }));',
[consumers[2]!]: 'vi.mock("example/runtime", () => ({ old: 1 }));',
"test/mock-factory.ts": 'vi.mock("../src/owner/runtime.js", () => ({ old: 1 }));',
"test/shared-layer.ts": '"."\nimport "./mock-factory.js";',
[consumers[3]!]: 'import "./shared-layer.js";',
"test/fixture-text.test.ts": `const fixture = 'vi.mock("../src/owner/runtime.js", () => ({}))';`,
"test/comment.test.ts": '// vi.mock("../src/owner/runtime.js", () => ({}));',
},
(cwd) => {
const baseRef = gitTree(cwd);
fs.writeFileSync(path.join(cwd, producer), after);
const reasons: string[] = [];
const result = changedPlan([producer], {
cwd,
baseRef,
boundedOwners: true,
resolveAliases: true,
runtimeOnly: true,
aggressive: { maxDirectImporters: 1, maxDirectoryTests: 1 },
onSelection: ({ rule, targets }) => {
if (rule === "mock-export-consumer") {
reasons.push(...targets);
}
},
});
expect(reasons.toSorted()).toEqual(selected ? consumers.toSorted() : []);
if (selected) {
expect(result.targets).toEqual(expect.arrayContaining(consumers));
}
expect(result.targets).not.toContain("test/fixture-text.test.ts");
expect(result.targets).not.toContain("test/comment.test.ts");
},
);
},
);
it.each`
name | before | after | selected
${"namespace re-export becomes star re-export"} | ${'export * as runtime from "./runtime.js";'} | ${'export * from "./runtime.js";'} | ${true}
${"literal star becomes namespace consumption"} | ${'import { "*" as star } from "./runtime.js"; void star;'} | ${'import * as runtime from "./runtime.js"; void runtime.existing;'} | ${true}
${"literal star becomes dynamic consumption"} | ${'import { "*" as star } from "./runtime.js"; void star;'} | ${'const runtime = await import("./runtime.js"); void runtime.existing;'} | ${true}
${"literal star becomes re-export consumption"} | ${'export { "*" as star } from "./runtime.js";'} | ${'export * from "./runtime.js";'} | ${true}
${"import after a period literal"} | ${'import { old } from "./runtime.js";'} | ${'"."\nimport { existing } from "./runtime.js";'} | ${true}
${"quoted closing brace is a consumed name"} | ${'import { old } from "./runtime.js";'} | ${'import { "}" as existing } from "./runtime.js";'} | ${true}
${"named re-export switches consumed binding"} | ${'export { old as run } from "./runtime.js";'} | ${'export { existing as run } from "./runtime.js";'} | ${true}
${"erased query becomes a runtime namespace import"} | ${'type Runtime = typeof import("./runtime.js");'} | ${'type Runtime = typeof import("./runtime.js"); import * as runtime from "./runtime.js";'} | ${true}
${"default binding named type"} | ${'import { old } from "./runtime.js";'} | ${'import type from "./runtime.js";'} | ${true}
${"default binding named type with named imports"} | ${'import { old } from "./runtime.js";'} | ${'import type, { old } from "./runtime.js";'} | ${true}
${"new named import"} | ${'import { old } from "./runtime.js";'} | ${'import { old, existing } from "./runtime.js";'} | ${true}
${"new default import"} | ${'import { old } from "./runtime.js";'} | ${'import value, { old } from "./runtime.js";'} | ${true}
${"local rename"} | ${'import { old } from "./runtime.js";'} | ${'import { old as renamed } from "./runtime.js";'} | ${false}
${"new erased binding"} | ${'import { old } from "./runtime.js";'} | ${'import { old, type Shape } from "./runtime.js";'} | ${false}
`(
"protects existing exports on $name",
({ before, after, selected }: { before: string; after: string; selected: boolean }) => {
const producer = "src/owner/caller.ts";
withTinyGitRepo(
{
[producer]: before,
"src/owner/caller.test.ts": "export {};",
"src/owner/runtime.ts":
'export const old = 1, existing = 2; export default 3; export { existing as "}", existing as "*" };',
"test/consumer.test.ts":
'import "./layer-one.js"; vi.mock("../src/owner/runtime.js", () => ({ old: 1, "*": 2 }));',
"test/layer-one.ts": 'import "./layer-two.js";',
"test/layer-two.ts": 'import "../src/owner/caller.js";',
"test/unrelated.test.ts":
'vi.mock("../src/owner/runtime.js", () => ({ old: 1, "*": 2 }));',
},
(cwd) => {
const baseRef = gitTree(cwd);
fs.writeFileSync(path.join(cwd, producer), after);
const result = changedPlan([producer], {
cwd,
baseRef,
boundedOwners: true,
aggressive: { maxDirectImporters: 1, maxDirectoryTests: 1 },
});
expect(result.targets.includes("test/consumer.test.ts")).toBe(selected);
expect(result.targets).not.toContain("test/unrelated.test.ts");
},
);
},
);
it.each([
'vi.mock<{ old: number }>(import("../src/owner/runtime.js"), () => ({ old: 1 }));',
'import { vi as mocker } from "vitest"; mocker.doMock("../src/owner/runtime.js", () => ({ old: 1 }));',
])("retains consumers of typed and aliased mock installers: %s", (installer) => {
withTinyGitRepo(
{
"src/owner/runtime.ts": "export const old = 1;",
"src/owner/runtime.test.ts": "export {};",
"test/installer.ts": installer,
"test/layer.ts": 'import "./installer.js";',
"test/consumer.test.ts": 'import "./layer.js";',
},
(cwd) => {
const baseRef = gitTree(cwd);
fs.appendFileSync(path.join(cwd, "src/owner/runtime.ts"), "export const added = 2;");
expect(
changedPlan(["src/owner/runtime.ts"], {
cwd,
baseRef,
boundedOwners: true,
runtimeOnly: true,
aggressive: { maxDirectImporters: 1, maxDirectoryTests: 1 },
}).targets,
).toContain("test/consumer.test.ts");
},
);
});
it("fails closed when current source cannot be read", () => {
withTinyGitRepo(
{
"src/owner/runtime.ts": "export const old = 1;",
"src/owner/runtime.test.ts": "export {};",
"test/consumer.test.ts": 'vi.mock("../src/owner/runtime.js", () => ({ old: 1 }));',
},
(cwd) => {
const baseRef = gitTree(cwd);
const read = fs.readFileSync;
const denied = Object.assign(new Error("Current source is unreadable"), { code: "EACCES" });
const spy = vi.spyOn(fs, "readFileSync").mockImplementation((...args) => {
if (args[0] === path.join(cwd, "src/owner/runtime.ts")) {
throw denied;
}
return read(...args);
});
try {
expect(() =>
changedPlan(["src/owner/runtime.ts"], {
cwd,
baseRef,
boundedOwners: true,
}),
).toThrow(denied);
} finally {
spy.mockRestore();
}
},
);
});
it.each([undefined, "missing-base"])(
"keeps mock coverage when base %s is unavailable",
(baseRef) => {
withTinyGitRepo(
{
"src/owner/runtime.ts": "export const old = 1;",
"src/owner/runtime.test.ts": "export {};",
"test/consumer.test.ts": 'vi.mock("../src/owner/runtime.js", () => ({ old: 1 }));',
},
(cwd) => {
expect(
changedPlan(["src/owner/runtime.ts"], {
cwd,
baseRef,
boundedOwners: true,
}).targets,
).toContain("test/consumer.test.ts");
},
);
},
);
});
describe("test selector native source facts", () => {
function sourceRow(overrides: Partial<ReturnType<typeof readSourceFacts>[number]> = {}) {
return {
imports: [],
typeOnlyImports: [],
mocks: [],
workerExports: [],
matches: [],
references: [],
...overrides,
};
}
it("classifies runtime-worker descriptors by their declaration and self-reads", () => {
const descriptor =
'{ currentModuleUrl: import.meta.url, sourceWorkerName: "fixtures/worker", package: { name: "worker" } }';
const sources = {
"inline.ts": `function run() { return ${descriptor}; }`,
"single.ts": `export const worker = ${descriptor} as const;`,
"group.ts": `export const workers = {
first: ${descriptor},
second: { nested: { sourceWorkerName: "fixtures/second", package: {}, currentModuleUrl: import.meta.url } },
} as const;`,
"array.ts": `export const workers = [${descriptor}];`,
"call.ts": `export const workers = register(${descriptor});`,
"local.ts": `const worker = ${descriptor}; export { worker };`,
"default.ts": `export default ${descriptor};`,
"wrong-base.ts":
'export const worker = { currentModuleUrl: someVariable, sourceWorkerName: "fixtures/wrong" };',
"dynamic.ts":
"export const worker = { currentModuleUrl: import.meta.url, sourceWorkerName: workerName };",
"nested-base.ts":
'const worker = { sourceWorkerName: "fixtures/wrong", nested: { currentModuleUrl: import.meta.url } };',
"literal-reference.ts": `export const worker = ${descriptor}; const label = "worker"; unrelated.worker;`,
"jsx.tsx":
'export const view = <View />; const worker = { currentModuleUrl: otherUrl, sourceWorkerName: "fixtures/conservative" };',
...Object.fromEntries(
["worker", "worker.distWorkerPath", "worker[key]", "({ ...worker })"].map((read, index) => [
`self-read-${index}.ts`,
`export const worker = ${descriptor}; void ${read};`,
]),
),
};
withTinyFileTree(sources, (cwd) => {
const facts = new Map(
readSourceFacts(
cwd,
Object.keys(sources).map((file) => ({ file, parseImports: true })),
[],
1024 * 1024,
).map(({ file, imports, typeOnlyImports, workerExports }) => [
file,
{ imports, typeOnlyImports, workerExports },
]),
);
const inline = { imports: ["./fixtures/worker.js"], typeOnlyImports: [], workerExports: [] };
for (const file of [
"inline.ts",
"local.ts",
"default.ts",
...[0, 1, 2, 3].map((index) => `self-read-${index}.ts`),
]) {
expect(facts.get(file), file).toEqual(inline);
}
const expectedExports: Record<string, [string, string][]> = {
"single.ts": [["worker", "fixtures/worker"]],
"group.ts": [
["workers.first", "fixtures/worker"],
["workers.second", "fixtures/second"],
],
"array.ts": [["workers", "fixtures/worker"]],
"call.ts": [["workers", "fixtures/worker"]],
"literal-reference.ts": [["worker", "fixtures/worker"]],
};
for (const [file, workerExports] of Object.entries(expectedExports)) {
expect(facts.get(file), file).toEqual({
imports: [],
typeOnlyImports: [],
workerExports,
});
}
for (const file of ["wrong-base.ts", "dynamic.ts", "nested-base.ts"]) {
expect(facts.get(file), file).toEqual({
imports: [],
typeOnlyImports: [],
workerExports: [],
});
}
expect(facts.get("jsx.tsx")).toEqual({
imports: ["./fixtures/conservative.js"],
typeOnlyImports: [],
workerExports: [],
});
});
});
const descriptorReads: Array<[string, Array<[string, string[]]>]> = [
['import { group as local } from "source"; void local.member;', [["source", ["group.member"]]]],
[
'import { group } from "source"; Object.values(group); void group[key];',
[["source", ["group"]]],
],
['import { group } from "source"; void { ...group };', [["source", ["group"]]]],
[
'import { group } from "source"; void { ...group }; void { group }; const { member } = group; typeof group; group?.member;',
[["source", ["group"]]],
],
[
'import * as ns from "source"; void ns.group.member; void ns.single;',
[["source", ["group.member", "single"]]],
],
['import * as ns from "source"; void ns[key];', [["source", ["*"]]]],
[
'import("dynamic"); require("required"); export * from "all";',
[
["dynamic", ["*"]],
["required", ["*"]],
["all", ["*"]],
],
],
['export { group as renamed, single } from "source";', [["source", ["group", "single"]]]],
[
'import type { group } from "types"; import { type single, group as local } from "source"; void group; void single; void local.member;',
[["source", ["group.member"]]],
],
[
'import local from "source"; void local.member; import { unused } from "unused"; const text = "unused"; other.unused;',
[],
],
['import { group } from "source"; const view = <View />;', [["source", ["*"]]]],
];
it.each(descriptorReads)(
"reads runtime-worker descriptor consumption in %s",
(source, expected) => {
expect([...readDescriptorUses(source)]).toEqual(expected);
},
);
it("keeps whole-area UI consumers and source readers across graph cache scopes", () => {
const pluginModule = "extensions/example/browser/view.ts";
const pluginConsumer = "test/plugin-browser-consumer.test.ts";
// This import belongs to the virtual repository, not this test's module graph.
const pluginImport = path.posix
.relative(path.posix.dirname(pluginConsumer), pluginModule)
.replace(/\.ts$/u, ".js");
withTinyGitRepo(
{
"src/owner/value.ts": "export const value = 1;\n",
"ui/src/presenter.ts": 'export { value } from "../../src/owner/value.js";\n',
"ui/src/catalog.json": '{"label":"Changed dynamically loaded data"}\n',
"ui/src/catalog-extra.json": '{"label":"Another dynamically loaded catalog"}\n',
"ui/src/presenter.test.ts": 'import { value } from "./presenter.js"; void value;\n',
[pluginModule]: "export const view = 1;\n",
"src/consumer.test.ts": 'import { value } from "../ui/src/presenter.js"; void value;\n',
"scripts/ui-consumer.mjs": 'export { value } from "../ui/src/presenter.js";\n',
"test/scripts/ui-consumer.test.ts":
'import { value } from "../../scripts/ui-consumer.mjs"; void value;\n',
[pluginConsumer]: `import { view } from ${JSON.stringify(pluginImport)}; void view;\n`,
"test/scripts/ui-catalog-reader.test.ts":
'import { readFileSync } from "node:fs"; readFileSync("ui/src/catalog.json", "utf8");\n',
"test/scripts/ui-extra-reader.test.ts":
'import { readFileSync } from "node:fs"; readFileSync("ui/src/catalog-extra.json", "utf8");\n',
"test/scripts/ui-shared-reader.test.ts":
'import { readFileSync } from "node:fs"; ["ui/src/catalog.json", "ui/src/catalog-extra.json"].map((file) => readFileSync(file, "utf8"));\n',
"test/ui-consumer.live.test.ts": 'import "../ui/src/presenter.js";\n',
"src/unrelated.test.ts": "export {};\n",
},
(cwd) => {
const sourcePlan = () =>
changedPlan(["src/owner/value.ts"], {
cwd,
forceFullImportGraph: true,
});
const expectedSourcePlan = {
mode: "targets",
targets: ["src/consumer.test.ts", "ui/src/presenter.test.ts"],
};
expect(sourcePlan()).toEqual(expectedSourcePlan);
const graphConsumers = [
"src/consumer.test.ts",
"test/plugin-browser-consumer.test.ts",
"test/scripts/ui-consumer.test.ts",
];
expect(uiConsumers(["ui/src/catalog.json"], cwd)).toEqual([
...graphConsumers,
"test/scripts/ui-catalog-reader.test.ts",
"test/scripts/ui-shared-reader.test.ts",
]);
expect(
uiConsumers(
["ui/src/catalog.json", "ui/src/catalog-extra.json", "ui/src/catalog.json"],
cwd,
),
).toEqual([
...graphConsumers,
"test/scripts/ui-catalog-reader.test.ts",
"test/scripts/ui-shared-reader.test.ts",
"test/scripts/ui-extra-reader.test.ts",
]);
expect(uiConsumers(["ui/src/catalog-extra.json", "ui/src/catalog.json"], cwd)).toEqual([
...graphConsumers,
"test/scripts/ui-extra-reader.test.ts",
"test/scripts/ui-shared-reader.test.ts",
"test/scripts/ui-catalog-reader.test.ts",
]);
expect(sourcePlan()).toEqual(expectedSourcePlan);
},
);
});
it("separates executable imports from source fixtures and erased type declarations", () => {
const regexContexts = {
arrow: `const pattern = () => /['"]/;`,
asyncArrow: `const pattern = async () => /['"]/;`,
typeof: `const pattern = typeof /['"]/;`,
void: `const pattern = void /['"]/;`,
delete: `const pattern = delete /['"]/.source;`,
binary: `const pattern = 1 + /['"]/.source;`,
comparison: `const pattern = 1 < /['"]/.source;`,
if: `if (true) /['"]/.test('');`,
while: `while (false) /['"]/.test('');`,
ifBlock: `if (true) {} /['"]/.test('');`,
functionDeclaration: `function example() {} /['"]/.test('');`,
asyncDeclaration: `async function example() {} /['"]/.test('');`,
classDeclaration: `class Example {} /['"]/.test('');`,
classExtends: `class Example extends factory({}) {} /['"]/.test('');`,
labeledBlock: `label: {} /['"]/.test('');`,
caseBlock: `switch (1) { case 1: {} /['"]/.test(''); }`,
asiDeclaration: `const value = 1\nfunction example() {} /['"]/.test('');`,
};
const divisionContexts = {
object: `const value = ({}) / await import('./during.js') / 2;`,
function: `const value = function() {} / await import('./during.js') / 2;`,
asyncFunction: `const value = async function() {} / await import('./during.js') / 2;`,
class: `const value = class {} / await import('./during.js') / 2;`,
arrowBody: `const value = (() => {}) / await import('./during.js') / 2;`,
increment: `let value = 1; value++ / await import('./during.js') / 2;`,
decrement: `let value = 1; value-- / await import('./during.js') / 2;`,
};
const syntaxFiles = Object.fromEntries(
Object.entries({ ...regexContexts, ...divisionContexts }).map(([name, prefix]) => [
`${name}.ts`,
`${prefix}\nawait import('./consumer.js');`,
]),
);
syntaxFiles["ambiguous-template.ts"] =
`${regexContexts.labeledBlock} await import(\`./template-consumer.js\`);`;
syntaxFiles["generic-callback.ts"] = "export type Callback = <T>(value: T) => T;";
syntaxFiles["generic-callback-import.ts"] =
'export type Callback = <T>(value: T) => import("./callback-type.js").Value;';
withTinyFileTree(
{
...syntaxFiles,
"source.ts": [
'// import "./comment.js";',
'/* export * from "./block-comment.js"; */',
`const fixture = 'import "./quoted.js";';`,
'const template = `import "./template.js"; ${import("./expression.js")}`;',
String.raw`const pattern = /import "\.\/regexp\.js"/;`,
'import type { Value } from "./types.js";',
'export type * from "./reexport-types.js";',
'import { type Value, type Other as Alias } from "./named-types.js";',
'import { type Value, runtime } from "./mixed.js";',
'type Shape = typeof import("./import-type.js");',
'interface Options { input: import("./interface-type.js").Value }',
'const runtimeTypeof = typeof import("./runtime-expression.js");',
'import type from "./default-value.js";',
'import {} from "./side-effect.js";',
'import { type as value } from "./named-value.js";',
'export { type Value } from "./shared.js";',
'import "./shared.js";',
'import /* boundary */ "./real.js";',
].join("\n"),
},
(cwd) => {
const [facts, ...syntaxFacts] = readSourceFacts(
cwd,
[
{ file: "source.ts", parseImports: true },
...Object.keys(syntaxFiles).map((file) => ({ file, parseImports: true })),
],
[],
16 * 1024 * 1024,
);
expect(facts?.imports).toEqual([
"./expression.js",
"./types.js",
"./reexport-types.js",
"./named-types.js",
"./mixed.js",
"./import-type.js",
"./interface-type.js",
"./runtime-expression.js",
"./default-value.js",
"./side-effect.js",
"./named-value.js",
"./shared.js",
"./real.js",
]);
expect(facts?.typeOnlyImports).toEqual([
"./types.js",
"./reexport-types.js",
"./import-type.js",
"./interface-type.js",
]);
const importsByFile = new Map(syntaxFacts.map(({ file, imports }) => [file, imports]));
expect(syntaxFacts.find(({ file }) => file === "generic-callback.ts")).toMatchObject({
imports: [],
typeOnlyImports: [],
mocks: [],
workerExports: [],
});
expect(syntaxFacts.find(({ file }) => file === "generic-callback-import.ts")).toMatchObject(
{
imports: ["./callback-type.js"],
typeOnlyImports: ["./callback-type.js"],
},
);
expect(importsByFile.get("ambiguous-template.ts")).toEqual(["./template-consumer.js"]);
for (const [name, source] of Object.entries(regexContexts)) {
expect(importsByFile.get(`${name}.ts`), source).toEqual(["./consumer.js"]);
}
for (const [name, source] of Object.entries(divisionContexts)) {
expect(importsByFile.get(`${name}.ts`), source).toEqual(["./during.js", "./consumer.js"]);
}
},
);
});
it("preserves literal matches and whole-token references across a native source batch", () => {
const sources = {
"empty.txt": "",
"overlap.txt": "ushers ababa",
"paths.txt": "scripts/tool.mts @scope/name+tag_value-1.0 xabcdy abcd",
"unicode.txt": "éabcd😀foo/bar中 abc😀xyz",
"embedded.txt": "xabcdy scripts/tool.mts scripts/tool abcd",
"binary.txt": "null\0abcd\0tail",
"repeated.txt": "aaaaaaaaa aba aaa aaaa",
"late-references.txt":
"ushers ababa xabcdy scripts/tool.mts @scope/name+tag_value-1.0 xfoo/bary 😀 null\0 aaaaaa absent\n abcd scripts/tool foo/bar aaaa",
};
const terms = [
"",
"he",
"she",
"hers",
"aba",
"ba",
"aba",
"abcd",
"bcd",
"scripts/tool",
"scripts/tool.mts",
"@scope/name+tag_value-1.0",
"foo/bar",
"😀",
"\ud83d",
"\ude00",
// Failure links cross between dense ASCII rows and sparse non-ASCII edges.
"d😀foo",
"中 abc",
"😀xyz",
"é😀",
"null\0",
"aaa",
"aaaa",
"absent",
];
withTinyFileTree(sources, (cwd) => {
const files = Object.keys(sources).map((file) => ({ file, parseImports: false }));
const expected = Object.entries(sources).map(([file, source]) => {
const tokens = new Set(source.match(/[A-Za-z0-9_.@+/-]{4,}/gu));
return sourceRow({
file,
matches: terms.filter((term) => source.includes(term)),
references: terms.filter((term) => tokens.has(term)),
});
});
expect(readSourceFacts(cwd, files, terms, 1024 * 1024)).toEqual(expected);
});
});
it("keeps request order across a striped multi-worker source scan", () => {
// Enough files for several scan workers; each row must return to its request slot.
const rows = Array.from({ length: 600 }, (_, index) => ({
file: `f${String(index).padStart(3, "0")}.ts`,
dependency: `./dep-${index}.js`,
readable: index % 7 !== 3,
matched: index % 3 === 0,
}));
const sources = Object.fromEntries(
rows
.filter(({ readable }) => readable)
.map(({ file, dependency, matched }) => [
file,
`import "${dependency}";\n${matched ? "// needle\n" : ""}`,
]),
);
withTinyFileTree(sources, (cwd) => {
const files = rows.map(({ file }) => ({ file, parseImports: true }));
expect(
readSourceFacts(cwd, files, ["needle"], 16 * 1024 * 1024, {
matchingOnly: true,
}),
).toEqual(
rows
.filter(({ readable, matched }) => readable && matched)
.map(({ file, dependency }) =>
sourceRow({ file, imports: [dependency], matches: ["needle"], references: ["needle"] }),
),
);
expect(readSourceFacts(cwd, files, [], 16 * 1024 * 1024)).toEqual(
rows
.filter(({ readable }) => readable)
.map(({ file, dependency }) => sourceRow({ file, imports: [dependency] })),
);
});
});
it("reads complete files without installed packages, inherited hooks, or reparsing cached imports", () => {
withTinyFileTree(
{
"unterminated.ts": '// "\nconst value = "\\u{000',
"large.mts": `${"// padding\n".repeat(220_000)}export type {\n Value\n } from "./barrel.js";\nimport(\n "./dynamic.mjs"\n);\nconst fixture = "scripts/tool.mts";\nnew URL(\n "./native-fixture.mjs?generation=1#child", import.meta.url,\n);\nnew URL("./other-base.mjs", "file:///elsewhere/");\nrequire("dependency/runtime");\nrequire.resolve("dependency/package.json");\nimport.meta.resolve("other-dependency");`,
},
(cwd) => {
const files = [
{ file: "large.mts", parseImports: true },
{ file: "unterminated.ts", parseImports: true },
{ file: "deleted.ts", parseImports: true },
];
// Rows omit empty descriptor exports; parsed facts restore them.
const unterminatedFacts = {
imports: [],
typeOnlyImports: [],
mocks: [],
matches: [],
references: [],
};
const expectedFacts = {
imports: [
"./barrel.js",
"./dynamic.mjs",
"./native-fixture.mjs",
"dependency/runtime",
"dependency/package.json",
"other-dependency",
],
typeOnlyImports: ["./barrel.js"],
mocks: [],
matches: ["scripts/tool.mts", "scripts/tool"],
references: ["scripts/tool.mts"],
};
for (const file of [
"node-runtime-env.mjs",
"runtime-process-scripts.generated.mjs",
"scripts/lib/test-selector-source-facts.mts",
"scripts/lib/test-selector-source-facts.worker.mts",
"scripts/lib/test-source-term-matcher.mts",
"src/infra/node-runtime-executable.ts",
"src/infra/runtime-process-manifest.ts",
]) {
const target = path.join(cwd, file);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(path.resolve(file), target);
}
const scanner = path.join(
fs.realpathSync(cwd),
"scripts/lib/test-selector-source-facts.mts",
);
const native = spawnSync(requireNodeTool("node"), [scanner], {
cwd,
input: JSON.stringify({ files, terms: ["scripts/tool.mts", "scripts/tool"] }),
encoding: "utf8",
// A malformed escape must not rewind the scanner's cursor forever.
timeout: 5_000,
});
expect(native.error).toBeUndefined();
expect(native.status, native.stderr).toBe(0);
expect(JSON.parse(native.stdout)).toEqual([expectedFacts, unterminatedFacts, null]);
vi.stubEnv(
"NODE_OPTIONS",
"--import=data:text/javascript,throw%20Error('inherited-loader')",
);
try {
expect(
readSourceFacts(cwd, files, ["scripts/tool.mts", "scripts/tool"], 16 * 1024 * 1024, {
matchingOnly: true,
}),
).toEqual([{ file: "large.mts", ...expectedFacts, workerExports: [] }]);
expect(
readSourceFacts(cwd, files, ["scripts/tool.mts", "scripts/tool"], 16 * 1024 * 1024),
).toEqual([
{ file: "large.mts", ...expectedFacts, workerExports: [] },
{ file: "unterminated.ts", ...unterminatedFacts, workerExports: [] },
]);
expect(
readSourceFacts(
cwd,
[{ file: "large.mts", parseImports: false }],
["scripts/tool.mts"],
16 * 1024 * 1024,
),
).toEqual([
sourceRow({
file: "large.mts",
matches: ["scripts/tool.mts"],
references: ["scripts/tool.mts"],
}),
]);
} finally {
vi.unstubAllEnvs();
}
},
);
});
it("fails loudly on child launch, output overflow, and invalid native requests", () => {
withTinyFileTree({ "value.ts": 'import "./dependency.js";' }, (cwd) => {
const files = [{ file: "value.ts", parseImports: true }];
expect(() => readSourceFacts(path.join(cwd, "missing"), files, [], 1024)).toThrow(
"Test selector source scan failed",
);
expect(() => readSourceFacts(cwd, files, [], 1)).toThrow("Test selector source scan failed");
const result = spawnSync(
requireNodeTool("node"),
[path.resolve("scripts/lib/test-selector-source-facts.mts")],
{
cwd,
input: '{"files":false}',
encoding: "utf8",
},
);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("[test-selector-source-facts] FAILED (exit 1)");
});
});
});
describe("scripts/test-projects full-suite sharding", () => {
function expectChunkedFiles(
plans: ReturnType<typeof planFullSuite>,
files: string[],
limit: number,
) {
expect(plans.every((plan) => plan.forwardedArgs.length <= limit)).toBe(true);
expect(plans.flatMap((plan) => plan.forwardedArgs).toSorted()).toEqual(
[...new Set(files)].toSorted(),
);
}
const fastTooling = {
config: configFile("tooling"),
includePatterns: ["test/scripts/run-with-env.test.ts"],
};
const slowTooling = {
...fastTooling,
includePatterns: ["test/scripts/vitest-worker-artifacts.test.ts"],
};
const runtime = {
config: configFile("runtime-config"),
includePatterns: ["src/config/sessions/session-accessor.sqlite-archive.worker.test.ts"],
};
it.each([
[
"null whole-config selection",
fastTooling,
{ config: configFile("gateway"), includePatterns: null },
],
[
"empty whole-config selection",
fastTooling,
{ config: configFile("gateway"), includePatterns: [] },
],
["expensive selected files", runtime, slowTooling],
[
"expanded chunks",
{ ...runtime, includePatterns: null },
{ ...slowTooling, includePatterns: null, timingTargets: slowTooling.includePatterns },
],
] as const)("prioritizes %s", (_name, cheaper, expensive) => {
expect(orderFullSuiteSpecsForParallelRun([cheaper, expensive])).toEqual([expensive, cheaper]);
});
it("uses observed selection timings without substituting a whole-config sample", () => {
const timings = new Map([
[fastTooling.config, 1_000_000],
[resolveShardTimingKey(fastTooling), 500],
[resolveShardTimingKey(slowTooling), 153_000],
[resolveShardTimingKey(runtime), 35_000],
]);
expect(orderFullSuiteSpecsForParallelRun([fastTooling, slowTooling, runtime], timings)).toEqual(
[slowTooling, fastTooling, runtime],
);
});
it("raises the effective last aggregate extension heap override", () => {
const specs = applyFullExtensionsHeapBudget([
{
config: configFile("full-extensions"),
env: { NODE_OPTIONS: "--max-old-space-size=12288 --max_old_space_size=4096" },
},
]);
expect(specs[0]?.env.NODE_OPTIONS).toBe("--max-old-space-size=12288 --max_old_space_size=8192");
});
it.each([false, true])("bounds full-suite CI processes for Testbox=%s", (testbox) => {
vi.stubEnv("CI", "1");
vi.stubEnv("GITHUB_ACTIONS", "");
vi.stubEnv("OPENCLAW_TESTBOX_REMOTE_RUN", testbox ? "1" : "");
vi.stubEnv("OPENCLAW_TEST_PROJECTS_LEAF_SHARDS", "");
vi.stubEnv("OPENCLAW_TEST_PROJECTS_PARALLEL", "");
try {
const plans = planFullSuite([], process.cwd());
const configs = plans.map((plan) => plan.config);
if (!testbox) {
expect(configs).toContain(configFile("full-agentic"));
expect(configs).toContain(configFile("full-extensions"));
expect(configs).not.toContain(configFile("gateway-server"));
expect(configs).not.toContain(configFile("extension-telegram"));
return;
}
expect(configs).not.toContain(configFile("full-agentic"));
expect(configs).not.toContain(configFile("full-extensions"));
expect(configs).toContain(configFile("agents-core"));
expect(configs).toContain(configFile("extension-telegram"));
const targetedPlans = (config: string) =>
plans.filter((plan) => plan.config === config && plan.forwardedArgs.length > 0);
expect(targetedPlans(configFile("agents-core"))).toHaveLength(6);
expect(
targetedPlans(configFile("agents-core")).flatMap((plan) => plan.forwardedArgs),
).not.toContain("src/agents/agent-command-local.test.ts");
expect(configs.filter((config) => config === configFile("cli-process"))).toHaveLength(1);
const gatewayTargets = targetedPlans(configFile("gateway-server")).map(
(plan) => plan.forwardedArgs,
);
expect(gatewayTargets.every((files) => files.length > 0 && files.length <= 16)).toBe(true);
expect(gatewayTargets.flat()).toEqual(
listGitTrackedFiles({ pathspecs: "src/gateway" })
?.filter(isGatewayServerTestFile)
.toSorted((a, b) => a.localeCompare(b)),
);
} finally {
vi.unstubAllEnvs();
}
});
it("expands untargeted local runs to leaf project configs by default", async () => {
const infraConfig = configFile("infra");
const infraFiles = await listVitestConfigTestFiles(infraConfig);
const toolingFiles = await listVitestConfigTestFiles(configFile("tooling"));
const uiFiles = await listVitestConfigTestFiles(configFile("ui"));
const gatewayFiles = await listVitestConfigTestFiles(configFile("gateway-database-workers"));
const extensionDatabaseFiles = await listVitestConfigTestFiles(
configFile("extension-database-workers"),
);
withEnv(
{
OPENCLAW_TEST_PROJECTS_LEAF_SHARDS: undefined,
OPENCLAW_TEST_PROJECTS_PARALLEL: undefined,
OPENCLAW_TEST_PROJECTS_SERIAL: undefined,
CI: undefined,
GITHUB_ACTIONS: undefined,
OPENCLAW_VITEST_MAX_WORKERS: undefined,
OPENCLAW_TEST_WORKERS: undefined,
},
() => {
const plans = planFullSuite([], process.cwd());
const configs = plans.map((plan) => plan.config);
expect(configs).toContain(configFile("gateway-server"));
expect(configs).toContain(configFile("extension-telegram"));
expect(configs).not.toContain(configFile("full-agentic"));
expect(configs).not.toContain(configFile("full-core-unit-fast"));
const targetedPlans = (config: string) =>
plans.filter((plan) => plan.config === config && plan.forwardedArgs.length > 0);
const unitFastPlans = targetedPlans(configFile("unit-fast"));
expect(unitFastPlans.length).toBeGreaterThan(1);
expect(unitFastPlans.every((plan) => plan.forwardedArgs.length <= 70)).toBe(true);
const unitSrcPlans = targetedPlans(configFile("unit-src"));
expect(unitSrcPlans.length).toBeGreaterThan(1);
expect(unitSrcPlans.every((plan) => plan.forwardedArgs.length <= 150)).toBe(true);
const toolingPlans = targetedPlans(configFile("tooling"));
expect(toolingPlans.length).toBeGreaterThan(1);
expect(toolingPlans).toHaveLength(Math.ceil(toolingFiles.length / 8));
expectChunkedFiles(toolingPlans, toolingFiles, 8);
const uiPlans = targetedPlans(configFile("ui"));
expect(uiPlans).toHaveLength(Math.ceil(uiFiles.length / 8));
expectChunkedFiles(uiPlans, uiFiles, 8);
const gatewayPlans = targetedPlans(configFile("gateway-database-workers"));
expect(gatewayPlans).toHaveLength(Math.ceil(gatewayFiles.length / 50));
expectChunkedFiles(gatewayPlans, gatewayFiles, 50);
const extensionDatabasePlans = targetedPlans(configFile("extension-database-workers"));
expectChunkedFiles(extensionDatabasePlans, extensionDatabaseFiles, 20);
expect(
extensionDatabasePlans
.filter((plan) =>
plan.forwardedArgs.some((file) => file.startsWith("extensions/telegram/")),
)
.every((plan) => plan.forwardedArgs.length === 1),
).toBe(true);
const infraPlans = plans.filter((plan) => plan.config === infraConfig);
expect(infraPlans.length).toBeGreaterThan(1);
expect(
infraPlans.every(
(plan) => plan.forwardedArgs.length > 0 && plan.forwardedArgs.length <= 64,
),
).toBe(true);
expect(infraPlans.flatMap((plan) => plan.forwardedArgs).toSorted()).toEqual(
[...new Set(infraFiles)].toSorted(),
);
const toolingTargets = toolingPlans.flatMap((plan) => plan.forwardedArgs);
expect(toolingTargets.filter((file) => file.startsWith("test/fixtures/"))).toEqual([]);
expect(plans.flatMap((plan) => plan.forwardedArgs)).toEqual(
expect.arrayContaining(testFiles("test/scripts", "oxlint-boundary-guards ts-topology")),
);
for (const plan of plans.filter((entry) => entry.forwardedArgs.length > 0)) {
expect(plan.timingTargets).toEqual(plan.forwardedArgs);
expect(plan.includePatterns).toBeNull();
}
},
);
});
it("preserves one database invocation when Vitest owns suite-wide sharding", () => {
withEnv(
{ CI: undefined, GITHUB_ACTIONS: undefined, OPENCLAW_TEST_PROJECTS_PARALLEL: undefined },
() => {
const plans = planFullSuite(["--shard=1/2"], process.cwd()).filter(
(plan) => plan.config === configFile("extension-database-workers"),
);
expect(plans).toHaveLength(1);
expect(plans[0]?.forwardedArgs).toContain("--shard=1/2");
},
);
});
it("keeps shared Vitest config helpers out of whole-config targets", () => {
const args = [configFile("shared")];
expect(unmatchedTargets(args, process.cwd())).toEqual([]);
expectRun(planRuns(args, process.cwd()), "tooling", {
includePatterns: ["test/vitest/**/*.test.ts"],
});
withTinyFileTree({ [configFile("helper")]: "export default {};\n" }, (cwd) => {
const helperArgs = [configFile("helper")];
expect(unmatchedTargets(helperArgs, cwd)).toEqual([
{
target: configFile("helper"),
reason: "target-matched-no-test-files",
includePattern: "test/vitest/**/*.test.ts",
},
]);
expectRun(planRuns(helperArgs, cwd), "tooling", {
includePatterns: ["test/vitest/**/*.test.ts"],
});
});
});
});
describe("scripts/test-projects Vitest stall watchdog", () => {
it("arms non-watch jobs without shortening the direct runner's slow-config watchdog", () => {
const configs = [configFile("extension-feishu"), configFile("gateway-server")];
const specs = applyDefaultVitestNoOutputTimeout(
configs.map((config) => ({ config, env: {}, watchMode: false })),
{ env: {} },
);
for (const [index, spec] of specs.entries()) {
const timeout = Number(spec.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS);
const heartbeat = Number(spec.env.OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS);
expect(timeout).toBeGreaterThan(0);
expect(heartbeat).toBeGreaterThan(0);
expect(heartbeat).toBeLessThan(timeout);
expect(timeout).toBeGreaterThanOrEqual(
resolveDefaultVitestNoOutputTimeoutMs(["run", "--config", configs[index]!]),
);
}
expect(Number(specs[1]?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS)).toBeGreaterThan(
Number(specs[0]?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS),
);
});
});