重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。

同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 08:34:33 +08:00
parent 4a23b715a2
commit dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions

View file

@ -0,0 +1,11 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createAcpVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/acp/**/*.test.ts"], {
dir: "src/acp",
env,
name: "acp",
});
}
export default createAcpVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createAgentsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/agents/**/*.test.ts"], {
dir: "src/agents",
env,
fileParallelism: false,
name: "agents",
});
}
export default createAgentsVitestConfig();

View file

@ -0,0 +1,13 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { autoReplyCoreTestExclude, autoReplyCoreTestInclude } from "./vitest.test-shards.mjs";
export function createAutoReplyCoreVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig([...autoReplyCoreTestInclude], {
dir: "src/auto-reply",
env,
exclude: [...autoReplyCoreTestExclude],
name: "auto-reply-core",
});
}
export default createAutoReplyCoreVitestConfig();

View file

@ -0,0 +1,17 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { autoReplyReplySubtreeTestInclude } from "./vitest.test-shards.mjs";
export function createAutoReplyReplyVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig([...autoReplyReplySubtreeTestInclude], {
dir: "src/auto-reply",
env,
fileParallelism: false,
maxWorkers: 1,
name: "auto-reply-reply",
sequence: {
groupOrder: 1,
},
});
}
export default createAutoReplyReplyVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { autoReplyTopLevelReplyTestInclude } from "./vitest.test-shards.mjs";
export function createAutoReplyTopLevelVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig([...autoReplyTopLevelReplyTestInclude], {
dir: "src/auto-reply",
env,
name: "auto-reply-top-level",
});
}
export default createAutoReplyTopLevelVitestConfig();

View file

@ -0,0 +1,11 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createAutoReplyVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/auto-reply/**/*.test.ts"], {
dir: "src/auto-reply",
env,
name: "auto-reply",
});
}
export default createAutoReplyVitestConfig();

View file

@ -0,0 +1,35 @@
import { defineProject } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import { resolveVitestIsolation } from "./vitest.scoped-config.ts";
import { nonIsolatedRunnerPath, sharedVitestConfig } from "./vitest.shared.config.ts";
import { boundaryTestFiles } from "./vitest.unit-paths.mjs";
export function loadBoundaryIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createBoundaryVitestConfig(
env: Record<string, string | undefined> = process.env,
argv: string[] = process.argv,
) {
const cliIncludePatterns = narrowIncludePatternsForCli(boundaryTestFiles, argv);
const isolate = resolveVitestIsolation(env);
return defineProject({
...sharedVitestConfig,
test: {
...sharedVitestConfig.test,
name: "boundary",
isolate,
...(isolate ? { runner: undefined } : { runner: nonIsolatedRunnerPath }),
include: loadBoundaryIncludePatternsFromEnv(env) ?? cliIncludePatterns ?? boundaryTestFiles,
...(cliIncludePatterns !== null ? { passWithNoTests: true } : {}),
// Boundary workers still need the shared isolated HOME/bootstrap. Only
// per-file module isolation is disabled here.
setupFiles: sharedVitestConfig.test.setupFiles,
},
});
}
export default createBoundaryVitestConfig();

View file

@ -0,0 +1,5 @@
export const BUNDLED_PLUGIN_ROOT_DIR = "extensions";
export const BUNDLED_PLUGIN_PATH_PREFIX = `${BUNDLED_PLUGIN_ROOT_DIR}/`;
export const BUNDLED_PLUGIN_TEST_GLOB = `${BUNDLED_PLUGIN_ROOT_DIR}/**/*.test.ts`;
export const BUNDLED_PLUGIN_E2E_TEST_GLOB = `${BUNDLED_PLUGIN_ROOT_DIR}/**/*.e2e.test.ts`;
export const BUNDLED_PLUGIN_LIVE_TEST_GLOB = `${BUNDLED_PLUGIN_ROOT_DIR}/**/*.live.test.ts`;

View file

@ -0,0 +1,36 @@
import path from "node:path";
import {
bundledPluginDependentUnitTestFiles,
unitTestAdditionalExcludePatterns,
} from "./vitest.unit-paths.mjs";
import { createUnitVitestConfigWithOptions } from "./vitest.unit.config.ts";
function normalizeGlobCandidate(value: string): string {
return value.split(path.sep).join("/");
}
function excludePatternCouldMatchFile(pattern: string, file: string): boolean {
const normalizedPattern = normalizeGlobCandidate(pattern);
const normalizedFile = normalizeGlobCandidate(file);
if (normalizedPattern === normalizedFile) {
return true;
}
if (normalizedPattern.endsWith("/**")) {
const prefix = normalizedPattern.slice(0, -3);
return normalizedFile === prefix || normalizedFile.startsWith(`${prefix}/`);
}
return path.matchesGlob(normalizedFile, normalizedPattern);
}
const bundledUnitExcludePatterns = unitTestAdditionalExcludePatterns.filter(
(pattern) =>
!bundledPluginDependentUnitTestFiles.some((file) =>
excludePatternCouldMatchFile(pattern, file),
),
);
export default createUnitVitestConfigWithOptions(process.env, {
includePatterns: bundledPluginDependentUnitTestFiles,
extraExcludePatterns: bundledUnitExcludePatterns,
name: "bundled",
});

View file

@ -0,0 +1,75 @@
import path from "node:path";
import {
BUNDLED_PLUGIN_PATH_PREFIX,
bundledPluginRoot,
} from "../../scripts/lib/bundled-plugin-paths.mjs";
const normalizeRepoPath = (value) => value.split(path.sep).join("/");
export const extensionRoutedChannelTestFiles = [];
const extensionRoutedChannelTestFileSet = new Set(extensionRoutedChannelTestFiles);
export const channelTestRoots = [
"src/channels",
bundledPluginRoot("discord"),
bundledPluginRoot("slack"),
bundledPluginRoot("signal"),
bundledPluginRoot("imessage"),
bundledPluginRoot("line"),
];
export const extensionChannelTestRoots = channelTestRoots.filter((root) =>
root.startsWith(BUNDLED_PLUGIN_PATH_PREFIX),
);
export const coreChannelTestRoots = channelTestRoots.filter(
(root) => !root.startsWith(BUNDLED_PLUGIN_PATH_PREFIX),
);
export const channelTestPrefixes = channelTestRoots.map((root) => `${root}/`);
export const channelTestInclude = channelTestRoots.map((root) => `${root}/**/*.test.ts`);
export const extensionChannelTestInclude = extensionChannelTestRoots.map(
(root) => `${root}/**/*.test.ts`,
);
export const coreChannelTestInclude = coreChannelTestRoots.map((root) => `${root}/**/*.test.ts`);
export const channelTestExclude = channelTestRoots.map((root) => `${root}/**`);
const extensionChannelRootOverrideBasenames = new Map();
for (const file of extensionRoutedChannelTestFiles) {
if (!file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
continue;
}
const relativeFile = file.slice(BUNDLED_PLUGIN_PATH_PREFIX.length);
const separator = relativeFile.indexOf("/");
if (separator === -1) {
continue;
}
const root = relativeFile.slice(0, separator);
const baseName = path.basename(relativeFile, ".test.ts");
const current = extensionChannelRootOverrideBasenames.get(root) ?? [];
current.push(baseName);
extensionChannelRootOverrideBasenames.set(root, current);
}
export const extensionExcludedChannelTestGlobs = channelTestRoots
.filter((root) => root.startsWith(BUNDLED_PLUGIN_PATH_PREFIX))
.map((root) => root.slice(BUNDLED_PLUGIN_PATH_PREFIX.length))
.map((relativeRoot) => {
const allowedBasenames = extensionChannelRootOverrideBasenames.get(relativeRoot) ?? [];
if (allowedBasenames.length === 0) {
return `${relativeRoot}/**`;
}
const alternation = allowedBasenames.join("|");
return `${relativeRoot}/**/!(${alternation}).test.ts`;
});
export const extensionChannelOverrideExcludeGlobs = extensionRoutedChannelTestFiles
.filter((file) => file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX))
.map((file) => file.slice(BUNDLED_PLUGIN_PATH_PREFIX.length));
export function isChannelSurfaceTestFile(filePath) {
const normalizedFile = normalizeRepoPath(filePath);
return (
channelTestPrefixes.some((prefix) => normalizedFile.startsWith(prefix)) &&
!extensionRoutedChannelTestFileSet.has(normalizedFile)
);
}

View file

@ -0,0 +1,20 @@
import { coreChannelTestInclude } from "./vitest.channel-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createChannelsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(loadIncludePatternsFromEnv(env) ?? coreChannelTestInclude, {
env,
exclude: ["src/gateway/**", "src/channels/plugins/contracts/**"],
name: "channels",
passWithNoTests: true,
});
}
export default createChannelsVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createCliVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/cli/**/*.test.ts"], {
dir: "src/cli",
env,
name: "cli",
passWithNoTests: true,
});
}
export default createCliVitestConfig();

View file

@ -0,0 +1,69 @@
const normalizeRepoPath = (value) => value.replaceAll("\\", "/");
const commandsLightEntries = [
{ source: "src/commands/cleanup-utils.ts", test: "src/commands/cleanup-utils.test.ts" },
{
source: "src/commands/dashboard.links.ts",
test: "src/commands/dashboard.links.test.ts",
},
{ source: "src/commands/doctor-browser.ts", test: "src/commands/doctor-browser.test.ts" },
{
source: "src/commands/doctor-gateway-auth-token.ts",
test: "src/commands/doctor-gateway-auth-token.test.ts",
},
{
source: "src/commands/gateway-status/helpers.ts",
test: "src/commands/gateway-status/helpers.test.ts",
},
{
source: "src/commands/sandbox-formatters.ts",
test: "src/commands/sandbox-formatters.test.ts",
},
{
source: "src/commands/status-json-command.ts",
test: "src/commands/status-json-command.test.ts",
},
{
source: "src/commands/status-json-payload.ts",
test: "src/commands/status-json-payload.test.ts",
},
{
source: "src/commands/status-json-runtime.ts",
test: "src/commands/status-json-runtime.test.ts",
},
{
source: "src/commands/status-overview-rows.ts",
test: "src/commands/status-overview-rows.test.ts",
},
{
source: "src/commands/status-overview-surface.ts",
test: "src/commands/status-overview-surface.test.ts",
},
{
source: "src/commands/status-overview-values.ts",
test: "src/commands/status-overview-values.test.ts",
},
{ source: "src/commands/text-format.ts", test: "src/commands/text-format.test.ts" },
];
const commandsLightIncludePatternByFile = new Map(
commandsLightEntries.flatMap(({ source, test }) => [
[source, test],
[test, test],
]),
);
export const commandsLightSourceFiles = commandsLightEntries.map(({ source }) => source);
export const commandsLightTestFiles = commandsLightEntries.map(({ test }) => test);
export function isCommandsLightTarget(file) {
return commandsLightIncludePatternByFile.has(normalizeRepoPath(file));
}
export function isCommandsLightTestFile(file) {
return commandsLightTestFiles.includes(normalizeRepoPath(file));
}
export function resolveCommandsLightIncludePattern(file) {
return commandsLightIncludePatternByFile.get(normalizeRepoPath(file)) ?? null;
}

