test(auto-reply): consolidate command suites (#109342)

This commit is contained in:
Peter Steinberger
2026-07-16 14:23:11 -07:00
committed by GitHub
parent 786ad1dbf8
commit 58f43c703e
12 changed files with 837 additions and 874 deletions

View File

@@ -187,7 +187,7 @@ without exceptions outside doctor/import/export/debug boundaries.
Proof: no schema change in this pass; `pnpm db:kysely:check`;
`pnpm lint:kysely`.
- [x] Re-run focused tests for touched stores, commands, and scripts.
Proof: `pnpm test src/cron/service/store.test.ts src/cron/store.test.ts src/cron/service.heartbeat-ok-summary-suppressed.test.ts src/cron/service.main-job-passes-heartbeat-target-last.test.ts src/cron/service.every-jobs-fire.test.ts src/cron/service.persists-delivered-status.test.ts src/cron/service.runs-one-shot-main-job-disables-it.test.ts src/cron/service/ops.test.ts src/cron/service/timer.regression.test.ts src/auto-reply/reply/commands-export-trajectory.test.ts extensions/telegram/src/thread-bindings.test.ts extensions/slack/src/monitor/message-handler/prepare.test.ts src/acp/translator.session-lineage-meta.test.ts`; `git diff --check`.
Proof: `pnpm test src/cron/service/store.test.ts src/cron/store.test.ts src/cron/service.heartbeat-ok-summary-suppressed.test.ts src/cron/service.main-job-passes-heartbeat-target-last.test.ts src/cron/service.every-jobs-fire.test.ts src/cron/service.persists-delivered-status.test.ts src/cron/service.runs-one-shot-main-job-disables-it.test.ts src/cron/service/ops.test.ts src/cron/service/timer.regression.test.ts src/auto-reply/reply/commands-export-session.test.ts extensions/telegram/src/thread-bindings.test.ts extensions/slack/src/monitor/message-handler/prepare.test.ts src/acp/translator.session-lineage-meta.test.ts`; `git diff --check`.
- [x] Before declaring `done`, run the changed gate or remote broad proof.
Proof: `pnpm check:changed --timed -- <changed extension paths>` passed on
Hetzner Crabbox run `run_3f1cabf6b25c` after temporary Node 24/pnpm setup and

View File

@@ -1,11 +1,17 @@
/** Tests context report command output and generated report files. */
/** Tests context command behavior, token reporting, and generated report files. */
import { mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { buildContextReply } from "./commands-context-report.js";
import { buildCommandContext } from "./commands-context.js";
import type { HandleCommandsParams } from "./commands-types.js";
import { stripStructuralPrefixes } from "./mentions.js";
import { buildTestCtx } from "./test-ctx.js";
/** Tests context report command output and generated report files. */
function makeParams(
commandBodyNormalized: string,
@@ -412,3 +418,106 @@ describe("buildContextReply", () => {
expect(result.mediaUrl).toBeUndefined();
});
});
/** Tests context command behavior and token reporting. */
describe("buildCommandContext", () => {
it("canonicalizes registered aliases like /id to their primary command", () => {
const ctx = buildTestCtx({
Provider: "webchat",
Surface: "webchat",
From: "user",
To: "bot",
Body: "/id",
RawBody: "/id",
CommandBody: "/id",
BodyForCommands: "/id",
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: "/id",
commandAuthorized: true,
});
expect(result.commandBodyNormalized).toBe("/whoami");
});
it("preserves multiline soft reset tails after structural normalization", () => {
const ctx = buildTestCtx({
Provider: "whatsapp",
Surface: "whatsapp",
From: "user",
To: "bot",
Body: "/reset soft\nre-read persona files",
RawBody: "/reset soft\nre-read persona files",
CommandBody: "/reset soft\nre-read persona files",
BodyForCommands: "/reset soft\nre-read persona files",
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: stripStructuralPrefixes("/reset soft\nre-read persona files"),
commandAuthorized: true,
});
expect(result.commandBodyNormalized).toBe("/reset soft re-read persona files");
});
it("preserves multiline slash skill payloads after structural normalization", () => {
const body = "/skill demo_skill first line\nsecond line";
const ctx = buildTestCtx({
Provider: "whatsapp",
Surface: "whatsapp",
From: "user",
To: "bot",
Body: body,
RawBody: body,
CommandBody: body,
BodyForCommands: body,
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: stripStructuralPrefixes(body),
commandAuthorized: true,
});
expect(result.commandBodyNormalized).toBe("/skill demo_skill first line\nsecond line");
});
it("maps explicit gateway origin into command context", () => {
const ctx = buildTestCtx({
Provider: "internal",
Surface: "internal",
OriginatingChannel: "slack",
OriginatingTo: "user:U123",
SenderId: "gateway-client",
From: undefined,
To: undefined,
Body: "/codex bind",
RawBody: "/codex bind",
CommandBody: "/codex bind",
BodyForCommands: "/codex bind",
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: "/codex bind",
commandAuthorized: true,
});
expect(result.channel).toBe("slack");
expect(result.channelId).toBe("slack");
expect(result.from).toBe("gateway-client");
expect(result.to).toBe("user:U123");
});
});

View File

@@ -1,107 +0,0 @@
/** Tests context command behavior and token reporting. */
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { buildCommandContext } from "./commands-context.js";
import { stripStructuralPrefixes } from "./mentions.js";
import { buildTestCtx } from "./test-ctx.js";
describe("buildCommandContext", () => {
it("canonicalizes registered aliases like /id to their primary command", () => {
const ctx = buildTestCtx({
Provider: "webchat",
Surface: "webchat",
From: "user",
To: "bot",
Body: "/id",
RawBody: "/id",
CommandBody: "/id",
BodyForCommands: "/id",
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: "/id",
commandAuthorized: true,
});
expect(result.commandBodyNormalized).toBe("/whoami");
});
it("preserves multiline soft reset tails after structural normalization", () => {
const ctx = buildTestCtx({
Provider: "whatsapp",
Surface: "whatsapp",
From: "user",
To: "bot",
Body: "/reset soft\nre-read persona files",
RawBody: "/reset soft\nre-read persona files",
CommandBody: "/reset soft\nre-read persona files",
BodyForCommands: "/reset soft\nre-read persona files",
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: stripStructuralPrefixes("/reset soft\nre-read persona files"),
commandAuthorized: true,
});
expect(result.commandBodyNormalized).toBe("/reset soft re-read persona files");
});
it("preserves multiline slash skill payloads after structural normalization", () => {
const body = "/skill demo_skill first line\nsecond line";
const ctx = buildTestCtx({
Provider: "whatsapp",
Surface: "whatsapp",
From: "user",
To: "bot",
Body: body,
RawBody: body,
CommandBody: body,
BodyForCommands: body,
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: stripStructuralPrefixes(body),
commandAuthorized: true,
});
expect(result.commandBodyNormalized).toBe("/skill demo_skill first line\nsecond line");
});
it("maps explicit gateway origin into command context", () => {
const ctx = buildTestCtx({
Provider: "internal",
Surface: "internal",
OriginatingChannel: "slack",
OriginatingTo: "user:U123",
SenderId: "gateway-client",
From: undefined,
To: undefined,
Body: "/codex bind",
RawBody: "/codex bind",
CommandBody: "/codex bind",
BodyForCommands: "/codex bind",
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: "/codex bind",
commandAuthorized: true,
});
expect(result.channel).toBe("slack");
expect(result.channelId).toBe("slack");
expect(result.from).toBe("gateway-client");
expect(result.to).toBe("user:U123");
});
});

View File

@@ -1,120 +0,0 @@
// Tests command send policy behavior for visible replies and message-tool routing.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CommandHandler, HandleCommandsParams } from "./commands-types.js";
const loadCommandHandlersMock = vi.hoisted(
(): ReturnType<typeof vi.fn<() => CommandHandler[]>> => vi.fn<() => CommandHandler[]>(() => []),
);
vi.mock("./commands-handlers.runtime.js", () => ({
loadCommandHandlers: () => loadCommandHandlersMock(),
}));
vi.mock("./commands-reset.js", () => ({
maybeHandleResetCommand: vi.fn(async () => null),
}));
vi.mock("../commands-registry.js", () => ({
shouldHandleTextCommands: vi.fn(() => true),
}));
function makeParams(): HandleCommandsParams {
return {
cfg: {
commands: { text: true },
session: {
sendPolicy: {
default: "allow",
rules: [{ action: "deny", match: { channel: "telegram" } }],
},
},
},
ctx: {
Provider: "whatsapp",
Surface: "whatsapp",
CommandSource: "text",
},
command: {
commandBodyNormalized: "/unknown",
rawBodyNormalized: "/unknown",
isAuthorizedSender: true,
senderIsOwner: true,
senderId: "owner",
channel: "whatsapp",
channelId: "whatsapp",
surface: "whatsapp",
ownerList: [],
from: "owner",
to: "bot",
},
directives: {},
elevated: { enabled: true, allowed: true, failures: [] },
sessionKey: "agent:target:main",
sessionEntry: {
sessionId: "wrapper-session",
updatedAt: Date.now(),
channel: "whatsapp",
chatType: "direct",
},
sessionStore: {
"agent:target:main": {
sessionId: "target-session",
updatedAt: Date.now(),
channel: "telegram",
chatType: "direct",
},
},
workspaceDir: "/tmp/workspace",
defaultGroupActivation: () => "mention",
resolvedVerboseLevel: "off",
resolvedReasoningLevel: "off",
resolveDefaultThinkingLevel: async () => undefined,
provider: "openai",
model: "gpt-5.4",
contextTokens: 0,
isGroup: false,
} as unknown as HandleCommandsParams;
}
describe("handleCommands send policy", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
loadCommandHandlersMock.mockReturnValue([]);
});
it("allows processing to continue even when send policy is deny (#53328)", async () => {
const { handleCommands } = await import("./commands-core.js");
// sendPolicy deny now only suppresses outbound delivery, not inbound processing.
// The deny gate moved to dispatch-from-config.ts where it suppresses delivery
// after the agent has processed the message.
const result = await handleCommands(makeParams());
expect(result).toEqual({ shouldContinue: true });
});
it("marks command replies as non-threaded", async () => {
const { handleCommands } = await import("./commands-core.js");
loadCommandHandlersMock.mockReturnValue([
vi.fn(async () => ({
shouldContinue: false,
reply: {
text: "done",
replyToId: "msg-123",
replyToCurrent: true,
},
})),
]);
const result = await handleCommands(makeParams());
expect(result).toEqual({
shouldContinue: false,
reply: {
text: "done",
replyToId: undefined,
replyToCurrent: false,
},
});
});
});

