From 125d82cab2952f87f532106a368d54e526141026 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 24 May 2026 15:26:25 +0200 Subject: [PATCH] fix(test): repair split agent shard runs --- CHANGELOG.md | 2 + scripts/test-projects.test-support.mjs | 8 ++- scripts/windows-cmd-helpers.d.mts | 1 - .../bash-tools.exec-approval-followup.test.ts | 16 +++--- src/agents/models-config.ts | 1 - ...orward-compat.errors-and-overrides.test.ts | 2 + .../docker.config-hash-recreate.test.ts | 23 +++++--- src/agents/sandbox/workspace-mounts.test.ts | 9 ++-- src/agents/subagent-announce-delivery.test.ts | 50 ++++++++++++++--- src/agents/subagent-announce.test.ts | 1 + src/agents/subagent-announce.timeout.test.ts | 1 + .../tools/manifest-capability-availability.ts | 1 - .../reply/completion-delivery-policy.test.ts | 9 +--- .../reply/completion-delivery-policy.ts | 6 +-- .../reply/queue/settings-runtime.test.ts | 42 +++++++++++++++ .../reply/queue/settings-runtime.ts | 4 +- src/scripts/test-projects.test.ts | 53 ++++++++++++++++++- test/setup.shared.ts | 3 ++ 18 files changed, 183 insertions(+), 49 deletions(-) create mode 100644 src/auto-reply/reply/queue/settings-runtime.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b3184fcc08..67787b07e8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Docs: https://docs.openclaw.ai - Tests: suppress the current Rolldown plugin timing warning format in the Vitest wrapper so tiny focused runs do not drown useful stderr in repeated build-timing noise. - Models/OpenRouter: use endpoint-specific OpenRouter context limits from `top_provider` metadata so provider-routed models no longer overstate available context. (#85949) Thanks @TurboTheTurtle. - Crabbox: sync clean sparse-checkout remote changed gates from a temporary full checkout with local-only commits overlaid as worktree changes so git-backed script checks can seed the runner repository. +- Agents: avoid loading bundled channel plugins while resolving completion delivery policy and queue defaults on subagent handoff paths. +- Tests: allow split Vitest config shards through the explicit-target preflight so CI shard jobs run their intended projects. - Tests: make startup memory and startup bench smoke scripts build CLI startup artifacts when run from a fresh source checkout. - iMessage: mark authorized slash-command turns as text-sourced commands so `/status`, `/new`, and `/restart` acknowledgements return to the source conversation. (#82642) thanks @homer-byte. - Crabbox: install Corepack shims into the writable hydration `PNPM_HOME` so local AWS runner hydration no longer tries to overwrite `/usr/local/bin/pnpm`. diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 848aa8838629..e8c6206b5de3 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -47,11 +47,11 @@ import { isCiLikeEnv, resolveLocalFullSuiteProfile } from "./lib/vitest-local-sc import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs"; const DEFAULT_VITEST_CONFIG = "test/vitest/vitest.unit.config.ts"; -const AGENTS_VITEST_CONFIG = "test/vitest/vitest.agents.config.ts"; const AGENTS_CORE_VITEST_CONFIG = "test/vitest/vitest.agents-core.config.ts"; const AGENTS_PI_EMBEDDED_VITEST_CONFIG = "test/vitest/vitest.agents-pi-embedded.config.ts"; const AGENTS_SUPPORT_VITEST_CONFIG = "test/vitest/vitest.agents-support.config.ts"; const AGENTS_TOOLS_VITEST_CONFIG = "test/vitest/vitest.agents-tools.config.ts"; +const AGENTS_VITEST_CONFIG = "test/vitest/vitest.agents.config.ts"; const ACP_VITEST_CONFIG = "test/vitest/vitest.acp.config.ts"; const AUTO_REPLY_CORE_VITEST_CONFIG = "test/vitest/vitest.auto-reply-core.config.ts"; const AUTO_REPLY_VITEST_CONFIG = "test/vitest/vitest.auto-reply.config.ts"; @@ -234,11 +234,11 @@ const FS_MODULE_CACHE_PATH_ENV_KEY = "OPENCLAW_VITEST_FS_MODULE_CACHE_PATH"; const CHANGED_ARGS_PATTERN = /^--changed(?:=(.+))?$/u; const VITEST_CONFIG_BY_KIND = { acp: ACP_VITEST_CONFIG, - agent: AGENTS_VITEST_CONFIG, agentCore: AGENTS_CORE_VITEST_CONFIG, agentPiEmbedded: AGENTS_PI_EMBEDDED_VITEST_CONFIG, agentSupport: AGENTS_SUPPORT_VITEST_CONFIG, agentTools: AGENTS_TOOLS_VITEST_CONFIG, + agent: AGENTS_VITEST_CONFIG, autoReplyCore: AUTO_REPLY_CORE_VITEST_CONFIG, autoReplyReply: AUTO_REPLY_REPLY_VITEST_CONFIG, autoReplyTopLevel: AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG, @@ -1724,6 +1724,10 @@ export function buildVitestRunPlans( "autoReplyCore", "autoReplyReply", "autoReplyTopLevel", + "agentCore", + "agentPiEmbedded", + "agentSupport", + "agentTools", "agent", "plugin", "ui", diff --git a/scripts/windows-cmd-helpers.d.mts b/scripts/windows-cmd-helpers.d.mts index ef9c3bd2015e..8945cf560461 100644 --- a/scripts/windows-cmd-helpers.d.mts +++ b/scripts/windows-cmd-helpers.d.mts @@ -1,3 +1,2 @@ export function resolvePathEnvKey(env: NodeJS.ProcessEnv): string; - export function buildCmdExeCommandLine(command: string, args: string[]): string; diff --git a/src/agents/bash-tools.exec-approval-followup.test.ts b/src/agents/bash-tools.exec-approval-followup.test.ts index b9da116077e4..61a912a4c54f 100644 --- a/src/agents/bash-tools.exec-approval-followup.test.ts +++ b/src/agents/bash-tools.exec-approval-followup.test.ts @@ -305,9 +305,7 @@ describe("exec approval followup", () => { expect(callGatewayTool).not.toHaveBeenCalled(); }); - it("uses safe denied copy for nested-parentheses denial metadata when session resume fails", async () => { - vi.mocked(callGatewayTool).mockRejectedValueOnce(new Error("session missing")); - + it("uses safe direct denied copy for nested-parentheses denial metadata without resuming the session", async () => { await sendExecApprovalFollowup({ approvalId: "req-denied-resume-failed-nested", sessionKey: "agent:main:telegram:-100123", @@ -319,13 +317,11 @@ describe("exec approval followup", () => { "Exec denied (gateway id=req-denied-resume-failed-nested, approval-timeout (allowlist-miss)): uname -a", }); - expect(sendMessage).toHaveBeenCalledWith( - expect.objectContaining({ - content: - "Automatic session resume failed, so sending the status directly.\n\nCommand did not run: approval timed out.", - idempotencyKey: "exec-approval-followup:req-denied-resume-failed-nested", - }), - ); + expectDirectSend({ + content: "Command did not run: approval timed out.", + idempotencyKey: "exec-approval-followup:req-denied-resume-failed-nested", + }); + expect(callGatewayTool).not.toHaveBeenCalled(); }); it("suppresses denied followups for subagent sessions", async () => { diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 6704d5f8d311..45a4bdd6a2ff 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -178,7 +178,6 @@ export async function ensureOpenClawModelsJson( config: cfg, env: createConfigRuntimeEnv(cfg), ...(workspaceDir ? { workspaceDir } : {}), - allowWorkspaceScopedCurrent: workspaceDir === undefined, }); const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveDefaultAgentDir(cfg); const targetPath = path.join(agentDir, "models.json"); diff --git a/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts b/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts index e423bb36aefa..32b8aeb397d1 100644 --- a/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts +++ b/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts @@ -44,6 +44,7 @@ vi.mock("../pi-model-discovery.js", () => ({ })); import type { OpenClawConfig } from "../../config/config.js"; +import { resetModelDiscoveryCacheForTest } from "./model-discovery-cache.js"; import { expectResolvedForwardCompatFallbackResult, expectUnknownModelErrorResult, @@ -57,6 +58,7 @@ import { } from "./model.test-harness.js"; beforeEach(() => { + resetModelDiscoveryCacheForTest(); resetMockDiscoverModels(discoverModels); }); diff --git a/src/agents/sandbox/docker.config-hash-recreate.test.ts b/src/agents/sandbox/docker.config-hash-recreate.test.ts index ce3f3ae361bf..6564031cee77 100644 --- a/src/agents/sandbox/docker.config-hash-recreate.test.ts +++ b/src/agents/sandbox/docker.config-hash-recreate.test.ts @@ -212,9 +212,13 @@ describe("ensureSandboxContainer config-hash recreation", () => { }); it("recreates shared container when array-order change alters hash", async () => { - const workspaceDir = "/tmp/workspace"; - const oldCfg = createSandboxConfig(["1.1.1.1", "8.8.8.8"]); - const newCfg = createSandboxConfig(["8.8.8.8", "1.1.1.1"]); + const workspaceDir = makeTempDir(); + const oldCfg = createSandboxConfig(["1.1.1.1", "8.8.8.8"], [ + `${workspaceDir}:/workspace:rw`, + ]); + const newCfg = createSandboxConfig(["8.8.8.8", "1.1.1.1"], [ + `${workspaceDir}:/workspace:rw`, + ]); const oldHash = computeSandboxConfigHash({ docker: oldCfg.docker, @@ -270,11 +274,12 @@ describe("ensureSandboxContainer config-hash recreation", () => { }); it("recreates shared container when previously filtered explicit env becomes allowed", async () => { - const workspaceDir = "/tmp/workspace"; + const workspaceDir = makeTempDir(); const cfg = createSandboxConfig(["1.1.1.1"], undefined, "rw", { LANG: "C.UTF-8", GEMINI_API_KEY: "dummy-gemini", }); + cfg.docker.binds = [`${workspaceDir}:/workspace:rw`]; const oldHash = computeSandboxConfigHash({ docker: cfg.docker, @@ -316,10 +321,12 @@ describe("ensureSandboxContainer config-hash recreation", () => { }); it("applies custom binds after workspace mounts so overlapping binds can override", async () => { - const workspaceDir = "/tmp/workspace"; + const workspaceDir = makeTempDir(); + const customRoot = makeTempDir(); + const customUserFile = path.join(customRoot, "USER.md"); const cfg = createSandboxConfig( ["1.1.1.1"], - ["/tmp/workspace-shared/USER.md:/workspace/USER.md:ro"], + [`${customUserFile}:/workspace/USER.md:ro`], ); cfg.docker.dangerouslyAllowExternalBindSources = true; const expectedHash = computeSandboxConfigHash({ @@ -346,8 +353,8 @@ describe("ensureSandboxContainer config-hash recreation", () => { expect(createCall.args).toContain(`openclaw.configHash=${expectedHash}`); const bindArgs = collectDockerFlagValues(createCall.args, "-v"); - const workspaceMountIdx = bindArgs.indexOf("/tmp/workspace:/workspace:z"); - const customMountIdx = bindArgs.indexOf("/tmp/workspace-shared/USER.md:/workspace/USER.md:ro"); + const workspaceMountIdx = bindArgs.indexOf(`${workspaceDir}:/workspace:z`); + const customMountIdx = bindArgs.indexOf(`${customUserFile}:/workspace/USER.md:ro`); expect(workspaceMountIdx).toBeGreaterThanOrEqual(0); expect(customMountIdx).toBeGreaterThan(workspaceMountIdx); }); diff --git a/src/agents/sandbox/workspace-mounts.test.ts b/src/agents/sandbox/workspace-mounts.test.ts index 24a756c6b6bc..ac0094a63c78 100644 --- a/src/agents/sandbox/workspace-mounts.test.ts +++ b/src/agents/sandbox/workspace-mounts.test.ts @@ -51,17 +51,18 @@ describe("appendWorkspaceMountArgs", () => { }); it("omits agent workspace mount when paths are identical", () => { + const workspaceDir = makeTempWorkspace(); const args: string[] = []; appendWorkspaceMountArgs({ args, - workspaceDir: "/tmp/workspace", - agentWorkspaceDir: "/tmp/workspace", + workspaceDir, + agentWorkspaceDir: workspaceDir, workdir: "/workspace", workspaceAccess: "rw", }); - const mounts = args.filter((arg) => arg.startsWith("/tmp/")); - expect(mounts).toEqual(["/tmp/workspace:/workspace:z"]); + const mounts = args.filter((arg) => arg.startsWith(workspaceDir)); + expect(mounts).toEqual([`${workspaceDir}:/workspace:z`]); }); it("marks split agent workspace mounts shared for SELinux", () => { diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts index 8ac586fb8a3e..71e6b7db8a1f 100644 --- a/src/agents/subagent-announce-delivery.test.ts +++ b/src/agents/subagent-announce-delivery.test.ts @@ -247,9 +247,11 @@ async function deliverTelegramDirectMessageCompletion(params: { sendMessage?: typeof runtimeSendMessage; internalEvents?: AgentInternalEvent[]; isActive?: boolean; + requesterSessionId?: string | null; queueEmbeddedPiMessageWithOutcome?: QueueEmbeddedPiMessageWithOutcome; requesterSessionKey?: string; sourceTool?: string; + runtimeConfig?: Record; origin?: { channel: "telegram"; to: string; @@ -266,10 +268,13 @@ async function deliverTelegramDirectMessageCompletion(params: { testing.setDepsForTest({ callGateway: params.callGateway, getRequesterSessionActivity: () => ({ - sessionId: "requester-session-telegram", + sessionId: + params.requesterSessionId === null + ? undefined + : (params.requesterSessionId ?? "requester-session-telegram"), isActive: params.isActive === true, }), - getRuntimeConfig: () => ({}) as never, + getRuntimeConfig: () => (params.runtimeConfig ?? {}) as never, sendMessage: params.sendMessage ?? runtimeSendMessage, ...(params.queueEmbeddedPiMessageWithOutcome ? { queueEmbeddedPiMessageWithOutcome: params.queueEmbeddedPiMessageWithOutcome } @@ -1416,13 +1421,35 @@ describe("deliverSubagentAnnouncement completion delivery", () => { }); it("reports failure for Telegram DMs when announce-agent delivery fails", async () => { - const callGateway = vi.fn(async () => { - throw new Error("UNAVAILABLE: requester wake failed"); - }) as unknown as typeof runtimeCallGateway; + const callGateway = createGatewayMock({ + result: { + deliveryStatus: { + status: "failed", + errorMessage: "requester wake failed", + }, + }, + }); const sendMessage = createSendMessageMock(); const result = await deliverTelegramDirectMessageCompletion({ callGateway, sendMessage, + queueEmbeddedPiMessageWithOutcome: createQueueOutcomeMock(false), + requesterSessionId: null, + requesterSessionKey: "agent:main:telegram:direct:123456789", + origin: { + channel: "telegram", + to: "direct:123456789", + accountId: "bot-1", + }, + runtimeConfig: { + agents: { + defaults: { + subagents: { + announceTimeoutMs: 10, + }, + }, + }, + }, internalEvents: [ { type: "task_completion", @@ -1442,7 +1469,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { expectRecordFields(result, { delivered: false, path: "direct", - error: "UNAVAILABLE: requester wake failed", + error: "requester wake failed", }); expect(sendMessage).not.toHaveBeenCalled(); }); @@ -1460,6 +1487,15 @@ describe("deliverSubagentAnnouncement completion delivery", () => { sendMessage, isActive: true, queueEmbeddedPiMessageWithOutcome, + runtimeConfig: { + agents: { + defaults: { + subagents: { + announceTimeoutMs: 10, + }, + }, + }, + }, internalEvents: [ { type: "task_completion", @@ -1496,7 +1532,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { steeringMode: "all", debounceMs: 500, waitForTranscriptCommit: true, - deliveryTimeoutMs: 120_000, + deliveryTimeoutMs: 10, }, ); expect(callGateway).toHaveBeenCalledTimes(1); diff --git a/src/agents/subagent-announce.test.ts b/src/agents/subagent-announce.test.ts index 32f242a0ccf1..75e8285883d3 100644 --- a/src/agents/subagent-announce.test.ts +++ b/src/agents/subagent-announce.test.ts @@ -60,6 +60,7 @@ vi.mock("./subagent-announce.runtime.js", () => ({ isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId), getRuntimeConfig: () => mockConfig, loadSessionStore: (storePath: string) => loadSessionStoreMock(storePath), + readSessionMessagesAsync: vi.fn(async () => []), readSessionEntry: (storePath: string, sessionKey: string) => (loadSessionStoreMock(storePath) as Record)[sessionKey], resolveAgentIdFromSessionKey: (sessionKey: string) => diff --git a/src/agents/subagent-announce.timeout.test.ts b/src/agents/subagent-announce.timeout.test.ts index 674e80a8a9f3..bfcff5ee7b2a 100644 --- a/src/agents/subagent-announce.timeout.test.ts +++ b/src/agents/subagent-announce.timeout.test.ts @@ -191,6 +191,7 @@ vi.mock("./subagent-announce.runtime.js", () => ({ }, getRuntimeConfig: () => configOverride, loadSessionStore: vi.fn(() => sessionStore), + readSessionMessagesAsync: vi.fn(async () => []), readSessionEntry: (_storePath: string, sessionKey: string) => sessionStore[sessionKey], resolveAgentIdFromSessionKey: () => "main", resolveStorePath: () => "/tmp/sessions-main.json", diff --git a/src/agents/tools/manifest-capability-availability.ts b/src/agents/tools/manifest-capability-availability.ts index b166561237b2..e54ca77a3abd 100644 --- a/src/agents/tools/manifest-capability-availability.ts +++ b/src/agents/tools/manifest-capability-availability.ts @@ -82,7 +82,6 @@ export function loadCapabilityMetadataSnapshot(params: { config: params.config ?? {}, env: params.env ?? process.env, ...(workspaceDir ? { workspaceDir } : {}), - allowWorkspaceScopedCurrent: workspaceDir === undefined, }); } diff --git a/src/auto-reply/reply/completion-delivery-policy.test.ts b/src/auto-reply/reply/completion-delivery-policy.test.ts index 4d1c211c4f78..3c0c0cf3ac70 100644 --- a/src/auto-reply/reply/completion-delivery-policy.test.ts +++ b/src/auto-reply/reply/completion-delivery-policy.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { completionRequiresMessageToolDelivery, resolveCompletionChatType, @@ -6,13 +6,6 @@ import { } from "./completion-delivery-policy.js"; describe("completion delivery policy", () => { - beforeAll(() => { - resolveCompletionChatType({ requesterSessionKey: "agent:main:whatsapp:warmup@g.us" }); - resolveCompletionChatType({ - requesterSessionKey: "agent:main:discord:guild-warmup:channel-warmup", - }); - }); - it.each([ { name: "canonical group key", diff --git a/src/auto-reply/reply/completion-delivery-policy.ts b/src/auto-reply/reply/completion-delivery-policy.ts index 2fa0c2d12d7d..33377b42fc06 100644 --- a/src/auto-reply/reply/completion-delivery-policy.ts +++ b/src/auto-reply/reply/completion-delivery-policy.ts @@ -1,6 +1,6 @@ import { normalizeChatType, type ChatType } from "../../channels/chat-type.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { deriveSessionChatType } from "../../sessions/session-chat-type.js"; +import { deriveSessionChatTypeFromKey } from "../../sessions/session-chat-type-shared.js"; import type { DeliveryContext } from "../../utils/delivery-context.types.js"; import { resolveSourceReplyDeliveryMode } from "./source-reply-delivery-mode.js"; @@ -26,7 +26,7 @@ export function resolveCompletionChatType(params: { } for (const key of [params.targetRequesterSessionKey, params.requesterSessionKey]) { - const derived = deriveSessionChatType(key); + const derived = deriveSessionChatTypeFromKey(key); if (derived !== "unknown") { return derived; } @@ -60,7 +60,7 @@ export function completionRequiresMessageToolDelivery(params: { export function shouldRouteCompletionThroughRequesterSession( sessionKey: string | undefined | null, ): boolean { - const chatType = deriveSessionChatType(sessionKey); + const chatType = deriveSessionChatTypeFromKey(sessionKey); return chatType === "group" || chatType === "channel"; } diff --git a/src/auto-reply/reply/queue/settings-runtime.test.ts b/src/auto-reply/reply/queue/settings-runtime.test.ts new file mode 100644 index 000000000000..9a107cd21e2b --- /dev/null +++ b/src/auto-reply/reply/queue/settings-runtime.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../../config/types.openclaw.js"; + +const getLoadedChannelPluginMock = vi.hoisted(() => vi.fn()); + +vi.mock("../../../channels/plugins/index.js", () => ({ + getLoadedChannelPlugin: getLoadedChannelPluginMock, +})); + +describe("resolveQueueSettings runtime defaults", () => { + it("uses defaults from already-loaded channel plugins", async () => { + getLoadedChannelPluginMock.mockReturnValueOnce({ + defaults: { + queue: { + debounceMs: 125, + }, + }, + }); + const { resolveQueueSettings } = await import("./settings-runtime.js"); + + expect(resolveQueueSettings({ cfg: {} as OpenClawConfig, channel: "demo" })).toEqual({ + mode: "steer", + debounceMs: 125, + cap: 20, + dropPolicy: "summarize", + }); + expect(getLoadedChannelPluginMock).toHaveBeenCalledWith("demo"); + }); + + it("falls back without loading bundled channel plugins", async () => { + getLoadedChannelPluginMock.mockReturnValueOnce(undefined); + const { resolveQueueSettings } = await import("./settings-runtime.js"); + + expect(resolveQueueSettings({ cfg: {} as OpenClawConfig, channel: "telegram" })).toEqual({ + mode: "steer", + debounceMs: 500, + cap: 20, + dropPolicy: "summarize", + }); + expect(getLoadedChannelPluginMock).toHaveBeenCalledWith("telegram"); + }); +}); diff --git a/src/auto-reply/reply/queue/settings-runtime.ts b/src/auto-reply/reply/queue/settings-runtime.ts index 02db313a1c99..5e40e13bba63 100644 --- a/src/auto-reply/reply/queue/settings-runtime.ts +++ b/src/auto-reply/reply/queue/settings-runtime.ts @@ -1,4 +1,4 @@ -import { getChannelPlugin } from "../../../channels/plugins/index.js"; +import { getLoadedChannelPlugin } from "../../../channels/plugins/index.js"; import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js"; import { resolveQueueSettings as resolveQueueSettingsCore } from "./settings.js"; import type { QueueSettings, ResolveQueueSettingsParams } from "./types.js"; @@ -7,7 +7,7 @@ function resolvePluginDebounce(channelKey: string | undefined): number | undefin if (!channelKey) { return undefined; } - const plugin = getChannelPlugin(channelKey); + const plugin = getLoadedChannelPlugin(channelKey); const value = plugin?.defaults?.queue?.debounceMs; return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined; } diff --git a/src/scripts/test-projects.test.ts b/src/scripts/test-projects.test.ts index ea01e3f47cea..8c729ae440a7 100644 --- a/src/scripts/test-projects.test.ts +++ b/src/scripts/test-projects.test.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; @@ -106,8 +107,23 @@ const { ) => number; }; -const VITEST_CLI_ENTRY = path.join(process.cwd(), "node_modules", "vitest", "vitest.mjs"); -const VITEST_NODE_PREFIX = ["exec", "node", "--no-maglev", VITEST_CLI_ENTRY]; +const require = createRequire(import.meta.url); +const resolveExpectedVitestCliEntry = () => { + const vitestPackageJson = require.resolve("vitest/package.json"); + return path.join(path.dirname(vitestPackageJson), "vitest.mjs"); +}; +const resolveExpectedVitestNodeArgs = (env: NodeJS.ProcessEnv) => + ["1", "true", "yes", "on"].includes( + env.OPENCLAW_VITEST_ENABLE_MAGLEV?.trim().toLowerCase() ?? "", + ) + ? [] + : ["--no-maglev"]; +const VITEST_NODE_PREFIX = [ + "exec", + "node", + ...resolveExpectedVitestNodeArgs(process.env), + resolveExpectedVitestCliEntry(), +]; describe("test-projects args", () => { it("drops a pnpm passthrough separator while preserving targeted filters", () => { @@ -1058,6 +1074,39 @@ describe("test-projects args", () => { ).toEqual([]); }); + it("accepts split CI Vitest config targets routed as whole config runs", () => { + expect( + findUnmatchedExplicitTestTargets([ + "test/vitest/vitest.agents-core.config.ts", + "test/vitest/vitest.agents-pi-embedded.config.ts", + "test/vitest/vitest.agents-support.config.ts", + "test/vitest/vitest.agents-tools.config.ts", + ]), + ).toEqual([]); + }); + + it("keeps split CI Vitest config targets on their own configs", () => { + expect( + buildVitestRunPlans([ + "test/vitest/vitest.agents-core.config.ts", + "test/vitest/vitest.agents-tools.config.ts", + ]), + ).toEqual([ + { + config: "test/vitest/vitest.agents-core.config.ts", + forwardedArgs: [], + includePatterns: null, + watchMode: false, + }, + { + config: "test/vitest/vitest.agents-tools.config.ts", + forwardedArgs: [], + includePatterns: null, + watchMode: false, + }, + ]); + }); + it("accepts sentinel targets routed as whole config runs", () => { expect(findUnmatchedExplicitTestTargets(["ui/src/test-helpers/control-ui-e2e.ts"])).toEqual([]); }); diff --git a/test/setup.shared.ts b/test/setup.shared.ts index 43348c4d66b2..1b3e262fd7fc 100644 --- a/test/setup.shared.ts +++ b/test/setup.shared.ts @@ -6,9 +6,12 @@ type GlobalWithOpenAiCodexTokenRefreshTestHook = typeof globalThis & { }; vi.mock("@earendil-works/pi-ai/oauth", () => ({ + getOAuthProvider: () => undefined, getOAuthApiKey: () => undefined, getOAuthProviders: () => [], loginOpenAICodex: vi.fn(), + registerOAuthProvider: vi.fn(), + resetOAuthProviders: vi.fn(), refreshOpenAICodexToken: vi.fn((...args: unknown[]) => (globalThis as GlobalWithOpenAiCodexTokenRefreshTestHook)[openAiCodexTokenRefreshTestHook]?.( ...args,