View file

@ -0,0 +1,16 @@
import { commandsLightTestFiles } from "./vitest.commands-light-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createCommandsLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(commandsLightTestFiles, {
dir: "src/commands",
env,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "commands-light",
passWithNoTests: true,
});
}
export default createCommandsLightVitestConfig();

View file

@ -0,0 +1,13 @@
import { commandsLightTestFiles } from "./vitest.commands-light-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createCommandsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/commands/**/*.test.ts"], {
dir: "src/commands",
env,
exclude: commandsLightTestFiles,
name: "commands",
});
}
export default createCommandsVitestConfig();

View file

@ -0,0 +1,75 @@
import { defineConfig } from "vitest/config";
import {
resolveDefaultVitestPool,
resolveLocalVitestMaxWorkers,
resolveLocalVitestScheduling,
nonIsolatedRunnerPath,
sharedVitestConfig,
} from "./vitest.shared.config.ts";
export { resolveDefaultVitestPool, resolveLocalVitestMaxWorkers, resolveLocalVitestScheduling };
export const rootVitestProjects = [
"test/vitest/vitest.unit.config.ts",
"test/vitest/vitest.infra.config.ts",
"test/vitest/vitest.boundary.config.ts",
"test/vitest/vitest.contracts.config.ts",
"test/vitest/vitest.bundled.config.ts",
"test/vitest/vitest.gateway-core.config.ts",
"test/vitest/vitest.gateway-client.config.ts",
"test/vitest/vitest.gateway-methods.config.ts",
"test/vitest/vitest.gateway-server.config.ts",
"test/vitest/vitest.hooks.config.ts",
"test/vitest/vitest.acp.config.ts",
"test/vitest/vitest.runtime-config.config.ts",
"test/vitest/vitest.secrets.config.ts",
"test/vitest/vitest.cli.config.ts",
"test/vitest/vitest.commands-light.config.ts",
"test/vitest/vitest.commands.config.ts",
"test/vitest/vitest.auto-reply.config.ts",
"test/vitest/vitest.agents.config.ts",
"test/vitest/vitest.daemon.config.ts",
"test/vitest/vitest.media.config.ts",
"test/vitest/vitest.unit-fast.config.ts",
"test/vitest/vitest.plugin-sdk-light.config.ts",
"test/vitest/vitest.plugin-sdk.config.ts",
"test/vitest/vitest.plugins.config.ts",
"test/vitest/vitest.logging.config.ts",
"test/vitest/vitest.process.config.ts",
"test/vitest/vitest.cron.config.ts",
"test/vitest/vitest.media-understanding.config.ts",
"test/vitest/vitest.shared-core.config.ts",
"test/vitest/vitest.tasks.config.ts",
"test/vitest/vitest.tooling.config.ts",
"test/vitest/vitest.tui.config.ts",
"test/vitest/vitest.ui.config.ts",
"test/vitest/vitest.utils.config.ts",
"test/vitest/vitest.wizard.config.ts",
"test/vitest/vitest.channels.config.ts",
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-channels.config.ts",
"test/vitest/vitest.extension-diffs.config.ts",
"test/vitest/vitest.extension-feishu.config.ts",
"test/vitest/vitest.extension-irc.config.ts",
"test/vitest/vitest.extension-mattermost.config.ts",
"test/vitest/vitest.extension-matrix.config.ts",
"test/vitest/vitest.extension-memory.config.ts",
"test/vitest/vitest.extension-msteams.config.ts",
"test/vitest/vitest.extension-messaging.config.ts",
"test/vitest/vitest.extension-providers.config.ts",
"test/vitest/vitest.extension-telegram.config.ts",
"test/vitest/vitest.extension-voice-call.config.ts",
"test/vitest/vitest.extension-whatsapp.config.ts",
"test/vitest/vitest.extension-zalo.config.ts",
"test/vitest/vitest.extensions.config.ts",
] as const;
export default defineConfig({
...sharedVitestConfig,
test: {
...sharedVitestConfig.test,
runner: nonIsolatedRunnerPath,
projects: [...rootVitestProjects],
},
});

View file

@ -0,0 +1,41 @@
import { defineConfig } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import { nonIsolatedRunnerPath, sharedVitestConfig } from "./vitest.shared.config.ts";
const base = sharedVitestConfig as Record<string, unknown>;
const baseTest = sharedVitestConfig.test ?? {};
const contractIncludePatterns = [
"src/channels/plugins/contracts/**/*.test.ts",
"src/plugins/contracts/**/*.test.ts",
];
export function loadContractsIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createContractsVitestConfig(
env: Record<string, string | undefined> = process.env,
argv: string[] = process.argv,
) {
const cliIncludePatterns = narrowIncludePatternsForCli(contractIncludePatterns, argv);
return defineConfig({
...base,
test: {
...baseTest,
isolate: false,
// The contracts shard intentionally runs non-isolated and loads hundreds of
// contract files. Use forks so full-suite parallel runs do not hit
// Vitest worker-thread heap limits.
pool: "forks",
runner: nonIsolatedRunnerPath,
setupFiles: baseTest.setupFiles ?? [],
include:
loadContractsIncludePatternsFromEnv(env) ?? cliIncludePatterns ?? contractIncludePatterns,
passWithNoTests: true,
},
});
}
export default createContractsVitestConfig();

View file

@ -0,0 +1,22 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createCronVitestConfig(env?: Record<string, string | undefined>) {
const config = createScopedVitestConfig(["src/cron/**/*.test.ts"], {
dir: "src",
env,
name: "cron",
passWithNoTests: true,
});
config.test = {
...config.test,
maxWorkers: 1,
fileParallelism: false,
sequence: {
...config.test?.sequence,
groupOrder: 1,
},
};
return config;
}
export default createCronVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createDaemonVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/daemon/**/*.test.ts"], {
dir: "src",
env,
name: "daemon",
passWithNoTests: true,
});
}
export default createDaemonVitestConfig();

View file

@ -0,0 +1,50 @@
import os from "node:os";
import { defineConfig } from "vitest/config";
import { BUNDLED_PLUGIN_E2E_TEST_GLOB } from "./vitest.bundled-plugin-paths.ts";
import baseConfig from "./vitest.config.ts";
import { resolveRepoRootPath } from "./vitest.shared.config.ts";
const base = baseConfig as unknown as Record<string, unknown>;
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
const cpuCount = os.cpus().length;
// Keep e2e runs cheap by default; callers can still override via OPENCLAW_E2E_WORKERS.
const defaultWorkers = isCI ? Math.min(2, Math.max(1, Math.floor(cpuCount * 0.25))) : 1;
const requestedWorkers = Number.parseInt(process.env.OPENCLAW_E2E_WORKERS ?? "", 10);
const e2eWorkers =
Number.isFinite(requestedWorkers) && requestedWorkers > 0
? Math.min(16, requestedWorkers)
: defaultWorkers;
const verboseE2E = process.env.OPENCLAW_E2E_VERBOSE === "1";
const baseTestWithProjects =
(baseConfig as { test?: { exclude?: string[]; projects?: string[]; setupFiles?: string[] } })
.test ?? {};
const { projects: _projects, ...baseTest } = baseTestWithProjects as {
exclude?: string[];
projects?: string[];
setupFiles?: string[];
};
const exclude = (baseTest.exclude ?? []).filter((p) => p !== "**/*.e2e.test.ts");
export default defineConfig({
...base,
test: {
...baseTest,
maxWorkers: e2eWorkers,
silent: !verboseE2E,
setupFiles: [
...new Set(
[...(baseTest.setupFiles ?? []), "test/setup-openclaw-runtime.ts"].map(resolveRepoRootPath),
),
],
include: [
"test/**/*.e2e.test.ts",
"src/**/*.e2e.test.ts",
"src/gateway/gateway.test.ts",
"src/gateway/server.startup-matrix-migration.integration.test.ts",
"src/gateway/sessions-history-http.test.ts",
BUNDLED_PLUGIN_E2E_TEST_GLOB,
],
exclude,
},
});

View file

@ -0,0 +1,5 @@
export const acpxExtensionTestRoots = ["extensions/acpx"];
export function isAcpxExtensionRoot(root) {
return acpxExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,26 @@
import { acpxExtensionTestRoots } from "./vitest.extension-acpx-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionAcpxVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ?? acpxExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-acpx",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionAcpxVitestConfig();

View file

@ -0,0 +1,5 @@
export const blueBubblesExtensionTestRoots = ["extensions/bluebubbles"];
export function isBlueBubblesExtensionRoot(root) {
return blueBubblesExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { blueBubblesExtensionTestRoots } from "./vitest.extension-bluebubbles-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionBlueBubblesVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
blueBubblesExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-bluebubbles",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionBlueBubblesVitestConfig();

View file

@ -0,0 +1,8 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export default createScopedVitestConfig(["browser/**/*.test.ts"], {
dir: "extensions",
name: "extension-browser",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
});

View file

@ -0,0 +1,19 @@
import {
extensionChannelOverrideExcludeGlobs,
extensionChannelTestInclude,
} from "./vitest.channel-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createExtensionChannelsVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(extensionChannelTestInclude, {
dir: "extensions",
env,
exclude: extensionChannelOverrideExcludeGlobs,
name: "extension-channels",
passWithNoTests: true,
});
}
export default createExtensionChannelsVitestConfig();

View file

