diff --git a/scripts/lib/extension-test-plan.mjs b/scripts/lib/extension-test-plan.mjs index fc0e099bec3..059a9f4385e 100644 --- a/scripts/lib/extension-test-plan.mjs +++ b/scripts/lib/extension-test-plan.mjs @@ -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, diff --git a/scripts/lib/extension-vitest-paths.mjs b/scripts/lib/extension-vitest-paths.mjs new file mode 100644 index 00000000000..f638333be53 --- /dev/null +++ b/scripts/lib/extension-vitest-paths.mjs @@ -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; +} diff --git a/scripts/test-extension-batch.mjs b/scripts/test-extension-batch.mjs index 90553be8a07..0848d3c8abf 100644 --- a/scripts/test-extension-batch.mjs +++ b/scripts/test-extension-batch.mjs @@ -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)), }); } diff --git a/scripts/test-extension.mjs b/scripts/test-extension.mjs index df870372011..a9497bcc8f9 100644 --- a/scripts/test-extension.mjs +++ b/scripts/test-extension.mjs @@ -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); } diff --git a/test/scripts/test-extension.test.ts b/test/scripts/test-extension.test.ts index 47db0fb62de..30f389975cb 100644 --- a/test/scripts/test-extension.test.ts +++ b/test/scripts/test-extension.test.ts @@ -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(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>().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>().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");', diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index e013fa2495f..67df2ca052e 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -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"], diff --git a/test/vitest/vitest.pattern-file.ts b/test/vitest/vitest.pattern-file.ts index cab6159362a..decafe881a1 100644 --- a/test/vitest/vitest.pattern-file.ts +++ b/test/vitest/vitest.pattern-file.ts @@ -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; } diff --git a/test/vitest/vitest.scoped-config.ts b/test/vitest/vitest.scoped-config.ts index ad5d4fdb468..85e9cdfe308 100644 --- a/test/vitest/vitest.scoped-config.ts +++ b/test/vitest/vitest.scoped-config.ts @@ -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(