diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index 4a3554e0b0d..da06ebdd7df 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -39,12 +39,30 @@ const resolvedOverride = const parallelRuns = runs.filter((entry) => entry.name !== "gateway"); const serialRuns = runs.filter((entry) => entry.name === "gateway"); const localWorkers = Math.max(4, Math.min(16, os.cpus().length)); -const parallelCount = Math.max(1, parallelRuns.length); -const perRunWorkers = Math.max(1, Math.floor(localWorkers / parallelCount)); -const macCiWorkers = isCI && isMacOS ? 1 : perRunWorkers; +const defaultUnitWorkers = localWorkers; +const defaultExtensionsWorkers = Math.max(1, Math.min(4, Math.floor(localWorkers / 4))); +const defaultGatewayWorkers = Math.max(1, Math.min(4, localWorkers)); + // Keep worker counts predictable for local runs; trim macOS CI workers to avoid worker crashes/OOM. // In CI on linux/windows, prefer Vitest defaults to avoid cross-test interference from lower worker counts. -const maxWorkers = resolvedOverride ?? (isCI && !isMacOS ? null : macCiWorkers); +const maxWorkersForRun = (name) => { + if (resolvedOverride) { + return resolvedOverride; + } + if (isCI && !isMacOS) { + return null; + } + if (isCI && isMacOS) { + return 1; + } + if (name === "extensions") { + return defaultExtensionsWorkers; + } + if (name === "gateway") { + return defaultGatewayWorkers; + } + return defaultUnitWorkers; +}; const WARNING_SUPPRESSION_FLAGS = [ "--disable-warning=ExperimentalWarning", @@ -54,6 +72,7 @@ const WARNING_SUPPRESSION_FLAGS = [ const runOnce = (entry, extraArgs = []) => new Promise((resolve) => { + const maxWorkers = maxWorkersForRun(entry.name); const args = maxWorkers ? [...entry.args, "--maxWorkers", String(maxWorkers), ...windowsCiArgs, ...extraArgs] : [...entry.args, ...windowsCiArgs, ...extraArgs]; diff --git a/src/cli/plugin-registry.ts b/src/cli/plugin-registry.ts index 5ce740437bb..d66103d5256 100644 --- a/src/cli/plugin-registry.ts +++ b/src/cli/plugin-registry.ts @@ -3,6 +3,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent import { loadConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging.js"; import { loadOpenClawPlugins } from "../plugins/loader.js"; +import { getActivePluginRegistry } from "../plugins/runtime.js"; const log = createSubsystemLogger("plugins"); let pluginRegistryLoaded = false; @@ -11,6 +12,16 @@ export function ensurePluginRegistryLoaded(): void { if (pluginRegistryLoaded) { return; } + const active = getActivePluginRegistry(); + // Tests (and callers) can pre-seed a registry (e.g. `test/setup.ts`); avoid + // doing an expensive load when we already have plugins/channels/tools. + if ( + active && + (active.plugins.length > 0 || active.channels.length > 0 || active.tools.length > 0) + ) { + pluginRegistryLoaded = true; + return; + } const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const logger: PluginLogger = { diff --git a/src/config/zod-schema.hooks.ts b/src/config/zod-schema.hooks.ts index 3130f8cb9e3..38651c4f24b 100644 --- a/src/config/zod-schema.hooks.ts +++ b/src/config/zod-schema.hooks.ts @@ -59,7 +59,10 @@ const HookConfigSchema = z enabled: z.boolean().optional(), env: z.record(z.string(), z.string()).optional(), }) - .strict(); + // Hook configs are intentionally open-ended (handlers can define their own keys). + // Keep enabled/env typed, but allow additional per-hook keys without marking the + // whole config invalid (which triggers doctor/best-effort loads). + .passthrough(); const HookInstallRecordSchema = z .object({ diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index 27d937d5320..fed2bbdde2f 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -122,8 +122,15 @@ const saveSessionToMemory: HookHandler = async (event) => { messageCount, }); - // Avoid calling the model provider in unit tests, keep hooks fast and deterministic. - if (sessionContent && cfg && !process.env.VITEST && process.env.NODE_ENV !== "test") { + // Avoid calling the model provider in unit tests; keep hooks fast and deterministic. + const isTestEnv = + process.env.OPENCLAW_TEST_FAST === "1" || + process.env.VITEST === "true" || + process.env.VITEST === "1" || + process.env.NODE_ENV === "test"; + const allowLlmSlug = !isTestEnv && hookConfig?.llmSlug !== false; + + if (sessionContent && cfg && allowLlmSlug) { log.debug("Calling generateSlugViaLLM..."); // Use LLM to generate a descriptive slug slug = await generateSlugViaLLM({ sessionContent, cfg }); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 56180482405..360022ea80a 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -14,6 +14,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveUserPath } from "../utils.js"; import { clearPluginCommands } from "./commands.js"; import { + applyTestPluginDefaults, normalizePluginsConfig, resolveEnableState, resolveMemorySlotDecision, @@ -167,7 +168,9 @@ function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnost } export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry { - const cfg = options.config ?? {}; + // Test env: default-disable plugins unless explicitly configured. + // This keeps unit/gateway suites fast and avoids loading heavyweight plugin deps by accident. + const cfg = applyTestPluginDefaults(options.config ?? {}, process.env); const logger = options.logger ?? defaultLogger(); const validateOnly = options.mode === "validate"; const normalized = normalizePluginsConfig(cfg.plugins); diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 4284c87d60e..313b7af91df 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -2,6 +2,7 @@ import type { AnyAgentTool } from "../agents/tools/common.js"; import type { OpenClawPluginToolContext } from "./types.js"; import { normalizeToolName } from "../agents/tool-policy.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js"; import { loadOpenClawPlugins } from "./loader.js"; const log = createSubsystemLogger("plugins"); @@ -45,8 +46,16 @@ export function resolvePluginTools(params: { existingToolNames?: Set; toolAllowlist?: string[]; }): AnyAgentTool[] { + // Fast path: when plugins are effectively disabled, avoid discovery/jiti entirely. + // This matters a lot for unit tests and for tool construction hot paths. + const effectiveConfig = applyTestPluginDefaults(params.context.config ?? {}, process.env); + const normalized = normalizePluginsConfig(effectiveConfig.plugins); + if (!normalized.enabled) { + return []; + } + const registry = loadOpenClawPlugins({ - config: params.context.config, + config: effectiveConfig, workspaceDir: params.context.workspaceDir, logger: { info: (msg) => log.info(msg),