@ -0,0 +1,5 @@
export const diffsExtensionTestRoots = ["extensions/diffs"];
export function isDiffsExtensionRoot(root) {
return diffsExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { diffsExtensionTestRoots } from "./vitest.extension-diffs-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionDiffsVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
diffsExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-diffs",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionDiffsVitestConfig();

View file

@ -0,0 +1,9 @@
import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs";
export const feishuExtensionIds = ["feishu"];
export const feishuExtensionTestRoots = feishuExtensionIds.map((id) => bundledPluginRoot(id));
export function isFeishuExtensionRoot(root) {
return feishuExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { feishuExtensionTestRoots } from "./vitest.extension-feishu-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionFeishuVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
feishuExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-feishu",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionFeishuVitestConfig();

View file

@ -0,0 +1,5 @@
export const ircExtensionTestRoots = ["extensions/irc"];
export function isIrcExtensionRoot(root) {
return ircExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,26 @@
import { ircExtensionTestRoots } from "./vitest.extension-irc-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionIrcVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ?? ircExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-irc",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionIrcVitestConfig();

View file

@ -0,0 +1,9 @@
import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs";
export const matrixExtensionIds = ["matrix"];
export const matrixExtensionTestRoots = matrixExtensionIds.map((id) => bundledPluginRoot(id));
export function isMatrixExtensionRoot(root) {
return matrixExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,17 @@
import { matrixExtensionTestRoots } from "./vitest.extension-matrix-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createExtensionMatrixVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(
matrixExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-matrix",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionMatrixVitestConfig();

View file

@ -0,0 +1,11 @@
import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs";
export const mattermostExtensionIds = ["mattermost"];
export const mattermostExtensionTestRoots = mattermostExtensionIds.map((id) =>
bundledPluginRoot(id),
);
export function isMattermostExtensionRoot(root) {
return mattermostExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { mattermostExtensionTestRoots } from "./vitest.extension-mattermost-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionMattermostVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
mattermostExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-mattermost",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionMattermostVitestConfig();

View file

@ -0,0 +1,22 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export default createScopedVitestConfig(
[
"alibaba/**/*.test.ts",
"deepgram/**/*.test.ts",
"elevenlabs/**/*.test.ts",
"fal/**/*.test.ts",
"image-generation-core/**/*.test.ts",
"runway/**/*.test.ts",
"talk-voice/**/*.test.ts",
"video-generation-core/**/*.test.ts",
"vydra/**/*.test.ts",
"xiaomi/**/*.test.ts",
],
{
dir: "extensions",
name: "extension-media",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);

View file

@ -0,0 +1,9 @@
export const memoryExtensionTestRoots = [
"extensions/memory-core",
"extensions/memory-lancedb",
"extensions/memory-wiki",
];
export function isMemoryExtensionRoot(root) {
return memoryExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,28 @@
import { memoryExtensionTestRoots } from "./vitest.extension-memory-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionMemoryVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
memoryExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-memory",
pool: "forks",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionMemoryVitestConfig();

View file

@ -0,0 +1,17 @@
import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs";
export const messagingExtensionIds = [
"googlechat",
"nextcloud-talk",
"nostr",
"qqbot",
"synology-chat",
"tlon",
"twitch",
];
export const messagingExtensionTestRoots = messagingExtensionIds.map((id) => bundledPluginRoot(id));
export function isMessagingExtensionRoot(root) {
return messagingExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { messagingExtensionTestRoots } from "./vitest.extension-messaging-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionMessagingVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
messagingExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-messaging",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionMessagingVitestConfig();

View file

@ -0,0 +1,35 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export default createScopedVitestConfig(
[
"arcee/**/*.test.ts",
"brave/**/*.test.ts",
"device-pair/**/*.test.ts",
"diagnostics-otel/**/*.test.ts",
"duckduckgo/**/*.test.ts",
"exa/**/*.test.ts",
"firecrawl/**/*.test.ts",
"fireworks/**/*.test.ts",
"kilocode/**/*.test.ts",
"litellm/**/*.test.ts",
"llm-task/**/*.test.ts",
"lobster/**/*.test.ts",
"opencode/**/*.test.ts",
"opencode-go/**/*.test.ts",
"openshell/**/*.test.ts",
"perplexity/**/*.test.ts",
"phone-control/**/*.test.ts",
"searxng/**/*.test.ts",
"synthetic/**/*.test.ts",
"tavily/**/*.test.ts",
"thread-ownership/**/*.test.ts",
"vercel-ai-gateway/**/*.test.ts",
"webhooks/**/*.test.ts",
],
{
dir: "extensions",
name: "extension-misc",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);

View file

@ -0,0 +1,9 @@
import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs";
export const msTeamsExtensionIds = ["msteams"];
export const msTeamsExtensionTestRoots = msTeamsExtensionIds.map((id) => bundledPluginRoot(id));
export function isMsTeamsExtensionRoot(root) {
return msTeamsExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,17 @@
import { msTeamsExtensionTestRoots } from "./vitest.extension-msteams-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createExtensionMsTeamsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(
msTeamsExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-msteams",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionMsTeamsVitestConfig();

View file

@ -0,0 +1,40 @@
import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs";
export const providerExtensionIds = [
"amazon-bedrock",
"amazon-bedrock-mantle",
"anthropic",
"anthropic-vertex",
"byteplus",
"chutes",
"comfy",
"deepseek",
"github-copilot",
"google",
"groq",
"huggingface",
"kimi-coding",
"microsoft",
"microsoft-foundry",
"minimax",
"mistral",
"qwen",
"moonshot",
"nvidia",
"ollama",
"openai",
"openrouter",
"qianfan",
"stepfun",
"together",
"venice",
"volcengine",
"xai",
"zai",
];
export const providerExtensionTestRoots = providerExtensionIds.map((id) => bundledPluginRoot(id));
export function isProviderExtensionRoot(root) {
return providerExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { providerExtensionTestRoots } from "./vitest.extension-provider-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionProvidersVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
providerExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-providers",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionProvidersVitestConfig();

View file

@ -0,0 +1,8 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export default createScopedVitestConfig(["qa-channel/**/*.test.ts", "qa-lab/**/*.test.ts"], {
dir: "extensions",
name: "extension-qa",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
});

View file

@ -0,0 +1,5 @@
export const telegramExtensionTestRoots = ["extensions/telegram"];
export function isTelegramExtensionRoot(root) {
return telegramExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { telegramExtensionTestRoots } from "./vitest.extension-telegram-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionTelegramVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
telegramExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-telegram",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionTelegramVitestConfig();

View file

@ -0,0 +1,9 @@
import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs";
export const voiceCallExtensionIds = ["voice-call"];
export const voiceCallExtensionTestRoots = voiceCallExtensionIds.map((id) => bundledPluginRoot(id));
export function isVoiceCallExtensionRoot(root) {
return voiceCallExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { voiceCallExtensionTestRoots } from "./vitest.extension-voice-call-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionVoiceCallVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
voiceCallExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-voice-call",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionVoiceCallVitestConfig();

View file

@ -0,0 +1,9 @@
import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs";
export const whatsAppExtensionIds = ["whatsapp"];
export const whatsAppExtensionTestRoots = whatsAppExtensionIds.map((id) => bundledPluginRoot(id));
export function isWhatsAppExtensionRoot(root) {
return whatsAppExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,27 @@
import { whatsAppExtensionTestRoots } from "./vitest.extension-whatsapp-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionWhatsAppVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
whatsAppExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-whatsapp",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionWhatsAppVitestConfig();

View file

@ -0,0 +1,5 @@
export const zaloExtensionTestRoots = ["extensions/zalo", "extensions/zalouser"];
export function isZaloExtensionRoot(root) {
return zaloExtensionTestRoots.includes(root);
}

View file

@ -0,0 +1,26 @@
import { zaloExtensionTestRoots } from "./vitest.extension-zalo-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionZaloVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ?? zaloExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
dir: "extensions",
env,
name: "extension-zalo",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
},
);
}
export default createExtensionZaloVitestConfig();

View file

@ -0,0 +1,59 @@
import { BUNDLED_PLUGIN_TEST_GLOB } from "./vitest.bundled-plugin-paths.ts";
import { extensionExcludedChannelTestGlobs } from "./vitest.channel-paths.mjs";
import { acpxExtensionTestRoots } from "./vitest.extension-acpx-paths.mjs";
import { blueBubblesExtensionTestRoots } from "./vitest.extension-bluebubbles-paths.mjs";
import { diffsExtensionTestRoots } from "./vitest.extension-diffs-paths.mjs";
import { feishuExtensionTestRoots } from "./vitest.extension-feishu-paths.mjs";
import { ircExtensionTestRoots } from "./vitest.extension-irc-paths.mjs";
import { matrixExtensionTestRoots } from "./vitest.extension-matrix-paths.mjs";
import { mattermostExtensionTestRoots } from "./vitest.extension-mattermost-paths.mjs";
import { memoryExtensionTestRoots } from "./vitest.extension-memory-paths.mjs";
import { messagingExtensionTestRoots } from "./vitest.extension-messaging-paths.mjs";
import { msTeamsExtensionTestRoots } from "./vitest.extension-msteams-paths.mjs";
import { providerExtensionTestRoots } from "./vitest.extension-provider-paths.mjs";
import { telegramExtensionTestRoots } from "./vitest.extension-telegram-paths.mjs";
import { voiceCallExtensionTestRoots } from "./vitest.extension-voice-call-paths.mjs";
import { whatsAppExtensionTestRoots } from "./vitest.extension-whatsapp-paths.mjs";
import { zaloExtensionTestRoots } from "./vitest.extension-zalo-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createExtensionsVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(loadIncludePatternsFromEnv(env) ?? [BUNDLED_PLUGIN_TEST_GLOB], {
dir: "extensions",
env,
name: "extensions",
passWithNoTests: true,
setupFiles: ["test/setup.extensions.ts"],
// Some bundled plugins still run on the channel surface; keep those roots
// out of the shared extensions lane.
exclude: [
...extensionExcludedChannelTestGlobs,
...acpxExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...blueBubblesExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...diffsExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...feishuExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...ircExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...matrixExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...mattermostExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...memoryExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...messagingExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...msTeamsExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...providerExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...telegramExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...voiceCallExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...whatsAppExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...zaloExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
],
});
}
export default createExtensionsVitestConfig();

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-agentic.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-auto-reply.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-core-bundled.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-core-contracts.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-core-runtime.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-core-support-boundary.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,11 @@
import { defineConfig } from "vitest/config";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
export default defineConfig({
...sharedVitestConfig,
test: {
...sharedVitestConfig.test,
runner: undefined,
projects: ["test/vitest/vitest.unit-fast.config.ts"],
},
});

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-core-unit-security.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-core-unit-src.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-core-unit-support.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-core-unit-ui.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,4 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(fullSuiteVitestShards[0].projects);

View file

@ -0,0 +1,8 @@
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
export default createProjectShardVitestConfig(
fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-extensions.config.ts",
)?.projects ?? [],
);

View file

@ -0,0 +1,21 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createGatewayClientVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(
[
"src/gateway/protocol/**/*.test.ts",
"src/gateway/**/*client*.test.ts",
"src/gateway/**/*reconnect*.test.ts",
"src/gateway/**/*android-node*.test.ts",
"src/gateway/**/*gateway-cli-backend*.test.ts",
],
{
dir: "src/gateway",
env,
exclude: ["src/gateway/**/*server*.test.ts"],
name: "gateway-client",
},
);
}
export default createGatewayClientVitestConfig();

View file

@ -0,0 +1,30 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
const nonCoreGatewayTestExclude = [
"src/gateway/server-methods/**/*.test.ts",
"src/gateway/protocol/**/*.test.ts",
"src/gateway/**/*client*.test.ts",
"src/gateway/**/*reconnect*.test.ts",
"src/gateway/**/*android-node*.test.ts",
"src/gateway/**/*gateway-cli-backend*.test.ts",
"src/gateway/**/*server*.test.ts",
"src/gateway/gateway.test.ts",
"src/gateway/embeddings-http.test.ts",
"src/gateway/models-http.test.ts",
"src/gateway/openai-http.test.ts",
"src/gateway/openresponses-http.test.ts",
"src/gateway/probe.auth.integration.test.ts",
"src/gateway/server.startup-matrix-migration.integration.test.ts",
"src/gateway/sessions-history-http.test.ts",
];
export function createGatewayCoreVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/gateway/**/*.test.ts"], {
dir: "src/gateway",
env,
exclude: nonCoreGatewayTestExclude,
name: "gateway-core",
});
}
export default createGatewayCoreVitestConfig();

View file

@ -0,0 +1,11 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createGatewayMethodsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/gateway/server-methods/**/*.test.ts"], {
dir: "src/gateway",
env,
name: "gateway-methods",
});
}
export default createGatewayMethodsVitestConfig();

