test: share auto-reply command fixtures

This commit is contained in:
Peter Steinberger
2026-04-20 20:07:25 +01:00
parent bccd429f70
commit aa52d1be42
18 changed files with 317 additions and 485 deletions

View File

@@ -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<typeof runReplyAgent>[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(

View File

@@ -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["run"]> = {}): 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<void> {
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,

View File

@@ -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<void> {
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: () => {},
});

View File

@@ -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["run"]> = {}): 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<void> {
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: entry }, null, 2), "utf8");
}

View File

@@ -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(),

View File

@@ -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<typeof import("../../agents/agent-scope.js")>(
"../../agents/agent-scope.js",
);
return {
...actual,
resolveSessionAgentId: resolveSessionAgentIdMock,
resolveAgentDir: resolveAgentDirMock,
};
});

View File

@@ -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<typeof import("../../agents/agent-scope.js")>(
"../../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",

View File

@@ -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<typeof import("../../agents/agent-scope.js")>(
"../../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 () => {

View File

@@ -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", () => {

View File

@@ -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", () => {

View File

@@ -0,0 +1,5 @@
import { vi } from "vitest";
vi.mock("./queue.js", () => ({
clearSessionQueues: vi.fn(() => ({ followupCleared: 0, laneCleared: 0, keys: [] })),
}));

View File

@@ -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();

View File

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

View File

@@ -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,

View File

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

View File

@@ -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<typeof import("../../agents/agent-scope.js")>(
@@ -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();

View File

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

View File

@@ -87,6 +87,39 @@ function createCoordinator(onReplyStart?: (...args: unknown[]) => Promise<void>)
});
}
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<void> {
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 () => {