View File

@@ -1,7 +1,9 @@
// Tests core command dispatch, aliases, authorization, and handler outcomes.
// Tests core command dispatch, reset hooks, authorization, and send policy.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { HookRunner } from "../../plugins/hooks.js";
import type { HandleCommandsParams } from "./commands-types.js";
import type { CommandHandler, HandleCommandsParams } from "./commands-types.js";
// Tests core command dispatch, aliases, authorization, and handler outcomes.
const hookRunnerMocks = vi.hoisted(() => ({
hasHooks: vi.fn<HookRunner["hasHooks"]>(),
@@ -208,3 +210,122 @@ describe("emitResetCommandHooks", () => {
]);
});
});
// Tests command send policy behavior for visible replies and message-tool routing.
const loadCommandHandlersMock = vi.hoisted(
(): ReturnType<typeof vi.fn<() => CommandHandler[]>> => vi.fn<() => CommandHandler[]>(() => []),
);
vi.mock("./commands-handlers.runtime.js", () => ({
loadCommandHandlers: () => loadCommandHandlersMock(),
}));
vi.mock("./commands-reset.js", () => ({
maybeHandleResetCommand: vi.fn(async () => null),
}));
vi.mock("../commands-registry.js", () => ({
shouldHandleTextCommands: vi.fn(() => true),
}));
function makeParams(): HandleCommandsParams {
return {
cfg: {
commands: { text: true },
session: {
sendPolicy: {
default: "allow",
rules: [{ action: "deny", match: { channel: "telegram" } }],
},
},
},
ctx: {
Provider: "whatsapp",
Surface: "whatsapp",
CommandSource: "text",
},
command: {
commandBodyNormalized: "/unknown",
rawBodyNormalized: "/unknown",
isAuthorizedSender: true,
senderIsOwner: true,
senderId: "owner",
channel: "whatsapp",
channelId: "whatsapp",
surface: "whatsapp",
ownerList: [],
from: "owner",
to: "bot",
},
directives: {},
elevated: { enabled: true, allowed: true, failures: [] },
sessionKey: "agent:target:main",
sessionEntry: {
sessionId: "wrapper-session",
updatedAt: Date.now(),
channel: "whatsapp",
chatType: "direct",
},
sessionStore: {
"agent:target:main": {
sessionId: "target-session",
updatedAt: Date.now(),
channel: "telegram",
chatType: "direct",
},
},
workspaceDir: "/tmp/workspace",
defaultGroupActivation: () => "mention",
resolvedVerboseLevel: "off",
resolvedReasoningLevel: "off",
resolveDefaultThinkingLevel: async () => undefined,
provider: "openai",
model: "gpt-5.4",
contextTokens: 0,
isGroup: false,
} as unknown as HandleCommandsParams;
}
describe("handleCommands send policy", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
loadCommandHandlersMock.mockReturnValue([]);
});
it("allows processing to continue even when send policy is deny (#53328)", async () => {
const { handleCommands } = await import("./commands-core.js");
// sendPolicy deny now only suppresses outbound delivery, not inbound processing.
// The deny gate moved to dispatch-from-config.ts where it suppresses delivery
// after the agent has processed the message.
const result = await handleCommands(makeParams());
expect(result).toEqual({ shouldContinue: true });
});
it("marks command replies as non-threaded", async () => {
const { handleCommands } = await import("./commands-core.js");
loadCommandHandlersMock.mockReturnValue([
vi.fn(async () => ({
shouldContinue: false,
reply: {
text: "done",
replyToId: "msg-123",
replyToCurrent: true,
},
})),
]);
const result = await handleCommands(makeParams());
expect(result).toEqual({
shouldContinue: false,
reply: {
text: "done",
replyToId: undefined,
replyToCurrent: false,
},
});
});
});