View file

@ -0,0 +1,28 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
const gatewayServerBackedHttpTests = [
"src/gateway/embeddings-http.test.ts",
"src/gateway/models-http.test.ts",
"src/gateway/openai-http.test.ts",
"src/gateway/openresponses-http.test.ts",
"src/gateway/probe.auth.integration.test.ts",
];
export function createGatewayServerVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(
["src/gateway/**/*server*.test.ts", ...gatewayServerBackedHttpTests],
{
dir: "src/gateway",
env,
exclude: [
"src/gateway/server-methods/**/*.test.ts",
"src/gateway/gateway.test.ts",
"src/gateway/server.startup-matrix-migration.integration.test.ts",
"src/gateway/sessions-history-http.test.ts",
],
name: "gateway-server",
},
);
}
export default createGatewayServerVitestConfig();

View file

@ -0,0 +1,16 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createGatewayVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/gateway/**/*.test.ts"], {
dir: "src/gateway",
env,
exclude: [
"src/gateway/gateway.test.ts",
"src/gateway/server.startup-matrix-migration.integration.test.ts",
"src/gateway/sessions-history-http.test.ts",
],
name: "gateway",
});
}
export default createGatewayVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createHooksVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/hooks/**/*.test.ts"], {
dir: "src/hooks",
env,
name: "hooks",
passWithNoTests: true,
});
}
export default createHooksVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createInfraVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/infra/**/*.test.ts"], {
dir: "src",
env,
name: "infra",
passWithNoTests: true,
});
}
export default createInfraVitestConfig();

View file

@ -0,0 +1,32 @@
import { defineConfig } from "vitest/config";
import { BUNDLED_PLUGIN_LIVE_TEST_GLOB } from "./vitest.bundled-plugin-paths.ts";
import baseConfig from "./vitest.config.ts";
import { resolveRepoRootPath } from "./vitest.shared.config.ts";
const base = baseConfig as unknown as Record<string, unknown>;
const baseTestWithProjects =
(baseConfig as { test?: { exclude?: string[]; setupFiles?: string[] } }).test ?? {};
const { projects: _projects, ...baseTest } = baseTestWithProjects as {
exclude?: string[];
projects?: string[];
setupFiles?: string[];
};
const exclude = (baseTest.exclude ?? []).filter((p) => p !== "**/*.live.test.ts");
export default defineConfig({
...base,
test: {
...baseTest,
// Live suites need immediate provider/gateway progress output rather than
// Vitest's buffered per-test console capture.
disableConsoleIntercept: true,
maxWorkers: 1,
setupFiles: [
...new Set(
[...(baseTest.setupFiles ?? []), "test/setup-openclaw-runtime.ts"].map(resolveRepoRootPath),
),
],
include: ["src/**/*.live.test.ts", "test/**/*.live.test.ts", BUNDLED_PLUGIN_LIVE_TEST_GLOB],
exclude,
},
});

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createLoggingVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/logging/**/*.test.ts"], {
dir: "src",
env,
name: "logging",
passWithNoTests: true,
});
}
export default createLoggingVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createMediaUnderstandingVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/media-understanding/**/*.test.ts"], {
dir: "src",
env,
name: "media-understanding",
passWithNoTests: true,
});
}
export default createMediaUnderstandingVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createMediaVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/media/**/*.test.ts"], {
dir: "src",
env,
name: "media",
passWithNoTests: true,
});
}
export default createMediaVitestConfig();

View file

@ -0,0 +1,108 @@
import fs from "node:fs";
import path from "node:path";
function normalizeCliPattern(value: string): string {
let normalized = value
.trim()
.replace(/^\.\/+/u, "")
.replace(/\/+$/u, "");
if (
/^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(normalized) &&
!/[?*[\]{}]/u.test(normalized) &&
!/\.(?:[cm]?[jt]sx?)$/u.test(normalized)
) {
normalized = `${normalized}/**/*.test.*`;
}
return normalized;
}
function looksLikeCliIncludePattern(value: string): boolean {
const normalized = normalizeCliPattern(value);
return (
normalized.includes(".test.") ||
normalized.includes(".e2e.") ||
normalized.includes(".live.") ||
/^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(normalized)
);
}
export function loadPatternListFile(filePath: string, label: string): string[] {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
if (!Array.isArray(parsed)) {
throw new TypeError(`${label} must point to a JSON array: ${filePath}`);
}
return parsed.filter((value): value is string => typeof value === "string" && value.length > 0);
}
export function loadPatternListFromEnv(
envKey: string,
env: Record<string, string | undefined> = process.env,
): string[] | null {
const filePath = env[envKey]?.trim();
if (!filePath) {
return null;
}
return loadPatternListFile(filePath, envKey);
}
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",
]);
const values: string[] = [];
let skipNext = false;
for (const value of argv.slice(2)) {
if (skipNext) {
skipNext = false;
continue;
}
if (value === "run" || value === "watch" || value === "bench") {
continue;
}
if (optionValueFlags.has(value)) {
skipNext = true;
continue;
}
if (value.startsWith("-")) {
continue;
}
values.push(value);
}
const patterns = values.filter(looksLikeCliIncludePattern).map(normalizeCliPattern);
return patterns.length > 0 ? [...new Set(patterns)] : null;
}
export function narrowIncludePatternsForCli(
includePatterns: string[],
argv: string[] = process.argv,
): string[] | null {
const cliPatterns = loadPatternListFromArgv(argv);
if (!cliPatterns) {
return null;
}
const matched = cliPatterns.filter((value) =>
includePatterns.some(
(pattern) => path.matchesGlob(value, pattern) || path.matchesGlob(pattern, value),
),
);
return [...new Set(matched)];
}

View file

@ -0,0 +1,59 @@
type EnvMap = Record<string, string | undefined>;
const isEnabled = (value: string | undefined): boolean => {
const normalized = value?.trim().toLowerCase();
return normalized === "1" || normalized === "true";
};
const isDisabled = (value: string | undefined): boolean => {
const normalized = value?.trim().toLowerCase();
return normalized === "0" || normalized === "false";
};
const isWindowsEnv = (env: EnvMap, platform: NodeJS.Platform): boolean => {
if (platform === "win32") {
return true;
}
const runnerOs = env.RUNNER_OS?.trim().toLowerCase();
return runnerOs === "windows";
};
type VitestExperimentalConfig = {
experimental?: {
fsModuleCache?: true;
fsModuleCachePath?: string;
importDurations?: { print: true };
printImportBreakdown?: true;
};
};
export function loadVitestExperimentalConfig(
env: EnvMap = process.env,
platform: NodeJS.Platform = process.platform,
): VitestExperimentalConfig {
const experimental: {
fsModuleCache?: true;
fsModuleCachePath?: string;
importDurations?: { print: true };
printImportBreakdown?: true;
} = {};
const windowsEnv = isWindowsEnv(env, platform);
if (!windowsEnv && !isDisabled(env.OPENCLAW_VITEST_FS_MODULE_CACHE)) {
experimental.fsModuleCache = true;
}
if (windowsEnv && isEnabled(env.OPENCLAW_VITEST_FS_MODULE_CACHE)) {
experimental.fsModuleCache = true;
}
if (experimental.fsModuleCache && env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.trim()) {
experimental.fsModuleCachePath = env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH.trim();
}
if (isEnabled(env.OPENCLAW_VITEST_IMPORT_DURATIONS)) {
experimental.importDurations = { print: true };
}
if (isEnabled(env.OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN)) {
experimental.printImportBreakdown = true;
}
return Object.keys(experimental).length > 0 ? { experimental } : {};
}

View file

@ -0,0 +1,16 @@
import { pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createPluginSdkLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(pluginSdkLightTestFiles, {
dir: "src",
env,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "plugin-sdk-light",
passWithNoTests: true,
});
}
export default createPluginSdkLightVitestConfig();

View file

