fix(test): repair split agent shard runs

This commit is contained in:
Vincent Koc
2026-05-24 15:26:25 +02:00
parent ce48e4c197
commit 125d82cab2
18 changed files with 183 additions and 49 deletions

View File

@@ -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. - 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. - 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. - 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. - 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. - 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`. - Crabbox: install Corepack shims into the writable hydration `PNPM_HOME` so local AWS runner hydration no longer tries to overwrite `/usr/local/bin/pnpm`.

View File

@@ -47,11 +47,11 @@ import { isCiLikeEnv, resolveLocalFullSuiteProfile } from "./lib/vitest-local-sc
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs"; import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs";
const DEFAULT_VITEST_CONFIG = "test/vitest/vitest.unit.config.ts"; 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_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_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_SUPPORT_VITEST_CONFIG = "test/vitest/vitest.agents-support.config.ts";
const AGENTS_TOOLS_VITEST_CONFIG = "test/vitest/vitest.agents-tools.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 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_CORE_VITEST_CONFIG = "test/vitest/vitest.auto-reply-core.config.ts";
const AUTO_REPLY_VITEST_CONFIG = "test/vitest/vitest.auto-reply.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 CHANGED_ARGS_PATTERN = /^--changed(?:=(.+))?$/u;
const VITEST_CONFIG_BY_KIND = { const VITEST_CONFIG_BY_KIND = {
acp: ACP_VITEST_CONFIG, acp: ACP_VITEST_CONFIG,
agent: AGENTS_VITEST_CONFIG,
agentCore: AGENTS_CORE_VITEST_CONFIG, agentCore: AGENTS_CORE_VITEST_CONFIG,
agentPiEmbedded: AGENTS_PI_EMBEDDED_VITEST_CONFIG, agentPiEmbedded: AGENTS_PI_EMBEDDED_VITEST_CONFIG,
agentSupport: AGENTS_SUPPORT_VITEST_CONFIG, agentSupport: AGENTS_SUPPORT_VITEST_CONFIG,
agentTools: AGENTS_TOOLS_VITEST_CONFIG, agentTools: AGENTS_TOOLS_VITEST_CONFIG,
agent: AGENTS_VITEST_CONFIG,
autoReplyCore: AUTO_REPLY_CORE_VITEST_CONFIG, autoReplyCore: AUTO_REPLY_CORE_VITEST_CONFIG,
autoReplyReply: AUTO_REPLY_REPLY_VITEST_CONFIG, autoReplyReply: AUTO_REPLY_REPLY_VITEST_CONFIG,
autoReplyTopLevel: AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG, autoReplyTopLevel: AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG,
@@ -1724,6 +1724,10 @@ export function buildVitestRunPlans(
"autoReplyCore", "autoReplyCore",
"autoReplyReply", "autoReplyReply",
"autoReplyTopLevel", "autoReplyTopLevel",
"agentCore",
"agentPiEmbedded",
"agentSupport",
"agentTools",
"agent", "agent",
"plugin", "plugin",
"ui", "ui",

View File

@@ -1,3 +1,2 @@
export function resolvePathEnvKey(env: NodeJS.ProcessEnv): string; export function resolvePathEnvKey(env: NodeJS.ProcessEnv): string;
export function buildCmdExeCommandLine(command: string, args: string[]): string; export function buildCmdExeCommandLine(command: string, args: string[]): string;

View File

@@ -305,9 +305,7 @@ describe("exec approval followup", () => {
expect(callGatewayTool).not.toHaveBeenCalled(); expect(callGatewayTool).not.toHaveBeenCalled();
}); });
it("uses safe denied copy for nested-parentheses denial metadata when session resume fails", async () => { it("uses safe direct denied copy for nested-parentheses denial metadata without resuming the session", async () => {
vi.mocked(callGatewayTool).mockRejectedValueOnce(new Error("session missing"));
await sendExecApprovalFollowup({ await sendExecApprovalFollowup({
approvalId: "req-denied-resume-failed-nested", approvalId: "req-denied-resume-failed-nested",
sessionKey: "agent:main:telegram:-100123", 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", "Exec denied (gateway id=req-denied-resume-failed-nested, approval-timeout (allowlist-miss)): uname -a",
}); });
expect(sendMessage).toHaveBeenCalledWith( expectDirectSend({
expect.objectContaining({ content: "Command did not run: approval timed out.",
content: idempotencyKey: "exec-approval-followup:req-denied-resume-failed-nested",
"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", expect(callGatewayTool).not.toHaveBeenCalled();
}),
);
}); });
it("suppresses denied followups for subagent sessions", async () => { it("suppresses denied followups for subagent sessions", async () => {

View File

@@ -178,7 +178,6 @@ export async function ensureOpenClawModelsJson(
config: cfg, config: cfg,
env: createConfigRuntimeEnv(cfg), env: createConfigRuntimeEnv(cfg),
...(workspaceDir ? { workspaceDir } : {}), ...(workspaceDir ? { workspaceDir } : {}),
allowWorkspaceScopedCurrent: workspaceDir === undefined,
}); });
const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveDefaultAgentDir(cfg); const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveDefaultAgentDir(cfg);
const targetPath = path.join(agentDir, "models.json"); const targetPath = path.join(agentDir, "models.json");

View File

@@ -44,6 +44,7 @@ vi.mock("../pi-model-discovery.js", () => ({
})); }));
import type { OpenClawConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/config.js";
import { resetModelDiscoveryCacheForTest } from "./model-discovery-cache.js";
import { import {
expectResolvedForwardCompatFallbackResult, expectResolvedForwardCompatFallbackResult,
expectUnknownModelErrorResult, expectUnknownModelErrorResult,
@@ -57,6 +58,7 @@ import {
} from "./model.test-harness.js"; } from "./model.test-harness.js";
beforeEach(() => { beforeEach(() => {
resetModelDiscoveryCacheForTest();
resetMockDiscoverModels(discoverModels); resetMockDiscoverModels(discoverModels);
}); });

View File

@@ -212,9 +212,13 @@ describe("ensureSandboxContainer config-hash recreation", () => {
}); });
it("recreates shared container when array-order change alters hash", async () => { it("recreates shared container when array-order change alters hash", async () => {
const workspaceDir = "/tmp/workspace"; const workspaceDir = makeTempDir();
const oldCfg = createSandboxConfig(["1.1.1.1", "8.8.8.8"]); const oldCfg = createSandboxConfig(["1.1.1.1", "8.8.8.8"], [
const newCfg = createSandboxConfig(["8.8.8.8", "1.1.1.1"]); `${workspaceDir}:/workspace:rw`,
]);
const newCfg = createSandboxConfig(["8.8.8.8", "1.1.1.1"], [
`${workspaceDir}:/workspace:rw`,
]);
const oldHash = computeSandboxConfigHash({ const oldHash = computeSandboxConfigHash({
docker: oldCfg.docker, docker: oldCfg.docker,
@@ -270,11 +274,12 @@ describe("ensureSandboxContainer config-hash recreation", () => {
}); });
it("recreates shared container when previously filtered explicit env becomes allowed", async () => { 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", { const cfg = createSandboxConfig(["1.1.1.1"], undefined, "rw", {
LANG: "C.UTF-8", LANG: "C.UTF-8",
GEMINI_API_KEY: "dummy-gemini", GEMINI_API_KEY: "dummy-gemini",
}); });
cfg.docker.binds = [`${workspaceDir}:/workspace:rw`];
const oldHash = computeSandboxConfigHash({ const oldHash = computeSandboxConfigHash({
docker: cfg.docker, 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 () => { 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( const cfg = createSandboxConfig(
["1.1.1.1"], ["1.1.1.1"],
["/tmp/workspace-shared/USER.md:/workspace/USER.md:ro"], [`${customUserFile}:/workspace/USER.md:ro`],
); );
cfg.docker.dangerouslyAllowExternalBindSources = true; cfg.docker.dangerouslyAllowExternalBindSources = true;
const expectedHash = computeSandboxConfigHash({ const expectedHash = computeSandboxConfigHash({
@@ -346,8 +353,8 @@ describe("ensureSandboxContainer config-hash recreation", () => {
expect(createCall.args).toContain(`openclaw.configHash=${expectedHash}`); expect(createCall.args).toContain(`openclaw.configHash=${expectedHash}`);
const bindArgs = collectDockerFlagValues(createCall.args, "-v"); const bindArgs = collectDockerFlagValues(createCall.args, "-v");
const workspaceMountIdx = bindArgs.indexOf("/tmp/workspace:/workspace:z"); const workspaceMountIdx = bindArgs.indexOf(`${workspaceDir}:/workspace:z`);
const customMountIdx = bindArgs.indexOf("/tmp/workspace-shared/USER.md:/workspace/USER.md:ro"); const customMountIdx = bindArgs.indexOf(`${customUserFile}:/workspace/USER.md:ro`);
expect(workspaceMountIdx).toBeGreaterThanOrEqual(0); expect(workspaceMountIdx).toBeGreaterThanOrEqual(0);
expect(customMountIdx).toBeGreaterThan(workspaceMountIdx); expect(customMountIdx).toBeGreaterThan(workspaceMountIdx);
}); });

View File

@@ -51,17 +51,18 @@ describe("appendWorkspaceMountArgs", () => {
}); });
it("omits agent workspace mount when paths are identical", () => { it("omits agent workspace mount when paths are identical", () => {
const workspaceDir = makeTempWorkspace();
const args: string[] = []; const args: string[] = [];
appendWorkspaceMountArgs({ appendWorkspaceMountArgs({
args, args,
workspaceDir: "/tmp/workspace", workspaceDir,
agentWorkspaceDir: "/tmp/workspace", agentWorkspaceDir: workspaceDir,
workdir: "/workspace", workdir: "/workspace",
workspaceAccess: "rw", workspaceAccess: "rw",
}); });
const mounts = args.filter((arg) => arg.startsWith("/tmp/")); const mounts = args.filter((arg) => arg.startsWith(workspaceDir));
expect(mounts).toEqual(["/tmp/workspace:/workspace:z"]); expect(mounts).toEqual([`${workspaceDir}:/workspace:z`]);
}); });
it("marks split agent workspace mounts shared for SELinux", () => { it("marks split agent workspace mounts shared for SELinux", () => {

View File

@@ -247,9 +247,11 @@ async function deliverTelegramDirectMessageCompletion(params: {
sendMessage?: typeof runtimeSendMessage; sendMessage?: typeof runtimeSendMessage;
internalEvents?: AgentInternalEvent[]; internalEvents?: AgentInternalEvent[];
isActive?: boolean; isActive?: boolean;
requesterSessionId?: string | null;
queueEmbeddedPiMessageWithOutcome?: QueueEmbeddedPiMessageWithOutcome; queueEmbeddedPiMessageWithOutcome?: QueueEmbeddedPiMessageWithOutcome;
requesterSessionKey?: string; requesterSessionKey?: string;
sourceTool?: string; sourceTool?: string;
runtimeConfig?: Record<string, unknown>;
origin?: { origin?: {
channel: "telegram"; channel: "telegram";
to: string; to: string;
@@ -266,10 +268,13 @@ async function deliverTelegramDirectMessageCompletion(params: {
testing.setDepsForTest({ testing.setDepsForTest({
callGateway: params.callGateway, callGateway: params.callGateway,
getRequesterSessionActivity: () => ({ getRequesterSessionActivity: () => ({
sessionId: "requester-session-telegram", sessionId:
params.requesterSessionId === null
? undefined
: (params.requesterSessionId ?? "requester-session-telegram"),
isActive: params.isActive === true, isActive: params.isActive === true,
}), }),
getRuntimeConfig: () => ({}) as never, getRuntimeConfig: () => (params.runtimeConfig ?? {}) as never,
sendMessage: params.sendMessage ?? runtimeSendMessage, sendMessage: params.sendMessage ?? runtimeSendMessage,
...(params.queueEmbeddedPiMessageWithOutcome ...(params.queueEmbeddedPiMessageWithOutcome
? { queueEmbeddedPiMessageWithOutcome: 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 () => { it("reports failure for Telegram DMs when announce-agent delivery fails", async () => {
const callGateway = vi.fn(async () => { const callGateway = createGatewayMock({
throw new Error("UNAVAILABLE: requester wake failed"); result: {
}) as unknown as typeof runtimeCallGateway; deliveryStatus: {
status: "failed",
errorMessage: "requester wake failed",
},
},
});
const sendMessage = createSendMessageMock(); const sendMessage = createSendMessageMock();
const result = await deliverTelegramDirectMessageCompletion({ const result = await deliverTelegramDirectMessageCompletion({
callGateway, callGateway,
sendMessage, 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: [ internalEvents: [
{ {
type: "task_completion", type: "task_completion",
@@ -1442,7 +1469,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
expectRecordFields(result, { expectRecordFields(result, {
delivered: false, delivered: false,
path: "direct", path: "direct",
error: "UNAVAILABLE: requester wake failed", error: "requester wake failed",
}); });
expect(sendMessage).not.toHaveBeenCalled(); expect(sendMessage).not.toHaveBeenCalled();
}); });
@@ -1460,6 +1487,15 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
sendMessage, sendMessage,
isActive: true, isActive: true,
queueEmbeddedPiMessageWithOutcome, queueEmbeddedPiMessageWithOutcome,
runtimeConfig: {
agents: {
defaults: {
subagents: {
announceTimeoutMs: 10,
},
},
},
},
internalEvents: [ internalEvents: [
{ {
type: "task_completion", type: "task_completion",
@@ -1496,7 +1532,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
steeringMode: "all", steeringMode: "all",
debounceMs: 500, debounceMs: 500,
waitForTranscriptCommit: true, waitForTranscriptCommit: true,
deliveryTimeoutMs: 120_000, deliveryTimeoutMs: 10,
}, },
); );
expect(callGateway).toHaveBeenCalledTimes(1); expect(callGateway).toHaveBeenCalledTimes(1);

View File

@@ -60,6 +60,7 @@ vi.mock("./subagent-announce.runtime.js", () => ({
isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId), isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId),
getRuntimeConfig: () => mockConfig, getRuntimeConfig: () => mockConfig,
loadSessionStore: (storePath: string) => loadSessionStoreMock(storePath), loadSessionStore: (storePath: string) => loadSessionStoreMock(storePath),
readSessionMessagesAsync: vi.fn(async () => []),
readSessionEntry: (storePath: string, sessionKey: string) => readSessionEntry: (storePath: string, sessionKey: string) =>
(loadSessionStoreMock(storePath) as Record<string, unknown>)[sessionKey], (loadSessionStoreMock(storePath) as Record<string, unknown>)[sessionKey],
resolveAgentIdFromSessionKey: (sessionKey: string) => resolveAgentIdFromSessionKey: (sessionKey: string) =>

View File

@@ -191,6 +191,7 @@ vi.mock("./subagent-announce.runtime.js", () => ({
}, },
getRuntimeConfig: () => configOverride, getRuntimeConfig: () => configOverride,
loadSessionStore: vi.fn(() => sessionStore), loadSessionStore: vi.fn(() => sessionStore),
readSessionMessagesAsync: vi.fn(async () => []),
readSessionEntry: (_storePath: string, sessionKey: string) => sessionStore[sessionKey], readSessionEntry: (_storePath: string, sessionKey: string) => sessionStore[sessionKey],
resolveAgentIdFromSessionKey: () => "main", resolveAgentIdFromSessionKey: () => "main",
resolveStorePath: () => "/tmp/sessions-main.json", resolveStorePath: () => "/tmp/sessions-main.json",

View File

@@ -82,7 +82,6 @@ export function loadCapabilityMetadataSnapshot(params: {
config: params.config ?? {}, config: params.config ?? {},
env: params.env ?? process.env, env: params.env ?? process.env,
...(workspaceDir ? { workspaceDir } : {}), ...(workspaceDir ? { workspaceDir } : {}),
allowWorkspaceScopedCurrent: workspaceDir === undefined,
}); });
} }

View File

@@ -1,4 +1,4 @@
import { beforeAll, describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
completionRequiresMessageToolDelivery, completionRequiresMessageToolDelivery,
resolveCompletionChatType, resolveCompletionChatType,
@@ -6,13 +6,6 @@ import {
} from "./completion-delivery-policy.js"; } from "./completion-delivery-policy.js";
describe("completion delivery policy", () => { describe("completion delivery policy", () => {
beforeAll(() => {
resolveCompletionChatType({ requesterSessionKey: "agent:main:whatsapp:warmup@g.us" });
resolveCompletionChatType({
requesterSessionKey: "agent:main:discord:guild-warmup:channel-warmup",
});
});
it.each([ it.each([
{ {
name: "canonical group key", name: "canonical group key",

View File

@@ -1,6 +1,6 @@
import { normalizeChatType, type ChatType } from "../../channels/chat-type.js"; import { normalizeChatType, type ChatType } from "../../channels/chat-type.js";
import type { OpenClawConfig } from "../../config/types.openclaw.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 type { DeliveryContext } from "../../utils/delivery-context.types.js";
import { resolveSourceReplyDeliveryMode } from "./source-reply-delivery-mode.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]) { for (const key of [params.targetRequesterSessionKey, params.requesterSessionKey]) {
const derived = deriveSessionChatType(key); const derived = deriveSessionChatTypeFromKey(key);
if (derived !== "unknown") { if (derived !== "unknown") {
return derived; return derived;
} }
@@ -60,7 +60,7 @@ export function completionRequiresMessageToolDelivery(params: {
export function shouldRouteCompletionThroughRequesterSession( export function shouldRouteCompletionThroughRequesterSession(
sessionKey: string | undefined | null, sessionKey: string | undefined | null,
): boolean { ): boolean {
const chatType = deriveSessionChatType(sessionKey); const chatType = deriveSessionChatTypeFromKey(sessionKey);
return chatType === "group" || chatType === "channel"; return chatType === "group" || chatType === "channel";
} }

View File

@@ -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");
});
});

View File

@@ -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 { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js";
import { resolveQueueSettings as resolveQueueSettingsCore } from "./settings.js"; import { resolveQueueSettings as resolveQueueSettingsCore } from "./settings.js";
import type { QueueSettings, ResolveQueueSettingsParams } from "./types.js"; import type { QueueSettings, ResolveQueueSettingsParams } from "./types.js";
@@ -7,7 +7,7 @@ function resolvePluginDebounce(channelKey: string | undefined): number | undefin
if (!channelKey) { if (!channelKey) {
return undefined; return undefined;
} }
const plugin = getChannelPlugin(channelKey); const plugin = getLoadedChannelPlugin(channelKey);
const value = plugin?.defaults?.queue?.debounceMs; const value = plugin?.defaults?.queue?.debounceMs;
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined; return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined;
} }

View File

@@ -1,4 +1,5 @@
import fs from "node:fs"; import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
@@ -106,8 +107,23 @@ const {
) => number; ) => number;
}; };
const VITEST_CLI_ENTRY = path.join(process.cwd(), "node_modules", "vitest", "vitest.mjs"); const require = createRequire(import.meta.url);
const VITEST_NODE_PREFIX = ["exec", "node", "--no-maglev", VITEST_CLI_ENTRY]; 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", () => { describe("test-projects args", () => {
it("drops a pnpm passthrough separator while preserving targeted filters", () => { it("drops a pnpm passthrough separator while preserving targeted filters", () => {
@@ -1058,6 +1074,39 @@ describe("test-projects args", () => {
).toEqual([]); ).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", () => { it("accepts sentinel targets routed as whole config runs", () => {
expect(findUnmatchedExplicitTestTargets(["ui/src/test-helpers/control-ui-e2e.ts"])).toEqual([]); expect(findUnmatchedExplicitTestTargets(["ui/src/test-helpers/control-ui-e2e.ts"])).toEqual([]);
}); });

View File

@@ -6,9 +6,12 @@ type GlobalWithOpenAiCodexTokenRefreshTestHook = typeof globalThis & {
}; };
vi.mock("@earendil-works/pi-ai/oauth", () => ({ vi.mock("@earendil-works/pi-ai/oauth", () => ({
getOAuthProvider: () => undefined,
getOAuthApiKey: () => undefined, getOAuthApiKey: () => undefined,
getOAuthProviders: () => [], getOAuthProviders: () => [],
loginOpenAICodex: vi.fn(), loginOpenAICodex: vi.fn(),
registerOAuthProvider: vi.fn(),
resetOAuthProviders: vi.fn(),
refreshOpenAICodexToken: vi.fn((...args: unknown[]) => refreshOpenAICodexToken: vi.fn((...args: unknown[]) =>
(globalThis as GlobalWithOpenAiCodexTokenRefreshTestHook)[openAiCodexTokenRefreshTestHook]?.( (globalThis as GlobalWithOpenAiCodexTokenRefreshTestHook)[openAiCodexTokenRefreshTestHook]?.(
...args, ...args,