diff --git a/docs/help/testing.md b/docs/help/testing.md index 4c30e0a95e..840b6e25a3 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -24,7 +24,7 @@ Most days: - Full gate (expected before push): `pnpm build && pnpm check && pnpm test` - Faster local full-suite run on a roomy machine: `pnpm test:max` -- Direct Vitest watch loop (modern projects config): `pnpm test:watch` +- Direct Vitest watch loop: `pnpm test:watch` - Direct file targeting now routes extension/channel paths too: `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` When you touch tests or want extra confidence: @@ -57,8 +57,9 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost): - No real keys required - Should be fast and stable - Projects note: - - `pnpm test`, `pnpm test:watch`, and `pnpm test:changed` all use the same native Vitest root `projects` config now. - - Direct file filters route natively through the root project graph, so `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` works without a custom wrapper. + - Untargeted `pnpm test` still uses the native Vitest root `projects` config. + - `pnpm test`, `pnpm test:watch`, and `pnpm test:perf:imports` route explicit file/directory targets through scoped lanes first, so `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` avoids paying the full root project startup tax. + - `pnpm test:changed` expands changed git paths into the same scoped lanes when the diff only touches routable source/test files; config/setup edits still fall back to the broad root-project rerun. - Embedded runner note: - When you change message-tool discovery inputs or compaction runtime context, keep both levels of coverage. @@ -77,8 +78,8 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost): - `pnpm test` inherits the same `threads` + `isolate: false` defaults from the root `vitest.config.ts` projects config. - The shared `scripts/run-vitest.mjs` launcher now also adds `--no-maglev` for Vitest child Node processes by default to reduce V8 compile churn during big local runs. Set `OPENCLAW_VITEST_ENABLE_MAGLEV=1` if you need to compare against stock V8 behavior. - Fast-local iteration note: - - `pnpm test:changed` runs the native projects config with `--changed origin/main`. - - `pnpm test:max` and `pnpm test:changed:max` keep the same native projects config, just with a higher worker cap. + - `pnpm test:changed` routes through scoped lanes when the changed paths map cleanly to a smaller suite. + - `pnpm test:max` and `pnpm test:changed:max` keep the same routing behavior, just with a higher worker cap. - Local worker auto-scaling is intentionally conservative now and also backs off when the host load average is already high, so multiple concurrent Vitest runs do less damage by default. - The base Vitest config marks the projects/config files as `forceRerunTriggers` so changed-mode reruns stay correct when test wiring changes. - The config keeps `OPENCLAW_VITEST_FS_MODULE_CACHE` enabled on supported hosts; set `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH=/abs/path` if you want one explicit cache location for direct profiling. diff --git a/docs/reference/test.md b/docs/reference/test.md index 9a56b84502..0407abc2ba 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -12,13 +12,13 @@ title: "Tests" - `pnpm test:force`: Kills any lingering gateway process holding the default control port, then runs the full Vitest suite with an isolated gateway port so server tests don’t collide with a running instance. Use this when a prior gateway run left port 18789 occupied. - `pnpm test:coverage`: Runs the unit suite with V8 coverage (via `vitest.unit.config.ts`). Global thresholds are 70% lines/branches/functions/statements. Coverage excludes integration-heavy entrypoints (CLI wiring, gateway/telegram bridges, webchat static server) to keep the target focused on unit-testable logic. - `pnpm test:coverage:changed`: Runs unit coverage only for files changed since `origin/main`. -- `pnpm test:changed`: runs the native Vitest projects config with `--changed origin/main`. The base config treats the projects/config files as `forceRerunTriggers` so wiring changes still rerun broadly when needed. -- `pnpm test`: runs the native Vitest root projects config directly. File filters work natively across the configured projects. +- `pnpm test:changed`: expands changed git paths into scoped Vitest lanes when the diff only touches routable source/test files. Config/setup changes still fall back to the native root projects run so wiring edits rerun broadly when needed. +- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes, but still falls back to the native root projects run when you do a full untargeted sweep. - Base Vitest config now defaults to `pool: "threads"` and `isolate: false`, with the shared non-isolated runner enabled across the repo configs. - `pnpm test:channels` runs `vitest.channels.config.ts`. - `pnpm test:extensions` runs `vitest.extensions.config.ts`. - `pnpm test:extensions`: runs extension/plugin suites. -- `pnpm test:perf:imports`: enables Vitest import-duration + import-breakdown reporting for the native root projects run. +- `pnpm test:perf:imports`: enables Vitest import-duration + import-breakdown reporting, while still using scoped lane routing for explicit file/directory targets. - `pnpm test:perf:imports:changed`: same import profiling, but only for files changed since `origin/main`. - `pnpm test:perf:profile:main`: writes a CPU profile for the Vitest main thread (`.artifacts/vitest-main-profile`). - `pnpm test:perf:profile:runner`: writes CPU + heap profiles for the unit runner (`.artifacts/vitest-runner-profile`). diff --git a/package.json b/package.json index ad7571582b..32d59e053e 100644 --- a/package.json +++ b/package.json @@ -1120,13 +1120,13 @@ "runtime-sidecars:gen": "node --import tsx scripts/generate-runtime-sidecar-paths-baseline.ts --write", "stage:bundled-plugin-runtime-deps": "node scripts/stage-bundled-plugin-runtime-deps.mjs", "start": "node scripts/run-node.mjs", - "test": "node scripts/run-vitest.mjs run --config vitest.config.ts", + "test": "node scripts/test-projects.mjs", "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all", "test:auth:compat": "node scripts/run-vitest.mjs run --config vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts", "test:build:singleton": "node scripts/test-built-plugin-singleton.mjs", "test:bundled": "node scripts/run-vitest.mjs run --config vitest.bundled.config.ts", - "test:changed": "node scripts/run-vitest.mjs run --config vitest.config.ts --changed origin/main", - "test:changed:max": "OPENCLAW_VITEST_MAX_WORKERS=8 node scripts/run-vitest.mjs run --config vitest.config.ts --changed origin/main", + "test:changed": "node scripts/test-projects.mjs --changed origin/main", + "test:changed:max": "OPENCLAW_VITEST_MAX_WORKERS=8 node scripts/test-projects.mjs --changed origin/main", "test:channels": "node scripts/run-vitest.mjs run --config vitest.channels.config.ts", "test:contracts": "pnpm test:contracts:channels && pnpm test:contracts:plugins", "test:contracts:channels": "node scripts/run-vitest.mjs run --config vitest.contracts.config.ts --maxWorkers=1 src/channels/plugins/contracts", @@ -1165,15 +1165,15 @@ "test:live:cache": "bun scripts/check-live-cache.ts", "test:live:gateway-profiles": "node scripts/test-live.mjs -- src/gateway/gateway-models.profiles.live.test.ts", "test:live:models-profiles": "node scripts/test-live.mjs -- src/agents/models.profiles.live.test.ts", - "test:max": "OPENCLAW_VITEST_MAX_WORKERS=8 node scripts/run-vitest.mjs run --config vitest.config.ts", + "test:max": "OPENCLAW_VITEST_MAX_WORKERS=8 node scripts/test-projects.mjs", "test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh", "test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh", "test:parallels:npm-update": "bash scripts/e2e/parallels-npm-update-smoke.sh", "test:parallels:windows": "bash scripts/e2e/parallels-windows-smoke.sh", "test:perf:budget": "node scripts/test-perf-budget.mjs", "test:perf:hotspots": "node scripts/test-hotspots.mjs", - "test:perf:imports": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 node scripts/run-vitest.mjs run --config vitest.config.ts", - "test:perf:imports:changed": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 node scripts/run-vitest.mjs run --config vitest.config.ts --changed origin/main", + "test:perf:imports": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 node scripts/test-projects.mjs", + "test:perf:imports:changed": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 node scripts/test-projects.mjs --changed origin/main", "test:perf:profile:main": "node scripts/run-vitest-profile.mjs main", "test:perf:profile:runner": "node scripts/run-vitest-profile.mjs runner", "test:sectriage": "node scripts/run-vitest.mjs run --config vitest.gateway.config.ts && node scripts/run-vitest.mjs run --config vitest.unit.config.ts --exclude src/daemon/launchd.integration.test.ts --exclude src/process/exec.test.ts", @@ -1186,7 +1186,7 @@ "test:startup:memory": "node scripts/check-cli-startup-memory.mjs", "test:ui": "pnpm ui:i18n:check && pnpm lint:ui:no-raw-window-open && pnpm --dir ui test", "test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs", - "test:watch": "node scripts/run-vitest.mjs --config vitest.config.ts", + "test:watch": "node scripts/test-projects.mjs --watch", "tool-display:check": "node --import tsx scripts/tool-display.ts --check", "tool-display:write": "node --import tsx scripts/tool-display.ts --write", "ts-topology": "node --import tsx scripts/ts-topology.ts", diff --git a/scripts/test-projects.mjs b/scripts/test-projects.mjs index 55f00d3edf..e0ea03de96 100644 --- a/scripts/test-projects.mjs +++ b/scripts/test-projects.mjs @@ -1,9 +1,11 @@ import fs from "node:fs"; import { acquireLocalHeavyCheckLockSync } from "./lib/local-heavy-check-runtime.mjs"; import { spawnPnpmRunner } from "./pnpm-runner.mjs"; +import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs"; import { createVitestRunSpecs, parseTestProjectsArgs, + resolveChangedTargetArgs, writeVitestIncludeFile, } from "./test-projects.test-support.mjs"; @@ -66,7 +68,9 @@ function createRootVitestRunSpec(args) { includePatterns: null, pnpmArgs: [ "exec", - "vitest", + "node", + ...resolveVitestNodeArgs(process.env), + resolveVitestCliEntry(), ...(watchMode ? [] : ["run"]), "--config", "vitest.config.ts", @@ -79,8 +83,10 @@ function createRootVitestRunSpec(args) { async function main() { const args = process.argv.slice(2); const { targetArgs } = parseTestProjectsArgs(args, process.cwd()); + const changedTargetArgs = + targetArgs.length === 0 ? resolveChangedTargetArgs(args, process.cwd()) : null; const runSpecs = - targetArgs.length === 0 + targetArgs.length === 0 && changedTargetArgs === null ? [createRootVitestRunSpec(args)] : createVitestRunSpecs(args, { baseEnv: process.env, diff --git a/scripts/test-projects.test-support.d.mts b/scripts/test-projects.test-support.d.mts index b4a7a7a124..d744597473 100644 --- a/scripts/test-projects.test-support.d.mts +++ b/scripts/test-projects.test-support.d.mts @@ -23,7 +23,17 @@ export function parseTestProjectsArgs( watchMode: boolean; }; -export function buildVitestRunPlans(args: string[], cwd?: string): VitestRunPlan[]; +export function buildVitestRunPlans( + args: string[], + cwd?: string, + listChangedPaths?: (baseRef: string, cwd: string) => string[], +): VitestRunPlan[]; + +export function resolveChangedTargetArgs( + args: string[], + cwd?: string, + listChangedPaths?: (baseRef: string, cwd: string) => string[], +): string[] | null; export function createVitestRunSpecs( args: string[], diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index bc569ba8bd..b75eac2f3c 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -18,6 +19,7 @@ import { isVoiceCallExtensionRoot } from "../vitest.extension-voice-call-paths.m import { isWhatsAppExtensionRoot } from "../vitest.extension-whatsapp-paths.mjs"; import { isZaloExtensionRoot } from "../vitest.extension-zalo-paths.mjs"; import { isBoundaryTestFile, isBundledPluginDependentUnitTestFile } from "../vitest.unit-paths.mjs"; +import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs"; const DEFAULT_VITEST_CONFIG = "vitest.unit.config.ts"; const AGENTS_VITEST_CONFIG = "vitest.agents.config.ts"; @@ -68,6 +70,15 @@ const UI_VITEST_CONFIG = "vitest.ui.config.ts"; const UTILS_VITEST_CONFIG = "vitest.utils.config.ts"; const WIZARD_VITEST_CONFIG = "vitest.wizard.config.ts"; const INCLUDE_FILE_ENV_KEY = "OPENCLAW_VITEST_INCLUDE_FILE"; +const CHANGED_ARGS_PATTERN = /^--changed(?:=(.+))?$/u; +const BROAD_CHANGED_RERUN_PATTERNS = [ + /^package\.json$/u, + /^pnpm-lock\.yaml$/u, + /^test\/setup(?:\.shared|\.extensions|-openclaw-runtime)?\.ts$/u, + /^vitest(?:\..+)?\.(?:config\.ts|paths\.mjs)$/u, + /^scripts\/run-vitest\.mjs$/u, + /^scripts\/test-projects(?:\.test-support)?\.mjs$/u, +]; function normalizePathPattern(value) { return value.replaceAll("\\", "/"); @@ -93,6 +104,10 @@ function isFileLikeTarget(arg) { return /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(arg); } +function isLikelyFileTarget(arg) { + return /(?:^|\/)[^/]+\.[A-Za-z0-9]+$/u.test(arg); +} + function isPathLikeTargetArg(arg, cwd) { if (!arg || arg === "--" || arg.startsWith("-")) { return false; @@ -113,13 +128,86 @@ function toScopedIncludePattern(arg, cwd) { if (isGlobTarget(relative) || isFileLikeTarget(relative)) { return relative; } - if (isExistingFileTarget(arg, cwd)) { + if (isExistingFileTarget(arg, cwd) || isLikelyFileTarget(relative)) { const directory = normalizePathPattern(path.posix.dirname(relative)); return directory === "." ? "**/*.test.ts" : `${directory}/**/*.test.ts`; } return `${relative.replace(/\/+$/u, "")}/**/*.test.ts`; } +function listChangedPathsFromGit(baseRef, cwd) { + return execFileSync("git", ["diff", "--name-only", `${baseRef}...HEAD`], { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + .split("\n") + .map((line) => normalizePathPattern(line.trim())) + .filter((line) => line.length > 0); +} + +function extractChangedBaseRef(args) { + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + const match = arg.match(CHANGED_ARGS_PATTERN); + if (!match) { + continue; + } + if (match[1]) { + return match[1]; + } + const nextArg = args[index + 1]; + return nextArg && nextArg !== "--" && !nextArg.startsWith("-") ? nextArg : "HEAD"; + } + return null; +} + +function stripChangedArgs(args) { + const strippedArgs = []; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + const match = arg.match(CHANGED_ARGS_PATTERN); + if (!match) { + strippedArgs.push(arg); + continue; + } + if (!match[1]) { + const nextArg = args[index + 1]; + if (nextArg && nextArg !== "--" && !nextArg.startsWith("-")) { + index += 1; + } + } + } + return strippedArgs; +} + +function shouldKeepBroadChangedRun(changedPaths) { + return changedPaths.some((changedPath) => + BROAD_CHANGED_RERUN_PATTERNS.some((pattern) => pattern.test(changedPath)), + ); +} + +function isRoutableChangedTarget(changedPath) { + return /^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(changedPath); +} + +export function resolveChangedTargetArgs( + args, + cwd = process.cwd(), + listChangedPaths = listChangedPathsFromGit, +) { + const baseRef = extractChangedBaseRef(args); + if (!baseRef) { + return null; + } + const changedPaths = listChangedPaths(baseRef, cwd); + if (changedPaths.length === 0 || shouldKeepBroadChangedRun(changedPaths)) { + return null; + } + const routablePaths = changedPaths.filter(isRoutableChangedTarget); + return routablePaths.length > 0 ? [...new Set(routablePaths)] : null; +} + function classifyTarget(arg, cwd) { const relative = toRepoRelativeTarget(arg, cwd); if (relative.endsWith(".e2e.test.ts")) { @@ -278,7 +366,9 @@ function classifyTarget(arg, cwd) { function createVitestArgs(params) { return [ "exec", - "vitest", + "node", + ...resolveVitestNodeArgs(params.env), + resolveVitestCliEntry(), ...(params.watchMode ? [] : ["run"]), "--config", params.config, @@ -308,13 +398,21 @@ export function parseTestProjectsArgs(args, cwd = process.cwd()) { return { forwardedArgs, targetArgs, watchMode }; } -export function buildVitestRunPlans(args, cwd = process.cwd()) { +export function buildVitestRunPlans( + args, + cwd = process.cwd(), + listChangedPaths = listChangedPathsFromGit, +) { const { forwardedArgs, targetArgs, watchMode } = parseTestProjectsArgs(args, cwd); - if (targetArgs.length === 0) { + const changedTargetArgs = + targetArgs.length === 0 ? resolveChangedTargetArgs(args, cwd, listChangedPaths) : null; + const activeTargetArgs = changedTargetArgs ?? targetArgs; + const activeForwardedArgs = changedTargetArgs ? stripChangedArgs(forwardedArgs) : forwardedArgs; + if (activeTargetArgs.length === 0) { return [ { config: DEFAULT_VITEST_CONFIG, - forwardedArgs, + forwardedArgs: activeForwardedArgs, includePatterns: null, watchMode, }, @@ -322,7 +420,7 @@ export function buildVitestRunPlans(args, cwd = process.cwd()) { } const groupedTargets = new Map(); - for (const targetArg of targetArgs) { + for (const targetArg of activeTargetArgs) { const kind = classifyTarget(targetArg, cwd); const current = groupedTargets.get(kind) ?? []; current.push(targetArg); @@ -335,7 +433,7 @@ export function buildVitestRunPlans(args, cwd = process.cwd()) { ); } - const nonTargetArgs = forwardedArgs.filter((arg) => !targetArgs.includes(arg)); + const nonTargetArgs = activeForwardedArgs.filter((arg) => !activeTargetArgs.includes(arg)); const orderedKinds = [ "default", "boundary", @@ -502,11 +600,14 @@ export function buildVitestRunPlans(args, cwd = process.cwd()) { "extension" ? EXTENSIONS_VITEST_CONFIG : DEFAULT_VITEST_CONFIG; - const includePatterns = - kind === "default" || kind === "e2e" - ? null - : grouped.map((targetArg) => toScopedIncludePattern(targetArg, cwd)); - const scopedTargetArgs = kind === "default" || kind === "e2e" ? grouped : []; + const useCliTargetArgs = + kind === "e2e" || + (kind === "default" && + grouped.every((targetArg) => isFileLikeTarget(toRepoRelativeTarget(targetArg, cwd)))); + const includePatterns = useCliTargetArgs + ? null + : grouped.map((targetArg) => toScopedIncludePattern(targetArg, cwd)); + const scopedTargetArgs = useCliTargetArgs ? grouped : []; plans.push({ config, forwardedArgs: [...nonTargetArgs, ...scopedTargetArgs], diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts new file mode 100644 index 0000000000..17909e4c64 --- /dev/null +++ b/test/scripts/test-projects.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { + buildVitestRunPlans, + resolveChangedTargetArgs, +} from "../../scripts/test-projects.test-support.mjs"; + +describe("scripts/test-projects changed-target routing", () => { + it("maps changed source files into scoped lane targets", () => { + expect( + resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [ + "src/shared/string-normalization.ts", + "src/utils/provider-utils.ts", + ]), + ).toEqual(["src/shared/string-normalization.ts", "src/utils/provider-utils.ts"]); + }); + + it("keeps the broad changed run for Vitest wiring edits", () => { + expect( + resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [ + "vitest.shared.config.ts", + "src/utils/provider-utils.ts", + ]), + ).toBeNull(); + }); + + it("ignores changed files that cannot map to test lanes", () => { + expect( + resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [ + "docs/help/testing.md", + ]), + ).toBeNull(); + }); + + it("narrows default-lane changed source files to include globs", () => { + const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [ + "packages/sdk/src/index.ts", + ]); + + expect(plans).toEqual([ + { + config: "vitest.unit.config.ts", + forwardedArgs: [], + includePatterns: ["packages/sdk/src/**/*.test.ts"], + watchMode: false, + }, + ]); + }); + + it("routes changed utils and shared files to their light scoped lanes", () => { + const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [ + "src/shared/string-normalization.ts", + "src/utils/provider-utils.ts", + ]); + + expect(plans).toEqual([ + { + config: "vitest.shared-core.config.ts", + forwardedArgs: [], + includePatterns: ["src/shared/**/*.test.ts"], + watchMode: false, + }, + { + config: "vitest.utils.config.ts", + forwardedArgs: [], + includePatterns: ["src/utils/**/*.test.ts"], + watchMode: false, + }, + ]); + }); +}); diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index 984604fa82..6d7a4a0dd1 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -101,6 +101,25 @@ describe("createScopedVitestConfig", () => { expect(config.test?.passWithNoTests).toBe(true); }); + it("loads scoped include overrides from OPENCLAW_VITEST_INCLUDE_FILE", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-vitest-scoped-")); + try { + const includeFile = path.join(tempDir, "include.json"); + fs.writeFileSync(includeFile, JSON.stringify(["src/utils/utils-misc.test.ts"]), "utf8"); + + const config = createScopedVitestConfig(["src/utils/**/*.test.ts"], { + dir: "src", + env: { + OPENCLAW_VITEST_INCLUDE_FILE: includeFile, + }, + }); + + expect(config.test?.include).toEqual(["utils/utils-misc.test.ts"]); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + it("overrides setup files when a scoped config requests them", () => { const config = createScopedVitestConfig(["src/example.test.ts"], { env: {}, @@ -508,6 +527,7 @@ describe("scoped vitest configs", () => { it("normalizes shared-core include patterns relative to the scoped dir", () => { expect(defaultSharedCoreConfig.test?.dir).toBe("src"); expect(defaultSharedCoreConfig.test?.include).toEqual(["shared/**/*.test.ts"]); + expect(defaultSharedCoreConfig.test?.setupFiles).toEqual(["test/setup.ts"]); }); it("normalizes process include patterns relative to the scoped dir", () => { @@ -585,5 +605,6 @@ describe("scoped vitest configs", () => { it("normalizes utils include patterns relative to the scoped dir", () => { expect(defaultUtilsConfig.test?.dir).toBe("src"); expect(defaultUtilsConfig.test?.include).toEqual(["utils/**/*.test.ts"]); + expect(defaultUtilsConfig.test?.setupFiles).toEqual(["test/setup.ts"]); }); }); diff --git a/vitest.scoped-config.ts b/vitest.scoped-config.ts index 85253cef2d..463a8cec35 100644 --- a/vitest.scoped-config.ts +++ b/vitest.scoped-config.ts @@ -1,5 +1,5 @@ import { defineConfig } from "vitest/config"; -import { narrowIncludePatternsForCli } from "./vitest.pattern-file.ts"; +import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts"; import { sharedVitestConfig } from "./vitest.shared.config.ts"; function normalizePathPattern(value: string): string { @@ -55,6 +55,8 @@ export function createScopedVitestConfig( const base = sharedVitestConfig as Record; const baseTest = sharedVitestConfig.test ?? {}; const scopedDir = options?.dir; + const env = options?.env; + const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env); const cliInclude = narrowIncludePatternsForCli(include, options?.argv); const exclude = relativizeScopedPatterns( [...(baseTest.exclude ?? []), ...(options?.exclude ?? [])], @@ -82,7 +84,7 @@ export function createScopedVitestConfig( ...(runner ? { runner } : { runner: undefined }), setupFiles, ...(scopedDir ? { dir: scopedDir } : {}), - include: relativizeScopedPatterns(cliInclude ?? include, scopedDir), + include: relativizeScopedPatterns(includeFromEnv ?? cliInclude ?? include, scopedDir), exclude, ...(options?.pool ? { pool: options.pool } : {}), ...(options?.passWithNoTests !== undefined || cliInclude !== null diff --git a/vitest.shared-core.config.ts b/vitest.shared-core.config.ts index 945b171a26..84afa7a450 100644 --- a/vitest.shared-core.config.ts +++ b/vitest.shared-core.config.ts @@ -4,6 +4,7 @@ export function createSharedCoreVitestConfig(env?: Record return createScopedVitestConfig(["src/utils/**/*.test.ts"], { dir: "src", env, + includeOpenClawRuntimeSetup: false, name: "utils", passWithNoTests: true, });