@ -0,0 +1,56 @@
const normalizeRepoPath = (value) => value.replaceAll("\\", "/");
const pluginSdkLightEntries = [
{ source: "src/plugin-sdk/acp-runtime.ts", test: "src/plugin-sdk/acp-runtime.test.ts" },
{ source: "src/plugin-sdk/allow-from.ts", test: "src/plugin-sdk/allow-from.test.ts" },
{
source: "src/plugin-sdk/keyed-async-queue.ts",
test: "src/plugin-sdk/keyed-async-queue.test.ts",
},
{ source: "src/plugin-sdk/lazy-value.ts", test: "src/plugin-sdk/lazy-value.test.ts" },
{
source: "src/plugin-sdk/persistent-dedupe.ts",
test: "src/plugin-sdk/persistent-dedupe.test.ts",
},
{ source: "src/plugin-sdk/provider-entry.ts", test: "src/plugin-sdk/provider-entry.test.ts" },
{
source: "src/plugin-sdk/provider-model-shared.ts",
test: "src/plugin-sdk/provider-model-shared.test.ts",
},
{ source: "src/plugin-sdk/provider-tools.ts", test: "src/plugin-sdk/provider-tools.test.ts" },
{
source: "src/plugin-sdk/status-helpers.ts",
test: "src/plugin-sdk/status-helpers.test.ts",
},
{ source: "src/plugin-sdk/temp-path.ts", test: "src/plugin-sdk/temp-path.test.ts" },
{
source: "src/plugin-sdk/text-chunking.ts",
test: "src/plugin-sdk/text-chunking.test.ts",
},
{
source: "src/plugin-sdk/webhook-targets.ts",
test: "src/plugin-sdk/webhook-targets.test.ts",
},
];
const pluginSdkLightIncludePatternByFile = new Map(
pluginSdkLightEntries.flatMap(({ source, test }) => [
[source, test],
[test, test],
]),
);
export const pluginSdkLightSourceFiles = pluginSdkLightEntries.map(({ source }) => source);
export const pluginSdkLightTestFiles = pluginSdkLightEntries.map(({ test }) => test);
export function isPluginSdkLightTarget(file) {
return pluginSdkLightIncludePatternByFile.has(normalizeRepoPath(file));
}
export function isPluginSdkLightTestFile(file) {
return pluginSdkLightTestFiles.includes(normalizeRepoPath(file));
}
export function resolvePluginSdkLightIncludePattern(file) {
return pluginSdkLightIncludePatternByFile.get(normalizeRepoPath(file)) ?? null;
}

View file

@ -0,0 +1,14 @@
import { pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createPluginSdkVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/plugin-sdk/**/*.test.ts"], {
dir: "src",
env,
exclude: pluginSdkLightTestFiles,
name: "plugin-sdk",
passWithNoTests: true,
});
}
export default createPluginSdkVitestConfig();

View file

@ -0,0 +1,14 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createPluginsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/plugins/**/*.test.ts"], {
dir: "src/plugins",
env,
exclude: ["src/plugins/contracts/**"],
isolate: true,
name: "plugins",
passWithNoTests: true,
});
}
export default createPluginsVitestConfig();

View file

@ -0,0 +1,23 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createProcessVitestConfig(env?: Record<string, string | undefined>) {
const config = createScopedVitestConfig(["src/process/**/*.test.ts"], {
dir: "src",
env,
includeOpenClawRuntimeSetup: false,
name: "process",
passWithNoTests: true,
});
return {
...config,
test: {
...config.test,
sequence: {
...config.test?.sequence,
groupOrder: 2,
},
},
};
}
export default createProcessVitestConfig();

View file

@ -0,0 +1,17 @@
import { defineConfig } from "vitest/config";
import { nonIsolatedRunnerPath, sharedVitestConfig } from "./vitest.shared.config.ts";
export function createProjectShardVitestConfig(projects: readonly string[]) {
const maxWorkers = sharedVitestConfig.test.maxWorkers;
if (!process.env.OPENCLAW_VITEST_MAX_WORKERS && typeof maxWorkers === "number") {
process.env.OPENCLAW_VITEST_MAX_WORKERS = String(maxWorkers);
}
return defineConfig({
...sharedVitestConfig,
test: {
...sharedVitestConfig.test,
runner: nonIsolatedRunnerPath,
projects: [...projects],
},
});
}

View file

@ -0,0 +1,23 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createRuntimeConfigVitestConfig(env?: Record<string, string | undefined>) {
const config = createScopedVitestConfig(["src/config/**/*.test.ts"], {
dir: "src",
env,
includeOpenClawRuntimeSetup: false,
name: "runtime-config",
passWithNoTests: true,
});
return {
...config,
test: {
...config.test,
sequence: {
...config.test?.sequence,
groupOrder: 3,
},
},
};
}
export default createRuntimeConfigVitestConfig();

View file

@ -0,0 +1,207 @@
import path from "node:path";
import { defineConfig } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import {
nonIsolatedRunnerPath,
repoRoot,
resolveRepoRootPath,
sharedVitestConfig,
} from "./vitest.shared.config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
function normalizePathPattern(value: string): string {
return value.replaceAll("\\", "/");
}
function relativizeScopedPattern(value: string, dir: string): string {
const normalizedValue = normalizePathPattern(value);
const normalizedDir = normalizePathPattern(dir).replace(/\/+$/u, "");
if (!normalizedDir) {
return normalizedValue;
}
if (normalizedValue === normalizedDir) {
return ".";
}
const prefix = `${normalizedDir}/`;
return normalizedValue.startsWith(prefix)
? normalizedValue.slice(prefix.length)
: normalizedValue;
}
function relativizeScopedPatterns(values: string[], dir?: string): string[] {
if (!dir) {
return values.map(normalizePathPattern);
}
return values.map((value) => relativizeScopedPattern(value, dir));
}
export function resolveVitestIsolation(
_env: Record<string, string | undefined> = process.env,
): boolean {
return false;
}
const SCOPED_PROJECT_GROUP_ORDER_BY_NAME = new Map(
[
"acp",
"agents",
"auto-reply",
"auto-reply-core",
"auto-reply-reply",
"auto-reply-top-level",
"boundary",
"bundled",
"channels",
"cli",
"commands",
"commands-light",
"cron",
"daemon",
"extension-acpx",
"extension-bluebubbles",
"extension-channels",
"extension-diffs",
"extension-feishu",
"extension-irc",
"extension-mattermost",
"extension-matrix",
"extension-memory",
"extension-messaging",
"extension-msteams",
"extension-providers",
"extension-telegram",
"extension-voice-call",
"extension-whatsapp",
"extension-zalo",
"extensions",
"gateway",
"hooks",
"infra",
"logging",
"media",
"media-understanding",
"plugin-sdk",
"plugin-sdk-light",
"plugins",
"process",
"runtime-config",
"secrets",
"shared-core",
"tasks",
"tooling",
"tui",
"ui",
"unit-fast",
"unit-security",
"unit-src",
"unit-support",
"unit-ui",
"utils",
"wizard",
].map((name, index) => [name, index + 10]),
);
function hashFallbackScopedProjectGroupOrder(key: string): number {
let hash = 0;
for (const char of key) {
hash = (hash * 33 + char.charCodeAt(0)) % 10_000;
}
return hash + 1_000;
}
function resolveScopedProjectGroupOrder(
name?: string,
dir?: string,
include?: string[],
): number | undefined {
const normalizedName = name?.trim();
if (normalizedName) {
return (
SCOPED_PROJECT_GROUP_ORDER_BY_NAME.get(normalizedName) ??
hashFallbackScopedProjectGroupOrder(normalizedName)
);
}
const normalizedInclude = include?.map(normalizePathPattern).join("|") ?? "";
const key = [dir?.trim(), normalizedInclude].filter(Boolean).join("|");
if (!key) {
return undefined;
}
return hashFallbackScopedProjectGroupOrder(key);
}
export function createScopedVitestConfig(
include: string[],
options?: {
deps?: Record<string, unknown>;
dir?: string;
env?: Record<string, string | undefined>;
environment?: string;
exclude?: string[];
argv?: string[];
includeOpenClawRuntimeSetup?: boolean;
isolate?: boolean;
name?: string;
fileParallelism?: boolean;
pool?: "forks" | "threads";
passWithNoTests?: boolean;
excludeUnitFastTests?: boolean;
setupFiles?: string[];
useNonIsolatedRunner?: boolean;
},
) {
const base = sharedVitestConfig as Record<string, unknown>;
const baseTest = sharedVitestConfig.test ?? {};
const scopedDir = options?.dir;
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 unitFastExcludePatterns =
options?.excludeUnitFastTests === false ? [] : getUnitFastTestFiles();
const exclude = relativizeScopedPatterns(
[...(baseTest.exclude ?? []), ...unitFastExcludePatterns, ...(options?.exclude ?? [])],
scopedDir,
);
const isolate = options?.isolate ?? resolveVitestIsolation(options?.env);
const setupFiles = [
...new Set([
...(baseTest.setupFiles ?? []),
...(options?.setupFiles ?? []),
...(options?.includeOpenClawRuntimeSetup === false ? [] : ["test/setup-openclaw-runtime.ts"]),
]),
].map(resolveRepoRootPath);
const useNonIsolatedRunner = options?.useNonIsolatedRunner ?? !isolate;
const runner = useNonIsolatedRunner ? nonIsolatedRunnerPath : undefined;
const scopedGroupOrder = resolveScopedProjectGroupOrder(options?.name, scopedDir, include);
return defineConfig({
...base,
test: {
...baseTest,
...(options?.deps ? { deps: options.deps } : {}),
...(options?.name ? { name: options.name } : {}),
...(options?.environment ? { environment: options.environment } : {}),
isolate,
...(runner ? { runner } : { runner: undefined }),
setupFiles,
...(resolvedScopedDir ? { dir: resolvedScopedDir } : {}),
include: relativizeScopedPatterns(includeFromEnv ?? cliInclude ?? include, scopedDir),
exclude,
...(options?.pool ? { pool: options.pool } : {}),
...(options?.fileParallelism === undefined
? {}
: { fileParallelism: options.fileParallelism }),
...(scopedGroupOrder === undefined
? {}
: {
sequence: {
...baseTest.sequence,
groupOrder: scopedGroupOrder,
},
}),
...(options?.passWithNoTests !== undefined || cliInclude !== null
? { passWithNoTests: options?.passWithNoTests ?? true }
: {}),
},
});
}

View file

@ -0,0 +1,13 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createSecretsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/secrets/**/*.test.ts"], {
dir: "src/secrets",
env,
name: "secrets",
pool: "forks",
passWithNoTests: true,
});
}
export default createSecretsVitestConfig();

View file

@ -0,0 +1,15 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createSharedCoreVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/shared/**/*.test.ts"], {
dir: "src",
env,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "shared-core",
passWithNoTests: true,
});
}
export default createSharedCoreVitestConfig();

View file

