fix(test): keep extension batches inside requested directories (#162009)

* fix(test): keep extension batches inside requested directories

Preserve canonical plugin roots as directory filters and intersect plain directory test globs with their owning scope. Prevent policy batches from selecting sibling policy filenames or nested policy directories while retaining watch discovery, explicit files, exclusions, and complex owner patterns.

Add real runner-to-selector regression cases, proven failing before the fix, and keep existing aggregation and scoped-config assertions aligned.

* fix(test): preserve explicit extension file filters
This commit is contained in:
Peter Steinberger 2026-09-30 11:58:15 -07:00 • committed by GitHub
parent 8eadb661cd
commit a58e5babbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 11 deletions

View file

@ -33,7 +33,7 @@ import { isDirectScriptRun, runVitestBatch } from "./lib/vitest-batch-runner.mts
import type { VitestBatchRunParams } from "./lib/vitest-batch-runner.mts";
import { prepareVitestRuntime } from "./lib/vitest-build-prerequisites.mts";
import { resolveVitestCacheRoot, resolveVitestCacheSlotPath } from "./lib/vitest-cache-slots.mts";
import { resolveExplicitVitestMode } from "./lib/vitest-cli-mode.mts";
import { collectVitestFileFilters, resolveExplicitVitestMode } from "./lib/vitest-cli-mode.mts";
import { resolveVitestHomeSelection } from "./lib/vitest-home-selection.mts";
import { createVitestReportOwner, type VitestReportOutcome } from "./lib/vitest-report-owner.mts";
import { resolveVitestRuntimeCliSelections } from "./lib/vitest-runtime-selection.mts";
@ -178,6 +178,7 @@ function preparePlanGroup(
exactExcludePaths: Set<string>,
) {
const targets = resolveGroupTargets(group, exactExcludePaths);
const hasFileFilters = collectVitestFileFilters(vitestArgs).length > 0;
const targetChunks =
targets.length === 0
? []
@ -195,7 +196,11 @@ function preparePlanGroup(
group,
watchMode: resolveExplicitVitestMode(["run", ...vitestArgs]) === "watch",
}),
targets: chunk.map((target) => relativizeExtensionVitestPath(target)),
targets: chunk.map((target) => {
const relative = relativizeExtensionVitestPath(target);
// Bound default discovery without widening an explicit CLI file selection.
return !hasFileFilters && group.extensionIds.includes(relative) ? `${relative}/` : relative;
}),
}),
);
const bunInvocations: typeof invocations = [];
@ -238,7 +243,12 @@ function combineSinglePluginGroups(
config,
args: relativizeExtensionVitestArgs(vitestArgs),
targets: targets.filter(
(target) => !targets.some((root) => target.startsWith(`${root}/`)),
(target) =>
!targets.some(
(root) =>
root !== target &&
target.startsWith(root.endsWith("/") ? root : `${root}/`),
),
),
env: {
...createGroupEnv({

View file

@ -62,7 +62,10 @@ import { providerExtensionTestRoots } from "../vitest/vitest.extension-provider-
import { qaExtensionTestRoots } from "../vitest/vitest.extension-qa-paths.mjs";
import { zaloExtensionTestRoots } from "../vitest/vitest.extension-zalo-paths.mjs";
import { extensionCatchAllExcludedTestRoots } from "../vitest/vitest.extensions.config.ts";
import { isSharedVitestExcludedPath } from "../vitest/vitest.pattern-file.ts";
import {
isSharedVitestExcludedPath,
matchesVitestCliSelection,
} from "../vitest/vitest.pattern-file.ts";
vi.mock("../../scripts/lib/vitest-build-prerequisites.mts", async (importOriginal) => ({
...(await importOriginal<typeof import("../../scripts/lib/vitest-build-prerequisites.mts")>()),
@ -689,7 +692,7 @@ describe("scripts/test-extension.mts", () => {
env: {
OPENCLAW_EXTENSION_BATCH_PARALLEL: "2",
},
targets: ["two"],
targets: ["two/"],
});
const cachePaths = runGroup.mock.calls.map(([params]) =>
params.env?.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.replaceAll("\\", "/"),
@ -1009,6 +1012,53 @@ await new Promise(()=>{});export default {};`,
},
);
it.each([
{ ids: ["policy"], args: [], selected: ["extensions/policy/src/example.test.ts"] },
{
ids: ["policy", "file-transfer"],
args: [],
selected: [
"extensions/policy/src/example.test.ts",
"extensions/file-transfer/src/shared/policy.test.ts",
],
},
{
ids: ["policy"],
args: ["--watch"],
selected: ["extensions/policy/src/example.test.ts"],
},
])("confines extension roots $ids with $args", async ({ ids, args, selected }) => {
const runGroup = vi.fn<(params: RunGroupParams) => Promise<number>>().mockResolvedValue(0);
await expect(
runExtensionBatchPlan(resolveExtensionBatchPlan({ extensionIds: ids }), {
env: {},
runGroup,
vitestArgs: args,
}),
).resolves.toBe(0);
expect(runGroup).toHaveBeenCalledOnce();
const invocation = requireFirstMockArg<RunGroupParams>(runGroup);
const candidates = [
"extensions/policy/src/example.test.ts",
"extensions/file-transfer/src/shared/policy.test.ts",
"extensions/other/src/policy/example.test.ts",
"extensions/policy-extra/src/example.test.ts",
"extensions/policy/src/example.test.tsx",
];
expect(
candidates.filter((file) =>
matchesVitestCliSelection(
file,
["extensions/**/*.test.ts"],
["run", "--config", invocation.config, ...invocation.args, ...invocation.targets],
"extensions",
invocation.env ?? {},
),
),
).toEqual(selected);
});
it("expands extension batch roots before applying exact Vitest excludes", async () => {
const runGroup = vi.fn<() => Promise<number>>().mockResolvedValue(0);
await runExtensionBatchPlan(
@ -1222,7 +1272,7 @@ await new Promise(()=>{});export default {};`,
expect(result).toBe(0);
expect(runGroup).toHaveBeenCalledOnce();
const invocation = requireFirstMockArg<RunGroupParams>(runGroup);
expect(invocation.targets).toEqual(["matrix"]);
expect(invocation.targets).toEqual(["matrix/"]);
expect(invocation.config).toBe("test/vitest/vitest.database-worker-watch.config.ts");
expect(invocation.homeMode).toBe("live-aware");
expect(invocation.env?.OPENCLAW_VITEST_DATABASE_WORKER_WATCH_OWNER).toBe(

View file

@ -69,7 +69,7 @@ function expectForkedNonIsolatedRunner(
}
describe("createScopedVitestConfig", () => {
it("keeps broad package scoped cli directory filters aligned with repo-root include patterns", () => {
it("narrows package CLI directories within their owner", () => {
const config = createScopedVitestConfig(["packages/**/*.test.ts"], {
argv: ["vitest", "run", "packages/normalization-core"],
dir: "packages",
@ -77,7 +77,7 @@ describe("createScopedVitestConfig", () => {
passWithNoTests: true,
});
expect(requireTestConfig(config).include).toEqual(["**/*.test.ts"]);
expect(requireTestConfig(config).include).toEqual(["normalization-core/**/*.test.ts"]);
});
it("relativizes scoped include and exclude patterns to the configured dir", () => {

View file

@ -62,18 +62,25 @@ export function narrowIncludePatterns(
return null;
}
// Vitest applies CLI filters after discovery. Prefix overlap cannot prove glob
// containment, so retain the owner's patterns unless selecting an owned literal file.
// Prefix overlap alone cannot prove containment. Only narrow literal files
// and plain directory test patterns; preserve more complex owner globs.
const narrowed = new Set<string>();
for (const candidate of candidatePatterns) {
const isLiteral = !/[?*[\]{}]/u.test(candidate);
const directoryCandidate = candidate.replace(/\.test\.\*$/u, ".test.ts");
const candidateRoot = directoryTestPatternRoot(directoryCandidate);
for (const laneScope of includePatterns) {
if (isLiteral) {
if (matchesGlob(candidate, laneScope)) {
narrowed.add(candidate);
}
} else if (patternsCouldOverlap(candidate, laneScope, matchesGlob)) {
narrowed.add(laneScope);
const ownerRoot = directoryTestPatternRoot(laneScope);
narrowed.add(
candidateRoot !== null && ownerRoot !== null && isAtOrUnder(candidateRoot, ownerRoot)
? directoryCandidate
: laneScope,
);
}
}
}