View File

@@ -1,9 +1,11 @@
// Tests the real fs-safe boundary used by session-export artifacts.
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
import { writeSessionExportFile } from "./commands-export-session-file.js";
const { writeSessionExportFile } = await vi.importActual<
typeof import("./commands-export-session-file.js")
>("./commands-export-session-file.js");
const tempDirs = useAutoCleanupTempDirTracker(afterEach);

View File

@@ -1,8 +1,12 @@
// Tests session export command packaging, filesystem writes, and prompt bundle capture.
// Tests session and trajectory export command packaging, filesystem writes, and approval routing.
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { FsSafeError } from "../../infra/fs-safe.js";
import { buildExportSessionReply } from "./commands-export-session.js";
import type { HandleCommandsParams } from "./commands-types.js";
// Tests session export command packaging, filesystem writes, and prompt bundle capture.
const hoisted = await vi.hoisted(async () => {
const { createExportCommandSessionMocks } = await import("./commands-export-test-mocks.js");
return {
@@ -60,9 +64,9 @@ vi.mock("./commands-system-prompt.js", () => ({
resolveCommandsSystemPromptBundle: hoisted.resolveCommandsSystemPromptBundleMock,
}));
vi.mock("./commands-export-session-file.js", () => {
return { writeSessionExportFile: hoisted.writeSessionExportFileMock };
});
vi.mock("./commands-export-session-file.js", () => ({
writeSessionExportFile: hoisted.writeSessionExportFileMock,
}));
vi.mock("../../agents/sessions/session-manager.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/sessions/session-manager.js")>();
@@ -113,9 +117,6 @@ vi.mock("node:fs/promises", async () => {
};
});
import { FsSafeError } from "../../infra/fs-safe.js";
import { buildExportSessionReply } from "./commands-export-session.js";
function makeParams(): HandleCommandsParams {
return {
cfg: {},
@@ -776,3 +777,6 @@ describe("buildExportSessionReply", () => {
expect(sessionDataFromHtml(writtenHtml()).warning).toBeUndefined();
});
});
await import("./commands-export-session-file.test-support.js");
await import("./commands-export-trajectory.test-support.js");

View File

@@ -1,7 +1,13 @@
// Tests info-style commands that report context, status, skills, and session exports.
import { beforeEach, describe, expect, it, vi } from "vitest";
// Tests info-style command responses, including effective tool inventory.
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import type { EffectiveToolInventoryResult } from "../../agents/tools-effective-inventory.types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import {
createChannelTestPluginBase,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import type { MsgContext } from "../templating.js";
import { handleContextCommand } from "./commands-context-command.js";
import {
@@ -14,6 +20,8 @@ import { buildStatusPluginsReply, buildStatusReply } from "./commands-status.js"
import type { HandleCommandsParams } from "./commands-types.js";
import { handleWhoamiCommand } from "./commands-whoami.js";
// Tests info-style commands that report context, status, skills, and session exports.
const buildContextReplyMock = vi.hoisted(() => vi.fn());
const buildExportTrajectoryCommandReplyMock = vi.hoisted(() =>
vi.fn(async () => ({ text: "exported" })),
@@ -497,3 +505,400 @@ describe("info command handlers", () => {
expect(listParams.agentIds).toEqual(["target"]);
});
});
// Tests tool listing in info command responses.
function makeInventoryEntry(params: {
id: string;
label: string;
description: string;
source: "core" | "plugin" | "channel";
pluginId?: string;
channelId?: string;
}) {
return {
...params,
rawDescription: params.description,
};
}
function makeDefaultInventory(): EffectiveToolInventoryResult {
return {
agentId: "main",
profile: "coding",
groups: [
{
id: "core",
label: "Built-in tools",
source: "core",
tools: [
makeInventoryEntry({
id: "exec",
label: "Exec",
description: "Run shell commands",
source: "core",
}),
],
},
{
id: "plugin",
label: "Connected tools",
source: "plugin",
tools: [
makeInventoryEntry({
id: "docs_lookup",
label: "Docs Lookup",
description: "Search internal documentation",
source: "plugin",
pluginId: "docs",
}),
],
},
],
};
}
const toolsTestState = vi.hoisted(() => {
const defaultResolveTools = (): EffectiveToolInventoryResult => makeDefaultInventory();
return {
resolveToolsImpl: defaultResolveTools,
resolveToolsMock: vi.fn((..._args: unknown[]) => defaultResolveTools()),
threadingContext: {
currentChannelId: "channel-123",
currentMessageId: "message-456",
},
replyToMode: "all" as const,
};
});
vi.mock("../../agents/tools-effective-inventory.js", () => ({
resolveEffectiveToolInventory: (...args: unknown[]) => toolsTestState.resolveToolsMock(...args),
}));
vi.mock("./agent-runner-utils.js", () => ({
buildThreadingToolContext: () => toolsTestState.threadingContext,
}));
vi.mock("./reply-threading.js", () => ({
resolveReplyToMode: () => toolsTestState.replyToMode,
}));
let buildCommandTestParamsImpl: typeof import("./commands.test-harness.js").buildCommandTestParams;
let handleToolsCommandImpl: typeof import("./commands-info.js").handleToolsCommand;
async function loadToolsHarness(options?: { resolveTools?: () => EffectiveToolInventoryResult }) {
toolsTestState.resolveToolsImpl = options?.resolveTools ?? (() => makeDefaultInventory());
toolsTestState.resolveToolsMock.mockImplementation((..._args: unknown[]) =>
toolsTestState.resolveToolsImpl(),
);
return {
buildCommandTestParamsLocal: buildCommandTestParamsImpl,
handleToolsCommandLocal: handleToolsCommandImpl,
resolveToolsMock: toolsTestState.resolveToolsMock,
};
}
function buildConfig() {
return {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
}
function resolveToolsArg(resolveToolsMock: { mock: { calls: unknown[][] } }, index = 0) {
const [arg] = resolveToolsMock.mock.calls[index] ?? [];
if (!arg || typeof arg !== "object") {
throw new Error(`expected resolve tools call ${index + 1}`);
}
return arg as Record<string, unknown>;
}
describe("handleToolsCommand", () => {
beforeAll(async () => {
({ buildCommandTestParams: buildCommandTestParamsImpl } =
await import("./commands.test-harness.js"));
({ handleToolsCommand: handleToolsCommandImpl } = await import("./commands-info.js"));
});
beforeEach(() => {
vi.mocked(resolveSessionAgentId).mockReturnValue("main");
toolsTestState.resolveToolsMock.mockReset();
toolsTestState.resolveToolsImpl = () => makeDefaultInventory();
setActivePluginRegistry(createTestRegistry([]));
});
it("renders a product-facing tool list", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.agentId = "main";
params.provider = "openai";
params.model = "gpt-4.1";
params.ctx = {
...params.ctx,
From: "telegram:group:abc123",
GroupChannel: "#ops",
GroupSpace: "workspace-1",
SenderName: "User Name",
SenderUsername: "user_name",
SenderE164: "+1000",
MessageThreadId: 99,
AccountId: "acct-1",
Provider: "telegram",
ChatType: "group",
};
const result = await handleToolsCommandLocal(params, true);
expect(result?.reply?.text).toContain("Available tools");
expect(result?.reply?.text).toContain("Profile: coding");
expect(result?.reply?.text).toContain("Built-in tools");
expect(result?.reply?.text).toContain("exec");
expect(result?.reply?.text).toContain("Connected tools");
expect(result?.reply?.text).toContain("docs_lookup (docs)");
expect(result?.reply?.text).not.toContain("unavailable right now");
const toolsArg = resolveToolsArg(resolveToolsMock);
expect(toolsArg).not.toHaveProperty("senderIsOwner");
expect(toolsArg.senderId).toBeUndefined();
expect(toolsArg.senderName).toBe("User Name");
expect(toolsArg.senderUsername).toBe("user_name");
expect(toolsArg.senderE164).toBe("+1000");
expect(toolsArg.accountId).toBe("acct-1");
expect(toolsArg.currentChannelId).toBe("channel-123");
expect(toolsArg.currentThreadTs).toBe("99");
expect(toolsArg.currentMessageId).toBe("message-456");
expect(toolsArg.groupId).toBe("abc123");
expect(toolsArg.groupChannel).toBe("#ops");
expect(toolsArg.groupSpace).toBe("workspace-1");
expect(toolsArg.replyToMode).toBe("all");
});
it("returns usage when arguments are provided", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness();
const result = await handleToolsCommandLocal(
buildCommandTestParamsLocal("/tools extra", buildConfig(), undefined, {
workspaceDir: "/tmp",
}),
true,
);
expect(result).toEqual({
shouldContinue: false,
reply: { text: "Usage: /tools [compact|verbose]" },
});
});
it("does not synthesize group ids for direct-chat sender ids", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.ctx = {
...params.ctx,
From: "telegram:8231046597",
Provider: "telegram",
ChatType: "dm",
};
await handleToolsCommandLocal(params, true);
expect(resolveToolsArg(resolveToolsMock).groupId).toBeUndefined();
});
it("prefers the target session entry for tool inventory group metadata", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.sessionEntry = {
sessionId: "wrapper-session",
updatedAt: Date.now(),
groupId: "wrapper-group",
groupChannel: "#wrapper",
space: "wrapper-space",
};
params.sessionStore = {
[params.sessionKey]: {
sessionId: "target-session",
updatedAt: Date.now(),
groupId: "target-group",
groupChannel: "#target",
space: "target-space",
},
};
params.ctx = {
...params.ctx,
From: "telegram:group:abc123",
Provider: "telegram",
Surface: "telegram",
GroupChannel: "#ctx",
GroupSpace: "ctx-space",
};
await handleToolsCommandLocal(params, true);
const toolsArg = resolveToolsArg(resolveToolsMock);
expect(toolsArg.groupId).toBe("target-group");
expect(toolsArg.groupChannel).toBe("#target");
expect(toolsArg.groupSpace).toBe("target-space");
});
it("renders the detailed tool list in verbose mode", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness();
const result = await handleToolsCommandLocal(
buildCommandTestParamsLocal("/tools verbose", buildConfig(), undefined, {
workspaceDir: "/tmp",
}),
true,
);
expect(result?.reply?.text).toContain("What this agent can use right now:");
expect(result?.reply?.text).toContain("Profile: coding");
expect(result?.reply?.text).toContain("Exec - Run shell commands");
expect(result?.reply?.text).toContain("Docs Lookup - Search internal documentation");
});
it("accepts explicit compact mode", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness();
const result = await handleToolsCommandLocal(
buildCommandTestParamsLocal("/tools compact", buildConfig(), undefined, {
workspaceDir: "/tmp",
}),
true,
);
expect(result?.reply?.text).toContain("exec");
expect(result?.reply?.text).toContain("Use /tools verbose for descriptions.");
});
it("ignores unauthorized senders", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.command = {
...params.command,
isAuthorizedSender: false,
senderId: "unauthorized",
};
const result = await handleToolsCommandLocal(params, true);
expect(result).toEqual({ shouldContinue: false });
});
it("uses the configured default account when /tools omits AccountId", async () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "telegram",
source: "test",
plugin: {
...createChannelTestPluginBase({
id: "telegram",
label: "Telegram",
config: {
listAccountIds: () => ["default", "work"],
defaultAccountId: () => "work",
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "work" }),
},
}),
},
},
]),
);
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal(
"/tools",
{
commands: { text: true },
channels: { telegram: { defaultAccount: "work" } },
} as OpenClawConfig,
undefined,
{ workspaceDir: "/tmp" },
);
params.agentId = "main";
params.provider = "openai";
params.model = "gpt-4.1";
params.ctx = {
...params.ctx,
OriginatingChannel: "telegram",
Provider: "telegram",
Surface: "telegram",
ChatType: "group",
AccountId: undefined,
};
params.command = {
...params.command,
channel: "telegram",
};
await handleToolsCommandLocal(params, true);
expect(resolveToolsArg(resolveToolsMock).accountId).toBe("work");
});
it("returns a concise fallback error on effective inventory failures", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness({
resolveTools: () => {
throw new Error("boom");
},
});
const result = await handleToolsCommandLocal(
buildCommandTestParamsLocal("/tools", buildConfig(), undefined, { workspaceDir: "/tmp" }),
true,
);
expect(result).toEqual({
shouldContinue: false,
reply: { text: "Couldn't load available tools right now. Try again in a moment." },
});
});
it("uses the canonical target session agent for /tools inventory", async () => {
vi.mocked(resolveSessionAgentId).mockReturnValue("target");
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.agentId = "main";
params.sessionKey = "agent:target:whatsapp:direct:12345";
const result = await handleToolsCommandLocal(params, true);
expect(result?.shouldContinue).toBe(false);
const toolsArg = resolveToolsArg(resolveToolsMock);
expect(toolsArg.agentId).toBe("target");
expect(toolsArg.sessionKey).toBe("agent:target:whatsapp:direct:12345");
});
it("does not forward a stale ambient agentDir for session-bound /tools", async () => {
vi.mocked(resolveSessionAgentId).mockReturnValue("target");
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.agentId = "main";
params.agentDir = "/tmp/agents/main/agent";
params.sessionKey = "agent:target:whatsapp:direct:12345";
const result = await handleToolsCommandLocal(params, true);
expect(result?.shouldContinue).toBe(false);
const toolsArg = resolveToolsArg(resolveToolsMock);
expect(toolsArg.agentId).toBe("target");
expect(toolsArg.agentDir).toBeUndefined();
expect(toolsArg.sessionKey).toBe("agent:target:whatsapp:direct:12345");
});
});