@ -0,0 +1,315 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { pluginSdkSubpaths } from "../../scripts/lib/plugin-sdk-entries.mjs";
import {
detectVitestHostInfo as detectVitestHostInfoImpl,
isCiLikeEnv,
resolveLocalVitestMaxWorkers as resolveLocalVitestMaxWorkersImpl,
resolveLocalVitestScheduling as resolveLocalVitestSchedulingImpl,
} from "../../scripts/lib/vitest-local-scheduling.mjs";
import {
BUNDLED_PLUGIN_ROOT_DIR,
BUNDLED_PLUGIN_TEST_GLOB,
} from "./vitest.bundled-plugin-paths.ts";
import { loadVitestExperimentalConfig } from "./vitest.performance-config.ts";
import { shouldPrintVitestThrottle } from "./vitest.system-load.ts";
type VitestHostInfo = {
cpuCount?: number;
loadAverage1m?: number;
totalMemoryBytes?: number;
};
export type OpenClawVitestPool = "forks" | "threads";
export type LocalVitestScheduling = {
maxWorkers: number;
fileParallelism: boolean;
throttledBySystem: boolean;
};
export const jsdomOptimizedDeps = {
optimizer: {
web: {
enabled: true,
include: ["lit", "lit-html", "@lit/reactive-element", "marked"] as string[],
},
},
};
function detectVitestHostInfo(): Required<VitestHostInfo> {
return detectVitestHostInfoImpl() as Required<VitestHostInfo>;
}
export function resolveLocalVitestMaxWorkers(
env: Record<string, string | undefined> = process.env,
system: VitestHostInfo = detectVitestHostInfo(),
pool: OpenClawVitestPool = resolveDefaultVitestPool(env),
): number {
return resolveLocalVitestMaxWorkersImpl(env, system, pool);
}
export function resolveLocalVitestScheduling(
env: Record<string, string | undefined> = process.env,
system: VitestHostInfo = detectVitestHostInfo(),
pool: OpenClawVitestPool = resolveDefaultVitestPool(env),
): LocalVitestScheduling {
return resolveLocalVitestSchedulingImpl(env, system, pool) as LocalVitestScheduling;
}
export function resolveDefaultVitestPool(
_env: Record<string, string | undefined> = process.env,
): OpenClawVitestPool {
return "threads";
}
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
export const nonIsolatedRunnerPath = path.join(repoRoot, "test", "non-isolated-runner.ts");
export function resolveRepoRootPath(value: string): string {
return path.isAbsolute(value) ? value : path.join(repoRoot, value);
}
const isCI = isCiLikeEnv(process.env);
const isWindows = process.platform === "win32";
const defaultPool = resolveDefaultVitestPool();
const localScheduling = resolveLocalVitestScheduling(
process.env,
detectVitestHostInfo(),
defaultPool,
);
const ciWorkers = isWindows ? 2 : 3;
if (!isCI && localScheduling.throttledBySystem && shouldPrintVitestThrottle(process.env)) {
console.error(
`[vitest] throttling local workers to ${localScheduling.maxWorkers}${
localScheduling.fileParallelism ? "" : " with file parallelism disabled"
} because the host already looks busy.`,
);
}
export const sharedVitestConfig = {
root: repoRoot,
envFile: false,
resolve: {
alias: [
{
find: "openclaw/extension-api",
replacement: path.join(repoRoot, "src", "extensionAPI.ts"),
},
...pluginSdkSubpaths.map((subpath) => ({
find: `openclaw/plugin-sdk/${subpath}`,
replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`),
})),
...pluginSdkSubpaths.map((subpath) => ({
find: `@openclaw/plugin-sdk/${subpath}`,
replacement: path.join(repoRoot, "packages", "plugin-sdk", "src", `${subpath}.ts`),
})),
{
find: "openclaw/plugin-sdk",
replacement: path.join(repoRoot, "src", "plugin-sdk", "index.ts"),
},
],
},
test: {
dir: repoRoot,
testTimeout: 120_000,
hookTimeout: isWindows ? 180_000 : 120_000,
unstubEnvs: true,
unstubGlobals: true,
isolate: false,
pool: defaultPool,
runner: nonIsolatedRunnerPath,
maxWorkers: isCI ? ciWorkers : localScheduling.maxWorkers,
fileParallelism: isCI ? true : localScheduling.fileParallelism,
forceRerunTriggers: [
"package.json",
"pnpm-lock.yaml",
"test/setup.ts",
"test/setup.shared.ts",
"test/setup.extensions.ts",
"test/setup-openclaw-runtime.ts",
"test/vitest/vitest.channel-paths.mjs",
"test/vitest/vitest.channels.config.ts",
"test/vitest/vitest.acp.config.ts",
"test/vitest/vitest.boundary.config.ts",
"test/vitest/vitest.bundled.config.ts",
"test/vitest/vitest.cli.config.ts",
"vitest.config.ts",
"test/vitest/vitest.contracts.config.ts",
"test/vitest/vitest.cron.config.ts",
"test/vitest/vitest.daemon.config.ts",
"test/vitest/vitest.e2e.config.ts",
"test/vitest/vitest.extension-acpx-paths.mjs",
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles-paths.mjs",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-channels.config.ts",
"test/vitest/vitest.extension-diffs-paths.mjs",
"test/vitest/vitest.extension-diffs.config.ts",
"test/vitest/vitest.extension-feishu-paths.mjs",
"test/vitest/vitest.extension-feishu.config.ts",
"test/vitest/vitest.extension-irc-paths.mjs",
"test/vitest/vitest.extension-irc.config.ts",
"test/vitest/vitest.extension-mattermost-paths.mjs",
"test/vitest/vitest.extension-mattermost.config.ts",
"test/vitest/vitest.extension-matrix-paths.mjs",
"test/vitest/vitest.extension-matrix.config.ts",
"test/vitest/vitest.extension-memory-paths.mjs",
"test/vitest/vitest.extension-memory.config.ts",
"test/vitest/vitest.extension-messaging-paths.mjs",
"test/vitest/vitest.extension-messaging.config.ts",
"test/vitest/vitest.extension-msteams-paths.mjs",
"test/vitest/vitest.extension-msteams.config.ts",
"test/vitest/vitest.extensions.config.ts",
"test/vitest/vitest.gateway.config.ts",
"test/vitest/vitest.gateway-core.config.ts",
"test/vitest/vitest.gateway-client.config.ts",
"test/vitest/vitest.gateway-methods.config.ts",
"test/vitest/vitest.gateway-server.config.ts",
"test/vitest/vitest.hooks.config.ts",
"test/vitest/vitest.infra.config.ts",
"test/vitest/vitest.live.config.ts",
"test/vitest/vitest.media.config.ts",
"test/vitest/vitest.media-understanding.config.ts",
"test/vitest/vitest.performance-config.ts",
"test/vitest/vitest.unit-fast.config.ts",
"test/vitest/vitest.unit-fast-paths.mjs",
"test/vitest/vitest.scoped-config.ts",
"test/vitest/vitest.shared-core.config.ts",
"test/vitest/vitest.shared.config.ts",
"test/vitest/vitest.tooling.config.ts",
"test/vitest/vitest.tui.config.ts",
"test/vitest/vitest.ui.config.ts",
"test/vitest/vitest.utils.config.ts",
"test/vitest/vitest.unit.config.ts",
"test/vitest/vitest.unit-paths.mjs",
"test/vitest/vitest.runtime-config.config.ts",
"test/vitest/vitest.secrets.config.ts",
"test/vitest/vitest.plugin-sdk.config.ts",
"test/vitest/vitest.plugins.config.ts",
"test/vitest/vitest.extension-telegram-paths.mjs",
"test/vitest/vitest.extension-telegram.config.ts",
"test/vitest/vitest.extension-voice-call-paths.mjs",
"test/vitest/vitest.extension-voice-call.config.ts",
"test/vitest/vitest.extension-whatsapp-paths.mjs",
"test/vitest/vitest.extension-whatsapp.config.ts",
"test/vitest/vitest.extension-zalo-paths.mjs",
"test/vitest/vitest.extension-zalo.config.ts",
"test/vitest/vitest.extension-provider-paths.mjs",
"test/vitest/vitest.extension-providers.config.ts",
"test/vitest/vitest.logging.config.ts",
"test/vitest/vitest.process.config.ts",
"test/vitest/vitest.tasks.config.ts",
"test/vitest/vitest.wizard.config.ts",
],
include: [
"src/**/*.test.ts",
BUNDLED_PLUGIN_TEST_GLOB,
"packages/**/*.test.ts",
"test/**/*.test.ts",
"ui/src/ui/app-chat.test.ts",
"ui/src/ui/chat/**/*.test.ts",
"ui/src/ui/views/agents-utils.test.ts",
"ui/src/ui/views/channels.test.ts",
"ui/src/ui/views/chat.test.ts",
"ui/src/ui/views/nodes.devices.test.ts",
"ui/src/ui/views/skills.test.ts",
"ui/src/ui/views/dreams.test.ts",
"ui/src/ui/views/usage-render-details.test.ts",
"ui/src/ui/controllers/agents.test.ts",
"ui/src/ui/controllers/chat.test.ts",
"ui/src/ui/controllers/skills.test.ts",
"ui/src/ui/controllers/sessions.test.ts",
"ui/src/ui/views/sessions.test.ts",
"ui/src/ui/app-tool-stream.node.test.ts",
"ui/src/ui/app-gateway.sessions.node.test.ts",
"ui/src/ui/chat/slash-command-executor.node.test.ts",
],
setupFiles: [resolveRepoRootPath("test/setup.ts")],
exclude: [
"dist/**",
"test/fixtures/**",
"apps/macos/**",
"apps/macos/.build/**",
"**/node_modules/**",
"**/vendor/**",
"dist/OpenClaw.app/**",
"**/*.live.test.ts",
"**/*.e2e.test.ts",
],
coverage: {
provider: "v8" as const,
reporter: ["text", "lcov"],
all: false,
thresholds: {
lines: 70,
functions: 70,
branches: 55,
statements: 70,
},
include: ["./src/**/*.ts"],
exclude: [
`${BUNDLED_PLUGIN_ROOT_DIR}/**`,
"apps/**",
"ui/**",
"test/**",
"src/**/*.test.ts",
"src/entry.ts",
"src/index.ts",
"src/runtime.ts",
"src/channel-web.ts",
"src/logging.ts",
"src/cli/**",
"src/commands/**",
"src/daemon/**",
"src/hooks/**",
"src/macos/**",
"src/acp/**",
"src/agents/**",
"src/channels/**",
"src/gateway/**",
"src/line/**",
"src/media-understanding/**",
"src/node-host/**",
"src/plugins/**",
"src/providers/**",
"src/secrets/**",
"src/agents/model-scan.ts",
"src/agents/pi-embedded-runner.ts",
"src/agents/sandbox-paths.ts",
"src/agents/sandbox.ts",
"src/agents/skills-install.ts",
"src/agents/pi-tool-definition-adapter.ts",
"src/agents/tools/discord-actions*.ts",
"src/agents/tools/slack-actions.ts",
"src/infra/state-migrations.ts",
"src/infra/skills-remote.ts",
"src/infra/update-check.ts",
"src/infra/ports-inspect.ts",
"src/infra/outbound/outbound-session.ts",
"src/memory/batch-gemini.ts",
"src/gateway/control-ui.ts",
"src/gateway/server-bridge.ts",
"src/gateway/server-channels.ts",
"src/gateway/server-methods/config.ts",
"src/gateway/server-methods/send.ts",
"src/gateway/server-methods/skills.ts",
"src/gateway/server-methods/talk.ts",
"src/gateway/server-methods/web.ts",
"src/gateway/server-methods/wizard.ts",
"src/gateway/call.ts",
"src/process/tau-rpc.ts",
"src/process/exec.ts",
"src/tui/**",
"src/wizard/**",
"src/browser/**",
"src/channels/web/**",
"src/webchat/**",
"src/gateway/server.ts",
"src/gateway/client.ts",
"src/gateway/protocol/**",
"src/infra/tailscale.ts",
],
},
...loadVitestExperimentalConfig(),
},
};

View file

@ -0,0 +1,122 @@
import { spawnSync } from "node:child_process";
type EnvMap = Record<string, string | undefined>;
export type VitestProcessStats = {
otherVitestRootCount: number;
otherVitestWorkerCount: number;
otherVitestCpuPercent: number;
};
type PsResult = {
status: number | null;
stdout: string;
};
type DetectVitestProcessStatsOptions = {
platform?: NodeJS.Platform;
selfPid?: number;
runPs?: () => PsResult;
};
const EMPTY_VITEST_PROCESS_STATS: VitestProcessStats = {
otherVitestRootCount: 0,
otherVitestWorkerCount: 0,
otherVitestCpuPercent: 0,
};
const BOOLEAN_TRUE_VALUES = new Set(["1", "true"]);
function isExplicitlyEnabled(value: string | undefined): boolean {
const normalized = value?.trim().toLowerCase();
return normalized ? BOOLEAN_TRUE_VALUES.has(normalized) : false;
}
function isVitestWorkerArgs(args: string): boolean {
return args.includes("/vitest/dist/workers/") || args.includes("\\vitest\\dist\\workers\\");
}
function isVitestRootArgs(args: string): boolean {
return (
args.includes("node_modules/.bin/vitest") ||
/\bvitest(?:\.(?:m?js|cmd|exe))?\b/u.test(args) ||
args.includes("scripts/test-projects.mjs") ||
args.includes("scripts/run-vitest.mjs")
);
}
function normalizeCpu(rawCpu: string): number {
const parsed = Number.parseFloat(rawCpu);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
export function parseVitestProcessStats(
psOutput: string,
selfPid: number = process.pid,
): VitestProcessStats {
const stats = { ...EMPTY_VITEST_PROCESS_STATS };
for (const line of psOutput.split("\n")) {
const trimmed = line.trim();
if (trimmed.length === 0) {
continue;
}
const match = /^(\d+)\s+([0-9.]+)\s+(.*)$/u.exec(trimmed);
if (!match) {
continue;
}
const [, rawPid, rawCpu, args] = match;
const pid = Number.parseInt(rawPid, 10);
if (!Number.isFinite(pid) || pid === selfPid) {
continue;
}
if (!isVitestWorkerArgs(args) && !isVitestRootArgs(args)) {
continue;
}
stats.otherVitestCpuPercent += normalizeCpu(rawCpu);
if (isVitestWorkerArgs(args)) {
stats.otherVitestWorkerCount += 1;
} else {
stats.otherVitestRootCount += 1;
}
}
stats.otherVitestCpuPercent = Number.parseFloat(stats.otherVitestCpuPercent.toFixed(1));
return stats;
}
export function detectVitestProcessStats(
env: EnvMap = process.env,
options: DetectVitestProcessStatsOptions = {},
): VitestProcessStats {
const platform = options.platform ?? process.platform;
if (platform === "win32") {
return { ...EMPTY_VITEST_PROCESS_STATS };
}
if (isExplicitlyEnabled(env.OPENCLAW_VITEST_DISABLE_SYSTEM_THROTTLE)) {
return { ...EMPTY_VITEST_PROCESS_STATS };
}
const result =
options.runPs?.() ??
spawnSync("ps", ["-xao", "pid=,pcpu=,args="], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
if (result.status === 0 && typeof result.stdout === "string" && result.stdout.length > 0) {
return parseVitestProcessStats(result.stdout, options.selfPid ?? process.pid);
}
return { ...EMPTY_VITEST_PROCESS_STATS };
}
export function shouldPrintVitestThrottle(env: EnvMap = process.env): boolean {
const normalized = env.OPENCLAW_VITEST_PRINT_SYSTEM_THROTTLE?.trim().toLowerCase();
return normalized ? BOOLEAN_TRUE_VALUES.has(normalized) : false;
}

View file

@ -0,0 +1,25 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createTasksVitestConfig(env?: Record<string, string | undefined>) {
const config = createScopedVitestConfig(["src/tasks/**/*.test.ts"], {
dir: "src",
env,
name: "tasks",
passWithNoTests: true,
});
// Task tests mutate process.env and shared singleton registries/state dirs.
// Keep files serialized so temp-dir-backed sqlite stores do not fight each
// other under the non-isolated runner.
config.test = {
...config.test,
maxWorkers: 1,
fileParallelism: false,
sequence: {
...config.test?.sequence,
groupOrder: 1,
},
};
return config;
}
export default createTasksVitestConfig();

View file

@ -0,0 +1,126 @@
export const autoReplyCoreTestInclude = ["src/auto-reply/*.test.ts"];
export const autoReplyCoreTestExclude = ["src/auto-reply/reply*.test.ts"];
export const autoReplyTopLevelReplyTestInclude = ["src/auto-reply/reply*.test.ts"];
export const autoReplyReplySubtreeTestInclude = ["src/auto-reply/reply/**/*.test.ts"];
export const fullSuiteVitestShards = [
{
config: "test/vitest/vitest.full-core-unit-fast.config.ts",
name: "core-unit-fast",
projects: ["test/vitest/vitest.unit-fast.config.ts"],
},
{
config: "test/vitest/vitest.full-core-unit-src.config.ts",
name: "core-unit-src",
projects: ["test/vitest/vitest.unit-src.config.ts"],
},
{
config: "test/vitest/vitest.full-core-unit-security.config.ts",
name: "core-unit-security",
projects: ["test/vitest/vitest.unit-security.config.ts"],
},
{
config: "test/vitest/vitest.full-core-unit-ui.config.ts",
name: "core-unit-ui",
projects: ["test/vitest/vitest.unit-ui.config.ts"],
},
{
config: "test/vitest/vitest.full-core-unit-support.config.ts",
name: "core-unit-support",
projects: ["test/vitest/vitest.unit-support.config.ts"],
},
{
config: "test/vitest/vitest.full-core-support-boundary.config.ts",
name: "core-support-boundary",
projects: ["test/vitest/vitest.boundary.config.ts", "test/vitest/vitest.tooling.config.ts"],
},
{
config: "test/vitest/vitest.full-core-contracts.config.ts",
name: "core-contracts",
projects: ["test/vitest/vitest.contracts.config.ts"],
},
{
config: "test/vitest/vitest.full-core-bundled.config.ts",
name: "core-bundled",
projects: ["test/vitest/vitest.bundled.config.ts"],
},
{
config: "test/vitest/vitest.full-core-runtime.config.ts",
name: "core-runtime",
projects: [
"test/vitest/vitest.infra.config.ts",
"test/vitest/vitest.hooks.config.ts",
"test/vitest/vitest.acp.config.ts",
"test/vitest/vitest.runtime-config.config.ts",
"test/vitest/vitest.secrets.config.ts",
"test/vitest/vitest.logging.config.ts",
"test/vitest/vitest.process.config.ts",
"test/vitest/vitest.cron.config.ts",
"test/vitest/vitest.media.config.ts",
"test/vitest/vitest.media-understanding.config.ts",
"test/vitest/vitest.shared-core.config.ts",
"test/vitest/vitest.tasks.config.ts",
"test/vitest/vitest.tui.config.ts",
"test/vitest/vitest.ui.config.ts",
"test/vitest/vitest.utils.config.ts",
"test/vitest/vitest.wizard.config.ts",
],
},
{
config: "test/vitest/vitest.full-agentic.config.ts",
name: "agentic",
projects: [
"test/vitest/vitest.gateway-core.config.ts",
"test/vitest/vitest.gateway-client.config.ts",
"test/vitest/vitest.gateway-methods.config.ts",
"test/vitest/vitest.gateway-server.config.ts",
"test/vitest/vitest.cli.config.ts",
"test/vitest/vitest.commands-light.config.ts",
"test/vitest/vitest.commands.config.ts",
"test/vitest/vitest.agents.config.ts",
"test/vitest/vitest.daemon.config.ts",
"test/vitest/vitest.plugin-sdk-light.config.ts",
"test/vitest/vitest.plugin-sdk.config.ts",
"test/vitest/vitest.plugins.config.ts",
"test/vitest/vitest.channels.config.ts",
],
},
{
config: "test/vitest/vitest.full-auto-reply.config.ts",
name: "auto-reply",
projects: [
"test/vitest/vitest.auto-reply-core.config.ts",
"test/vitest/vitest.auto-reply-top-level.config.ts",
"test/vitest/vitest.auto-reply-reply.config.ts",
],
},
{
config: "test/vitest/vitest.full-extensions.config.ts",
name: "extensions",
projects: [
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-channels.config.ts",
"test/vitest/vitest.extension-diffs.config.ts",
"test/vitest/vitest.extension-feishu.config.ts",
"test/vitest/vitest.extension-irc.config.ts",
"test/vitest/vitest.extension-mattermost.config.ts",
"test/vitest/vitest.extension-matrix.config.ts",
"test/vitest/vitest.extension-memory.config.ts",
"test/vitest/vitest.extension-messaging.config.ts",
"test/vitest/vitest.extension-msteams.config.ts",
"test/vitest/vitest.extension-providers.config.ts",
"test/vitest/vitest.extension-telegram.config.ts",
"test/vitest/vitest.extension-voice-call.config.ts",
"test/vitest/vitest.extension-whatsapp.config.ts",
"test/vitest/vitest.extension-zalo.config.ts",
"test/vitest/vitest.extension-browser.config.ts",
"test/vitest/vitest.extension-qa.config.ts",
"test/vitest/vitest.extension-media.config.ts",
"test/vitest/vitest.extension-misc.config.ts",
],
},
];

View file

@ -0,0 +1,27 @@
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
}
export function createToolingVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ?? [
"test/**/*.test.ts",
"src/scripts/**/*.test.ts",
"src/config/doc-baseline.integration.test.ts",
"src/config/schema.base.generated.test.ts",
"src/config/schema.help.quality.test.ts",
],
{
env,
name: "tooling",
passWithNoTests: true,
},
);
}
export default createToolingVitestConfig();

View file

@ -0,0 +1,12 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export function createTuiVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/tui/**/*.test.ts"], {
dir: "src",
env,
name: "tui",
passWithNoTests: true,
});
}
export default createTuiVitestConfig();

