fix(test): route extension tests through scoped paths

This commit is contained in:
Vincent Koc 2026-06-05 22:59:48 -07:00
parent 743051d400
commit 92242f4f68
No known key found for this signature in database
8 changed files with 425 additions and 78 deletions

View file

@ -6,6 +6,7 @@ import { channelTestRoots } from "../../test/vitest/vitest.channel-paths.mjs";
import { isAcpxExtensionRoot } from "../../test/vitest/vitest.extension-acpx-paths.mjs";
import { isBrowserExtensionRoot } from "../../test/vitest/vitest.extension-browser-paths.mjs";
import { resolveSplitChannelExtensionShard } from "../../test/vitest/vitest.extension-channel-split-paths.mjs";
import { isCodexExtensionRoot } from "../../test/vitest/vitest.extension-codex-paths.mjs";
import { isDiffsExtensionRoot } from "../../test/vitest/vitest.extension-diffs-paths.mjs";
import { isFeishuExtensionRoot } from "../../test/vitest/vitest.extension-feishu-paths.mjs";
import { isIrcExtensionRoot } from "../../test/vitest/vitest.extension-irc-paths.mjs";
@ -38,6 +39,7 @@ const EXTENSION_TEST_COST_MULTIPLIERS = {
// suites vary widely, and file count alone leaves long tail shards.
"test/vitest/vitest.extension-acpx.config.ts": 0.75,
"test/vitest/vitest.extension-browser.config.ts": 0.5,
"test/vitest/vitest.extension-codex.config.ts": 1.3,
"test/vitest/vitest.extension-diffs.config.ts": 0.6,
"test/vitest/vitest.extension-discord.config.ts": 0.62,
"test/vitest/vitest.extension-feishu.config.ts": 0.18,
@ -207,6 +209,7 @@ export function resolveExtensionTestPlan(params = {}) {
const usesChannelConfig = roots.some((root) => channelTestRoots.includes(root));
const usesAcpxConfig = roots.some((root) => isAcpxExtensionRoot(root));
const usesBrowserConfig = roots.some((root) => isBrowserExtensionRoot(root));
const usesCodexConfig = roots.some((root) => isCodexExtensionRoot(root));
const usesDiffsConfig = roots.some((root) => isDiffsExtensionRoot(root));
const usesFeishuConfig = roots.some((root) => isFeishuExtensionRoot(root));
const usesIrcConfig = roots.some((root) => isIrcExtensionRoot(root));
@ -232,41 +235,43 @@ export function resolveExtensionTestPlan(params = {}) {
? "test/vitest/vitest.extension-acpx.config.ts"
: usesBrowserConfig
? "test/vitest/vitest.extension-browser.config.ts"
: usesDiffsConfig
? "test/vitest/vitest.extension-diffs.config.ts"
: usesFeishuConfig
? "test/vitest/vitest.extension-feishu.config.ts"
: usesIrcConfig
? "test/vitest/vitest.extension-irc.config.ts"
: usesMattermostConfig
? "test/vitest/vitest.extension-mattermost.config.ts"
: usesMatrixConfig
? "test/vitest/vitest.extension-matrix.config.ts"
: usesMediaConfig
? "test/vitest/vitest.extension-media.config.ts"
: usesMemoryConfig
? "test/vitest/vitest.extension-memory.config.ts"
: usesMessagingConfig
? "test/vitest/vitest.extension-messaging.config.ts"
: usesMiscConfig
? "test/vitest/vitest.extension-misc.config.ts"
: usesMsTeamsConfig
? "test/vitest/vitest.extension-msteams.config.ts"
: usesQaConfig
? "test/vitest/vitest.extension-qa.config.ts"
: usesTelegramConfig
? "test/vitest/vitest.extension-telegram.config.ts"
: usesVoiceCallConfig
? "test/vitest/vitest.extension-voice-call.config.ts"
: usesWhatsAppConfig
? "test/vitest/vitest.extension-whatsapp.config.ts"
: usesZaloConfig
? "test/vitest/vitest.extension-zalo.config.ts"
: usesProviderOpenAiConfig
? "test/vitest/vitest.extension-provider-openai.config.ts"
: usesProviderConfig
? "test/vitest/vitest.extension-providers.config.ts"
: "test/vitest/vitest.extensions.config.ts";
: usesCodexConfig
? "test/vitest/vitest.extension-codex.config.ts"
: usesDiffsConfig
? "test/vitest/vitest.extension-diffs.config.ts"
: usesFeishuConfig
? "test/vitest/vitest.extension-feishu.config.ts"
: usesIrcConfig
? "test/vitest/vitest.extension-irc.config.ts"
: usesMattermostConfig
? "test/vitest/vitest.extension-mattermost.config.ts"
: usesMatrixConfig
? "test/vitest/vitest.extension-matrix.config.ts"
: usesMediaConfig
? "test/vitest/vitest.extension-media.config.ts"
: usesMemoryConfig
? "test/vitest/vitest.extension-memory.config.ts"
: usesMessagingConfig
? "test/vitest/vitest.extension-messaging.config.ts"
: usesMiscConfig
? "test/vitest/vitest.extension-misc.config.ts"
: usesMsTeamsConfig
? "test/vitest/vitest.extension-msteams.config.ts"
: usesQaConfig
? "test/vitest/vitest.extension-qa.config.ts"
: usesTelegramConfig
? "test/vitest/vitest.extension-telegram.config.ts"
: usesVoiceCallConfig
? "test/vitest/vitest.extension-voice-call.config.ts"
: usesWhatsAppConfig
? "test/vitest/vitest.extension-whatsapp.config.ts"
: usesZaloConfig
? "test/vitest/vitest.extension-zalo.config.ts"
: usesProviderOpenAiConfig
? "test/vitest/vitest.extension-provider-openai.config.ts"
: usesProviderConfig
? "test/vitest/vitest.extension-providers.config.ts"
: "test/vitest/vitest.extensions.config.ts";
const testFileCount = roots.reduce(
(sum, root) => sum + countTestFiles(path.join(repoRoot, root)),
0,

View file

@ -0,0 +1,97 @@
import path from "node:path";
const EXTENSIONS_PATH_PREFIX = "extensions/";
const repoRoot = path.resolve(import.meta.dirname, "../..");
const VITEST_VALUE_FLAGS = new Set([
"-c",
"-r",
"-t",
"--browser",
"--changed",
"--config",
"--coverage.all",
"--coverage.exclude",
"--coverage.extension",
"--coverage.include",
"--coverage.provider",
"--coverage.reporter",
"--coverage.reportsDirectory",
"--dir",
"--environment",
"--environmentOptions",
"--hookTimeout",
"--inspect",
"--inspectBrk",
"--maxConcurrency",
"--maxWorkers",
"--minWorkers",
"--mode",
"--name",
"--outputFile",
"--pool",
"--project",
"--reporter",
"--retry",
"--root",
"--sequence",
"--shard",
"--testNamePattern",
"--testTimeout",
"--workspace",
]);
export function normalizeRelativePath(inputPath, cwd = process.cwd()) {
const absolutePath = path.isAbsolute(inputPath)
? inputPath
: inputPath.startsWith(EXTENSIONS_PATH_PREFIX)
? path.resolve(repoRoot, inputPath)
: path.resolve(cwd, inputPath);
const repoRelative = path.relative(repoRoot, absolutePath).split(path.sep).join("/");
return repoRelative === ".." || repoRelative.startsWith("../")
? inputPath.split(path.sep).join("/")
: repoRelative;
}
export function relativizeExtensionVitestPath(inputPath, cwd = process.cwd()) {
const normalized = normalizeRelativePath(inputPath, cwd);
return normalized.startsWith(EXTENSIONS_PATH_PREFIX)
? normalized.slice(EXTENSIONS_PATH_PREFIX.length)
: normalized;
}
export function relativizeExtensionVitestArgs(vitestArgs, cwd = process.cwd()) {
const args = [];
for (let index = 0; index < vitestArgs.length; index += 1) {
const arg = vitestArgs[index];
if (arg === "--exclude") {
const value = vitestArgs[index + 1];
args.push(arg);
if (value) {
args.push(relativizeExtensionVitestPath(value, cwd));
index += 1;
}
continue;
}
if (VITEST_VALUE_FLAGS.has(arg)) {
args.push(arg);
const value = vitestArgs[index + 1];
if (value) {
args.push(value);
index += 1;
}
continue;
}
const excludePrefix = "--exclude=";
if (arg.startsWith(excludePrefix)) {
args.push(
`${excludePrefix}${relativizeExtensionVitestPath(arg.slice(excludePrefix.length), cwd)}`,
);
continue;
}
args.push(arg.startsWith("-") ? arg : relativizeExtensionVitestPath(arg, cwd));
}
return args;
}

View file

@ -6,6 +6,11 @@ import {
listTrackedTestFilesForRoots,
resolveExtensionBatchPlan,
} from "./lib/extension-test-plan.mjs";
import {
normalizeRelativePath,
relativizeExtensionVitestArgs,
relativizeExtensionVitestPath,
} from "./lib/extension-vitest-paths.mjs";
import { parsePositiveInt } from "./lib/numeric-options.mjs";
import { isDirectScriptRun, runVitestBatch } from "./lib/vitest-batch-runner.mjs";
@ -100,17 +105,18 @@ function orderPlanGroups(planGroups, parallelism) {
});
}
function normalizeRelativePath(inputPath) {
return path
.relative(process.cwd(), path.resolve(process.cwd(), inputPath))
.split(path.sep)
.join("/");
}
function isExactExcludePath(inputPath) {
return !/[*!?[\]{}]/u.test(inputPath);
}
function addExactExcludePath(excludePaths, value) {
const normalized = normalizeRelativePath(value);
excludePaths.add(normalized);
if (!normalized.startsWith("extensions/")) {
excludePaths.add(`extensions/${normalized}`);
}
}
/**
* Collects exact --exclude paths so empty groups can be reported accurately.
*/
@ -121,7 +127,7 @@ export function parseExactVitestExcludePaths(vitestArgs) {
if (arg === "--exclude") {
const value = vitestArgs[index + 1];
if (value && isExactExcludePath(value)) {
excludePaths.add(normalizeRelativePath(value));
addExactExcludePath(excludePaths, value);
}
index += 1;
continue;
@ -130,7 +136,7 @@ export function parseExactVitestExcludePaths(vitestArgs) {
if (arg.startsWith(prefix)) {
const value = arg.slice(prefix.length);
if (value && isExactExcludePath(value)) {
excludePaths.add(normalizeRelativePath(value));
addExactExcludePath(excludePaths, value);
}
}
}
@ -161,7 +167,7 @@ async function runPlanGroup(group, params) {
`[test-extension-batch] ${group.config}: ${group.extensionIds.join(", ")} (${targets.length} targets)`,
);
return await params.runGroup({
args: params.vitestArgs,
args: relativizeExtensionVitestArgs(params.vitestArgs),
config: group.config,
env: createGroupEnv({
baseEnv: params.env,
@ -169,7 +175,7 @@ async function runPlanGroup(group, params) {
groupIndex: params.groupIndex,
useDedicatedCache: params.useDedicatedCache,
}),
targets,
targets: targets.map((target) => relativizeExtensionVitestPath(target)),
});
}

View file

@ -3,6 +3,10 @@
// Runs the Vitest plan for one bundled plugin by id or path.
import { formatErrorMessage } from "./lib/error-format.mjs";
import { resolveExtensionTestPlan } from "./lib/extension-test-plan.mjs";
import {
relativizeExtensionVitestArgs,
relativizeExtensionVitestPath,
} from "./lib/extension-vitest-paths.mjs";
import { isDirectScriptRun, runVitestBatch } from "./lib/vitest-batch-runner.mjs";
const ALLOW_NO_TESTS_FLAG = "--allow-no-tests";
@ -54,10 +58,10 @@ async function run() {
console.log(`[test-extension] Running ${plan.testFileCount} test files for ${plan.extensionId}`);
const exitCode = await runVitestBatch({
args: passthroughArgs,
args: relativizeExtensionVitestArgs(passthroughArgs),
config: plan.config,
env: process.env,
targets: plan.roots,
targets: plan.roots.map((target) => relativizeExtensionVitestPath(target)),
});
process.exit(exitCode);
}

View file

@ -17,6 +17,7 @@ import {
resolveExtensionBatchPlan,
resolveExtensionTestPlan,
} from "../../scripts/lib/extension-test-plan.mjs";
import { relativizeExtensionVitestArgs } from "../../scripts/lib/extension-vitest-paths.mjs";
import { buildVitestBatchPnpmArgs } from "../../scripts/lib/vitest-batch-runner.mjs";
import {
parseExtensionIds,
@ -227,11 +228,11 @@ describe("scripts/test-extension.mjs", () => {
);
});
it("keeps unmatched non-provider extensions on the shared extensions vitest config", () => {
it("resolves codex onto the codex vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "codex", cwd: process.cwd() });
expect(plan.extensionId).toBe("codex");
expect(plan.config).toBe("test/vitest/vitest.extensions.config.ts");
expect(plan.config).toBe("test/vitest/vitest.extension-codex.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("codex"));
expect(plan.hasTests).toBe(true);
});
@ -614,7 +615,7 @@ describe("scripts/test-extension.mjs", () => {
"0-heavy",
),
},
targets: ["extensions/two"],
targets: ["two"],
});
});
@ -650,9 +651,9 @@ describe("scripts/test-extension.mjs", () => {
it("places Vitest passthrough options before batch target roots", () => {
expect(
buildVitestBatchPnpmArgs({
args: ["--exclude", "extensions/codex/src/app-server/run-attempt.test.ts"],
args: ["--exclude", "codex/src/app-server/run-attempt.test.ts"],
config: "test/vitest/vitest.extensions.config.ts",
targets: ["extensions/codex"],
targets: ["codex"],
}),
).toEqual([
"exec",
@ -661,11 +662,86 @@ describe("scripts/test-extension.mjs", () => {
"--config",
"test/vitest/vitest.extensions.config.ts",
"--exclude",
"extensions/codex/src/app-server/run-attempt.test.ts",
"extensions/codex",
"codex/src/app-server/run-attempt.test.ts",
"codex",
]);
});
it("relativizes extension Vitest path args to the scoped extensions dir", () => {
expect(
relativizeExtensionVitestArgs([
"--exclude",
"extensions/codex/src/app-server/run-attempt.test.ts",
"--outputFile",
"extensions/codex/report.json",
"-c",
"./vitest.local.ts",
"-r",
".",
"--exclude=extensions/codex/src/app-server/client.test.ts",
"extensions/codex/src/app-server/models.test.ts",
"--reporter=dot",
]),
).toEqual([
"--exclude",
"codex/src/app-server/run-attempt.test.ts",
"--outputFile",
"extensions/codex/report.json",
"-c",
"./vitest.local.ts",
"-r",
".",
"--exclude=codex/src/app-server/client.test.ts",
"codex/src/app-server/models.test.ts",
"--reporter=dot",
]);
});
posixIt("relativizes single-extension Vitest paths from extension cwd", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-test-extension-args-"));
const fakePnpmPath = path.join(root, "pnpm");
const argsPath = path.join(root, "args.json");
const extensionCwd = path.join(process.cwd(), "extensions", "codex");
writeFakePnpm(fakePnpmPath);
try {
const result = spawnSync(
process.execPath,
[
scriptPath,
"codex",
"--exclude",
path.join(extensionCwd, "src", "app-server", "run-attempt.test.ts"),
path.join(extensionCwd, "src", "app-server", "client.test.ts"),
],
{
cwd: extensionCwd,
encoding: "utf8",
env: {
...process.env,
OPENCLAW_FAKE_PNPM_ARGS_PATH: argsPath,
npm_execpath: fakePnpmPath,
},
},
);
expect(result.status).toBe(0);
expect(JSON.parse(readFileSync(argsPath, "utf8"))).toEqual([
"exec",
"vitest",
"run",
"--config",
"test/vitest/vitest.extension-codex.config.ts",
"--exclude",
"codex/src/app-server/run-attempt.test.ts",
"codex/src/app-server/client.test.ts",
"codex",
]);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
posixIt(
"preserves wrapper termination when the pnpm child exits cleanly after SIGTERM",
async () => {
@ -739,7 +815,8 @@ describe("scripts/test-extension.mjs", () => {
const runParams = requireFirstMockArg<RunGroupParams>(runGroup);
expect(runParams.targets).not.toContain("extensions/codex/src/app-server/run-attempt.test.ts");
expect(runParams.targets).toContain("extensions/codex/src/app-server/client.test.ts");
expect(runParams.targets).not.toContain("codex/src/app-server/run-attempt.test.ts");
expect(runParams.targets).toContain("codex/src/app-server/client.test.ts");
});
it("fails extension batch groups when exact excludes remove every test", async () => {
@ -756,6 +833,20 @@ describe("scripts/test-extension.mjs", () => {
expect(runGroup).not.toHaveBeenCalled();
});
it("fails extension batch groups when dir-relative exact excludes remove every test", async () => {
const runGroup = vi.fn<() => Promise<number>>().mockResolvedValue(0);
const result = await runExtensionBatchPlan(
resolveExtensionBatchPlan({ cwd: process.cwd(), extensionIds: ["firecrawl"] }),
{
runGroup,
vitestArgs: ["--exclude", "firecrawl/src/firecrawl-tools.test.ts"],
},
);
expect(result).toBe(1);
expect(runGroup).not.toHaveBeenCalled();
});
it("allows extension batch groups to opt into empty exact excludes", async () => {
const runGroup = vi.fn<() => Promise<number>>().mockResolvedValue(0);
const result = await runExtensionBatchPlan(
@ -811,6 +902,10 @@ function writeFakePnpm(filePath: string): void {
[
"#!/usr/bin/env node",
'const fs = require("node:fs");',
"if (process.env.OPENCLAW_FAKE_PNPM_ARGS_PATH) {",
" fs.writeFileSync(process.env.OPENCLAW_FAKE_PNPM_ARGS_PATH, JSON.stringify(process.argv.slice(2)));",
" process.exit(0);",
"}",
"fs.writeFileSync(process.env.OPENCLAW_FAKE_PNPM_PID_PATH, String(process.pid));",
'process.on("SIGTERM", () => {',
' fs.writeFileSync(process.env.OPENCLAW_FAKE_PNPM_SIGNALED_PATH, "SIGTERM");',

View file

@ -263,6 +263,78 @@ describe("createScopedVitestConfig", () => {
expect(testConfig.passWithNoTests).toBeUndefined();
});
it("narrows scoped includes to matching dot-prefixed CLI file filters", () => {
const config = createScopedVitestConfig(["extensions/codex/**/*.test.ts"], {
argv: ["node", "vitest", "run", "./extensions/codex/src/app-server/client.test.ts"],
dir: "extensions",
env: {},
});
const testConfig = requireTestConfig(config);
expect(testConfig.include).toEqual(["codex/src/app-server/client.test.ts"]);
expect(testConfig.passWithNoTests).toBeUndefined();
});
it("narrows scoped includes to matching dir-relative CLI file filters", () => {
const config = createScopedVitestConfig(["extensions/codex/**/*.test.ts"], {
argv: ["node", "vitest", "run", "codex/src/app-server/client.test.ts"],
dir: "extensions",
env: {},
});
const testConfig = requireTestConfig(config);
expect(testConfig.include).toEqual(["codex/src/app-server/client.test.ts"]);
expect(testConfig.passWithNoTests).toBeUndefined();
});
it("does not narrow scoped includes for bare Vitest name filters", () => {
const config = createScopedVitestConfig(["extensions/codex/**/*.test.ts"], {
argv: ["node", "vitest", "run", "client"],
dir: "extensions",
env: {},
});
const testConfig = requireTestConfig(config);
expect(testConfig.include).toEqual(["codex/**/*.test.ts"]);
expect(testConfig.passWithNoTests).toBeUndefined();
});
it("does not narrow scoped includes for changed refs", () => {
const config = createScopedVitestConfig(["extensions/codex/**/*.test.ts"], {
argv: ["node", "vitest", "run", "--changed", "origin/main"],
dir: "extensions",
env: {},
});
const testConfig = requireTestConfig(config);
expect(testConfig.include).toEqual(["codex/**/*.test.ts"]);
expect(testConfig.passWithNoTests).toBeUndefined();
});
it("does not narrow scoped includes for coverage option values", () => {
const config = createScopedVitestConfig(["extensions/codex/**/*.test.ts"], {
argv: ["node", "vitest", "run", "--coverage.include", "codex/src/app-server/client.ts"],
dir: "extensions",
env: {},
});
const testConfig = requireTestConfig(config);
expect(testConfig.include).toEqual(["codex/**/*.test.ts"]);
expect(testConfig.passWithNoTests).toBeUndefined();
});
it("does not narrow scoped includes for exclude option values", () => {
const config = createScopedVitestConfig(["extensions/codex/**/*.test.ts"], {
argv: ["node", "vitest", "run", "--exclude", "codex/src/app-server/run-attempt.test.ts"],
dir: "extensions",
env: {},
});
const testConfig = requireTestConfig(config);
expect(testConfig.include).toEqual(["codex/**/*.test.ts"]);
expect(testConfig.passWithNoTests).toBeUndefined();
});
it("lets root Vitest project runs skip scoped files owned by unit-fast", () => {
const config = createScopedVitestConfig(["src/acp/**/*.test.ts"], {
argv: ["node", "vitest", "run", "src/acp/client.test.ts"],

View file

@ -2,6 +2,45 @@
import fs from "node:fs";
import path from "node:path";
const VITEST_OPTION_VALUE_FLAGS = new Set([
"-c",
"-r",
"-t",
"--browser",
"--changed",
"--config",
"--coverage.all",
"--coverage.exclude",
"--coverage.extension",
"--coverage.include",
"--coverage.provider",
"--coverage.reporter",
"--coverage.reportsDirectory",
"--dir",
"--environment",
"--environmentOptions",
"--exclude",
"--hookTimeout",
"--inspect",
"--inspectBrk",
"--maxConcurrency",
"--maxWorkers",
"--minWorkers",
"--mode",
"--name",
"--outputFile",
"--pool",
"--project",
"--reporter",
"--retry",
"--root",
"--sequence",
"--shard",
"--testNamePattern",
"--testTimeout",
"--workspace",
]);
function normalizeCliPattern(value: string): string {
let normalized = value
.trim()
@ -17,6 +56,39 @@ function normalizeCliPattern(value: string): string {
return normalized;
}
function normalizeScopedDir(value: string | undefined): string {
return value?.trim().replaceAll("\\", "/").replace(/\/+$/u, "") ?? "";
}
function hasRepoRootPrefix(value: string): boolean {
return /^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(value);
}
function looksLikeDirRelativePath(value: string): boolean {
return (
value.includes("/") ||
value.includes(".test.") ||
value.includes(".e2e.") ||
value.includes(".live.")
);
}
function applyScopedDir(value: string, scopedDir: string): string {
const normalizedValue = value
.trim()
.replace(/^\.\/+/u, "")
.replaceAll("\\", "/");
if (
!scopedDir ||
hasRepoRootPrefix(normalizedValue) ||
path.isAbsolute(value) ||
!looksLikeDirRelativePath(normalizedValue)
) {
return normalizedValue;
}
return `${scopedDir}/${normalizedValue}`;
}
function looksLikeCliIncludePattern(value: string): boolean {
const normalized = normalizeCliPattern(value);
return (
@ -72,24 +144,13 @@ export function loadPatternListFromEnv(
}
export function loadPatternListFromArgv(argv: string[] = process.argv): string[] | null {
const optionValueFlags = new Set([
"-c",
"-r",
"-t",
"--config",
"--dir",
"--environment",
"--exclude",
"--maxWorkers",
"--mode",
"--outputFile",
"--pool",
"--project",
"--reporter",
"--root",
"--shard",
"--testNamePattern",
]);
return loadPatternListFromArgvForScope(argv);
}
function loadPatternListFromArgvForScope(
argv: string[] = process.argv,
options: { scopedDir?: string } = {},
): string[] | null {
const values: string[] = [];
let skipNext = false;
for (const value of argv.slice(2)) {
@ -100,7 +161,7 @@ export function loadPatternListFromArgv(argv: string[] = process.argv): string[]
if (value === "run" || value === "watch" || value === "bench") {
continue;
}
if (optionValueFlags.has(value)) {
if (VITEST_OPTION_VALUE_FLAGS.has(value)) {
skipNext = true;
continue;
}
@ -110,7 +171,11 @@ export function loadPatternListFromArgv(argv: string[] = process.argv): string[]
values.push(value);
}
const patterns = values.filter(looksLikeCliIncludePattern).map(normalizeCliPattern);
const scopedDir = normalizeScopedDir(options.scopedDir);
const patterns = values
.map((value) => applyScopedDir(value, scopedDir))
.filter(looksLikeCliIncludePattern)
.map(normalizeCliPattern);
return patterns.length > 0 ? [...new Set(patterns)] : null;
}
@ -118,8 +183,9 @@ export function loadPatternListFromArgv(argv: string[] = process.argv): string[]
export function narrowIncludePatternsForCli(
includePatterns: string[],
argv: string[] = process.argv,
options: { scopedDir?: string } = {},
): string[] | null {
const cliPatterns = loadPatternListFromArgv(argv);
const cliPatterns = loadPatternListFromArgvForScope(argv, options);
if (!cliPatterns) {
return null;
}

View file

@ -219,7 +219,9 @@ export function createScopedVitestConfig(
const resolvedScopedDir = scopedDir ? path.join(repoRoot, scopedDir) : undefined;
const env = options?.env;
const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
const cliInclude = narrowIncludePatternsForCli(include, options?.argv);
const cliInclude = narrowIncludePatternsForCli(include, options?.argv, {
scopedDir,
});
const unitFastExcludePatterns =
options?.excludeUnitFastTests === false ? [] : getUnitFastTestFiles();
const exclude = relativizeScopedPatterns(