View File

@@ -1,415 +0,0 @@
// Tests tool listing in info command responses.
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { EffectiveToolInventoryResult } from "../../agents/tools-effective-inventory.types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import {
createChannelTestPluginBase,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
function makeInventoryEntry(params: {
id: string;
label: string;
description: string;
source: "core" | "plugin" | "channel";
pluginId?: string;
channelId?: string;
}) {
return {
...params,
rawDescription: params.description,
};
}
function makeDefaultInventory(): EffectiveToolInventoryResult {
return {
agentId: "main",
profile: "coding",
groups: [
{
id: "core",
label: "Built-in tools",
source: "core",
tools: [
makeInventoryEntry({
id: "exec",
label: "Exec",
description: "Run shell commands",
source: "core",
}),
],
},
{
id: "plugin",
label: "Connected tools",
source: "plugin",
tools: [
makeInventoryEntry({
id: "docs_lookup",
label: "Docs Lookup",
description: "Search internal documentation",
source: "plugin",
pluginId: "docs",
}),
],
},
],
};
}
const toolsTestState = vi.hoisted(() => {
const defaultResolveTools = (): EffectiveToolInventoryResult => makeDefaultInventory();
return {
resolveToolsImpl: defaultResolveTools,
resolveToolsMock: vi.fn((..._args: unknown[]) => defaultResolveTools()),
threadingContext: {
currentChannelId: "channel-123",
currentMessageId: "message-456",
},
replyToMode: "all" as const,
};
});
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: vi.fn(() => "main"),
};
});
vi.mock("../../agents/tools-effective-inventory.js", () => ({
resolveEffectiveToolInventory: (...args: unknown[]) => toolsTestState.resolveToolsMock(...args),
}));
vi.mock("./agent-runner-utils.js", () => ({
buildThreadingToolContext: () => toolsTestState.threadingContext,
}));
vi.mock("./reply-threading.js", () => ({
resolveReplyToMode: () => toolsTestState.replyToMode,
}));
let buildCommandTestParamsImpl: typeof import("./commands.test-harness.js").buildCommandTestParams;
let handleToolsCommandImpl: typeof import("./commands-info.js").handleToolsCommand;
async function loadToolsHarness(options?: { resolveTools?: () => EffectiveToolInventoryResult }) {
toolsTestState.resolveToolsImpl = options?.resolveTools ?? (() => makeDefaultInventory());
toolsTestState.resolveToolsMock.mockImplementation((..._args: unknown[]) =>
toolsTestState.resolveToolsImpl(),
);
return {
buildCommandTestParamsLocal: buildCommandTestParamsImpl,
handleToolsCommandLocal: handleToolsCommandImpl,
resolveToolsMock: toolsTestState.resolveToolsMock,
};
}
function buildConfig() {
return {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
}
function resolveToolsArg(resolveToolsMock: { mock: { calls: unknown[][] } }, index = 0) {
const [arg] = resolveToolsMock.mock.calls[index] ?? [];
if (!arg || typeof arg !== "object") {
throw new Error(`expected resolve tools call ${index + 1}`);
}
return arg as Record<string, unknown>;
}
describe("handleToolsCommand", () => {
beforeAll(async () => {
({ buildCommandTestParams: buildCommandTestParamsImpl } =
await import("./commands.test-harness.js"));
({ handleToolsCommand: handleToolsCommandImpl } = await import("./commands-info.js"));
});
beforeEach(() => {
toolsTestState.resolveToolsMock.mockReset();
toolsTestState.resolveToolsImpl = () => makeDefaultInventory();
setActivePluginRegistry(createTestRegistry([]));
});
it("renders a product-facing tool list", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.agentId = "main";
params.provider = "openai";
params.model = "gpt-4.1";
params.ctx = {
...params.ctx,
From: "telegram:group:abc123",
GroupChannel: "#ops",
GroupSpace: "workspace-1",
SenderName: "User Name",
SenderUsername: "user_name",
SenderE164: "+1000",
MessageThreadId: 99,
AccountId: "acct-1",
Provider: "telegram",
ChatType: "group",
};
const result = await handleToolsCommandLocal(params, true);
expect(result?.reply?.text).toContain("Available tools");
expect(result?.reply?.text).toContain("Profile: coding");
expect(result?.reply?.text).toContain("Built-in tools");
expect(result?.reply?.text).toContain("exec");
expect(result?.reply?.text).toContain("Connected tools");
expect(result?.reply?.text).toContain("docs_lookup (docs)");
expect(result?.reply?.text).not.toContain("unavailable right now");
const toolsArg = resolveToolsArg(resolveToolsMock);
expect(toolsArg).not.toHaveProperty("senderIsOwner");
expect(toolsArg.senderId).toBeUndefined();
expect(toolsArg.senderName).toBe("User Name");
expect(toolsArg.senderUsername).toBe("user_name");
expect(toolsArg.senderE164).toBe("+1000");
expect(toolsArg.accountId).toBe("acct-1");
expect(toolsArg.currentChannelId).toBe("channel-123");
expect(toolsArg.currentThreadTs).toBe("99");
expect(toolsArg.currentMessageId).toBe("message-456");
expect(toolsArg.groupId).toBe("abc123");
expect(toolsArg.groupChannel).toBe("#ops");
expect(toolsArg.groupSpace).toBe("workspace-1");
expect(toolsArg.replyToMode).toBe("all");
});
it("returns usage when arguments are provided", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness();
const result = await handleToolsCommandLocal(
buildCommandTestParamsLocal("/tools extra", buildConfig(), undefined, {
workspaceDir: "/tmp",
}),
true,
);
expect(result).toEqual({
shouldContinue: false,
reply: { text: "Usage: /tools [compact|verbose]" },
});
});
it("does not synthesize group ids for direct-chat sender ids", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.ctx = {
...params.ctx,
From: "telegram:8231046597",
Provider: "telegram",
ChatType: "dm",
};
await handleToolsCommandLocal(params, true);
expect(resolveToolsArg(resolveToolsMock).groupId).toBeUndefined();
});
it("prefers the target session entry for tool inventory group metadata", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.sessionEntry = {
sessionId: "wrapper-session",
updatedAt: Date.now(),
groupId: "wrapper-group",
groupChannel: "#wrapper",
space: "wrapper-space",
};
params.sessionStore = {
[params.sessionKey]: {
sessionId: "target-session",
updatedAt: Date.now(),
groupId: "target-group",
groupChannel: "#target",
space: "target-space",
},
};
params.ctx = {
...params.ctx,
From: "telegram:group:abc123",
Provider: "telegram",
Surface: "telegram",
GroupChannel: "#ctx",
GroupSpace: "ctx-space",
};
await handleToolsCommandLocal(params, true);
const toolsArg = resolveToolsArg(resolveToolsMock);
expect(toolsArg.groupId).toBe("target-group");
expect(toolsArg.groupChannel).toBe("#target");
expect(toolsArg.groupSpace).toBe("target-space");
});
it("renders the detailed tool list in verbose mode", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness();
const result = await handleToolsCommandLocal(
buildCommandTestParamsLocal("/tools verbose", buildConfig(), undefined, {
workspaceDir: "/tmp",
}),
true,
);
expect(result?.reply?.text).toContain("What this agent can use right now:");
expect(result?.reply?.text).toContain("Profile: coding");
expect(result?.reply?.text).toContain("Exec - Run shell commands");
expect(result?.reply?.text).toContain("Docs Lookup - Search internal documentation");
});
it("accepts explicit compact mode", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness();
const result = await handleToolsCommandLocal(
buildCommandTestParamsLocal("/tools compact", buildConfig(), undefined, {
workspaceDir: "/tmp",
}),
true,
);
expect(result?.reply?.text).toContain("exec");
expect(result?.reply?.text).toContain("Use /tools verbose for descriptions.");
});
it("ignores unauthorized senders", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.command = {
...params.command,
isAuthorizedSender: false,
senderId: "unauthorized",
};
const result = await handleToolsCommandLocal(params, true);
expect(result).toEqual({ shouldContinue: false });
});
it("uses the configured default account when /tools omits AccountId", async () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "telegram",
source: "test",
plugin: {
...createChannelTestPluginBase({
id: "telegram",
label: "Telegram",
config: {
listAccountIds: () => ["default", "work"],
defaultAccountId: () => "work",
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "work" }),
},
}),
},
},
]),
);
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal(
"/tools",
{
commands: { text: true },
channels: { telegram: { defaultAccount: "work" } },
} as OpenClawConfig,
undefined,
{ workspaceDir: "/tmp" },
);
params.agentId = "main";
params.provider = "openai";
params.model = "gpt-4.1";
params.ctx = {
...params.ctx,
OriginatingChannel: "telegram",
Provider: "telegram",
Surface: "telegram",
ChatType: "group",
AccountId: undefined,
};
params.command = {
...params.command,
channel: "telegram",
};
await handleToolsCommandLocal(params, true);
expect(resolveToolsArg(resolveToolsMock).accountId).toBe("work");
});
it("returns a concise fallback error on effective inventory failures", async () => {
const { buildCommandTestParamsLocal, handleToolsCommandLocal } = await loadToolsHarness({
resolveTools: () => {
throw new Error("boom");
},
});
const result = await handleToolsCommandLocal(
buildCommandTestParamsLocal("/tools", buildConfig(), undefined, { workspaceDir: "/tmp" }),
true,
);
expect(result).toEqual({
shouldContinue: false,
reply: { text: "Couldn't load available tools right now. Try again in a moment." },
});
});
it("uses the canonical target session agent for /tools inventory", async () => {
const { resolveSessionAgentId } = await import("../../agents/agent-scope.js");
vi.mocked(resolveSessionAgentId).mockReturnValue("target");
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.agentId = "main";
params.sessionKey = "agent:target:whatsapp:direct:12345";
const result = await handleToolsCommandLocal(params, true);
expect(result?.shouldContinue).toBe(false);
const toolsArg = resolveToolsArg(resolveToolsMock);
expect(toolsArg.agentId).toBe("target");
expect(toolsArg.sessionKey).toBe("agent:target:whatsapp:direct:12345");
});
it("does not forward a stale ambient agentDir for session-bound /tools", async () => {
const { resolveSessionAgentId } = await import("../../agents/agent-scope.js");
vi.mocked(resolveSessionAgentId).mockReturnValue("target");
const { buildCommandTestParamsLocal, handleToolsCommandLocal, resolveToolsMock } =
await loadToolsHarness();
const params = buildCommandTestParamsLocal("/tools", buildConfig(), undefined, {
workspaceDir: "/tmp",
});
params.agentId = "main";
params.agentDir = "/tmp/agents/main/agent";
params.sessionKey = "agent:target:whatsapp:direct:12345";
const result = await handleToolsCommandLocal(params, true);
expect(result?.shouldContinue).toBe(false);
const toolsArg = resolveToolsArg(resolveToolsMock);
expect(toolsArg.agentId).toBe("target");
expect(toolsArg.agentDir).toBeUndefined();
expect(toolsArg.sessionKey).toBe("agent:target:whatsapp:direct:12345");
});
});

