From 55e43cbc7f2927bcde6a8dbe101b62ebc84d07fa Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Apr 2026 10:56:55 +0100 Subject: [PATCH] test: isolate bundled plugin coverage from unit --- package.json | 1 + scripts/test-parallel.mjs | 2 +- scripts/test-planner/catalog.mjs | 10 +- scripts/test-planner/planner.mjs | 90 ++++++- src/infra/matrix.test-helpers.ts | 1 + src/plugins/bundled-dir.test.ts | 31 +++ src/plugins/bundled-dir.ts | 17 ++ src/plugins/bundled-web-search.test.ts | 242 +++++++++++++----- .../cli.browser-plugin.integration.test.ts | 7 + src/plugins/discovery.test.ts | 1 + .../stage-bundled-plugin-runtime.test.ts | 1 + src/plugins/web-search-providers.test.ts | 136 +++++++++- src/secrets/runtime.coverage.test.ts | 56 +++- src/test-utils/env.ts | 2 + src/tts/provider-registry.test.ts | 13 + test/scripts/test-planner.test.ts | 60 ++++- test/vitest-unit-paths.test.ts | 3 + vitest.bundled.config.ts | 14 + vitest.config.ts | 1 + vitest.unit-paths.mjs | 11 + vitest.unit.config.ts | 19 +- 21 files changed, 629 insertions(+), 89 deletions(-) create mode 100644 vitest.bundled.config.ts diff --git a/package.json b/package.json index 935a2cb397d..05d6e1f02b7 100644 --- a/package.json +++ b/package.json @@ -1047,6 +1047,7 @@ "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all", "test:auth:compat": "vitest run --config vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts", "test:build:singleton": "node scripts/test-built-plugin-singleton.mjs", + "test:bundled": "node scripts/test-parallel.mjs --surface bundled", "test:changed": "pnpm test -- --changed origin/main", "test:changed:max": "node scripts/test-parallel.mjs --profile max --changed origin/main", "test:channels": "node scripts/test-parallel.mjs --surface channels", diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index c4c11744a5b..fce10459b16 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -139,7 +139,7 @@ if (rawCli.showHelp) { " --plan Print the resolved execution plan and exit", " --ci-manifest Print the planner-backed CI execution manifest as JSON and exit", " --explain Explain how a file is classified and run, then exit", - " --surface Select a surface: unit, extensions, channels, contracts, gateway", + " --surface Select a surface: unit, bundled, extensions, channels, contracts, gateway", " --files Add targeted files or path patterns (repeatable)", " --mode Override runtime mode", " --profile Override execution intent: normal, max, serial", diff --git a/scripts/test-planner/catalog.mjs b/scripts/test-planner/catalog.mjs index 4c1ca5b4505..958e7342ad7 100644 --- a/scripts/test-planner/catalog.mjs +++ b/scripts/test-planner/catalog.mjs @@ -1,7 +1,10 @@ import fs from "node:fs"; import path from "node:path"; import { channelTestPrefixes } from "../../vitest.channel-paths.mjs"; -import { isUnitConfigTestFile } from "../../vitest.unit-paths.mjs"; +import { + isBundledPluginDependentUnitTestFile, + isUnitConfigTestFile, +} from "../../vitest.unit-paths.mjs"; import { BUNDLED_PLUGIN_PATH_PREFIX, BUNDLED_PLUGIN_ROOT_DIR, @@ -111,7 +114,9 @@ export function loadTestCatalog() { } let surface = "base"; - if (isUnitConfigTestFile(normalizedFile)) { + if (isBundledPluginDependentUnitTestFile(normalizedFile)) { + surface = "bundled"; + } else if (isUnitConfigTestFile(normalizedFile)) { surface = "unit"; } else if (contractTestPrefixes.some((prefix) => normalizedFile.startsWith(prefix))) { surface = "contracts"; @@ -206,6 +211,7 @@ export function loadTestCatalog() { export const testSurfaces = [ "unit", + "bundled", "extensions", "channels", "contracts", diff --git a/scripts/test-planner/planner.mjs b/scripts/test-planner/planner.mjs index 5ae9ec3be57..7a285d9747e 100644 --- a/scripts/test-planner/planner.mjs +++ b/scripts/test-planner/planner.mjs @@ -1,5 +1,9 @@ import path from "node:path"; -import { isUnitConfigTestFile } from "../../vitest.unit-paths.mjs"; +import { + bundledPluginDependentUnitTestFiles, + isBundledPluginDependentUnitTestFile, + isUnitConfigTestFile, +} from "../../vitest.unit-paths.mjs"; import { BUNDLED_PLUGIN_PATH_PREFIX } from "../lib/bundled-plugin-paths.mjs"; import { loadChannelTimingManifest, @@ -112,14 +116,21 @@ const normalizeSurfaces = (values = []) => [ ), ]; -const EXPLICIT_PLAN_SURFACES = new Set(["unit", "extensions", "channels", "contracts", "gateway"]); +const EXPLICIT_PLAN_SURFACES = new Set([ + "unit", + "bundled", + "extensions", + "channels", + "contracts", + "gateway", +]); const FAILURE_POLICIES = new Set(["fail-fast", "collect-all"]); const validateExplicitSurfaces = (surfaces) => { const invalidSurfaces = surfaces.filter((surface) => !EXPLICIT_PLAN_SURFACES.has(surface)); if (invalidSurfaces.length > 0) { throw new Error( - `Unsupported --surface value(s): ${invalidSurfaces.join(", ")}. Supported surfaces: unit, extensions, channels, contracts, gateway.`, + `Unsupported --surface value(s): ${invalidSurfaces.join(", ")}. Supported surfaces: unit, bundled, extensions, channels, contracts, gateway.`, ); } }; @@ -135,6 +146,9 @@ const buildRequestedSurfaces = (request, env) => { if (!skipDefaultRuns) { surfaces.push("unit"); } + if (env.OPENCLAW_TEST_INCLUDE_BUNDLED === "1") { + surfaces.push("bundled"); + } if (env.OPENCLAW_TEST_INCLUDE_EXTENSIONS === "1") { surfaces.push("extensions"); } @@ -264,6 +278,11 @@ const resolveEntryTimingEstimator = (entry, context) => { context.unitTimingManifest.files[file]?.durationMs ?? context.unitTimingManifest.defaultDurationMs; } + if (config === "vitest.bundled.config.ts") { + return (file) => + context.unitTimingManifest.files[file]?.durationMs ?? + context.unitTimingManifest.defaultDurationMs; + } if (config === "vitest.channels.config.ts") { return (file) => context.channelTimingManifest.files[file]?.durationMs ?? @@ -348,6 +367,9 @@ const resolveMaxWorkersForUnit = (unit, context) => { if (unit.surface === "extensions") { return budget.extensionWorkers; } + if (unit.surface === "bundled") { + return budget.unitSharedWorkers; + } if (unit.surface === "channels") { return budget.channelSharedWorkers ?? budget.unitSharedWorkers; } @@ -392,6 +414,11 @@ const withIncludeFileEnv = (context, unitId, files) => ({ OPENCLAW_VITEST_INCLUDE_FILE: context.writeTempJsonArtifact(unitId, files), }); +const withBundledPluginsDisabled = (unitEnv) => ({ + ...unitEnv, + OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", +}); + const resolveUnitHeavyFileGroups = (context) => { const { env, runtime, executionBudget, catalog, unitTimingManifest, unitMemoryHotspotManifest } = context; @@ -509,6 +536,7 @@ const buildDefaultUnits = (context, request) => { const selectedSurfaces = buildRequestedSurfaces(request, env); const selectedSurfaceSet = new Set(selectedSurfaces); const unitOnlyRun = selectedSurfaceSet.size === 1 && selectedSurfaceSet.has("unit"); + const bundledOnlyRun = selectedSurfaceSet.size === 1 && selectedSurfaceSet.has("bundled"); const channelsOnlyRun = selectedSurfaceSet.size === 1 && selectedSurfaceSet.has("channels"); const contractsOnlyRun = selectedSurfaceSet.size === 1 && selectedSurfaceSet.has("contracts"); const extensionsOnlyRun = selectedSurfaceSet.size === 1 && selectedSurfaceSet.has("extensions"); @@ -631,10 +659,12 @@ const buildDefaultUnits = (context, request) => { batch, context, ), - env: withIncludeFileEnv( - context, - `vitest-unit-fast-include-${String(laneIndex + 1)}-${String(batchIndex + 1)}`, - batch, + env: withBundledPluginsDisabled( + withIncludeFileEnv( + context, + `vitest-unit-fast-include-${String(laneIndex + 1)}-${String(batchIndex + 1)}`, + batch, + ), ), args: [ "vitest", @@ -657,6 +687,7 @@ const buildDefaultUnits = (context, request) => { surface: "unit", isolate: true, estimatedDurationMs: estimateUnitDurationMs(file), + env: withBundledPluginsDisabled(), args: [ "vitest", "run", @@ -683,6 +714,7 @@ const buildDefaultUnits = (context, request) => { surface: "unit", isolate: false, estimatedDurationMs: files.reduce((sum, file) => sum + estimateUnitDurationMs(file), 0), + env: withBundledPluginsDisabled(), args: [ "vitest", "run", @@ -704,6 +736,7 @@ const buildDefaultUnits = (context, request) => { surface: "unit", isolate: true, estimatedDurationMs: estimateUnitDurationMs(file), + env: withBundledPluginsDisabled(), args: [ "vitest", "run", @@ -724,6 +757,7 @@ const buildDefaultUnits = (context, request) => { id: "unit-pinned", surface: "unit", isolate: false, + env: withBundledPluginsDisabled(), args: [ "vitest", "run", @@ -739,6 +773,23 @@ const buildDefaultUnits = (context, request) => { } } + if (selectedSurfaceSet.has("bundled")) { + units.push( + createExecutionUnit(context, { + id: "bundled", + surface: "bundled", + isolate: false, + serialPhase: bundledOnlyRun ? undefined : "bundled", + estimatedDurationMs: bundledPluginDependentUnitTestFiles.reduce( + (sum, file) => sum + estimateUnitDurationMs(file), + 0, + ), + args: ["vitest", "run", "--config", "vitest.bundled.config.ts", ...noIsolateArgs], + reasons: ["bundled-surface"], + }), + ); + } + if (selectedSurfaceSet.has("channels")) { for (const file of catalog.channelIsolatedFiles) { units.push( @@ -951,6 +1002,17 @@ const createTargetedUnit = (context, classification, filters) => { ...filters, ]; } + if (owner === "bundled") { + return [ + "vitest", + "run", + "--config", + "vitest.bundled.config.ts", + ...(classification.isolated ? ["--pool=forks"] : []), + ...context.noIsolateArgs, + ...filters, + ]; + } if (owner === "base-pinned") { return [ "vitest", @@ -1040,6 +1102,7 @@ const createTargetedUnit = (context, classification, filters) => { surface: classification.legacyBasePinned ? "base" : classification.surface, isolate: classification.isolated || owner === "base-pinned", args, + env: owner === "unit" ? withBundledPluginsDisabled() : undefined, reasons: classification.reasons, }); }; @@ -1250,6 +1313,13 @@ const estimateTopLevelEntryDurationMs = (unit, context) => { context.unitTimingManifest.defaultDurationMs) ); } + if (isBundledPluginDependentUnitTestFile(file)) { + return ( + totalMs + + (context.unitTimingManifest.files[file]?.durationMs ?? + context.unitTimingManifest.defaultDurationMs) + ); + } if (context.catalog.channelTestPrefixes.some((prefix) => file.startsWith(prefix))) { return ( totalMs + @@ -1374,6 +1444,12 @@ export function buildCIExecutionManifest(scopeInput = {}, options = {}) { const checksFastInclude = nodeEligible ? [ + { + check_name: "checks-fast-bundled", + runtime: "node", + task: "bundled", + command: "pnpm test:bundled", + }, ...createShardMatrixEntries({ checkNamePrefix: "checks-fast-extensions", runtime: "node", diff --git a/src/infra/matrix.test-helpers.ts b/src/infra/matrix.test-helpers.ts index b417935c58d..0737aa3e04e 100644 --- a/src/infra/matrix.test-helpers.ts +++ b/src/infra/matrix.test-helpers.ts @@ -12,6 +12,7 @@ export const MATRIX_OPS_DEVICE_ID = "DEVICEOPS"; export const matrixHelperEnv = { OPENCLAW_BUNDLED_PLUGINS_DIR: (home: string) => path.join(home, "bundled"), + OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined, OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1", OPENCLAW_VERSION: undefined, VITEST: "true", diff --git a/src/plugins/bundled-dir.test.ts b/src/plugins/bundled-dir.test.ts index 473c1484837..8f3464fa8af 100644 --- a/src/plugins/bundled-dir.test.ts +++ b/src/plugins/bundled-dir.test.ts @@ -6,6 +6,7 @@ import { resolveBundledPluginsDir } from "./bundled-dir.js"; const tempDirs: string[] = []; const originalBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; +const originalDisableBundledPlugins = process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS; const originalVitest = process.env.VITEST; const originalArgv1 = process.argv[1]; @@ -52,6 +53,7 @@ function expectResolvedBundledDir(params: { expectedDir: string; argv1?: string; bundledDirOverride?: string; + disableBundledPlugins?: string; vitest?: string; }) { vi.spyOn(process, "cwd").mockReturnValue(params.cwd); @@ -66,6 +68,11 @@ function expectResolvedBundledDir(params: { } else { process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = params.bundledDirOverride; } + if (params.disableBundledPlugins === undefined) { + delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS; + } else { + process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = params.disableBundledPlugins; + } expect(fs.realpathSync(resolveBundledPluginsDir() ?? "")).toBe( fs.realpathSync(params.expectedDir), @@ -122,6 +129,11 @@ afterEach(() => { } else { process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledDir; } + if (originalDisableBundledPlugins === undefined) { + delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS; + } else { + process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = originalDisableBundledPlugins; + } if (originalVitest === undefined) { delete process.env.VITEST; } else { @@ -192,6 +204,25 @@ describe("resolveBundledPluginsDir", () => { }); }); + it("returns a stable empty bundled plugin directory when bundled plugins are disabled", () => { + const repoRoot = createOpenClawRoot({ + prefix: "openclaw-bundled-dir-disabled-", + hasExtensions: true, + hasSrc: true, + hasGitCheckout: true, + }); + vi.spyOn(process, "cwd").mockReturnValue(repoRoot); + process.argv[1] = "/usr/bin/env"; + process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1"; + delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + + const bundledDir = resolveBundledPluginsDir(); + + expect(bundledDir).toBeTruthy(); + expect(fs.existsSync(bundledDir ?? "")).toBe(true); + expect(fs.readdirSync(bundledDir ?? "")).toEqual([]); + }); + it.each([ { name: "prefers the running CLI package root over an unrelated cwd checkout", diff --git a/src/plugins/bundled-dir.ts b/src/plugins/bundled-dir.ts index ed7fc4d1db9..f66959acd7b 100644 --- a/src/plugins/bundled-dir.ts +++ b/src/plugins/bundled-dir.ts @@ -1,9 +1,22 @@ import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; import { resolveUserPath } from "../utils.js"; +const DISABLED_BUNDLED_PLUGINS_DIR = path.join(os.tmpdir(), "openclaw-empty-bundled-plugins"); + +function bundledPluginsDisabled(env: NodeJS.ProcessEnv): boolean { + const raw = env.OPENCLAW_DISABLE_BUNDLED_PLUGINS?.trim().toLowerCase(); + return raw === "1" || raw === "true"; +} + +function resolveDisabledBundledPluginsDir(): string { + fs.mkdirSync(DISABLED_BUNDLED_PLUGINS_DIR, { recursive: true }); + return DISABLED_BUNDLED_PLUGINS_DIR; +} + function isSourceCheckoutRoot(packageRoot: string): boolean { return ( fs.existsSync(path.join(packageRoot, ".git")) && @@ -38,6 +51,10 @@ function resolveBundledDirFromPackageRoot( } export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env): string | undefined { + if (bundledPluginsDisabled(env)) { + return resolveDisabledBundledPluginsDir(); + } + const override = env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim(); if (override) { const resolvedOverride = resolveUserPath(override, env); diff --git a/src/plugins/bundled-web-search.test.ts b/src/plugins/bundled-web-search.test.ts index 2314d074d73..d37f2333594 100644 --- a/src/plugins/bundled-web-search.test.ts +++ b/src/plugins/bundled-web-search.test.ts @@ -1,58 +1,137 @@ -import { beforeAll, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js"; import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "./bundled-web-search-ids.js"; +import { hasBundledWebSearchCredential } from "./bundled-web-search-registry.js"; +import { + listBundledWebSearchPluginIds, + listBundledWebSearchProviders, + resolveBundledWebSearchPluginId, + resolveBundledWebSearchPluginIds, +} from "./bundled-web-search.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; -let hasBundledWebSearchCredential: typeof import("./bundled-web-search-registry.js").hasBundledWebSearchCredential; -let listBundledWebSearchProviders: typeof import("./bundled-web-search.js").listBundledWebSearchProviders; -let resolveBundledWebSearchPluginIds: typeof import("./bundled-web-search.js").resolveBundledWebSearchPluginIds; +vi.mock("./manifest-registry.js", () => ({ + loadPluginManifestRegistry: vi.fn(), +})); -function resolveManifestBundledWebSearchPluginIds() { - return loadPluginManifestRegistry({}) - .plugins.filter( - (plugin) => - plugin.origin === "bundled" && (plugin.contracts?.webSearchProviders?.length ?? 0) > 0, - ) - .map((plugin) => plugin.id) - .toSorted((left, right) => left.localeCompare(right)); -} +vi.mock("./bundled-capability-runtime.js", () => ({ + loadBundledCapabilityRuntimeRegistry: vi.fn(), +})); -async function resolveRegistryBundledWebSearchPluginIds() { - return listBundledWebSearchProviders() - .map(({ pluginId }) => pluginId) - .filter((value, index, values) => values.indexOf(value) === index) - .toSorted((left, right) => left.localeCompare(right)); -} +const resolveBundledPluginWebSearchProvidersMock = vi.hoisted(() => vi.fn()); -beforeAll(async () => { - ({ listBundledWebSearchProviders, resolveBundledWebSearchPluginIds } = - await import("./bundled-web-search.js")); - ({ hasBundledWebSearchCredential } = await import("./bundled-web-search-registry.js")); -}); +vi.mock("./web-search-providers.js", () => ({ + resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock, +})); -function expectBundledWebSearchIds(actual: readonly string[], expected: readonly string[]) { - expect(actual).toEqual(expected); -} - -function expectBundledWebSearchAlignment(params: { - actual: readonly string[]; - expected: readonly string[]; +function createMockedBundledWebSearchProvider(params: { + pluginId: string; + providerId: string; + configuredCredential?: unknown; + scopedCredential?: unknown; + envVars?: string[]; }) { - expectBundledWebSearchIds(params.actual, params.expected); + return { + pluginId: params.pluginId, + id: params.providerId, + label: params.providerId, + hint: `${params.providerId} provider`, + envVars: params.envVars ?? [], + placeholder: `${params.providerId}-key`, + signupUrl: `https://example.com/${params.providerId}`, + autoDetectOrder: 10, + credentialPath: `plugins.entries.${params.pluginId}.config.webSearch.apiKey`, + getCredentialValue: () => params.scopedCredential, + getConfiguredCredentialValue: () => params.configuredCredential, + setCredentialValue: () => {}, + createTool: () => ({ + description: params.providerId, + parameters: {}, + execute: async () => ({}), + }), + }; } -describe("bundled web search metadata", () => { - it("keeps bundled web search compat ids aligned with bundled manifests", async () => { - expectBundledWebSearchAlignment({ - actual: resolveBundledWebSearchPluginIds({}), - expected: resolveManifestBundledWebSearchPluginIds(), +describe("bundled web search helpers", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(loadPluginManifestRegistry).mockReturnValue({ + plugins: [ + { id: "xai", origin: "bundled" }, + { id: "google", origin: "bundled" }, + { id: "noise", origin: "bundled" }, + { id: "external-google", origin: "workspace" }, + ] as never[], + diagnostics: [], + }); + vi.mocked(loadBundledCapabilityRuntimeRegistry).mockReturnValue({ + webSearchProviders: [ + { + pluginId: "xai", + provider: createMockedBundledWebSearchProvider({ + pluginId: "xai", + providerId: "grok", + }), + }, + { + pluginId: "google", + provider: createMockedBundledWebSearchProvider({ + pluginId: "google", + providerId: "gemini", + }), + }, + ], + } as never); + }); + + it("filters bundled manifest entries down to known bundled web search plugins", () => { + expect( + resolveBundledWebSearchPluginIds({ + config: { + plugins: { + allow: ["google", "xai"], + }, + }, + workspaceDir: "/tmp/workspace", + env: { OPENCLAW_HOME: "/tmp/openclaw-home" }, + }), + ).toEqual(["google", "xai"]); + expect(loadPluginManifestRegistry).toHaveBeenCalledWith({ + config: { + plugins: { + allow: ["google", "xai"], + }, + }, + workspaceDir: "/tmp/workspace", + env: { OPENCLAW_HOME: "/tmp/openclaw-home" }, }); }); - it("keeps bundled web search fast-path ids aligned with the registry", async () => { - expectBundledWebSearchAlignment({ - actual: [...BUNDLED_WEB_SEARCH_PLUGIN_IDS], - expected: await resolveRegistryBundledWebSearchPluginIds(), + it("returns a copy of the bundled plugin id fast-path list", () => { + const listed = listBundledWebSearchPluginIds(); + expect(listed).toEqual([...BUNDLED_WEB_SEARCH_PLUGIN_IDS]); + expect(listed).not.toBe(BUNDLED_WEB_SEARCH_PLUGIN_IDS); + }); + + it("maps bundled provider ids back to their owning plugins", () => { + expect(resolveBundledWebSearchPluginId(" gemini ")).toBe("google"); + expect(resolveBundledWebSearchPluginId("missing")).toBeUndefined(); + }); + + it("loads bundled provider entries through the capability runtime registry once", () => { + expect(listBundledWebSearchProviders()).toEqual([ + expect.objectContaining({ pluginId: "xai", id: "grok" }), + expect.objectContaining({ pluginId: "google", id: "gemini" }), + ]); + expect(listBundledWebSearchProviders()).toEqual([ + expect.objectContaining({ pluginId: "xai", id: "grok" }), + expect.objectContaining({ pluginId: "google", id: "gemini" }), + ]); + expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(1); + expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({ + pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS, + pluginSdkResolution: "dist", }); }); }); @@ -64,45 +143,68 @@ describe("hasBundledWebSearchCredential", () => { tools: { web: { fetch: { enabled: false } } }, } satisfies OpenClawConfig; + beforeEach(() => { + resolveBundledPluginWebSearchProvidersMock.mockReset(); + }); + it.each([ { - name: "detects google plugin web search credentials", - config: { - ...baseCfg, - plugins: { - entries: { - google: { enabled: true, config: { webSearch: { apiKey: "AIza-test" } } }, - }, - }, - } satisfies OpenClawConfig, + name: "detects configured plugin credentials", + providers: [ + createMockedBundledWebSearchProvider({ + pluginId: "google", + providerId: "gemini", + configuredCredential: "AIza-test", + }), + ], + config: baseCfg, env: {}, }, { - name: "detects gemini env credentials", + name: "detects scoped tool credentials", + providers: [ + createMockedBundledWebSearchProvider({ + pluginId: "google", + providerId: "gemini", + scopedCredential: "AIza-test", + }), + ], config: baseCfg, - env: { GEMINI_API_KEY: "AIza-test" }, + env: {}, + searchConfig: { provider: "gemini" }, }, { - name: "detects xai env credentials", + name: "detects env credentials", + providers: [ + createMockedBundledWebSearchProvider({ + pluginId: "xai", + providerId: "grok", + envVars: ["XAI_API_KEY"], + }), + ], config: baseCfg, env: { XAI_API_KEY: "xai-test" }, }, - { - name: "detects kimi env credentials", - config: baseCfg, - env: { KIMI_API_KEY: "sk-kimi-test" }, - }, - { - name: "detects moonshot env credentials", - config: baseCfg, - env: { MOONSHOT_API_KEY: "sk-moonshot-test" }, - }, - { - name: "detects openrouter env credentials through bundled web search providers", - config: baseCfg, - env: { OPENROUTER_API_KEY: "sk-or-v1-test" }, - }, - ] as const)("$name", async ({ config, env }) => { - expect(hasBundledWebSearchCredential({ config, env })).toBe(true); + ] as const)("$name", ({ providers, config, env, searchConfig }) => { + resolveBundledPluginWebSearchProvidersMock.mockReturnValue(providers); + + expect(hasBundledWebSearchCredential({ config, env, searchConfig })).toBe(true); + expect(resolveBundledPluginWebSearchProvidersMock).toHaveBeenCalledWith({ + config, + env, + bundledAllowlistCompat: true, + }); + }); + + it("returns false when no bundled provider exposes a configured credential", () => { + resolveBundledPluginWebSearchProvidersMock.mockReturnValue([ + createMockedBundledWebSearchProvider({ + pluginId: "google", + providerId: "gemini", + envVars: ["GEMINI_API_KEY"], + }), + ]); + + expect(hasBundledWebSearchCredential({ config: baseCfg, env: {} })).toBe(false); }); }); diff --git a/src/plugins/cli.browser-plugin.integration.test.ts b/src/plugins/cli.browser-plugin.integration.test.ts index fd9d55eae85..5b151d0d438 100644 --- a/src/plugins/cli.browser-plugin.integration.test.ts +++ b/src/plugins/cli.browser-plugin.integration.test.ts @@ -40,6 +40,7 @@ describe("registerPluginCliCommands browser plugin integration", () => { cache: false, env: { ...process.env, + OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined, OPENCLAW_BUNDLED_PLUGINS_DIR: bundledFixture?.rootDir ?? path.join(process.cwd(), "extensions"), } as NodeJS.ProcessEnv, @@ -61,6 +62,12 @@ describe("registerPluginCliCommands browser plugin integration", () => { }, } as OpenClawConfig, cache: false, + env: { + ...process.env, + OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined, + OPENCLAW_BUNDLED_PLUGINS_DIR: + bundledFixture?.rootDir ?? path.join(process.cwd(), "extensions"), + } as NodeJS.ProcessEnv, }); expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain("browser"); diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index aa0782e8ead..14fb724e205 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -699,6 +699,7 @@ describe("discoverOpenClawPlugins", () => { const result = discoverOpenClawPlugins({ env: { ...process.env, + OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined, OPENCLAW_STATE_DIR: stateDir, OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir, }, diff --git a/src/plugins/stage-bundled-plugin-runtime.test.ts b/src/plugins/stage-bundled-plugin-runtime.test.ts index 866480bc58b..2f31a7ee37f 100644 --- a/src/plugins/stage-bundled-plugin-runtime.test.ts +++ b/src/plugins/stage-bundled-plugin-runtime.test.ts @@ -338,6 +338,7 @@ describe("stageBundledPluginRuntime", () => { const env = { ...process.env, + OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined, OPENCLAW_BUNDLED_PLUGINS_DIR: runtimeExtensionsDir, }; const discovery = discoverOpenClawPlugins({ diff --git a/src/plugins/web-search-providers.test.ts b/src/plugins/web-search-providers.test.ts index b3e6fe07b9e..31e98baa11d 100644 --- a/src/plugins/web-search-providers.test.ts +++ b/src/plugins/web-search-providers.test.ts @@ -1,7 +1,15 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { PluginWebSearchProviderEntry } from "./types.js"; import { resolveBundledPluginWebSearchProviders } from "./web-search-providers.js"; -const WEB_SEARCH_PROVIDER_TEST_TIMEOUT_MS = 300_000; +const listBundledWebSearchProvidersMock = vi.hoisted(() => vi.fn()); +const resolveBundledWebSearchPluginIdsMock = vi.hoisted(() => vi.fn()); + +vi.mock("./bundled-web-search.js", () => ({ + listBundledWebSearchProviders: listBundledWebSearchProvidersMock, + resolveBundledWebSearchPluginIds: resolveBundledWebSearchPluginIdsMock, +})); + const EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_KEYS = [ "brave:brave", "duckduckgo:duckduckgo", @@ -39,6 +47,119 @@ const EXPECTED_BUNDLED_WEB_SEARCH_CREDENTIAL_PATHS = [ "plugins.entries.tavily.config.webSearch.apiKey", ] as const; +function createBundledWebSearchProviderEntry(params: { + pluginId: string; + providerId: string; + credentialPath: string; + order: number; + withApplySelectionConfig?: boolean; + withResolveRuntimeMetadata?: boolean; +}): PluginWebSearchProviderEntry { + return { + pluginId: params.pluginId, + id: params.providerId, + label: params.providerId, + hint: `${params.providerId} provider`, + envVars: [], + placeholder: `${params.providerId}-key`, + signupUrl: `https://example.com/${params.providerId}`, + autoDetectOrder: params.order, + credentialPath: params.credentialPath, + getCredentialValue: () => undefined, + setCredentialValue: () => {}, + ...(params.withApplySelectionConfig + ? { + applySelectionConfig: () => ({ + plugins: { + entries: { + [params.pluginId]: { + enabled: true, + }, + }, + }, + }), + } + : {}), + ...(params.withResolveRuntimeMetadata + ? { + resolveRuntimeMetadata: () => ({ + selectedProvider: params.providerId, + }), + } + : {}), + createTool: () => ({ + description: params.providerId, + parameters: {}, + execute: async () => ({}), + }), + }; +} + +const BUNDLED_WEB_SEARCH_PROVIDERS: PluginWebSearchProviderEntry[] = [ + createBundledWebSearchProviderEntry({ + pluginId: "duckduckgo", + providerId: "duckduckgo", + credentialPath: "", + order: 100, + }), + createBundledWebSearchProviderEntry({ + pluginId: "moonshot", + providerId: "kimi", + credentialPath: "plugins.entries.moonshot.config.webSearch.apiKey", + order: 40, + }), + createBundledWebSearchProviderEntry({ + pluginId: "brave", + providerId: "brave", + credentialPath: "plugins.entries.brave.config.webSearch.apiKey", + order: 10, + }), + createBundledWebSearchProviderEntry({ + pluginId: "perplexity", + providerId: "perplexity", + credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey", + order: 50, + withResolveRuntimeMetadata: true, + }), + createBundledWebSearchProviderEntry({ + pluginId: "firecrawl", + providerId: "firecrawl", + credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey", + order: 60, + withApplySelectionConfig: true, + }), + createBundledWebSearchProviderEntry({ + pluginId: "google", + providerId: "gemini", + credentialPath: "plugins.entries.google.config.webSearch.apiKey", + order: 20, + }), + createBundledWebSearchProviderEntry({ + pluginId: "tavily", + providerId: "tavily", + credentialPath: "plugins.entries.tavily.config.webSearch.apiKey", + order: 80, + }), + createBundledWebSearchProviderEntry({ + pluginId: "exa", + providerId: "exa", + credentialPath: "plugins.entries.exa.config.webSearch.apiKey", + order: 55, + }), + createBundledWebSearchProviderEntry({ + pluginId: "searxng", + providerId: "searxng", + credentialPath: "plugins.entries.searxng.config.webSearch.baseUrl", + order: 70, + }), + createBundledWebSearchProviderEntry({ + pluginId: "xai", + providerId: "grok", + credentialPath: "plugins.entries.xai.config.webSearch.apiKey", + order: 30, + }), +]; + function toProviderKeys( providers: ReturnType, ): string[] { @@ -91,6 +212,15 @@ function expectBundledWebSearchResolution(params: { } describe("resolveBundledPluginWebSearchProviders", () => { + beforeEach(() => { + listBundledWebSearchProvidersMock.mockReset(); + listBundledWebSearchProvidersMock.mockReturnValue(BUNDLED_WEB_SEARCH_PROVIDERS); + resolveBundledWebSearchPluginIdsMock.mockReset(); + resolveBundledWebSearchPluginIdsMock.mockReturnValue([ + ...EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS, + ]); + }); + it.each([ { title: "returns bundled providers in alphabetical order", @@ -102,7 +232,7 @@ describe("resolveBundledPluginWebSearchProviders", () => { bundledAllowlistCompat: true, }, }, - ] as const)("$title", { timeout: WEB_SEARCH_PROVIDER_TEST_TIMEOUT_MS }, ({ options }) => { + ] as const)("$title", ({ options }) => { const providers = resolveBundledPluginWebSearchProviders(options); expectBundledWebSearchProviders(providers); diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.ts index a8460d5648c..2f91496f1b6 100644 --- a/src/secrets/runtime.coverage.test.ts +++ b/src/secrets/runtime.coverage.test.ts @@ -1,7 +1,10 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { AuthProfileStore } from "../agents/auth-profiles.js"; import type { OpenClawConfig } from "../config/config.js"; -import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; +import type { + PluginWebFetchProviderEntry, + PluginWebSearchProviderEntry, +} from "../plugins/types.js"; import { getPath, setPathCreateStrict } from "./path-utils.js"; import { canonicalizeSecretTargetCoverageId } from "./target-registry-test-helpers.js"; import { listSecretTargetRegistryEntries } from "./target-registry.js"; @@ -13,6 +16,11 @@ const { resolveBundledPluginWebSearchProvidersMock, resolvePluginWebSearchProvid resolveBundledPluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()), resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()), })); +const { resolveBundledPluginWebFetchProvidersMock, resolvePluginWebFetchProvidersMock } = + vi.hoisted(() => ({ + resolveBundledPluginWebFetchProvidersMock: vi.fn(() => buildTestWebFetchProviders()), + resolvePluginWebFetchProvidersMock: vi.fn(() => buildTestWebFetchProviders()), + })); let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; @@ -25,6 +33,14 @@ vi.mock("../plugins/web-search-providers.runtime.js", () => ({ resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock, })); +vi.mock("../plugins/web-fetch-providers.js", () => ({ + resolveBundledPluginWebFetchProviders: resolveBundledPluginWebFetchProvidersMock, +})); + +vi.mock("../plugins/web-fetch-providers.runtime.js", () => ({ + resolvePluginWebFetchProviders: resolvePluginWebFetchProvidersMock, +})); + function createTestProvider(params: { id: "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl" | "tavily"; pluginId: string; @@ -90,6 +106,42 @@ function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] { ]; } +function buildTestWebFetchProviders(): PluginWebFetchProviderEntry[] { + return [ + { + pluginId: "firecrawl", + id: "firecrawl", + label: "firecrawl", + hint: "firecrawl test provider", + envVars: ["FIRECRAWL_API_KEY"], + placeholder: "fc-...", + signupUrl: "https://example.com/firecrawl", + autoDetectOrder: 50, + credentialPath: "plugins.entries.firecrawl.config.webFetch.apiKey", + inactiveSecretPaths: ["plugins.entries.firecrawl.config.webFetch.apiKey"], + getCredentialValue: (fetchConfig) => fetchConfig?.apiKey, + setCredentialValue: (fetchConfigTarget, value) => { + fetchConfigTarget.apiKey = value; + }, + getConfiguredCredentialValue: (config) => { + const entryConfig = config?.plugins?.entries?.firecrawl?.config; + return entryConfig && typeof entryConfig === "object" + ? (entryConfig as { webFetch?: { apiKey?: unknown } }).webFetch?.apiKey + : undefined; + }, + setConfiguredCredentialValue: (configTarget, value) => { + const plugins = (configTarget.plugins ??= {}) as { entries?: Record }; + const entries = (plugins.entries ??= {}); + const entry = (entries.firecrawl ??= {}) as { config?: Record }; + const config = (entry.config ??= {}); + const webFetch = (config.webFetch ??= {}) as { apiKey?: unknown }; + webFetch.apiKey = value; + }, + createTool: () => null, + }, + ]; +} + function toConcretePathSegments(pathPattern: string): string[] { const segments = pathPattern.split(".").filter(Boolean); const out: string[] = []; @@ -250,6 +302,8 @@ describe("secrets runtime target coverage", () => { clearSecretsRuntimeSnapshot(); resolveBundledPluginWebSearchProvidersMock.mockReset(); resolvePluginWebSearchProvidersMock.mockReset(); + resolveBundledPluginWebFetchProvidersMock.mockReset(); + resolvePluginWebFetchProvidersMock.mockReset(); }); beforeEach(() => { diff --git a/src/test-utils/env.ts b/src/test-utils/env.ts index 7204e395031..b57a9829aa6 100644 --- a/src/test-utils/env.ts +++ b/src/test-utils/env.ts @@ -37,6 +37,7 @@ const PATH_RESOLUTION_ENV_KEYS = [ "OPENCLAW_HOME", "OPENCLAW_STATE_DIR", "OPENCLAW_BUNDLED_PLUGINS_DIR", + "OPENCLAW_DISABLE_BUNDLED_PLUGINS", ] as const; function resolveWindowsHomeParts(homeDir: string): { homeDrive?: string; homePath?: string } { @@ -65,6 +66,7 @@ export function createPathResolutionEnv( OPENCLAW_HOME: undefined, OPENCLAW_STATE_DIR: undefined, OPENCLAW_BUNDLED_PLUGINS_DIR: undefined, + OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined, }; const windowsHome = resolveWindowsHomeParts(resolvedHome); diff --git a/src/tts/provider-registry.test.ts b/src/tts/provider-registry.test.ts index 6bb4323d146..3b77b078424 100644 --- a/src/tts/provider-registry.test.ts +++ b/src/tts/provider-registry.test.ts @@ -4,12 +4,24 @@ import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import type { SpeechProviderPlugin } from "../plugins/types.js"; const resolveRuntimePluginRegistryMock = vi.fn(); +const loadPluginManifestRegistryMock = vi.fn(() => ({ + plugins: [ + { id: "elevenlabs", origin: "bundled", contracts: { speechProviders: [{}] } }, + { id: "microsoft", origin: "bundled", contracts: { speechProviders: [{}] } }, + { id: "openai", origin: "bundled", contracts: { speechProviders: [{}] } }, + ], +})); vi.mock("../plugins/loader.js", () => ({ resolveRuntimePluginRegistry: (...args: Parameters) => resolveRuntimePluginRegistryMock(...args), })); +vi.mock("../plugins/manifest-registry.js", () => ({ + loadPluginManifestRegistry: (...args: Parameters) => + loadPluginManifestRegistryMock(...args), +})); + let getSpeechProvider: typeof import("./provider-registry.js").getSpeechProvider; let listSpeechProviders: typeof import("./provider-registry.js").listSpeechProviders; let canonicalizeSpeechProviderId: typeof import("./provider-registry.js").canonicalizeSpeechProviderId; @@ -43,6 +55,7 @@ describe("speech provider registry", () => { beforeEach(() => { resolveRuntimePluginRegistryMock.mockReset(); resolveRuntimePluginRegistryMock.mockReturnValue(undefined); + loadPluginManifestRegistryMock.mockClear(); }); it("uses active plugin speech providers without reloading plugins", () => { resolveRuntimePluginRegistryMock.mockReturnValue({ diff --git a/test/scripts/test-planner.test.ts b/test/scripts/test-planner.test.ts index 058b766fbd4..e15fe973b80 100644 --- a/test/scripts/test-planner.test.ts +++ b/test/scripts/test-planner.test.ts @@ -78,6 +78,40 @@ describe("test planner", () => { artifacts.cleanupTempArtifacts(); }); + it("keeps bundled-plugin-dependent core tests off the default unit surface", () => { + const env = { + RUNNER_OS: "macOS", + OPENCLAW_TEST_HOST_CPU_COUNT: "10", + OPENCLAW_TEST_HOST_MEMORY_GIB: "64", + OPENCLAW_TEST_LOAD_AWARE: "0", + }; + const artifacts = createExecutionArtifacts(env); + const plan = buildExecutionPlan( + { + profile: null, + mode: "local", + surfaces: ["unit", "bundled"], + passthroughArgs: [], + }, + { + env, + platform: "darwin", + writeTempJsonArtifact: artifacts.writeTempJsonArtifact, + }, + ); + + expect(plan.selectedUnits.some((unit) => unit.surface === "bundled")).toBe(true); + expect( + plan.selectedUnits + .filter((unit) => unit.surface === "unit") + .every((unit) => unit.env?.OPENCLAW_DISABLE_BUNDLED_PLUGINS === "1"), + ).toBe(true); + expect(plan.selectedUnits.find((unit) => unit.surface === "bundled")?.args).toContain( + "vitest.bundled.config.ts", + ); + artifacts.cleanupTempArtifacts(); + }); + it("uses smaller shared extension batches on constrained local hosts", () => { const env = { RUNNER_OS: "macOS", @@ -491,6 +525,21 @@ describe("test planner", () => { expect(explanation.intentProfile).toBe("normal"); }); + it("routes synthetic bundled-plugin fixture tests through the bundled surface", () => { + const explanation = explainExecutionTarget( + { + mode: "local", + fileFilters: ["src/plugin-sdk/facade-runtime.test.ts"], + }, + { + env: {}, + }, + ); + + expect(explanation.surface).toBe("bundled"); + expect(explanation.args).toContain("vitest.bundled.config.ts"); + }); + it("uses hotspot-backed memory isolation when explaining unit tests", () => { const explanation = explainExecutionTarget( { @@ -724,7 +773,16 @@ describe("test planner", () => { expect(manifest.jobs.checksWindows.matrix.include).toHaveLength(6); expect(manifest.jobs.macosNode.matrix.include).toHaveLength(9); expect(manifest.jobs.checksFast.matrix.include).toHaveLength( - manifest.shardCounts.extensionFast + 1, + manifest.shardCounts.extensionFast + 2, + ); + expect(manifest.requiredCheckNames).toContain("checks-fast-bundled"); + expect(manifest.jobs.checksFast.matrix.include).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + check_name: "checks-fast-bundled", + command: "pnpm test:bundled", + }), + ]), ); expect( manifest.jobs.checksFast.matrix.include diff --git a/test/vitest-unit-paths.test.ts b/test/vitest-unit-paths.test.ts index b25bff3b78c..d936d1c7062 100644 --- a/test/vitest-unit-paths.test.ts +++ b/test/vitest-unit-paths.test.ts @@ -16,6 +16,9 @@ describe("isUnitConfigTestFile", () => { bundledPluginFile("imessage", "src/monitor.shutdown.unhandled-rejection.test.ts"), ), ).toBe(false); + expect(isUnitConfigTestFile("src/infra/matrix-plugin-helper.test.ts")).toBe(false); + expect(isUnitConfigTestFile("src/plugin-sdk/facade-runtime.test.ts")).toBe(false); + expect(isUnitConfigTestFile("src/plugins/loader.test.ts")).toBe(false); expect(isUnitConfigTestFile("src/agents/pi-embedded-runner.test.ts")).toBe(false); expect(isUnitConfigTestFile("src/commands/onboard.test.ts")).toBe(false); expect(isUnitConfigTestFile("ui/src/ui/views/other.test.ts")).toBe(false); diff --git a/vitest.bundled.config.ts b/vitest.bundled.config.ts new file mode 100644 index 00000000000..fe4906ed2fd --- /dev/null +++ b/vitest.bundled.config.ts @@ -0,0 +1,14 @@ +import { + bundledPluginDependentUnitTestFiles, + unitTestAdditionalExcludePatterns, +} from "./vitest.unit-paths.mjs"; +import { createUnitVitestConfigWithOptions } from "./vitest.unit.config.ts"; + +const bundledUnitExcludePatterns = unitTestAdditionalExcludePatterns.filter( + (pattern) => !bundledPluginDependentUnitTestFiles.includes(pattern), +); + +export default createUnitVitestConfigWithOptions(process.env, { + includePatterns: bundledPluginDependentUnitTestFiles, + extraExcludePatterns: bundledUnitExcludePatterns, +}); diff --git a/vitest.config.ts b/vitest.config.ts index 57e732d9006..91887901512 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -61,6 +61,7 @@ export default defineConfig({ "scripts/test-runner-manifest.mjs", "vitest.channel-paths.mjs", "vitest.channels.config.ts", + "vitest.bundled.config.ts", "vitest.config.ts", "vitest.contracts.config.ts", "vitest.e2e.config.ts", diff --git a/vitest.unit-paths.mjs b/vitest.unit-paths.mjs index 6870d70af9d..81f9f45ce73 100644 --- a/vitest.unit-paths.mjs +++ b/vitest.unit-paths.mjs @@ -15,6 +15,12 @@ export const unitTestIncludePatterns = [ "ui/src/ui/controllers/chat.test.ts", ]; +export const bundledPluginDependentUnitTestFiles = [ + "src/infra/matrix-plugin-helper.test.ts", + "src/plugin-sdk/facade-runtime.test.ts", + "src/plugins/loader.test.ts", +]; + export const unitTestAdditionalExcludePatterns = [ "src/gateway/**", `${BUNDLED_PLUGIN_ROOT_DIR}/**`, @@ -25,6 +31,7 @@ export const unitTestAdditionalExcludePatterns = [ "src/commands/**", "src/channels/plugins/contracts/**", "src/plugins/contracts/**", + ...bundledPluginDependentUnitTestFiles, ]; const sharedBaseExcludePatterns = [ @@ -50,3 +57,7 @@ export function isUnitConfigTestFile(file) { !matchesAny(normalizedFile, unitTestAdditionalExcludePatterns) ); } + +export function isBundledPluginDependentUnitTestFile(file) { + return bundledPluginDependentUnitTestFiles.includes(normalizeRepoPath(file)); +} diff --git a/vitest.unit.config.ts b/vitest.unit.config.ts index 7f904fef96a..bae9a6909fa 100644 --- a/vitest.unit.config.ts +++ b/vitest.unit.config.ts @@ -23,18 +23,25 @@ export function loadExtraExcludePatternsFromEnv( return loadPatternListFromEnv("OPENCLAW_VITEST_EXTRA_EXCLUDE_FILE", env) ?? []; } -export function createUnitVitestConfig(env: Record = process.env) { +export function createUnitVitestConfigWithOptions( + env: Record = process.env, + options: { + includePatterns?: string[]; + extraExcludePatterns?: string[]; + } = {}, +) { return defineConfig({ ...base, test: { ...baseTest, isolate: resolveVitestIsolation(env), runner: "./test/non-isolated-runner.ts", - include: loadIncludePatternsFromEnv(env) ?? unitTestIncludePatterns, + include: + loadIncludePatternsFromEnv(env) ?? options.includePatterns ?? unitTestIncludePatterns, exclude: [ ...new Set([ ...exclude, - ...unitTestAdditionalExcludePatterns, + ...(options.extraExcludePatterns ?? unitTestAdditionalExcludePatterns), ...loadExtraExcludePatternsFromEnv(env), ]), ], @@ -42,4 +49,8 @@ export function createUnitVitestConfig(env: Record = }); } -export default createUnitVitestConfig(); +export function createUnitVitestConfig(env: Record = process.env) { + return createUnitVitestConfigWithOptions(env); +} + +export default createUnitVitestConfigWithOptions();