View file

@ -0,0 +1,33 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { jsdomOptimizedDeps } from "./vitest.shared.config.ts";
export const unitUiIncludePatterns = [
"ui/src/ui/app-chat.test.ts",
"ui/src/ui/chat/**/*.test.ts",
"ui/src/ui/views/agents-utils.test.ts",
"ui/src/ui/views/channels.test.ts",
"ui/src/ui/views/chat.test.ts",
"ui/src/ui/views/dreams.test.ts",
"ui/src/ui/views/usage-render-details.test.ts",
"ui/src/ui/controllers/agents.test.ts",
"ui/src/ui/controllers/chat.test.ts",
];
export function createUiVitestConfig(
env?: Record<string, string | undefined>,
options?: { includePatterns?: string[]; name?: string },
) {
return createScopedVitestConfig(options?.includePatterns ?? ["ui/src/ui/**/*.test.ts"], {
deps: jsdomOptimizedDeps,
dir: "ui/src/ui",
environment: "jsdom",
env,
excludeUnitFastTests: false,
includeOpenClawRuntimeSetup: false,
isolate: true,
name: options?.name ?? "ui",
setupFiles: ["ui/src/test-helpers/lit-warnings.setup.ts"],
});
}
export default createUiVitestConfig();

View file

@ -0,0 +1,268 @@
import fs from "node:fs";
import path from "node:path";
import {
commandsLightSourceFiles,
commandsLightTestFiles,
} from "./vitest.commands-light-paths.mjs";
import { pluginSdkLightSourceFiles, pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
const normalizeRepoPath = (value) => value.replaceAll("\\", "/");
const unitFastCandidateGlobs = [
"packages/memory-host-sdk/**/*.test.ts",
"packages/plugin-package-contract/**/*.test.ts",
"src/acp/**/*.test.ts",
"src/agents/**/*.test.ts",
"src/auto-reply/**/*.test.ts",
"src/bootstrap/**/*.test.ts",
"src/channels/**/*.test.ts",
"src/cli/**/*.test.ts",
"src/commands/**/*.test.ts",
"src/config/**/*.test.ts",
"src/daemon/**/*.test.ts",
"src/i18n/**/*.test.ts",
"src/hooks/**/*.test.ts",
"src/image-generation/**/*.test.ts",
"src/infra/**/*.test.ts",
"src/interactive/**/*.test.ts",
"src/link-understanding/**/*.test.ts",
"src/logging/**/*.test.ts",
"src/markdown/**/*.test.ts",
"src/media/**/*.test.ts",
"src/media-generation/**/*.test.ts",
"src/media-understanding/**/*.test.ts",
"src/memory-host-sdk/**/*.test.ts",
"src/music-generation/**/*.test.ts",
"src/node-host/**/*.test.ts",
"src/plugin-sdk/**/*.test.ts",
"src/plugins/**/*.test.ts",
"src/poll-params.test.ts",
"src/polls.test.ts",
"src/process/**/*.test.ts",
"src/routing/**/*.test.ts",
"src/sessions/**/*.test.ts",
"src/shared/**/*.test.ts",
"src/terminal/**/*.test.ts",
"src/test-utils/**/*.test.ts",
"src/tasks/**/*.test.ts",
"src/tts/**/*.test.ts",
"src/utils/**/*.test.ts",
"src/video-generation/**/*.test.ts",
"src/wizard/**/*.test.ts",
"test/**/*.test.ts",
];
const unitFastCandidateExactFiles = [...pluginSdkLightTestFiles, ...commandsLightTestFiles];
const broadUnitFastCandidateGlobs = [
"src/**/*.test.ts",
"packages/**/*.test.ts",
"test/**/*.test.ts",
];
const broadUnitFastCandidateSkipGlobs = [
"**/*.e2e.test.ts",
"**/*.live.test.ts",
"test/fixtures/**/*.test.ts",
"test/setup-home-isolation.test.ts",
"src/agents/sandbox.resolveSandboxContext.test.ts",
"src/channels/plugins/contracts/**/*.test.ts",
"src/config/**/*.test.ts",
"src/gateway/**/*.test.ts",
"src/media-generation/**/*.contract.test.ts",
"src/plugins/contracts/**/*.test.ts",
"src/plugin-sdk/browser-subpaths.test.ts",
"src/security/**/*.test.ts",
"src/secrets/**/*.test.ts",
];
const disqualifyingPatterns = [
{
code: "jsdom-environment",
pattern: /@vitest-environment\s+jsdom/u,
},
{
code: "module-mocking",
pattern: /\bvi\.(?:mock|doMock|unmock|doUnmock|importActual|resetModules)\s*\(/u,
},
{
code: "module-mocking-helper",
pattern: /runtime-module-mocks/u,
},
{
code: "vitest-mock-api",
pattern: /\bvi\b/u,
},
{
code: "dynamic-import",
pattern: /\b(?:await\s+)?import\s*\(/u,
},
{
code: "fake-timers",
pattern:
/\bvi\.(?:useFakeTimers|setSystemTime|advanceTimers|runAllTimers|runOnlyPendingTimers)\s*\(/u,
},
{
code: "env-or-global-stub",
pattern: /\bvi\.(?:stubEnv|stubGlobal|unstubAllEnvs|unstubAllGlobals)\s*\(/u,
},
{
code: "process-env-mutation",
pattern: /(?:process\.env(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])?\s*=|delete\s+process\.env)/u,
},
{
code: "global-mutation",
pattern: /(?:globalThis|global)\s*\[[^\]]+\]\s*=/u,
},
{
code: "filesystem-state",
pattern:
/\b(?:mkdtemp|rmSync|writeFileSync|appendFileSync|mkdirSync|createTemp|makeTempDir|tempDir|tmpdir|node:fs|node:os)\b/u,
},
{
code: "runtime-singleton-state",
pattern: /\b(?:setActivePluginRegistry|resetPluginRuntimeStateForTest|reset.*ForTest)\s*\(/u,
},
];
function matchesAnyGlob(file, patterns) {
return patterns.some((pattern) => path.matchesGlob(file, pattern));
}
function walkFiles(directory, files = []) {
let entries;
try {
entries = fs.readdirSync(directory, { withFileTypes: true });
} catch {
return files;
}
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "vendor") {
continue;
}
walkFiles(entryPath, files);
continue;
}
if (entry.isFile() && entry.name.endsWith(".test.ts")) {
files.push(normalizeRepoPath(entryPath));
}
}
return files;
}
export function classifyUnitFastTestFileContent(source) {
const reasons = [];
for (const { code, pattern } of disqualifyingPatterns) {
if (pattern.test(source)) {
reasons.push(code);
}
}
return reasons;
}
export function collectUnitFastTestCandidates(cwd = process.cwd()) {
const discovered = ["src", "packages", "test"]
.flatMap((directory) => walkFiles(path.join(cwd, directory)))
.map((file) => normalizeRepoPath(path.relative(cwd, file)))
.filter(
(file) =>
matchesAnyGlob(file, unitFastCandidateGlobs) &&
!matchesAnyGlob(file, broadUnitFastCandidateSkipGlobs),
);
return [...new Set([...discovered, ...unitFastCandidateExactFiles])].toSorted((a, b) =>
a.localeCompare(b),
);
}
export function collectBroadUnitFastTestCandidates(cwd = process.cwd()) {
const discovered = ["src", "packages", "test"]
.flatMap((directory) => walkFiles(path.join(cwd, directory)))
.map((file) => normalizeRepoPath(path.relative(cwd, file)))
.filter(
(file) =>
matchesAnyGlob(file, broadUnitFastCandidateGlobs) &&
!matchesAnyGlob(file, broadUnitFastCandidateSkipGlobs),
);
return [...new Set([...discovered, ...unitFastCandidateExactFiles])].toSorted((a, b) =>
a.localeCompare(b),
);
}
export function collectUnitFastTestFileAnalysis(cwd = process.cwd(), options = {}) {
const candidates =
options.scope === "broad"
? collectBroadUnitFastTestCandidates(cwd)
: collectUnitFastTestCandidates(cwd);
return candidates.map((file) => {
const absolutePath = path.join(cwd, file);
let source = "";
try {
source = fs.readFileSync(absolutePath, "utf8");
} catch {
return {
file,
unitFast: false,
reasons: ["missing-file"],
};
}
const reasons = classifyUnitFastTestFileContent(source);
return {
file,
unitFast: reasons.length === 0,
reasons,
};
});
}
let cachedUnitFastTestFiles = null;
let cachedUnitFastTestFileSet = null;
let cachedSourceToUnitFastTestFile = null;
export function getUnitFastTestFiles() {
if (cachedUnitFastTestFiles !== null) {
return cachedUnitFastTestFiles;
}
cachedUnitFastTestFiles = collectUnitFastTestFileAnalysis()
.filter((entry) => entry.unitFast)
.map((entry) => entry.file);
return cachedUnitFastTestFiles;
}
function getUnitFastTestFileSet() {
if (cachedUnitFastTestFileSet !== null) {
return cachedUnitFastTestFileSet;
}
cachedUnitFastTestFileSet = new Set(getUnitFastTestFiles());
return cachedUnitFastTestFileSet;
}
function getSourceToUnitFastTestFile() {
if (cachedSourceToUnitFastTestFile !== null) {
return cachedSourceToUnitFastTestFile;
}
const unitFastTestFileSet = getUnitFastTestFileSet();
cachedSourceToUnitFastTestFile = new Map(
[...pluginSdkLightSourceFiles, ...commandsLightSourceFiles].flatMap((sourceFile) => {
const testFile = sourceFile.replace(/\.ts$/u, ".test.ts");
return unitFastTestFileSet.has(testFile) ? [[sourceFile, testFile]] : [];
}),
);
return cachedSourceToUnitFastTestFile;
}
export function isUnitFastTestFile(file) {
return getUnitFastTestFileSet().has(normalizeRepoPath(file));
}
export function resolveUnitFastTestIncludePattern(file) {
const normalized = normalizeRepoPath(file);
const unitFastTestFileSet = getUnitFastTestFileSet();
if (unitFastTestFileSet.has(normalized)) {
return normalized;
}
const siblingTestFile = normalized.replace(/\.ts$/u, ".test.ts");
if (unitFastTestFileSet.has(siblingTestFile)) {
return siblingTestFile;
}
return getSourceToUnitFastTestFile().get(normalized) ?? null;
}

Some files were not shown because too many files have changed in this diff Show more