View File

@@ -1,9 +1,9 @@
// Tests status command rendering for sessions, agents, and diagnostics.
// Tests status command rendering for sessions, agents, diagnostics, and model defaults.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { withTempHome } from "openclaw/plugin-sdk/test-env";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { normalizeTestText } from "../../../test/helpers/normalize-text.js";
import { saveAuthProfileStore } from "../../agents/auth-profiles/store.js";
import { testing as cliBackendsTesting } from "../../agents/cli-backends.test-support.js";
@@ -13,6 +13,7 @@ import {
addSubagentRunForTests,
resetSubagentRegistryForTests,
} from "../../agents/subagent-registry.test-helpers.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { ModelDefinitionConfig } from "../../config/types.models.js";
import {
completeTaskRunByRunId,
@@ -29,6 +30,8 @@ import {
configureInMemoryTaskRegistryStoreForTests,
} from "./commands.test-harness.js";
// Tests status command rendering for sessions, agents, and diagnostics.
type LoadProviderUsageSummary =
typeof import("../../infra/provider-usage.js").loadProviderUsageSummary;
@@ -2308,3 +2311,179 @@ describe("buildStatusReply subagent summary", () => {
});
});
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
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", () => {
beforeAll(async () => {
await buildKiraStatusReply({
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: "openai/gpt-5.4",
},
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig);
});
it("shows per-agent thinkingDefault in the status card", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: "openai/gpt-5.4",
},
list: [
{
id: "kira",
model: "openai/gpt-5.4",
thinkingDefault: "xhigh",
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildKiraStatusReply(cfg);
expect(reply?.text).toContain("Think: xhigh");
});
it("shows per-agent fallback overrides in the status card", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
fallbacks: ["google/gemini-2.5-flash"],
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
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");
});
it("keeps default fallback config when the agent has no explicit model", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildKiraStatusReply(cfg);
expect(reply?.text).toContain("Fallbacks: anthropic/claude-sonnet-4-6");
});
it("keeps agent primary strict when the agent has no explicit fallback override", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildKiraStatusReply(cfg);
expect(reply?.text).not.toContain("Fallbacks:");
});
it("treats an explicit empty per-agent fallback override as disabling inherited fallbacks", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
fallbacks: [],
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildKiraStatusReply(cfg);
expect(reply?.text).not.toContain("Fallbacks:");
});
});

