test: consolidate trigger handling suites

This commit is contained in:
Peter Steinberger
2026-02-23 19:40:39 +00:00
parent 87603b5c45
commit c88915b721
7 changed files with 964 additions and 1271 deletions

View File

@@ -0,0 +1,131 @@
import { describe, expect, it } from "vitest";
import {
getRunEmbeddedPiAgentMock,
makeCfg,
mockRunEmbeddedPiAgentOk,
withTempHome,
} from "./reply.triggers.trigger-handling.test-harness.js";
type GetReplyFromConfig = typeof import("./reply.js").getReplyFromConfig;
type InboundMessage = Parameters<GetReplyFromConfig>[0];
function getLastExtraSystemPrompt() {
return getRunEmbeddedPiAgentMock().mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? "";
}
export function registerGroupIntroPromptCases(params: {
getReplyFromConfig: () => GetReplyFromConfig;
}): void {
describe("group intro prompts", () => {
type GroupIntroCase = {
name: string;
message: InboundMessage;
expected: string[];
setup?: (cfg: ReturnType<typeof makeCfg>) => void;
};
const groupParticipationNote =
"Be a good group participant: mostly lurk and follow the conversation; reply only when directly addressed or you can add clear value. Emoji reactions are welcome when available. Write like a human. Avoid Markdown tables. Don't type literal \\n sequences; use real line breaks sparingly.";
it("labels group chats using channel-specific metadata", async () => {
await withTempHome(async (home) => {
const cases: GroupIntroCase[] = [
{
name: "discord",
message: {
Body: "status update",
From: "discord:group:dev",
To: "+1888",
ChatType: "group",
GroupSubject: "Release Squad",
GroupMembers: "Alice, Bob",
Provider: "discord",
},
expected: [
'"channel": "discord"',
`You are in the Discord group chat "Release Squad". Participants: Alice, Bob.`,
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
],
},
{
name: "whatsapp",
message: {
Body: "ping",
From: "123@g.us",
To: "+1999",
ChatType: "group",
GroupSubject: "Ops",
Provider: "whatsapp",
},
expected: [
'"channel": "whatsapp"',
`You are in the WhatsApp group chat "Ops".`,
`WhatsApp IDs: SenderId is the participant JID (group participant id).`,
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID (group participant id). ${groupParticipationNote} Address the specific sender noted in the message context.`,
],
},
{
name: "telegram",
message: {
Body: "ping",
From: "telegram:group:tg",
To: "+1777",
ChatType: "group",
GroupSubject: "Dev Chat",
Provider: "telegram",
},
expected: [
'"channel": "telegram"',
`You are in the Telegram group chat "Dev Chat".`,
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
],
},
{
name: "whatsapp-always-on",
setup: (cfg) => {
cfg.channels ??= {};
cfg.channels.whatsapp = {
...cfg.channels.whatsapp,
allowFrom: ["*"],
groups: { "*": { requireMention: false } },
};
cfg.messages = {
...cfg.messages,
groupChat: {},
};
},
message: {
Body: "hello group",
From: "123@g.us",
To: "+2000",
ChatType: "group",
Provider: "whatsapp",
SenderE164: "+2000",
GroupSubject: "Test Group",
GroupMembers: "Alice (+1), Bob (+2)",
},
expected: [
'"channel": "whatsapp"',
'"chat_type": "group"',
"Activation: always-on (you receive every group message).",
],
},
];
for (const testCase of cases) {
mockRunEmbeddedPiAgentOk();
const cfg = makeCfg(home);
testCase.setup?.(cfg);
await params.getReplyFromConfig()(testCase.message, {}, cfg);
expect(getRunEmbeddedPiAgentMock(), testCase.name).toHaveBeenCalledOnce();
const extraSystemPrompt = getLastExtraSystemPrompt();
for (const expectedFragment of testCase.expected) {
expect(extraSystemPrompt, `${testCase.name}:${expectedFragment}`).toContain(
expectedFragment,
);
}
getRunEmbeddedPiAgentMock().mockClear();
}
});
});
});
}

View File

@@ -1,110 +0,0 @@
import { beforeAll, describe, expect, it } from "vitest";
import {
getRunEmbeddedPiAgentMock,
installTriggerHandlingE2eTestHooks,
loadGetReplyFromConfig,
makeCfg,
mockRunEmbeddedPiAgentOk,
withTempHome,
} from "./reply.triggers.trigger-handling.test-harness.js";
let getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
beforeAll(async () => {
getReplyFromConfig = await loadGetReplyFromConfig();
});
installTriggerHandlingE2eTestHooks();
function getLastExtraSystemPrompt() {
return getRunEmbeddedPiAgentMock().mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? "";
}
describe("group intro prompts", () => {
const groupParticipationNote =
"Be a good group participant: mostly lurk and follow the conversation; reply only when directly addressed or you can add clear value. Emoji reactions are welcome when available. Write like a human. Avoid Markdown tables. Don't type literal \\n sequences; use real line breaks sparingly.";
it("labels Discord groups using the surface metadata", async () => {
await withTempHome(async (home) => {
mockRunEmbeddedPiAgentOk();
await getReplyFromConfig(
{
Body: "status update",
From: "discord:group:dev",
To: "+1888",
ChatType: "group",
GroupSubject: "Release Squad",
GroupMembers: "Alice, Bob",
Provider: "discord",
},
{},
makeCfg(home),
);
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
const extraSystemPrompt = getLastExtraSystemPrompt();
expect(extraSystemPrompt).toContain('"channel": "discord"');
expect(extraSystemPrompt).toContain(
`You are in the Discord group chat "Release Squad". Participants: Alice, Bob.`,
);
expect(extraSystemPrompt).toContain(
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});
it("keeps WhatsApp labeling for WhatsApp group chats", async () => {
await withTempHome(async (home) => {
mockRunEmbeddedPiAgentOk();
await getReplyFromConfig(
{
Body: "ping",
From: "123@g.us",
To: "+1999",
ChatType: "group",
GroupSubject: "Ops",
Provider: "whatsapp",
},
{},
makeCfg(home),
);
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
const extraSystemPrompt = getLastExtraSystemPrompt();
expect(extraSystemPrompt).toContain('"channel": "whatsapp"');
expect(extraSystemPrompt).toContain(`You are in the WhatsApp group chat "Ops".`);
expect(extraSystemPrompt).toContain(
`WhatsApp IDs: SenderId is the participant JID (group participant id).`,
);
expect(extraSystemPrompt).toContain(
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID (group participant id). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});
it("labels Telegram groups using their own surface", async () => {
await withTempHome(async (home) => {
mockRunEmbeddedPiAgentOk();
await getReplyFromConfig(
{
Body: "ping",
From: "telegram:group:tg",
To: "+1777",
ChatType: "group",
GroupSubject: "Dev Chat",
Provider: "telegram",
},
{},
makeCfg(home),
);
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
const extraSystemPrompt = getLastExtraSystemPrompt();
expect(extraSystemPrompt).toContain('"channel": "telegram"');
expect(extraSystemPrompt).toContain(`You are in the Telegram group chat "Dev Chat".`);
expect(extraSystemPrompt).toContain(
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});
});

View File

@@ -0,0 +1,401 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveSessionKey } from "../config/sessions.js";
import {
createBlockReplyCollector,
getProviderUsageMocks,
getRunEmbeddedPiAgentMock,
makeCfg,
requireSessionStorePath,
withTempHome,
} from "./reply.triggers.trigger-handling.test-harness.js";
type GetReplyFromConfig = typeof import("./reply.js").getReplyFromConfig;
const usageMocks = getProviderUsageMocks();
const modelStatusCtx = {
Body: "/model status",
From: "telegram:111",
To: "telegram:111",
ChatType: "direct",
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandAuthorized: true,
} as const;
async function readSessionStore(storePath: string): Promise<Record<string, unknown>> {
const raw = await readFile(storePath, "utf-8");
return JSON.parse(raw) as Record<string, unknown>;
}
function pickFirstStoreEntry<T>(store: Record<string, unknown>): T | undefined {
const entries = Object.values(store) as T[];
return entries[0];
}
function getReplyFromConfigNow(getReplyFromConfig: () => GetReplyFromConfig): GetReplyFromConfig {
return getReplyFromConfig();
}
async function runCommandAndCollectReplies(params: {
getReplyFromConfig: () => GetReplyFromConfig;
home: string;
body: string;
from?: string;
senderE164?: string;
}) {
const { blockReplies, handlers } = createBlockReplyCollector();
const res = await getReplyFromConfigNow(params.getReplyFromConfig)(
{
Body: params.body,
From: params.from ?? "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: params.senderE164 ?? params.from ?? "+1000",
CommandAuthorized: true,
},
handlers,
makeCfg(params.home),
);
const replies = res ? (Array.isArray(res) ? res : [res]) : [];
return { blockReplies, replies };
}
async function expectStopAbortWithoutAgent(params: {
getReplyFromConfig: () => GetReplyFromConfig;
home: string;
body: string;
from: string;
}) {
const res = await getReplyFromConfigNow(params.getReplyFromConfig)(
{
Body: params.body,
From: params.from,
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(params.home),
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("⚙️ Agent was aborted.");
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
}
export function registerTriggerHandlingUsageSummaryCases(params: {
getReplyFromConfig: () => GetReplyFromConfig;
}): void {
describe("usage and status command handling", () => {
it("handles status, usage cycles, restart/stop gating, and auth-profile status details", async () => {
await withTempHome(async (home) => {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
const getReplyFromConfig = getReplyFromConfigNow(params.getReplyFromConfig);
usageMocks.loadProviderUsageSummary.mockClear();
usageMocks.loadProviderUsageSummary.mockResolvedValue({
updatedAt: 0,
providers: [
{
provider: "anthropic",
displayName: "Anthropic",
windows: [
{
label: "5h",
usedPercent: 20,
},
],
},
],
});
{
const res = await getReplyFromConfig(
{
Body: "/status",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{},
makeCfg(home),
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Model:");
expect(text).toContain("OpenClaw");
expect(normalizeTestText(text ?? "")).toContain("Usage: Claude 80% left");
expect(usageMocks.loadProviderUsageSummary).toHaveBeenCalledWith(
expect.objectContaining({ providers: ["anthropic"] }),
);
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
{
runEmbeddedPiAgentMock.mockResolvedValue({
payloads: [{ text: "agent says hi" }],
meta: {
durationMs: 1,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
const { blockReplies, replies } = await runCommandAndCollectReplies({
getReplyFromConfig: params.getReplyFromConfig,
home,
body: "here we go /status now",
from: "+1002",
});
expect(blockReplies.length).toBe(1);
expect(String(blockReplies[0]?.text ?? "")).toContain("Model:");
expect(replies.length).toBe(1);
expect(replies[0]?.text).toBe("agent says hi");
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
expect(prompt).not.toContain("/status");
}
{
runEmbeddedPiAgentMock.mockClear();
const defaultCfg = makeCfg(home);
const cfg = {
...defaultCfg,
models: {
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
api: "anthropic-messages",
},
},
},
} as unknown as OpenClawConfig;
const defaultStatus = await getReplyFromConfig(modelStatusCtx, {}, defaultCfg);
const configuredStatus = await getReplyFromConfig(modelStatusCtx, {}, cfg);
expect(
normalizeTestText(
(Array.isArray(defaultStatus) ? defaultStatus[0]?.text : defaultStatus?.text) ?? "",
),
).toContain("endpoint: default");
const configuredText = Array.isArray(configuredStatus)
? configuredStatus[0]?.text
: configuredStatus?.text;
expect(normalizeTestText(configuredText ?? "")).toContain(
"[minimax] endpoint: https://api.minimax.io/anthropic api: anthropic-messages auth:",
);
}
{
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, "usage-cycle.sessions.json") };
const usageStorePath = requireSessionStorePath(cfg);
const explicitTokens = await getReplyFromConfig(
{
Body: "/usage tokens",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(
String(
(Array.isArray(explicitTokens) ? explicitTokens[0]?.text : explicitTokens?.text) ??
"",
),
).toContain("Usage footer: tokens");
const explicitStore = await readSessionStore(usageStorePath);
expect(
pickFirstStoreEntry<{ responseUsage?: string }>(explicitStore)?.responseUsage,
).toBe("tokens");
const r0 = await getReplyFromConfig(
{
Body: "/usage on",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(String((Array.isArray(r0) ? r0[0]?.text : r0?.text) ?? "")).toContain(
"Usage footer: tokens",
);
const r1 = await getReplyFromConfig(
{
Body: "/usage",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(String((Array.isArray(r1) ? r1[0]?.text : r1?.text) ?? "")).toContain(
"Usage footer: full",
);
const r2 = await getReplyFromConfig(
{
Body: "/usage",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(String((Array.isArray(r2) ? r2[0]?.text : r2?.text) ?? "")).toContain(
"Usage footer: off",
);
const r3 = await getReplyFromConfig(
{
Body: "/usage",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(String((Array.isArray(r3) ? r3[0]?.text : r3?.text) ?? "")).toContain(
"Usage footer: tokens",
);
const finalStore = await readSessionStore(usageStorePath);
expect(pickFirstStoreEntry<{ responseUsage?: string }>(finalStore)?.responseUsage).toBe(
"tokens",
);
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
{
runEmbeddedPiAgentMock.mockClear();
await expectStopAbortWithoutAgent({
getReplyFromConfig: params.getReplyFromConfig,
home,
body: "[Dec 5 10:00] stop",
from: "+1000",
});
const enabledRes = await getReplyFromConfig(
{
Body: " [Dec 5] /restart",
From: "+1001",
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(home),
);
const enabledText = Array.isArray(enabledRes) ? enabledRes[0]?.text : enabledRes?.text;
expect(
enabledText?.startsWith("⚙️ Restarting") ||
enabledText?.startsWith("⚠️ Restart failed"),
).toBe(true);
const disabledCfg = { ...makeCfg(home), commands: { restart: false } } as OpenClawConfig;
const disabledRes = await getReplyFromConfig(
{
Body: "/restart",
From: "+1001",
To: "+2000",
CommandAuthorized: true,
},
{},
disabledCfg,
);
const disabledText = Array.isArray(disabledRes)
? disabledRes[0]?.text
: disabledRes?.text;
expect(disabledText).toContain("/restart is disabled");
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
{
runEmbeddedPiAgentMock.mockClear();
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, "auth-profile-status.sessions.json") };
const agentDir = join(home, ".openclaw", "agents", "main", "agent");
await mkdir(agentDir, { recursive: true });
await writeFile(
join(agentDir, "auth-profiles.json"),
JSON.stringify(
{
version: 1,
profiles: {
"anthropic:work": {
type: "api_key",
provider: "anthropic",
key: "sk-test-1234567890abcdef",
},
},
lastGood: { anthropic: "anthropic:work" },
},
null,
2,
),
);
const sessionKey = resolveSessionKey("per-sender", {
From: "+1002",
To: "+2000",
Provider: "whatsapp",
} as Parameters<typeof resolveSessionKey>[1]);
await writeFile(
requireSessionStorePath(cfg),
JSON.stringify(
{
[sessionKey]: {
sessionId: "session-auth",
updatedAt: Date.now(),
authProfileOverride: "anthropic:work",
},
},
null,
2,
),
);
const res = await getReplyFromConfig(
{
Body: "/status",
From: "+1002",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1002",
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("api-key");
expect(text).toMatch(/\u2026|\.{3}/);
expect(text).toContain("sk-tes");
expect(text).toContain("abcdef");
expect(text).not.toContain("1234567890abcdef");
expect(text).toContain("(anthropic:work)");
expect(text).not.toContain("mixed");
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
});
});
});
}

View File

@@ -1,394 +0,0 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveSessionKey } from "../config/sessions.js";
import {
createBlockReplyCollector,
getProviderUsageMocks,
getRunEmbeddedPiAgentMock,
installTriggerHandlingE2eTestHooks,
makeCfg,
requireSessionStorePath,
withTempHome,
} from "./reply.triggers.trigger-handling.test-harness.js";
let getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
beforeAll(async () => {
({ getReplyFromConfig } = await import("./reply.js"));
});
installTriggerHandlingE2eTestHooks();
const usageMocks = getProviderUsageMocks();
const modelStatusCtx = {
Body: "/model status",
From: "telegram:111",
To: "telegram:111",
ChatType: "direct",
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandAuthorized: true,
} as const;
async function readSessionStore(home: string): Promise<Record<string, unknown>> {
const raw = await readFile(join(home, "sessions.json"), "utf-8");
return JSON.parse(raw) as Record<string, unknown>;
}
function pickFirstStoreEntry<T>(store: Record<string, unknown>): T | undefined {
const entries = Object.values(store) as T[];
return entries[0];
}
async function runCommandAndCollectReplies(params: {
home: string;
body: string;
from?: string;
senderE164?: string;
}) {
const { blockReplies, handlers } = createBlockReplyCollector();
const res = await getReplyFromConfig(
{
Body: params.body,
From: params.from ?? "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: params.senderE164 ?? params.from ?? "+1000",
CommandAuthorized: true,
},
handlers,
makeCfg(params.home),
);
const replies = res ? (Array.isArray(res) ? res : [res]) : [];
return { blockReplies, replies };
}
async function expectStopAbortWithoutAgent(params: { home: string; body: string; from: string }) {
const res = await getReplyFromConfig(
{
Body: params.body,
From: params.from,
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(params.home),
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("⚙️ Agent was aborted.");
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
}
describe("trigger handling", () => {
it("filters usage summary to the current model provider", async () => {
await withTempHome(async (home) => {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
usageMocks.loadProviderUsageSummary.mockClear();
usageMocks.loadProviderUsageSummary.mockResolvedValue({
updatedAt: 0,
providers: [
{
provider: "anthropic",
displayName: "Anthropic",
windows: [
{
label: "5h",
usedPercent: 20,
},
],
},
],
});
const res = await getReplyFromConfig(
{
Body: "/status",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{},
makeCfg(home),
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Model:");
expect(text).toContain("OpenClaw");
expect(normalizeTestText(text ?? "")).toContain("Usage: Claude 80% left");
expect(usageMocks.loadProviderUsageSummary).toHaveBeenCalledWith(
expect.objectContaining({ providers: ["anthropic"] }),
);
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
});
});
it("handles explicit /usage tokens, back-compat, and cycle persistence", async () => {
await withTempHome(async (home) => {
const cfg = makeCfg(home);
const explicitTokens = await getReplyFromConfig(
{
Body: "/usage tokens",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(
String(
(Array.isArray(explicitTokens) ? explicitTokens[0]?.text : explicitTokens?.text) ?? "",
),
).toContain("Usage footer: tokens");
const explicitStore = await readSessionStore(home);
expect(pickFirstStoreEntry<{ responseUsage?: string }>(explicitStore)?.responseUsage).toBe(
"tokens",
);
const r0 = await getReplyFromConfig(
{
Body: "/usage on",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(String((Array.isArray(r0) ? r0[0]?.text : r0?.text) ?? "")).toContain(
"Usage footer: tokens",
);
const r1 = await getReplyFromConfig(
{
Body: "/usage",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(String((Array.isArray(r1) ? r1[0]?.text : r1?.text) ?? "")).toContain(
"Usage footer: full",
);
const r2 = await getReplyFromConfig(
{
Body: "/usage",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(String((Array.isArray(r2) ? r2[0]?.text : r2?.text) ?? "")).toContain(
"Usage footer: off",
);
const r3 = await getReplyFromConfig(
{
Body: "/usage",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
undefined,
cfg,
);
expect(String((Array.isArray(r3) ? r3[0]?.text : r3?.text) ?? "")).toContain(
"Usage footer: tokens",
);
const finalStore = await readSessionStore(home);
expect(pickFirstStoreEntry<{ responseUsage?: string }>(finalStore)?.responseUsage).toBe(
"tokens",
);
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
});
});
it("sends one inline status and still returns agent reply for mixed text", async () => {
await withTempHome(async (home) => {
getRunEmbeddedPiAgentMock().mockResolvedValue({
payloads: [{ text: "agent says hi" }],
meta: {
durationMs: 1,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
const { blockReplies, replies } = await runCommandAndCollectReplies({
home,
body: "here we go /status now",
from: "+1002",
});
expect(blockReplies.length).toBe(1);
expect(String(blockReplies[0]?.text ?? "")).toContain("Model:");
expect(replies.length).toBe(1);
expect(replies[0]?.text).toBe("agent says hi");
const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? "";
expect(prompt).not.toContain("/status");
});
});
it("handles /stop command variants without invoking the agent", async () => {
await withTempHome(async (home) => {
for (const testCase of [
{ body: "[Dec 5 10:00] stop", from: "+1000" },
{ body: "/stop", from: "+1003" },
] as const) {
await expectStopAbortWithoutAgent({ home, body: testCase.body, from: testCase.from });
}
});
});
it("shows model status defaults and configured endpoint details", async () => {
await withTempHome(async (home) => {
const defaultCfg = makeCfg(home);
const cfg = {
...defaultCfg,
models: {
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
api: "anthropic-messages",
},
},
},
} as unknown as OpenClawConfig;
const defaultStatus = await getReplyFromConfig(modelStatusCtx, {}, defaultCfg);
const configuredStatus = await getReplyFromConfig(modelStatusCtx, {}, cfg);
expect(
normalizeTestText(
(Array.isArray(defaultStatus) ? defaultStatus[0]?.text : defaultStatus?.text) ?? "",
),
).toContain("endpoint: default");
const configuredText = Array.isArray(configuredStatus)
? configuredStatus[0]?.text
: configuredStatus?.text;
expect(normalizeTestText(configuredText ?? "")).toContain(
"[minimax] endpoint: https://api.minimax.io/anthropic api: anthropic-messages auth:",
);
});
});
it("restarts by default and rejects /restart when disabled", async () => {
await withTempHome(async (home) => {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
const enabledRes = await getReplyFromConfig(
{
Body: " [Dec 5] /restart",
From: "+1001",
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(home),
);
const enabledText = Array.isArray(enabledRes) ? enabledRes[0]?.text : enabledRes?.text;
expect(
enabledText?.startsWith("⚙️ Restarting") || enabledText?.startsWith("⚠️ Restart failed"),
).toBe(true);
const disabledCfg = { ...makeCfg(home), commands: { restart: false } } as OpenClawConfig;
const disabledRes = await getReplyFromConfig(
{
Body: "/restart",
From: "+1001",
To: "+2000",
CommandAuthorized: true,
},
{},
disabledCfg,
);
const disabledText = Array.isArray(disabledRes) ? disabledRes[0]?.text : disabledRes?.text;
expect(disabledText).toContain("/restart is disabled");
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
});
});
it("reports active auth profile and key snippet in status", async () => {
await withTempHome(async (home) => {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
const cfg = makeCfg(home);
const agentDir = join(home, ".openclaw", "agents", "main", "agent");
await mkdir(agentDir, { recursive: true });
await writeFile(
join(agentDir, "auth-profiles.json"),
JSON.stringify(
{
version: 1,
profiles: {
"anthropic:work": {
type: "api_key",
provider: "anthropic",
key: "sk-test-1234567890abcdef",
},
},
lastGood: { anthropic: "anthropic:work" },
},
null,
2,
),
);
const sessionKey = resolveSessionKey("per-sender", {
From: "+1002",
To: "+2000",
Provider: "whatsapp",
} as Parameters<typeof resolveSessionKey>[1]);
await writeFile(
requireSessionStorePath(cfg),
JSON.stringify(
{
[sessionKey]: {
sessionId: "session-auth",
updatedAt: Date.now(),
authProfileOverride: "anthropic:work",
},
},
null,
2,
),
);
const res = await getReplyFromConfig(
{
Body: "/status",
From: "+1002",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1002",
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("api-key");
expect(text).toMatch(/\u2026|\.{3}/);
expect(text).toContain("sk-tes");
expect(text).toContain("abcdef");
expect(text).not.toContain("1234567890abcdef");
expect(text).toContain("(anthropic:work)");
expect(text).not.toContain("mixed");
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,398 +0,0 @@
import fs from "node:fs/promises";
import { join } from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
import {
expectInlineCommandHandledAndStripped,
getRunEmbeddedPiAgentMock,
installTriggerHandlingE2eTestHooks,
loadGetReplyFromConfig,
MAIN_SESSION_KEY,
makeCfg,
makeWhatsAppElevatedCfg,
mockRunEmbeddedPiAgentOk,
readSessionStore,
requireSessionStorePath,
runGreetingPromptForBareNewOrReset,
withTempHome,
} from "./reply.triggers.trigger-handling.test-harness.js";
let getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
beforeAll(async () => {
getReplyFromConfig = await loadGetReplyFromConfig();
});
installTriggerHandlingE2eTestHooks();
function makeUnauthorizedWhatsAppCfg(home: string) {
const baseCfg = makeCfg(home);
return {
...baseCfg,
channels: {
...baseCfg.channels,
whatsapp: {
allowFrom: ["+1000"],
},
},
};
}
async function expectResetBlockedForNonOwner(params: { home: string }): Promise<void> {
const { home } = params;
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
runEmbeddedPiAgentMock.mockClear();
const cfg = makeCfg(home);
cfg.channels ??= {};
cfg.channels.whatsapp = {
...cfg.channels.whatsapp,
allowFrom: ["+1999"],
};
cfg.session = {
...cfg.session,
store: join(home, "blocked-reset.sessions.json"),
};
const res = await getReplyFromConfig(
{
Body: "/reset",
From: "+1003",
To: "+2000",
CommandAuthorized: true,
},
{},
cfg,
);
expect(res).toBeUndefined();
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
async function expectUnauthorizedCommandDropped(home: string, body: "/status") {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
const cfg = makeUnauthorizedWhatsAppCfg(home);
const res = await getReplyFromConfig(
{
Body: body,
From: "+2001",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+2001",
},
{},
cfg,
);
expect(res).toBeUndefined();
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
function mockEmbeddedOk() {
return mockRunEmbeddedPiAgentOk("ok");
}
async function runInlineUnauthorizedCommand(params: { home: string; command: "/status" }) {
const cfg = makeUnauthorizedWhatsAppCfg(params.home);
const res = await getReplyFromConfig(
{
Body: `please ${params.command} now`,
From: "+2001",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+2001",
},
{},
cfg,
);
return res;
}
describe("trigger handling", () => {
it("handles owner-admin commands without invoking the agent", async () => {
await withTempHome(async (home) => {
{
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
runEmbeddedPiAgentMock.mockClear();
const cfg = makeCfg(home);
const res = await getReplyFromConfig(
{
Body: "/activation mention",
From: "123@g.us",
To: "+2000",
ChatType: "group",
Provider: "whatsapp",
SenderE164: "+999",
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("⚙️ Group activation set to mention.");
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
{
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
runEmbeddedPiAgentMock.mockClear();
const cfg = makeUnauthorizedWhatsAppCfg(home);
const res = await getReplyFromConfig(
{
Body: "/send off",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Send policy set to off");
const storeRaw = await fs.readFile(requireSessionStorePath(cfg), "utf-8");
const store = JSON.parse(storeRaw) as Record<string, { sendPolicy?: string }>;
expect(store[MAIN_SESSION_KEY]?.sendPolicy).toBe("deny");
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
});
});
it("injects group activation context into the system prompt", async () => {
await withTempHome(async (home) => {
getRunEmbeddedPiAgentMock().mockResolvedValue({
payloads: [{ text: "ok" }],
meta: {
durationMs: 1,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
const cfg = makeCfg(home);
cfg.channels ??= {};
cfg.channels.whatsapp = {
...cfg.channels.whatsapp,
allowFrom: ["*"],
groups: { "*": { requireMention: false } },
};
cfg.messages = {
...cfg.messages,
groupChat: {},
};
const res = await getReplyFromConfig(
{
Body: "hello group",
From: "123@g.us",
To: "+2000",
ChatType: "group",
Provider: "whatsapp",
SenderE164: "+2000",
GroupSubject: "Test Group",
GroupMembers: "Alice (+1), Bob (+2)",
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("ok");
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
const extra = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.extraSystemPrompt ?? "";
expect(extra).toContain('"chat_type": "group"');
expect(extra).toContain("Activation: always-on");
});
});
it("runs a greeting prompt for bare /new and blocks unauthorized /reset", async () => {
await withTempHome(async (home) => {
await runGreetingPromptForBareNewOrReset({ home, body: "/new", getReplyFromConfig });
await expectResetBlockedForNonOwner({ home });
});
});
it("handles inline commands and strips directives before the agent", async () => {
await withTempHome(async (home) => {
await expectInlineCommandHandledAndStripped({
home,
getReplyFromConfig,
body: "please /whoami now",
stripToken: "/whoami",
blockReplyContains: "Identity",
requestOverrides: { SenderId: "12345" },
});
});
});
it("enforces top-level command auth while keeping inline text", async () => {
await withTempHome(async (home) => {
await expectUnauthorizedCommandDropped(home, "/status");
const runEmbeddedPiAgentMock = mockEmbeddedOk();
const res = await runInlineUnauthorizedCommand({
home,
command: "/status",
});
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("ok");
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
const prompt = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? "";
expect(prompt).toContain("/status");
});
});
it("enforces elevated toggles across enabled and mention scenarios", async () => {
await withTempHome(async (home) => {
const isolateStore = (cfg: ReturnType<typeof makeWhatsAppElevatedCfg>, label: string) => {
cfg.session = { ...cfg.session, store: join(home, `${label}.sessions.json`) };
return cfg;
};
{
const cfg = isolateStore(makeWhatsAppElevatedCfg(home, { elevatedEnabled: false }), "off");
const res = await getReplyFromConfig(
{
Body: "/elevated on",
From: "+1000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("tools.elevated.enabled");
const storeRaw = await fs.readFile(requireSessionStorePath(cfg), "utf-8");
const store = JSON.parse(storeRaw) as Record<string, { elevatedLevel?: string }>;
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBeUndefined();
}
{
const cfg = isolateStore(
makeWhatsAppElevatedCfg(home, { requireMentionInGroups: true }),
"group-on",
);
const res = await getReplyFromConfig(
{
Body: "/elevated on",
From: "whatsapp:group:123@g.us",
To: "whatsapp:+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
ChatType: "group",
WasMentioned: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Elevated mode set to ask");
const store = await readSessionStore(cfg);
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on");
}
{
const cfg = isolateStore(makeWhatsAppElevatedCfg(home), "inline-unapproved");
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
runEmbeddedPiAgentMock.mockClear();
runEmbeddedPiAgentMock.mockResolvedValue({
payloads: [{ text: "ok" }],
meta: {
durationMs: 1,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
const res = await getReplyFromConfig(
{
Body: "please /elevated on now",
From: "+2000",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+2000",
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).not.toContain("elevated is not available right now");
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
}
});
});
it("handles discord elevated allowlist and override behavior", async () => {
await withTempHome(async (home) => {
{
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, "discord-allow.sessions.json") };
cfg.tools = { elevated: { allowFrom: { discord: ["123"] } } };
const res = await getReplyFromConfig(
{
Body: "/elevated on",
From: "discord:123",
To: "user:123",
Provider: "discord",
SenderName: "Peter Steinberger",
SenderUsername: "steipete",
SenderTag: "steipete",
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Elevated mode set to ask");
const store = await readSessionStore(cfg);
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
}
{
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, "discord-deny.sessions.json") };
cfg.tools = {
elevated: {
allowFrom: { discord: [] },
},
};
const res = await getReplyFromConfig(
{
Body: "/elevated on",
From: "discord:123",
To: "user:123",
Provider: "discord",
SenderName: "steipete",
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("tools.elevated.allowFrom.discord");
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
}
});
});
it("returns a context overflow fallback when the embedded agent throws", async () => {
await withTempHome(async (home) => {
getRunEmbeddedPiAgentMock().mockRejectedValue(new Error("Context window exceeded"));
const res = await getReplyFromConfig(
{
Body: "hello",
From: "+1002",
To: "+2000",
},
{},
makeCfg(home),
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe(
"⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.",
);
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
});
});
});

View File

@@ -26,96 +26,79 @@ afterEach(() => {
});
describe("stageSandboxMedia", () => {
it("stages inbound media into the sandbox workspace", async () => {
it("stages allowed media and blocks unsafe paths", async () => {
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
const inboundDir = join(home, ".openclaw", "media", "inbound");
await fs.mkdir(inboundDir, { recursive: true });
const mediaPath = join(inboundDir, "photo.jpg");
await fs.writeFile(mediaPath, "test");
const cfg = createSandboxMediaStageConfig(home);
const workspaceDir = join(home, "openclaw");
const sandboxDir = join(home, "sandboxes", "session");
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir: sandboxDir,
containerWorkdir: "/work",
});
const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath);
{
const inboundDir = join(home, ".openclaw", "media", "inbound");
await fs.mkdir(inboundDir, { recursive: true });
const mediaPath = join(inboundDir, "photo.jpg");
await fs.writeFile(mediaPath, "test");
const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath);
await stageSandboxMedia({
ctx,
sessionCtx,
cfg: createSandboxMediaStageConfig(home),
sessionKey: "agent:main:main",
workspaceDir: join(home, "openclaw"),
});
await stageSandboxMedia({
ctx,
sessionCtx,
cfg,
sessionKey: "agent:main:main",
workspaceDir,
});
const stagedPath = `media/inbound/${basename(mediaPath)}`;
expect(ctx.MediaPath).toBe(stagedPath);
expect(sessionCtx.MediaPath).toBe(stagedPath);
expect(ctx.MediaUrl).toBe(stagedPath);
expect(sessionCtx.MediaUrl).toBe(stagedPath);
const stagedPath = `media/inbound/${basename(mediaPath)}`;
expect(ctx.MediaPath).toBe(stagedPath);
expect(sessionCtx.MediaPath).toBe(stagedPath);
expect(ctx.MediaUrl).toBe(stagedPath);
expect(sessionCtx.MediaUrl).toBe(stagedPath);
await expect(
fs.stat(join(sandboxDir, "media", "inbound", basename(mediaPath))),
).resolves.toBeTruthy();
}
const stagedFullPath = join(sandboxDir, "media", "inbound", basename(mediaPath));
await expect(fs.stat(stagedFullPath)).resolves.toBeTruthy();
});
});
{
const sensitiveFile = join(home, "secrets.txt");
await fs.writeFile(sensitiveFile, "SENSITIVE DATA");
const { ctx, sessionCtx } = createSandboxMediaContexts(sensitiveFile);
it("rejects staging host files from outside the media directory", async () => {
await withSandboxMediaTempHome("openclaw-triggers-bypass-", async (home) => {
// Sensitive host file outside .openclaw
const sensitiveFile = join(home, "secrets.txt");
await fs.writeFile(sensitiveFile, "SENSITIVE DATA");
await stageSandboxMedia({
ctx,
sessionCtx,
cfg,
sessionKey: "agent:main:main",
workspaceDir,
});
const sandboxDir = join(home, "sandboxes", "session");
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir: sandboxDir,
containerWorkdir: "/work",
});
await expect(
fs.stat(join(sandboxDir, "media", "inbound", basename(sensitiveFile))),
).rejects.toThrow();
expect(ctx.MediaPath).toBe(sensitiveFile);
}
const { ctx, sessionCtx } = createSandboxMediaContexts(sensitiveFile);
{
childProcessMocks.spawn.mockClear();
const { ctx, sessionCtx } = createSandboxMediaContexts("/etc/passwd");
ctx.Provider = "imessage";
ctx.MediaRemoteHost = "user@gateway-host";
sessionCtx.Provider = "imessage";
sessionCtx.MediaRemoteHost = "user@gateway-host";
// This should fail or skip the file
await stageSandboxMedia({
ctx,
sessionCtx,
cfg: createSandboxMediaStageConfig(home),
sessionKey: "agent:main:main",
workspaceDir: join(home, "openclaw"),
});
await stageSandboxMedia({
ctx,
sessionCtx,
cfg,
sessionKey: "agent:main:main",
workspaceDir,
});
const stagedFullPath = join(sandboxDir, "media", "inbound", basename(sensitiveFile));
// Expect the file NOT to be staged
await expect(fs.stat(stagedFullPath)).rejects.toThrow();
// Context should NOT be rewritten to a sandbox path if it failed to stage
expect(ctx.MediaPath).toBe(sensitiveFile);
});
});
it("blocks remote SCP staging for non-iMessage attachment paths", async () => {
await withSandboxMediaTempHome("openclaw-triggers-remote-block-", async (home) => {
const sandboxDir = join(home, "sandboxes", "session");
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir: sandboxDir,
containerWorkdir: "/work",
});
const { ctx, sessionCtx } = createSandboxMediaContexts("/etc/passwd");
ctx.Provider = "imessage";
ctx.MediaRemoteHost = "user@gateway-host";
sessionCtx.Provider = "imessage";
sessionCtx.MediaRemoteHost = "user@gateway-host";
await stageSandboxMedia({
ctx,
sessionCtx,
cfg: createSandboxMediaStageConfig(home),
sessionKey: "agent:main:main",
workspaceDir: join(home, "openclaw"),
});
expect(childProcessMocks.spawn).not.toHaveBeenCalled();
expect(ctx.MediaPath).toBe("/etc/passwd");
expect(childProcessMocks.spawn).not.toHaveBeenCalled();
expect(ctx.MediaPath).toBe("/etc/passwd");
}
});
});
});

View File

@@ -3,7 +3,10 @@ import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
import { registerGroupIntroPromptCases } from "./reply.triggers.group-intro-prompts.cases.js";
import { registerTriggerHandlingUsageSummaryCases } from "./reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.js";
import {
expectInlineCommandHandledAndStripped,
getAbortEmbeddedPiRunMock,
getCompactEmbeddedPiSessionMock,
getRunEmbeddedPiAgentMock,
@@ -12,6 +15,7 @@ import {
makeCfg,
mockRunEmbeddedPiAgentOk,
requireSessionStorePath,
runGreetingPromptForBareNewOrReset,
withTempHome,
} from "./reply.triggers.trigger-handling.test-harness.js";
import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./reply/queue.js";
@@ -75,25 +79,183 @@ function mockSuccessfulCompaction() {
});
}
function makeUnauthorizedWhatsAppCfg(home: string) {
const baseCfg = makeCfg(home);
return {
...baseCfg,
channels: {
...baseCfg.channels,
whatsapp: {
allowFrom: ["+1000"],
},
},
};
}
async function expectResetBlockedForNonOwner(params: { home: string }): Promise<void> {
const { home } = params;
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
runEmbeddedPiAgentMock.mockClear();
const cfg = makeCfg(home);
cfg.channels ??= {};
cfg.channels.whatsapp = {
...cfg.channels.whatsapp,
allowFrom: ["+1999"],
};
cfg.session = {
...cfg.session,
store: join(home, "blocked-reset.sessions.json"),
};
const res = await getReplyFromConfig(
{
Body: "/reset",
From: "+1003",
To: "+2000",
CommandAuthorized: true,
},
{},
cfg,
);
expect(res).toBeUndefined();
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
async function expectUnauthorizedCommandDropped(home: string, body: "/status") {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
const cfg = makeUnauthorizedWhatsAppCfg(home);
const res = await getReplyFromConfig(
{
Body: body,
From: "+2001",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+2001",
},
{},
cfg,
);
expect(res).toBeUndefined();
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
}
function mockEmbeddedOk() {
return mockRunEmbeddedPiAgentOk("ok");
}
async function runInlineUnauthorizedCommand(params: { home: string; command: "/status" }) {
const cfg = makeUnauthorizedWhatsAppCfg(params.home);
const res = await getReplyFromConfig(
{
Body: `please ${params.command} now`,
From: "+2001",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+2001",
},
{},
cfg,
);
return res;
}
describe("trigger handling", () => {
it("includes the error cause when the embedded agent throws", async () => {
await withTempHome(async (home) => {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
runEmbeddedPiAgentMock.mockRejectedValue(new Error("sandbox is not defined."));
const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home));
expect(maybeReplyText(res)).toBe(
"⚠️ Agent failed before reply: sandbox is not defined.\nLogs: openclaw logs --follow",
);
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
});
registerGroupIntroPromptCases({
getReplyFromConfig: () => getReplyFromConfig,
});
registerTriggerHandlingUsageSummaryCases({
getReplyFromConfig: () => getReplyFromConfig,
});
it("uses heartbeat override when configured and falls back to stored model override", async () => {
it("handles trigger command and heartbeat flows end-to-end", async () => {
await withTempHome(async (home) => {
const runEmbeddedPiAgentMock = mockEmbeddedOkPayload();
const cases = [
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
const errorCases = [
{
error: "sandbox is not defined.",
expected:
"⚠️ Agent failed before reply: sandbox is not defined.\nLogs: openclaw logs --follow",
},
{
error: "Context window exceeded",
expected:
"⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.",
},
] as const;
for (const testCase of errorCases) {
runEmbeddedPiAgentMock.mockClear();
runEmbeddedPiAgentMock.mockRejectedValue(new Error(testCase.error));
const errorRes = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home));
expect(maybeReplyText(errorRes), testCase.error).toBe(testCase.expected);
expect(runEmbeddedPiAgentMock, testCase.error).toHaveBeenCalledOnce();
}
const tokenCases = [
{ text: HEARTBEAT_TOKEN, expected: undefined },
{ text: `${HEARTBEAT_TOKEN} hello`, expected: "hello" },
] as const;
for (const testCase of tokenCases) {
runEmbeddedPiAgentMock.mockClear();
runEmbeddedPiAgentMock.mockResolvedValue({
payloads: [{ text: testCase.text }],
meta: {
durationMs: 1,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home));
expect(maybeReplyText(res)).toBe(testCase.expected);
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
}
const thinkCases = [
{
label: "context-wrapper",
request: {
Body: [
"[Chat messages since your last reply - for context]",
"Peter: /thinking high [2025-12-05T21:45:00.000Z]",
"",
"[Current message - respond to this]",
"Give me the status",
].join("\n"),
From: "+1002",
To: "+2000",
},
options: {},
assertPrompt: true,
},
{
label: "heartbeat",
request: {
Body: "HEARTBEAT /think:high",
From: "+1003",
To: "+1003",
},
options: { isHeartbeat: true },
assertPrompt: false,
},
] as const;
runEmbeddedPiAgentMock.mockClear();
for (const testCase of thinkCases) {
mockRunEmbeddedPiAgentOk();
const res = await getReplyFromConfig(testCase.request, testCase.options, makeCfg(home));
const text = maybeReplyText(res);
expect(text, testCase.label).toBe("ok");
expect(text, testCase.label).not.toMatch(/Thinking level set/i);
expect(getRunEmbeddedPiAgentMock(), testCase.label).toHaveBeenCalledOnce();
if (testCase.assertPrompt) {
const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? "";
expect(prompt).toContain("Give me the status");
expect(prompt).not.toContain("/thinking high");
expect(prompt).not.toContain("/think high");
}
getRunEmbeddedPiAgentMock().mockClear();
}
const modelCases = [
{
label: "heartbeat-override",
setup: (cfg: ReturnType<typeof makeCfg>) => {
@@ -114,7 +276,8 @@ describe("trigger handling", () => {
},
] as const;
for (const testCase of cases) {
for (const testCase of modelCases) {
mockEmbeddedOkPayload();
runEmbeddedPiAgentMock.mockClear();
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, `${testCase.label}.sessions.json`) };
@@ -126,62 +289,6 @@ describe("trigger handling", () => {
expect(call?.provider).toBe(testCase.expected.provider);
expect(call?.model).toBe(testCase.expected.model);
}
});
});
it("suppresses or strips HEARTBEAT_OK replies outside heartbeat runs", async () => {
await withTempHome(async (home) => {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
const cases = [
{ text: HEARTBEAT_TOKEN, expected: undefined },
{ text: `${HEARTBEAT_TOKEN} hello`, expected: "hello" },
] as const;
for (const testCase of cases) {
runEmbeddedPiAgentMock.mockClear();
runEmbeddedPiAgentMock.mockResolvedValue({
payloads: [{ text: testCase.text }],
meta: {
durationMs: 1,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home));
expect(maybeReplyText(res)).toBe(testCase.expected);
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
}
});
});
it("updates group activation when the owner sends /activation", async () => {
await withTempHome(async (home) => {
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
const cfg = makeCfg(home);
const res = await getReplyFromConfig(
{
Body: "/activation always",
From: "123@g.us",
To: "+2000",
ChatType: "group",
Provider: "whatsapp",
SenderE164: "+2000",
CommandAuthorized: true,
},
{},
cfg,
);
expect(maybeReplyText(res)).toContain("Group activation set to always");
const store = JSON.parse(await fs.readFile(requireSessionStorePath(cfg), "utf-8")) as Record<
string,
{ groupActivation?: string }
>;
expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe("always");
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
});
});
it("runs /compact for main and non-default agents without invoking the embedded run path", async () => {
await withTempHome(async (home) => {
{
const storePath = join(home, "compact-main.sessions.json");
const cfg = makeCfg(home);
@@ -213,6 +320,8 @@ describe("trigger handling", () => {
{
getCompactEmbeddedPiSessionMock().mockClear();
mockSuccessfulCompaction();
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, "compact-worker.sessions.json") };
const res = await getReplyFromConfig(
{
Body: "/compact",
@@ -222,7 +331,7 @@ describe("trigger handling", () => {
CommandAuthorized: true,
},
{},
makeCfg(home),
cfg,
);
const text = maybeReplyText(res);
@@ -233,240 +342,211 @@ describe("trigger handling", () => {
);
}
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
});
});
it("ignores think directives that only appear in the context wrapper", async () => {
await withTempHome(async (home) => {
mockRunEmbeddedPiAgentOk();
const res = await getReplyFromConfig(
{
Body: [
"[Chat messages since your last reply - for context]",
"Peter: /thinking high [2025-12-05T21:45:00.000Z]",
"",
"[Current message - respond to this]",
"Give me the status",
].join("\n"),
From: "+1002",
To: "+2000",
},
{},
makeCfg(home),
);
expect(maybeReplyText(res)).toBe("ok");
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? "";
expect(prompt).toContain("Give me the status");
expect(prompt).not.toContain("/thinking high");
expect(prompt).not.toContain("/think high");
});
});
it("does not emit directive acks for heartbeats with /think", async () => {
await withTempHome(async (home) => {
mockRunEmbeddedPiAgentOk();
const res = await getReplyFromConfig(
{
Body: "HEARTBEAT /think:high",
From: "+1003",
To: "+1003",
},
{ isHeartbeat: true },
makeCfg(home),
);
const text = maybeReplyText(res);
expect(text).toBe("ok");
expect(text).not.toMatch(/Thinking level set/i);
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
});
});
it("targets the active session for native /stop", async () => {
await withTempHome(async (home) => {
const cfg = makeCfg(home);
const storePath = cfg.session?.store;
if (!storePath) {
throw new Error("missing session store path");
}
const targetSessionKey = "agent:main:telegram:group:123";
const targetSessionId = "session-target";
await fs.writeFile(
storePath,
JSON.stringify({
[targetSessionKey]: {
{
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, "native-stop.sessions.json") };
getAbortEmbeddedPiRunMock().mockClear();
const storePath = cfg.session?.store;
if (!storePath) {
throw new Error("missing session store path");
}
const targetSessionKey = "agent:main:telegram:group:123";
const targetSessionId = "session-target";
await fs.writeFile(
storePath,
JSON.stringify({
[targetSessionKey]: {
sessionId: targetSessionId,
updatedAt: Date.now(),
},
}),
);
const followupRun: FollowupRun = {
prompt: "queued",
enqueuedAt: Date.now(),
run: {
agentId: "main",
agentDir: join(home, "agent"),
sessionId: targetSessionId,
updatedAt: Date.now(),
sessionKey: targetSessionKey,
messageProvider: "telegram",
agentAccountId: "acct",
sessionFile: join(home, "session.jsonl"),
workspaceDir: join(home, "workspace"),
config: cfg,
provider: "anthropic",
model: "claude-opus-4-5",
timeoutMs: 10,
blockReplyBreak: "text_end",
},
}),
);
const followupRun: FollowupRun = {
prompt: "queued",
enqueuedAt: Date.now(),
run: {
agentId: "main",
agentDir: join(home, "agent"),
sessionId: targetSessionId,
sessionKey: targetSessionKey,
messageProvider: "telegram",
agentAccountId: "acct",
sessionFile: join(home, "session.jsonl"),
workspaceDir: join(home, "workspace"),
config: cfg,
provider: "anthropic",
model: "claude-opus-4-5",
timeoutMs: 10,
blockReplyBreak: "text_end",
},
};
enqueueFollowupRun(
targetSessionKey,
followupRun,
{ mode: "collect", debounceMs: 0, cap: 20, dropPolicy: "summarize" },
"none",
);
expect(getFollowupQueueDepth(targetSessionKey)).toBe(1);
};
enqueueFollowupRun(
targetSessionKey,
followupRun,
{ mode: "collect", debounceMs: 0, cap: 20, dropPolicy: "summarize" },
"none",
);
expect(getFollowupQueueDepth(targetSessionKey)).toBe(1);
const res = await getReplyFromConfig(
{
Body: "/stop",
From: "telegram:111",
To: "telegram:111",
ChatType: "direct",
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandSource: "native",
CommandTargetSessionKey: targetSessionKey,
CommandAuthorized: true,
},
{},
cfg,
);
const res = await getReplyFromConfig(
{
Body: "/stop",
From: "telegram:111",
To: "telegram:111",
ChatType: "direct",
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandSource: "native",
CommandTargetSessionKey: targetSessionKey,
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("⚙️ Agent was aborted.");
expect(getAbortEmbeddedPiRunMock()).toHaveBeenCalledWith(targetSessionId);
const store = loadSessionStore(storePath);
expect(store[targetSessionKey]?.abortedLastRun).toBe(true);
expect(getFollowupQueueDepth(targetSessionKey)).toBe(0);
});
});
it("applies native /model to the target session", async () => {
await withTempHome(async (home) => {
const cfg = makeCfg(home);
const storePath = cfg.session?.store;
if (!storePath) {
throw new Error("missing session store path");
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("⚙️ Agent was aborted.");
expect(getAbortEmbeddedPiRunMock()).toHaveBeenCalledWith(targetSessionId);
const store = loadSessionStore(storePath);
expect(store[targetSessionKey]?.abortedLastRun).toBe(true);
expect(getFollowupQueueDepth(targetSessionKey)).toBe(0);
}
const slashSessionKey = "telegram:slash:111";
const targetSessionKey = MAIN_SESSION_KEY;
// Seed the target session to ensure the native command mutates it.
await fs.writeFile(
storePath,
JSON.stringify({
[targetSessionKey]: {
sessionId: "session-target",
updatedAt: Date.now(),
{
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, "native-model.sessions.json") };
getRunEmbeddedPiAgentMock().mockClear();
const storePath = cfg.session?.store;
if (!storePath) {
throw new Error("missing session store path");
}
const slashSessionKey = "telegram:slash:111";
const targetSessionKey = MAIN_SESSION_KEY;
// Seed the target session to ensure the native command mutates it.
await fs.writeFile(
storePath,
JSON.stringify({
[targetSessionKey]: {
sessionId: "session-target",
updatedAt: Date.now(),
},
}),
);
const res = await getReplyFromConfig(
{
Body: "/model openai/gpt-4.1-mini",
From: "telegram:111",
To: "telegram:111",
ChatType: "direct",
Provider: "telegram",
Surface: "telegram",
SessionKey: slashSessionKey,
CommandSource: "native",
CommandTargetSessionKey: targetSessionKey,
CommandAuthorized: true,
},
}),
);
{},
cfg,
);
const res = await getReplyFromConfig(
{
Body: "/model openai/gpt-4.1-mini",
From: "telegram:111",
To: "telegram:111",
ChatType: "direct",
Provider: "telegram",
Surface: "telegram",
SessionKey: slashSessionKey,
CommandSource: "native",
CommandTargetSessionKey: targetSessionKey,
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Model set to openai/gpt-4.1-mini");
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Model set to openai/gpt-4.1-mini");
const store = loadSessionStore(storePath);
expect(store[targetSessionKey]?.providerOverride).toBe("openai");
expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini");
expect(store[slashSessionKey]).toBeUndefined();
const store = loadSessionStore(storePath);
expect(store[targetSessionKey]?.providerOverride).toBe("openai");
expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini");
expect(store[slashSessionKey]).toBeUndefined();
getRunEmbeddedPiAgentMock().mockResolvedValue({
payloads: [{ text: "ok" }],
meta: {
durationMs: 5,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
getRunEmbeddedPiAgentMock().mockResolvedValue({
payloads: [{ text: "ok" }],
meta: {
durationMs: 5,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
await getReplyFromConfig(
{
Body: "hi",
From: "telegram:111",
To: "telegram:111",
ChatType: "direct",
Provider: "telegram",
Surface: "telegram",
},
{},
cfg,
);
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
expect(getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]).toEqual(
expect.objectContaining({
provider: "openai",
model: "gpt-4.1-mini",
}),
);
}
{
const cfg = makeCfg(home) as unknown as OpenClawConfig;
cfg.session = { ...cfg.session, store: join(home, "native-status.sessions.json") };
cfg.agents = {
...cfg.agents,
list: [{ id: "coding", model: "minimax/MiniMax-M2.1" }],
};
cfg.channels = {
...cfg.channels,
telegram: {
allowFrom: ["*"],
},
};
const res = await getReplyFromConfig(
{
Body: "/status",
From: "telegram:111",
To: "telegram:111",
ChatType: "group",
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandSource: "native",
CommandTargetSessionKey: "agent:coding:telegram:group:123",
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("minimax/MiniMax-M2.1");
}
await runGreetingPromptForBareNewOrReset({ home, body: "/new", getReplyFromConfig });
await expectResetBlockedForNonOwner({ home });
await expectInlineCommandHandledAndStripped({
home,
getReplyFromConfig,
body: "please /whoami now",
stripToken: "/whoami",
blockReplyContains: "Identity",
requestOverrides: { SenderId: "12345" },
});
getRunEmbeddedPiAgentMock().mockClear();
await expectUnauthorizedCommandDropped(home, "/status");
const inlineRunEmbeddedPiAgentMock = mockEmbeddedOk();
const res = await runInlineUnauthorizedCommand({
home,
command: "/status",
});
await getReplyFromConfig(
{
Body: "hi",
From: "telegram:111",
To: "telegram:111",
ChatType: "direct",
Provider: "telegram",
Surface: "telegram",
},
{},
cfg,
);
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
expect(getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]).toEqual(
expect.objectContaining({
provider: "openai",
model: "gpt-4.1-mini",
}),
);
});
});
it("uses the target agent model for native /status", async () => {
await withTempHome(async (home) => {
const cfg = makeCfg(home) as unknown as OpenClawConfig;
cfg.agents = {
...cfg.agents,
list: [{ id: "coding", model: "minimax/MiniMax-M2.1" }],
};
cfg.channels = {
...cfg.channels,
telegram: {
allowFrom: ["*"],
},
};
const res = await getReplyFromConfig(
{
Body: "/status",
From: "telegram:111",
To: "telegram:111",
ChatType: "group",
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandSource: "native",
CommandTargetSessionKey: "agent:coding:telegram:group:123",
CommandAuthorized: true,
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("minimax/MiniMax-M2.1");
expect(text).toBe("ok");
expect(inlineRunEmbeddedPiAgentMock).toHaveBeenCalled();
const prompt = inlineRunEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? "";
expect(prompt).toContain("/status");
});
});
});