From aa52d1be42e9c4b66169536cb528bb4eb39fac59 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 20:07:25 +0100 Subject: [PATCH] test: share auto-reply command fixtures --- ...agent-runner-direct-runtime-config.test.ts | 179 ++++++------------ .../reply/agent-runner-memory.test.ts | 50 +---- .../reply/agent-runner-session-reset.test.ts | 44 +---- .../reply/agent-runner.test-fixtures.ts | 42 ++++ .../reply/commands-abort-trigger.test.ts | 5 +- .../commands-agent-scope.test-support.ts | 17 ++ src/auto-reply/reply/commands-btw.test.ts | 25 +-- src/auto-reply/reply/commands-compact.test.ts | 27 +-- .../reply/commands-plugins.install.test.ts | 45 +---- src/auto-reply/reply/commands-plugins.test.ts | 38 +--- .../commands-session-abort.test-support.ts | 5 + src/auto-reply/reply/commands-status.test.ts | 33 +--- .../commands-status.thinking-default.test.ts | 87 +++------ .../reply/commands-stop-target.test.ts | 5 +- .../reply/commands-subagents-info.test.ts | 13 +- src/auto-reply/reply/commands-tasks.test.ts | 33 +--- src/auto-reply/reply/commands.test-harness.ts | 72 +++++++ .../reply/dispatch-acp-delivery.test.ts | 82 ++++---- 18 files changed, 317 insertions(+), 485 deletions(-) create mode 100644 src/auto-reply/reply/agent-runner.test-fixtures.ts create mode 100644 src/auto-reply/reply/commands-agent-scope.test-support.ts create mode 100644 src/auto-reply/reply/commands-session-abort.test-support.ts diff --git a/src/auto-reply/reply/agent-runner-direct-runtime-config.test.ts b/src/auto-reply/reply/agent-runner-direct-runtime-config.test.ts index 35637bdeefe..d254af8ea74 100644 --- a/src/auto-reply/reply/agent-runner-direct-runtime-config.test.ts +++ b/src/auto-reply/reply/agent-runner-direct-runtime-config.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { TemplateContext } from "../templating.js"; -import type { FollowupRun, QueueSettings } from "./queue.js"; +import { createTestFollowupRun } from "./agent-runner.test-fixtures.js"; +import type { QueueSettings } from "./queue.js"; import { createMockTypingController } from "./test-helpers.js"; const freshCfg = { runtimeFresh: true }; @@ -56,6 +57,56 @@ vi.mock("./queue.js", async () => { const { runReplyAgent } = await import("./agent-runner.js"); +function createTelegramSessionCtx(): TemplateContext { + return { + Provider: "telegram", + OriginatingChannel: "telegram", + OriginatingTo: "12345", + AccountId: "default", + ChatType: "dm", + MessageSid: "msg-1", + } as unknown as TemplateContext; +} + +function createDirectRuntimeReplyParams({ + shouldFollowup, + isActive, +}: { + shouldFollowup: boolean; + isActive: boolean; +}) { + const followupRun = createTestFollowupRun({ + sessionId: "session-1", + sessionKey: "agent:main:telegram:default:direct:test", + messageProvider: "telegram", + config: staleCfg, + provider: "openai", + model: "gpt-5.4", + }); + const resolvedQueue = { mode: "interrupt" } as QueueSettings; + const replyParams: Parameters[0] = { + commandBody: "hello", + followupRun, + queueKey: "main", + resolvedQueue, + shouldSteer: false, + shouldFollowup, + isActive, + isStreaming: false, + typing: createMockTypingController(), + sessionCtx: createTelegramSessionCtx(), + defaultModel: "openai/gpt-5.4", + resolvedVerboseLevel: "off", + isNewSession: false, + blockStreamingEnabled: false, + resolvedBlockStreamingBreak: "message_end", + shouldInjectGroupIntro: false, + typingMode: "instant", + }; + + return { followupRun, resolvedQueue, replyParams }; +} + describe("runReplyAgent runtime config", () => { beforeEach(() => { resolveQueuedReplyExecutionConfigMock.mockReset(); @@ -75,65 +126,12 @@ describe("runReplyAgent runtime config", () => { }); it("resolves direct reply runs before early helpers read config", async () => { - const followupRun = { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session-1", - sessionKey: "agent:main:telegram:default:direct:test", - messageProvider: "telegram", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: staleCfg, - skillsSnapshot: {}, - provider: "openai", - model: "gpt-5.4", - thinkLevel: "low", - verboseLevel: "off", - elevatedLevel: "off", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; + const { followupRun, replyParams } = createDirectRuntimeReplyParams({ + shouldFollowup: false, + isActive: false, + }); - const resolvedQueue = { mode: "interrupt" } as QueueSettings; - const typing = createMockTypingController(); - const sessionCtx = { - Provider: "telegram", - OriginatingChannel: "telegram", - OriginatingTo: "12345", - AccountId: "default", - ChatType: "dm", - MessageSid: "msg-1", - } as unknown as TemplateContext; - - await expect( - runReplyAgent({ - commandBody: "hello", - followupRun, - queueKey: "main", - resolvedQueue, - shouldSteer: false, - shouldFollowup: false, - isActive: false, - isStreaming: false, - typing, - sessionCtx, - defaultModel: "openai/gpt-5.4", - resolvedVerboseLevel: "off", - isNewSession: false, - blockStreamingEnabled: false, - resolvedBlockStreamingBreak: "message_end", - shouldInjectGroupIntro: false, - typingMode: "instant", - }), - ).rejects.toBe(sentinelError); + await expect(runReplyAgent(replyParams)).rejects.toBe(sentinelError); expect(followupRun.run.config).toBe(freshCfg); expect(resolveQueuedReplyExecutionConfigMock).toHaveBeenCalledWith( @@ -167,65 +165,12 @@ describe("runReplyAgent runtime config", () => { }); it("does not resolve secrets before the enqueue-followup queue path", async () => { - const followupRun = { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session-1", - sessionKey: "agent:main:telegram:default:direct:test", - messageProvider: "telegram", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: staleCfg, - skillsSnapshot: {}, - provider: "openai", - model: "gpt-5.4", - thinkLevel: "low", - verboseLevel: "off", - elevatedLevel: "off", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; + const { followupRun, resolvedQueue, replyParams } = createDirectRuntimeReplyParams({ + shouldFollowup: true, + isActive: true, + }); - const resolvedQueue = { mode: "interrupt" } as QueueSettings; - const typing = createMockTypingController(); - const sessionCtx = { - Provider: "telegram", - OriginatingChannel: "telegram", - OriginatingTo: "12345", - AccountId: "default", - ChatType: "dm", - MessageSid: "msg-1", - } as unknown as TemplateContext; - - await expect( - runReplyAgent({ - commandBody: "hello", - followupRun, - queueKey: "main", - resolvedQueue, - shouldSteer: false, - shouldFollowup: true, - isActive: true, - isStreaming: false, - typing, - sessionCtx, - defaultModel: "openai/gpt-5.4", - resolvedVerboseLevel: "off", - isNewSession: false, - blockStreamingEnabled: false, - resolvedBlockStreamingBreak: "message_end", - shouldInjectGroupIntro: false, - typingMode: "instant", - }), - ).resolves.toBeUndefined(); + await expect(runReplyAgent(replyParams)).resolves.toBeUndefined(); expect(resolveQueuedReplyExecutionConfigMock).not.toHaveBeenCalled(); expect(enqueueFollowupRunMock).toHaveBeenCalledWith( diff --git a/src/auto-reply/reply/agent-runner-memory.test.ts b/src/auto-reply/reply/agent-runner-memory.test.ts index 8196df99d2b..b9a09f8e654 100644 --- a/src/auto-reply/reply/agent-runner-memory.test.ts +++ b/src/auto-reply/reply/agent-runner-memory.test.ts @@ -9,7 +9,7 @@ import { } from "../../plugins/memory-state.js"; import type { TemplateContext } from "../templating.js"; import { runMemoryFlushIfNeeded, setAgentRunnerMemoryTestDeps } from "./agent-runner-memory.js"; -import type { FollowupRun } from "./queue.js"; +import { createTestFollowupRun, writeTestSessionStore } from "./agent-runner.test-fixtures.js"; const runWithModelFallbackMock = vi.fn(); const runEmbeddedPiAgentMock = vi.fn(); @@ -24,44 +24,6 @@ function createReplyOperation() { } as never; } -function createFollowupRun(overrides: Partial = {}): FollowupRun { - return { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - agentId: "main", - agentDir: "/tmp/agent", - sessionId: "session", - sessionKey: "main", - messageProvider: "whatsapp", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: {}, - skillsSnapshot: {}, - provider: "anthropic", - model: "claude", - thinkLevel: "low", - verboseLevel: "off", - elevatedLevel: "off", - bashElevated: { enabled: false, allowed: false, defaultLevel: "off" }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - skipProviderRuntimeHints: true, - ...overrides, - }, - } as unknown as FollowupRun; -} - -async function writeSessionStore( - storePath: string, - sessionKey: string, - entry: SessionEntry, -): Promise { - await fs.mkdir(path.dirname(storePath), { recursive: true }); - await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: entry }, null, 2), "utf8"); -} - describe("runMemoryFlushIfNeeded", () => { let rootDir = ""; @@ -100,7 +62,7 @@ describe("runMemoryFlushIfNeeded", () => { } params.sessionStore[sessionKey] = nextEntry; if (typeof params.storePath === "string") { - await writeSessionStore(params.storePath, sessionKey, nextEntry); + await writeTestSessionStore(params.storePath, sessionKey, nextEntry); } return nextEntry.compactionCount; }); @@ -131,7 +93,7 @@ describe("runMemoryFlushIfNeeded", () => { compactionCount: 1, }; const sessionStore = { [sessionKey]: sessionEntry }; - await writeSessionStore(storePath, sessionKey, sessionEntry); + await writeTestSessionStore(storePath, sessionKey, sessionEntry); runEmbeddedPiAgentMock.mockImplementationOnce( async (params: { @@ -145,7 +107,7 @@ describe("runMemoryFlushIfNeeded", () => { }, ); - const followupRun = createFollowupRun(); + const followupRun = createTestFollowupRun(); const entry = await runMemoryFlushIfNeeded({ cfg: { agents: { @@ -206,7 +168,7 @@ describe("runMemoryFlushIfNeeded", () => { const entry = await runMemoryFlushIfNeeded({ cfg: { agents: { defaults: { cliBackends: { "codex-cli": { command: "codex" } } } } }, - followupRun: createFollowupRun({ provider: "codex-cli" }), + followupRun: createTestFollowupRun({ provider: "codex-cli" }), sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext, defaultModel: "codex-cli/gpt-5.4", agentCfgContextTokens: 100_000, @@ -257,7 +219,7 @@ describe("runMemoryFlushIfNeeded", () => { await runMemoryFlushIfNeeded({ cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } }, - followupRun: createFollowupRun({ extraSystemPrompt: "extra system" }), + followupRun: createTestFollowupRun({ extraSystemPrompt: "extra system" }), sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext, defaultModel: "anthropic/claude-opus-4-6", agentCfgContextTokens: 100_000, diff --git a/src/auto-reply/reply/agent-runner-session-reset.test.ts b/src/auto-reply/reply/agent-runner-session-reset.test.ts index a8a0869c6c7..89257ee0567 100644 --- a/src/auto-reply/reply/agent-runner-session-reset.test.ts +++ b/src/auto-reply/reply/agent-runner-session-reset.test.ts @@ -7,45 +7,11 @@ import { resetReplyRunSession, setAgentRunnerSessionResetTestDeps, } from "./agent-runner-session-reset.js"; -import type { FollowupRun } from "./queue.js"; +import { createTestFollowupRun, writeTestSessionStore } from "./agent-runner.test-fixtures.js"; const refreshQueuedFollowupSessionMock = vi.fn(); const errorMock = vi.fn(); -function createFollowupRun(): FollowupRun { - return { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session", - sessionKey: "main", - messageProvider: "whatsapp", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: {}, - skillsSnapshot: {}, - provider: "anthropic", - model: "claude", - thinkLevel: "low", - verboseLevel: "off", - elevatedLevel: "off", - bashElevated: { enabled: false, allowed: false, defaultLevel: "off" }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; -} - -async function writeSessionStore( - storePath: string, - sessionKey: string, - entry: SessionEntry, -): Promise { - await fs.mkdir(path.dirname(storePath), { recursive: true }); - await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: entry }, null, 2), "utf8"); -} - describe("resetReplyRunSession", () => { let rootDir = ""; @@ -87,8 +53,8 @@ describe("resetReplyRunSession", () => { }, }; const sessionStore = { main: sessionEntry }; - const followupRun = createFollowupRun(); - await writeSessionStore(storePath, "main", sessionEntry); + const followupRun = createTestFollowupRun(); + await writeTestSessionStore(storePath, "main", sessionEntry); let activeSessionEntry: SessionEntry | undefined = sessionEntry; let isNewSession = false; @@ -147,7 +113,7 @@ describe("resetReplyRunSession", () => { sessionFile: oldTranscriptPath, }; const sessionStore = { main: sessionEntry }; - await writeSessionStore(storePath, "main", sessionEntry); + await writeTestSessionStore(storePath, "main", sessionEntry); await resetReplyRunSession({ options: { @@ -160,7 +126,7 @@ describe("resetReplyRunSession", () => { activeSessionEntry: sessionEntry, activeSessionStore: sessionStore, storePath, - followupRun: createFollowupRun(), + followupRun: createTestFollowupRun(), onActiveSessionEntry: () => {}, onNewSession: () => {}, }); diff --git a/src/auto-reply/reply/agent-runner.test-fixtures.ts b/src/auto-reply/reply/agent-runner.test-fixtures.ts new file mode 100644 index 00000000000..f66ee0912c7 --- /dev/null +++ b/src/auto-reply/reply/agent-runner.test-fixtures.ts @@ -0,0 +1,42 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import type { SessionEntry } from "../../config/sessions.js"; +import type { FollowupRun } from "./queue.js"; + +export function createTestFollowupRun(overrides: Partial = {}): FollowupRun { + return { + prompt: "hello", + summaryLine: "hello", + enqueuedAt: Date.now(), + run: { + agentId: "main", + agentDir: "/tmp/agent", + sessionId: "session", + sessionKey: "main", + messageProvider: "whatsapp", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + config: {}, + skillsSnapshot: {}, + provider: "anthropic", + model: "claude", + thinkLevel: "low", + verboseLevel: "off", + elevatedLevel: "off", + bashElevated: { enabled: false, allowed: false, defaultLevel: "off" }, + timeoutMs: 1_000, + blockReplyBreak: "message_end", + skipProviderRuntimeHints: true, + ...overrides, + }, + } as unknown as FollowupRun; +} + +export async function writeTestSessionStore( + storePath: string, + sessionKey: string, + entry: SessionEntry, +): Promise { + await fs.mkdir(path.dirname(storePath), { recursive: true }); + await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: entry }, null, 2), "utf8"); +} diff --git a/src/auto-reply/reply/commands-abort-trigger.test.ts b/src/auto-reply/reply/commands-abort-trigger.test.ts index 97c64d56168..40c5f785fbf 100644 --- a/src/auto-reply/reply/commands-abort-trigger.test.ts +++ b/src/auto-reply/reply/commands-abort-trigger.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { handleAbortTrigger } from "./commands-session-abort.js"; +import "./commands-session-abort.test-support.js"; import type { HandleCommandsParams } from "./commands-types.js"; const abortEmbeddedPiRunMock = vi.hoisted(() => vi.fn()); @@ -37,10 +38,6 @@ vi.mock("./commands-session-store.js", () => ({ persistAbortTargetEntry: persistAbortTargetEntryMock, })); -vi.mock("./queue.js", () => ({ - clearSessionQueues: vi.fn(() => ({ followupCleared: 0, laneCleared: 0, keys: [] })), -})); - vi.mock("./reply-run-registry.js", () => ({ replyRunRegistry: { abort: vi.fn(), diff --git a/src/auto-reply/reply/commands-agent-scope.test-support.ts b/src/auto-reply/reply/commands-agent-scope.test-support.ts new file mode 100644 index 00000000000..beb823242d3 --- /dev/null +++ b/src/auto-reply/reply/commands-agent-scope.test-support.ts @@ -0,0 +1,17 @@ +import { vi } from "vitest"; + +export const resolveSessionAgentIdMock = vi.fn(() => "main"); +export const resolveAgentDirMock = vi.fn( + (_cfg: unknown, agentId: string) => `/tmp/workspace/.openclaw/agents/${agentId}/agent`, +); + +vi.doMock("../../agents/agent-scope.js", async () => { + const actual = await vi.importActual( + "../../agents/agent-scope.js", + ); + return { + ...actual, + resolveSessionAgentId: resolveSessionAgentIdMock, + resolveAgentDir: resolveAgentDirMock, + }; +}); diff --git a/src/auto-reply/reply/commands-btw.test.ts b/src/auto-reply/reply/commands-btw.test.ts index f70c141a4f9..d126ac75020 100644 --- a/src/auto-reply/reply/commands-btw.test.ts +++ b/src/auto-reply/reply/commands-btw.test.ts @@ -1,22 +1,13 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; -import { resolveAgentDir } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/config.js"; +import { + resolveAgentDirMock, + resolveSessionAgentIdMock, +} from "./commands-agent-scope.test-support.js"; import { buildCommandTestParams } from "./commands.test-harness.js"; import { createMockTypingController } from "./test-helpers.js"; const runBtwSideQuestionMock = vi.fn(); -const resolveSessionAgentIdMock = vi.hoisted(() => vi.fn(() => "main")); - -vi.mock("../../agents/agent-scope.js", async () => { - const actual = await vi.importActual( - "../../agents/agent-scope.js", - ); - return { - ...actual, - resolveSessionAgentId: resolveSessionAgentIdMock, - resolveAgentDir: vi.fn(actual.resolveAgentDir), - }; -}); vi.mock("../../agents/btw.js", () => ({ runBtwSideQuestion: (...args: unknown[]) => runBtwSideQuestionMock(...args), @@ -35,6 +26,10 @@ function buildParams(commandBody: string) { describe("handleBtwCommand", () => { beforeEach(() => { runBtwSideQuestionMock.mockReset(); + resolveAgentDirMock.mockReset(); + resolveAgentDirMock.mockImplementation( + (_cfg: unknown, agentId: string) => `/tmp/workspace/.openclaw/agents/${agentId}/agent`, + ); resolveSessionAgentIdMock.mockReset(); resolveSessionAgentIdMock.mockReturnValue("main"); }); @@ -207,7 +202,7 @@ describe("handleBtwCommand", () => { updatedAt: Date.now(), }; resolveSessionAgentIdMock.mockReturnValue("worker-1"); - vi.mocked(resolveAgentDir).mockReturnValue("/tmp/worker-1-agent"); + resolveAgentDirMock.mockReturnValue("/tmp/worker-1-agent"); runBtwSideQuestionMock.mockResolvedValue({ text: "resolved fallback" }); const result = await handleBtwCommand(params, true); @@ -216,7 +211,7 @@ describe("handleBtwCommand", () => { sessionKey: "agent:worker-1:whatsapp:direct:12345", config: expect.any(Object), }); - expect(vi.mocked(resolveAgentDir)).toHaveBeenCalledWith(expect.any(Object), "worker-1"); + expect(resolveAgentDirMock).toHaveBeenCalledWith(expect.any(Object), "worker-1"); expect(runBtwSideQuestionMock).toHaveBeenCalledWith( expect.objectContaining({ agentDir: "/tmp/worker-1-agent", diff --git a/src/auto-reply/reply/commands-compact.test.ts b/src/auto-reply/reply/commands-compact.test.ts index 70f85c8e577..6a636c67a5f 100644 --- a/src/auto-reply/reply/commands-compact.test.ts +++ b/src/auto-reply/reply/commands-compact.test.ts @@ -1,22 +1,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveAgentDir } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { handleCompactCommand } from "./commands-compact.js"; +import { + resolveAgentDirMock, + resolveSessionAgentIdMock, +} from "./commands-agent-scope.test-support.js"; import type { HandleCommandsParams } from "./commands-types.js"; -const resolveSessionAgentIdMock = vi.hoisted(() => vi.fn(() => "main")); - -vi.mock("../../agents/agent-scope.js", async () => { - const actual = await vi.importActual( - "../../agents/agent-scope.js", - ); - return { - ...actual, - resolveSessionAgentId: resolveSessionAgentIdMock, - resolveAgentDir: vi.fn(actual.resolveAgentDir), - }; -}); - vi.mock("./commands-compact.runtime.js", () => ({ abortEmbeddedPiRun: vi.fn(), compactEmbeddedPiSession: vi.fn(), @@ -33,6 +22,7 @@ vi.mock("./commands-compact.runtime.js", () => ({ const { compactEmbeddedPiSession, incrementCompactionCount, resolveSessionFilePathOptions } = await import("./commands-compact.runtime.js"); +const { handleCompactCommand } = await import("./commands-compact.js"); function buildCompactParams( commandBodyNormalized: string, @@ -63,6 +53,9 @@ function buildCompactParams( describe("handleCompactCommand", () => { beforeEach(() => { vi.clearAllMocks(); + resolveAgentDirMock.mockImplementation( + (_cfg: unknown, agentId: string) => `/tmp/workspace/.openclaw/agents/${agentId}/agent`, + ); resolveSessionAgentIdMock.mockReturnValue("main"); }); @@ -202,7 +195,7 @@ describe("handleCompactCommand", () => { compacted: false, }); resolveSessionAgentIdMock.mockReturnValue("target"); - vi.mocked(resolveAgentDir).mockReturnValue("/tmp/target-agent"); + resolveAgentDirMock.mockReturnValue("/tmp/target-agent"); await handleCompactCommand( { @@ -226,7 +219,7 @@ describe("handleCompactCommand", () => { agentDir: "/tmp/target-agent", }), ); - expect(vi.mocked(resolveAgentDir)).toHaveBeenCalledWith(expect.any(Object), "target"); + expect(resolveAgentDirMock).toHaveBeenCalledWith(expect.any(Object), "target"); }); it("prefers the target session entry for compaction runtime metadata", async () => { diff --git a/src/auto-reply/reply/commands-plugins.install.test.ts b/src/auto-reply/reply/commands-plugins.install.test.ts index ef974acb6ee..75e5ad3debf 100644 --- a/src/auto-reply/reply/commands-plugins.install.test.ts +++ b/src/auto-reply/reply/commands-plugins.install.test.ts @@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { withTempHome } from "../../config/home-env.test-harness.js"; import { createCommandWorkspaceHarness } from "./commands-filesystem.test-support.js"; import { handlePluginsCommand } from "./commands-plugins.js"; -import type { HandleCommandsParams } from "./commands-types.js"; +import { buildPluginsCommandParams } from "./commands.test-harness.js"; const { installPluginFromPathMock, installPluginFromClawHubMock, persistPluginInstallMock } = vi.hoisted(() => ({ @@ -39,45 +39,12 @@ vi.mock("../../cli/plugins-install-persist.js", () => ({ const workspaceHarness = createCommandWorkspaceHarness("openclaw-command-plugins-install-"); -function buildPluginsParams( - commandBodyNormalized: string, - workspaceDir: string, -): HandleCommandsParams { - return { - cfg: { - commands: { - text: true, - plugins: true, - }, - plugins: { enabled: true }, - }, - ctx: { - Provider: "whatsapp", - Surface: "whatsapp", - CommandSource: "text", - GatewayClientScopes: ["operator.admin", "operator.write", "operator.pairing"], - AccountId: undefined, - }, - command: { - commandBodyNormalized, - rawBodyNormalized: commandBodyNormalized, - isAuthorizedSender: true, - senderIsOwner: true, - senderId: "owner", - channel: "whatsapp", - channelId: "whatsapp", - surface: "whatsapp", - ownerList: [], - from: "test-user", - to: "test-bot", - }, - sessionKey: "agent:main:whatsapp:direct:test-user", - sessionEntry: { - sessionId: "session-plugin-command", - updatedAt: Date.now(), - }, +function buildPluginsParams(commandBodyNormalized: string, workspaceDir: string) { + return buildPluginsCommandParams({ + commandBodyNormalized, workspaceDir, - } as unknown as HandleCommandsParams; + gatewayClientScopes: ["operator.admin", "operator.write", "operator.pairing"], + }); } describe("handleCommands /plugins install", () => { diff --git a/src/auto-reply/reply/commands-plugins.test.ts b/src/auto-reply/reply/commands-plugins.test.ts index db2fea9d176..15ca8e4b29f 100644 --- a/src/auto-reply/reply/commands-plugins.test.ts +++ b/src/auto-reply/reply/commands-plugins.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { handlePluginsCommand } from "./commands-plugins.js"; -import type { HandleCommandsParams } from "./commands-types.js"; +import { buildPluginsCommandParams } from "./commands.test-harness.js"; const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn()); const validateConfigObjectWithPluginsMock = vi.hoisted(() => vi.fn()); @@ -90,39 +90,11 @@ function buildCfg(): OpenClawConfig { }; } -function buildPluginsParams( - commandBodyNormalized: string, - cfg: OpenClawConfig, -): HandleCommandsParams { - return { +function buildPluginsParams(commandBodyNormalized: string, cfg: OpenClawConfig) { + return buildPluginsCommandParams({ + commandBodyNormalized, cfg, - ctx: { - Provider: "whatsapp", - Surface: "whatsapp", - CommandSource: "text", - GatewayClientScopes: ["operator.write", "operator.pairing"], - AccountId: undefined, - }, - command: { - commandBodyNormalized, - rawBodyNormalized: commandBodyNormalized, - isAuthorizedSender: true, - senderIsOwner: true, - senderId: "owner", - channel: "whatsapp", - channelId: "whatsapp", - surface: "whatsapp", - ownerList: [], - from: "test-user", - to: "test-bot", - }, - sessionKey: "agent:main:whatsapp:direct:test-user", - sessionEntry: { - sessionId: "session-plugin-command", - updatedAt: Date.now(), - }, - workspaceDir: "/tmp/plugins-workspace", - } as unknown as HandleCommandsParams; + }); } describe("handlePluginsCommand", () => { diff --git a/src/auto-reply/reply/commands-session-abort.test-support.ts b/src/auto-reply/reply/commands-session-abort.test-support.ts new file mode 100644 index 00000000000..5e80f328e55 --- /dev/null +++ b/src/auto-reply/reply/commands-session-abort.test-support.ts @@ -0,0 +1,5 @@ +import { vi } from "vitest"; + +vi.mock("./queue.js", () => ({ + clearSessionQueues: vi.fn(() => ({ followupCleared: 0, laneCleared: 0, keys: [] })), +})); diff --git a/src/auto-reply/reply/commands-status.test.ts b/src/auto-reply/reply/commands-status.test.ts index b17e77e1a94..74ab79d36ed 100644 --- a/src/auto-reply/reply/commands-status.test.ts +++ b/src/auto-reply/reply/commands-status.test.ts @@ -7,7 +7,6 @@ import { addSubagentRunForTests, resetSubagentRegistryForTests, } from "../../agents/subagent-registry.js"; -import type { OpenClawConfig } from "../../config/config.js"; import { completeTaskRunByRunId, createQueuedTaskRun, @@ -15,15 +14,14 @@ import { failTaskRunByRunId, } from "../../tasks/task-executor.js"; import { resetTaskRegistryForTests } from "../../tasks/task-registry.js"; -import { configureTaskRegistryRuntime } from "../../tasks/task-registry.store.js"; import { buildStatusReply, buildStatusText } from "./commands-status.js"; -import { buildCommandTestParams } from "./commands.test-harness.js"; +import { + baseCommandTestConfig, + buildCommandTestParams, + configureInMemoryTaskRegistryStoreForTests, +} from "./commands.test-harness.js"; -const baseCfg = { - commands: { text: true }, - channels: { whatsapp: { allowFrom: ["*"] } }, - session: { mainKey: "main", scope: "per-sender" }, -} as OpenClawConfig; +const baseCfg = baseCommandTestConfig; async function buildStatusReplyForTest(params: { sessionKey?: string; verbose?: boolean }) { const commandParams = buildCommandTestParams("/status", baseCfg); @@ -87,25 +85,6 @@ function writeTranscriptUsageLog(params: { ); } -function configureInMemoryTaskRegistryStoreForTests(): void { - configureTaskRegistryRuntime({ - store: { - loadSnapshot: () => ({ - tasks: new Map(), - deliveryStates: new Map(), - }), - saveSnapshot: () => {}, - upsertTaskWithDeliveryState: () => {}, - upsertTask: () => {}, - deleteTaskWithDeliveryState: () => {}, - deleteTask: () => {}, - upsertDeliveryState: () => {}, - deleteDeliveryState: () => {}, - close: () => {}, - }, - }); -} - describe("buildStatusReply subagent summary", () => { beforeEach(() => { resetSubagentRegistryForTests(); diff --git a/src/auto-reply/reply/commands-status.thinking-default.test.ts b/src/auto-reply/reply/commands-status.thinking-default.test.ts index 4b287364cb5..fa48344e754 100644 --- a/src/auto-reply/reply/commands-status.thinking-default.test.ts +++ b/src/auto-reply/reply/commands-status.thinking-default.test.ts @@ -33,6 +33,25 @@ vi.mock("./queue.js", () => ({ const { buildStatusReply } = await import("./commands-status.js"); +async function buildKiraStatusReply(cfg: OpenClawConfig) { + return await buildStatusReply({ + cfg, + command: { + isAuthorizedSender: true, + channel: "whatsapp", + } as never, + sessionKey: "agent:kira:main", + provider: "openai", + model: "gpt-5.4", + contextTokens: 0, + resolvedVerboseLevel: "off", + resolvedReasoningLevel: "off", + resolveDefaultThinkingLevel: async () => undefined, + isGroup: false, + defaultGroupActivation: () => "mention", + }); +} + describe("buildStatusReply", () => { it("shows per-agent thinkingDefault in the status card", async () => { const cfg = { @@ -54,22 +73,7 @@ describe("buildStatusReply", () => { }, } as OpenClawConfig; - const reply = await buildStatusReply({ - cfg, - command: { - isAuthorizedSender: true, - channel: "whatsapp", - } as never, - sessionKey: "agent:kira:main", - provider: "openai", - model: "gpt-5.4", - contextTokens: 0, - resolvedVerboseLevel: "off", - resolvedReasoningLevel: "off", - resolveDefaultThinkingLevel: async () => undefined, - isGroup: false, - defaultGroupActivation: () => "mention", - }); + const reply = await buildKiraStatusReply(cfg); expect(reply?.text).toContain("Think: xhigh"); }); @@ -99,22 +103,7 @@ describe("buildStatusReply", () => { }, } as OpenClawConfig; - const reply = await buildStatusReply({ - cfg, - command: { - isAuthorizedSender: true, - channel: "whatsapp", - } as never, - sessionKey: "agent:kira:main", - provider: "openai", - model: "gpt-5.4", - contextTokens: 0, - resolvedVerboseLevel: "off", - resolvedReasoningLevel: "off", - resolveDefaultThinkingLevel: async () => undefined, - isGroup: false, - defaultGroupActivation: () => "mention", - }); + const reply = await buildKiraStatusReply(cfg); expect(reply?.text).toContain("Fallbacks: google/gemini-2.5-flash"); expect(reply?.text).not.toContain("Fallbacks: anthropic/claude-sonnet-4-6"); @@ -144,22 +133,7 @@ describe("buildStatusReply", () => { }, } as OpenClawConfig; - const reply = await buildStatusReply({ - cfg, - command: { - isAuthorizedSender: true, - channel: "whatsapp", - } as never, - sessionKey: "agent:kira:main", - provider: "openai", - model: "gpt-5.4", - contextTokens: 0, - resolvedVerboseLevel: "off", - resolvedReasoningLevel: "off", - resolveDefaultThinkingLevel: async () => undefined, - isGroup: false, - defaultGroupActivation: () => "mention", - }); + const reply = await buildKiraStatusReply(cfg); expect(reply?.text).toContain("Fallbacks: anthropic/claude-sonnet-4-6"); }); @@ -189,22 +163,7 @@ describe("buildStatusReply", () => { }, } as OpenClawConfig; - const reply = await buildStatusReply({ - cfg, - command: { - isAuthorizedSender: true, - channel: "whatsapp", - } as never, - sessionKey: "agent:kira:main", - provider: "openai", - model: "gpt-5.4", - contextTokens: 0, - resolvedVerboseLevel: "off", - resolvedReasoningLevel: "off", - resolveDefaultThinkingLevel: async () => undefined, - isGroup: false, - defaultGroupActivation: () => "mention", - }); + const reply = await buildKiraStatusReply(cfg); expect(reply?.text).not.toContain("Fallbacks:"); }); diff --git a/src/auto-reply/reply/commands-stop-target.test.ts b/src/auto-reply/reply/commands-stop-target.test.ts index 10a3233ba9f..f3fca86b351 100644 --- a/src/auto-reply/reply/commands-stop-target.test.ts +++ b/src/auto-reply/reply/commands-stop-target.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { handleStopCommand } from "./commands-session-abort.js"; +import "./commands-session-abort.test-support.js"; import type { HandleCommandsParams } from "./commands-types.js"; const abortEmbeddedPiRunMock = vi.hoisted(() => vi.fn()); @@ -40,10 +41,6 @@ vi.mock("./commands-session-store.js", () => ({ persistAbortTargetEntry: persistAbortTargetEntryMock, })); -vi.mock("./queue.js", () => ({ - clearSessionQueues: vi.fn(() => ({ followupCleared: 0, laneCleared: 0, keys: [] })), -})); - vi.mock("./reply-run-registry.js", () => ({ replyRunRegistry: { abort: replyRunAbortMock, diff --git a/src/auto-reply/reply/commands-subagents-info.test.ts b/src/auto-reply/reply/commands-subagents-info.test.ts index e9c0ca079a0..4cdd000a8b6 100644 --- a/src/auto-reply/reply/commands-subagents-info.test.ts +++ b/src/auto-reply/reply/commands-subagents-info.test.ts @@ -9,6 +9,7 @@ import { failTaskRunByRunId } from "../../tasks/task-executor.js"; import { createTaskRecord, resetTaskRegistryForTests } from "../../tasks/task-registry.js"; import type { ReplyPayload } from "../types.js"; import { handleSubagentsInfoAction } from "./commands-subagents/action-info.js"; +import { baseCommandTestConfig } from "./commands.test-harness.js"; function buildInfoContext(params: { cfg: OpenClawConfig; runs: object[]; restTokens: string[] }) { return { @@ -71,11 +72,7 @@ describe("subagents info", () => { terminalSummary: "Completed the requested task", deliveryStatus: "delivered", }); - const cfg = { - commands: { text: true }, - channels: { whatsapp: { allowFrom: ["*"] } }, - session: { mainKey: "main", scope: "per-sender" }, - } as OpenClawConfig; + const cfg = baseCommandTestConfig; const result = handleSubagentsInfoAction( buildInfoContext({ cfg, runs: [run], restTokens: ["1"] }), ); @@ -135,11 +132,7 @@ describe("subagents info", () => { ].join("\n"), terminalSummary: "Needs manual follow-up.", }); - const cfg = { - commands: { text: true }, - channels: { whatsapp: { allowFrom: ["*"] } }, - session: { mainKey: "main", scope: "per-sender" }, - } as OpenClawConfig; + const cfg = baseCommandTestConfig; const result = handleSubagentsInfoAction( buildInfoContext({ cfg, runs: [run], restTokens: ["1"] }), ); diff --git a/src/auto-reply/reply/commands-tasks.test.ts b/src/auto-reply/reply/commands-tasks.test.ts index ac60872a715..aecbd680ac2 100644 --- a/src/auto-reply/reply/commands-tasks.test.ts +++ b/src/auto-reply/reply/commands-tasks.test.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; -import type { OpenClawConfig } from "../../config/config.js"; import { completeTaskRunByRunId, createQueuedTaskRun, @@ -8,9 +7,12 @@ import { failTaskRunByRunId, } from "../../tasks/task-executor.js"; import { resetTaskRegistryForTests } from "../../tasks/task-registry.js"; -import { configureTaskRegistryRuntime } from "../../tasks/task-registry.store.js"; import { buildTasksReply, handleTasksCommand } from "./commands-tasks.js"; -import { buildCommandTestParams } from "./commands.test-harness.js"; +import { + baseCommandTestConfig, + buildCommandTestParams, + configureInMemoryTaskRegistryStoreForTests, +} from "./commands.test-harness.js"; vi.mock("../../agents/agent-scope.js", async () => { const actual = await vi.importActual( @@ -22,11 +24,7 @@ vi.mock("../../agents/agent-scope.js", async () => { }; }); -const baseCfg = { - commands: { text: true }, - channels: { whatsapp: { allowFrom: ["*"] } }, - session: { mainKey: "main", scope: "per-sender" }, -} as OpenClawConfig; +const baseCfg = baseCommandTestConfig; async function buildTasksReplyForTest(params: { sessionKey?: string } = {}) { const commandParams = buildCommandTestParams("/tasks", baseCfg); @@ -36,25 +34,6 @@ async function buildTasksReplyForTest(params: { sessionKey?: string } = {}) { }); } -function configureInMemoryTaskRegistryStoreForTests(): void { - configureTaskRegistryRuntime({ - store: { - loadSnapshot: () => ({ - tasks: new Map(), - deliveryStates: new Map(), - }), - saveSnapshot: () => {}, - upsertTaskWithDeliveryState: () => {}, - upsertTask: () => {}, - deleteTaskWithDeliveryState: () => {}, - deleteTask: () => {}, - upsertDeliveryState: () => {}, - deleteDeliveryState: () => {}, - close: () => {}, - }, - }); -} - describe("buildTasksReply", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/auto-reply/reply/commands.test-harness.ts b/src/auto-reply/reply/commands.test-harness.ts index 29b36297659..c4599d41c62 100644 --- a/src/auto-reply/reply/commands.test-harness.ts +++ b/src/auto-reply/reply/commands.test-harness.ts @@ -1,9 +1,16 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { configureTaskRegistryRuntime } from "../../tasks/task-registry.store.js"; import type { MsgContext } from "../templating.js"; import { buildCommandContext } from "./commands-context.js"; import type { HandleCommandsParams } from "./commands-types.js"; import { parseInlineDirectives } from "./directive-handling.parse.js"; +export const baseCommandTestConfig = { + commands: { text: true }, + channels: { whatsapp: { allowFrom: ["*"] } }, + session: { mainKey: "main", scope: "per-sender" }, +} as OpenClawConfig; + export function buildCommandTestParams( commandBody: string, cfg: OpenClawConfig, @@ -49,3 +56,68 @@ export function buildCommandTestParams( }; return params; } + +export function configureInMemoryTaskRegistryStoreForTests(): void { + configureTaskRegistryRuntime({ + store: { + loadSnapshot: () => ({ + tasks: new Map(), + deliveryStates: new Map(), + }), + saveSnapshot: () => {}, + upsertTaskWithDeliveryState: () => {}, + upsertTask: () => {}, + deleteTaskWithDeliveryState: () => {}, + deleteTask: () => {}, + upsertDeliveryState: () => {}, + deleteDeliveryState: () => {}, + close: () => {}, + }, + }); +} + +export function buildPluginsCommandParams(params: { + commandBodyNormalized: string; + cfg?: OpenClawConfig; + workspaceDir?: string; + gatewayClientScopes?: string[]; +}): HandleCommandsParams { + const commandBodyNormalized = params.commandBodyNormalized; + return { + cfg: + params.cfg ?? + ({ + commands: { + text: true, + plugins: true, + }, + plugins: { enabled: true }, + } as OpenClawConfig), + ctx: { + Provider: "whatsapp", + Surface: "whatsapp", + CommandSource: "text", + GatewayClientScopes: params.gatewayClientScopes ?? ["operator.write", "operator.pairing"], + AccountId: undefined, + }, + command: { + commandBodyNormalized, + rawBodyNormalized: commandBodyNormalized, + isAuthorizedSender: true, + senderIsOwner: true, + senderId: "owner", + channel: "whatsapp", + channelId: "whatsapp", + surface: "whatsapp", + ownerList: [], + from: "test-user", + to: "test-bot", + }, + sessionKey: "agent:main:whatsapp:direct:test-user", + sessionEntry: { + sessionId: "session-plugin-command", + updatedAt: Date.now(), + }, + workspaceDir: params.workspaceDir ?? "/tmp/plugins-workspace", + } as unknown as HandleCommandsParams; +} diff --git a/src/auto-reply/reply/dispatch-acp-delivery.test.ts b/src/auto-reply/reply/dispatch-acp-delivery.test.ts index a941b11a0e4..fcd6d38bbb1 100644 --- a/src/auto-reply/reply/dispatch-acp-delivery.test.ts +++ b/src/auto-reply/reply/dispatch-acp-delivery.test.ts @@ -87,6 +87,39 @@ function createCoordinator(onReplyStart?: (...args: unknown[]) => Promise) }); } +function createDiscordAcpCoordinator(cfg: OpenClawConfig) { + return createAcpDispatchDeliveryCoordinator({ + cfg, + ctx: buildTestCtx({ + Provider: "discord", + Surface: "discord", + SessionKey: "agent:codex-acp:session-1", + }), + dispatcher: createDispatcher(), + inboundAudio: false, + shouldRouteToOriginating: true, + originatingChannel: "discord", + originatingTo: "channel:thread-1", + }); +} + +async function expectDiscordBlockRoutesToAccount( + cfg: OpenClawConfig, + accountId: string | undefined, +): Promise { + const coordinator = createDiscordAcpCoordinator(cfg); + + await coordinator.deliver("block", { text: "hello" }, { skipTts: true }); + + expect(deliveryMocks.routeReply).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "discord", + to: "channel:thread-1", + accountId, + }), + ); +} + describe("createAcpDispatchDeliveryCoordinator", () => { beforeEach(() => { deliveryMocks.routeReply.mockClear(); @@ -369,61 +402,20 @@ describe("createAcpDispatchDeliveryCoordinator", () => { }); it("routes ACP replies through the configured default account when AccountId is omitted", async () => { - const coordinator = createAcpDispatchDeliveryCoordinator({ - cfg: createAcpTestConfig({ + await expectDiscordBlockRoutesToAccount( + createAcpTestConfig({ channels: { discord: { defaultAccount: "work", }, }, }), - ctx: buildTestCtx({ - Provider: "discord", - Surface: "discord", - SessionKey: "agent:codex-acp:session-1", - }), - dispatcher: createDispatcher(), - inboundAudio: false, - shouldRouteToOriginating: true, - originatingChannel: "discord", - originatingTo: "channel:thread-1", - }); - - await coordinator.deliver("block", { text: "hello" }, { skipTts: true }); - - expect(deliveryMocks.routeReply).toHaveBeenCalledWith( - expect.objectContaining({ - channel: "discord", - to: "channel:thread-1", - accountId: "work", - }), + "work", ); }); it("routes ACP replies when cfg.channels is missing", async () => { - const coordinator = createAcpDispatchDeliveryCoordinator({ - cfg: {} as OpenClawConfig, - ctx: buildTestCtx({ - Provider: "discord", - Surface: "discord", - SessionKey: "agent:codex-acp:session-1", - }), - dispatcher: createDispatcher(), - inboundAudio: false, - shouldRouteToOriginating: true, - originatingChannel: "discord", - originatingTo: "channel:thread-1", - }); - - await coordinator.deliver("block", { text: "hello" }, { skipTts: true }); - - expect(deliveryMocks.routeReply).toHaveBeenCalledWith( - expect.objectContaining({ - channel: "discord", - to: "channel:thread-1", - accountId: undefined, - }), - ); + await expectDiscordBlockRoutesToAccount({} as OpenClawConfig, undefined); }); it("treats routed discord block text as visible", async () => {