View File

@@ -1,215 +0,0 @@
// Tests status command defaults for thinking and reasoning display.
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
vi.mock("../../agents/fast-mode.js", () => ({
resolveFastModeState: () => ({ mode: false, enabled: false, source: "default" }),
}));
vi.mock("../../agents/model-auth-label.js", () => ({
resolveModelAuthLabel: () => "api-key",
}));
vi.mock("../../agents/subagent-registry.js", () => ({
listSubagentRunsForRequester: () => [],
}));
vi.mock("../../infra/provider-usage.js", () => ({
resolveUsageProviderId: () => undefined,
loadProviderUsageSummary: async () => ({
updatedAt: Date.now(),
providers: [],
}),
formatUsageWindowSummary: () => undefined,
}));
vi.mock("../group-activation.js", () => ({
normalizeGroupActivation: (value: unknown) => value,
}));
vi.mock("./queue.js", async () => {
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
return {
...actual,
getFollowupQueueDepth: () => 0,
resolveQueueSettings: () => ({ mode: "interrupt" }),
};
});
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", () => {
beforeAll(async () => {
await buildKiraStatusReply({
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: "openai/gpt-5.4",
},
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig);
});
it("shows per-agent thinkingDefault in the status card", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: "openai/gpt-5.4",
},
list: [
{
id: "kira",
model: "openai/gpt-5.4",
thinkingDefault: "xhigh",
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildKiraStatusReply(cfg);
expect(reply?.text).toContain("Think: xhigh");
});
it("shows per-agent fallback overrides in the status card", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
fallbacks: ["google/gemini-2.5-flash"],
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
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");
});
it("keeps default fallback config when the agent has no explicit model", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildKiraStatusReply(cfg);
expect(reply?.text).toContain("Fallbacks: anthropic/claude-sonnet-4-6");
});
it("keeps agent primary strict when the agent has no explicit fallback override", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildKiraStatusReply(cfg);
expect(reply?.text).not.toContain("Fallbacks:");
});
it("treats an explicit empty per-agent fallback override as disabling inherited fallbacks", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
fallbacks: [],
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildKiraStatusReply(cfg);
expect(reply?.text).not.toContain("Fallbacks:");
});
});