mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
test: consolidate trigger handling suites
This commit is contained in:
131
src/auto-reply/reply.triggers.group-intro-prompts.cases.ts
Normal file
131
src/auto-reply/reply.triggers.group-intro-prompts.cases.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user