mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 13:02:10 +03:00
refactor(test): dedupe trigger-handling e2e setup
This commit is contained in:
+15
-97
@@ -1,97 +1,15 @@
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("allows /activation from allowFrom in groups", async () => {
|
it("allows /activation from allowFrom in groups", async () => {
|
||||||
@@ -112,12 +30,12 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("⚙️ Group activation set to mention.");
|
expect(text).toBe("⚙️ Group activation set to mention.");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("injects group activation context into the system prompt", async () => {
|
it("injects group activation context into the system prompt", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -159,15 +77,15 @@ describe("trigger handling", () => {
|
|||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
|
||||||
const extra = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.extraSystemPrompt ?? "";
|
const extra = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.extraSystemPrompt ?? "";
|
||||||
expect(extra).toContain('"chat_type": "group"');
|
expect(extra).toContain('"chat_type": "group"');
|
||||||
expect(extra).toContain("Activation: always-on");
|
expect(extra).toContain("Activation: always-on");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("runs a greeting prompt for a bare /new", async () => {
|
it("runs a greeting prompt for a bare /new", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "hello" }],
|
payloads: [{ text: "hello" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -202,8 +120,8 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("hello");
|
expect(text).toBe("hello");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).toContain("A new session was started via /new or /reset");
|
expect(prompt).toContain("A new session was started via /new or /reset");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+10
-92
@@ -1,97 +1,15 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
MAIN_SESSION_KEY,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("allows approved sender to toggle elevated mode", async () => {
|
it("allows approved sender to toggle elevated mode", async () => {
|
||||||
@@ -180,7 +98,7 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
it("ignores elevated directive in groups when not mentioned", async () => {
|
it("ignores elevated directive in groups when not mentioned", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -223,7 +141,7 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBeUndefined();
|
expect(text).toBeUndefined();
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+19
-119
@@ -1,128 +1,34 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { describe, expect, it } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { loadSessionStore } from "../config/sessions.js";
|
import { loadSessionStore } from "../config/sessions.js";
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
MAIN_SESSION_KEY,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("allows elevated off in groups without mention", async () => {
|
it("allows elevated off in groups without mention", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const baseCfg = makeCfg(home);
|
||||||
payloads: [{ text: "ok" }],
|
|
||||||
meta: {
|
|
||||||
durationMs: 1,
|
|
||||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
...baseCfg,
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tools: {
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
channels: {
|
channels: {
|
||||||
|
...baseCfg.channels,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
groups: { "*": { requireMention: false } },
|
groups: { "*": { requireMention: false } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
@@ -146,27 +52,24 @@ describe("trigger handling", () => {
|
|||||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("off");
|
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("off");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows elevated directive in groups when mentioned", async () => {
|
it("allows elevated directive in groups when mentioned", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const baseCfg = makeCfg(home);
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
...baseCfg,
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tools: {
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
channels: {
|
channels: {
|
||||||
|
...baseCfg.channels,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
groups: { "*": { requireMention: true } },
|
groups: { "*": { requireMention: true } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
@@ -191,26 +94,23 @@ describe("trigger handling", () => {
|
|||||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on");
|
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows elevated directive in direct chats without mentions", async () => {
|
it("allows elevated directive in direct chats without mentions", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const baseCfg = makeCfg(home);
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
...baseCfg,
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tools: {
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
channels: {
|
channels: {
|
||||||
|
...baseCfg.channels,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
|
|||||||
+17
-97
@@ -1,95 +1,19 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
formatUsageWindowSummary: vi.fn().mockReturnValue("Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getProviderUsageMocks,
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
const usageMocks = getProviderUsageMocks();
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readSessionStore(home: string): Promise<Record<string, unknown>> {
|
async function readSessionStore(home: string): Promise<Record<string, unknown>> {
|
||||||
const raw = await readFile(join(home, "sessions.json"), "utf-8");
|
const raw = await readFile(join(home, "sessions.json"), "utf-8");
|
||||||
@@ -101,10 +25,6 @@ function pickFirstStoreEntry<T>(store: Record<string, unknown>): T | undefined {
|
|||||||
return entries[0];
|
return entries[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("filters usage summary to the current model provider", async () => {
|
it("filters usage summary to the current model provider", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
@@ -193,7 +113,7 @@ describe("trigger handling", () => {
|
|||||||
expect(blockReplies.length).toBe(0);
|
expect(blockReplies.length).toBe(0);
|
||||||
expect(replies.length).toBe(1);
|
expect(replies.length).toBe(1);
|
||||||
expect(String(replies[0]?.text ?? "")).toContain("Usage footer: tokens");
|
expect(String(replies[0]?.text ?? "")).toContain("Usage footer: tokens");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -255,7 +175,7 @@ describe("trigger handling", () => {
|
|||||||
const s3 = await readSessionStore(home);
|
const s3 = await readSessionStore(home);
|
||||||
expect(pickFirstStoreEntry<{ responseUsage?: string }>(s3)?.responseUsage).toBeUndefined();
|
expect(pickFirstStoreEntry<{ responseUsage?: string }>(s3)?.responseUsage).toBeUndefined();
|
||||||
|
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -281,12 +201,12 @@ describe("trigger handling", () => {
|
|||||||
const store = await readSessionStore(home);
|
const store = await readSessionStore(home);
|
||||||
expect(pickFirstStoreEntry<{ responseUsage?: string }>(store)?.responseUsage).toBe("tokens");
|
expect(pickFirstStoreEntry<{ responseUsage?: string }>(store)?.responseUsage).toBe("tokens");
|
||||||
|
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("sends one inline status and still returns agent reply for mixed text", async () => {
|
it("sends one inline status and still returns agent reply for mixed text", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "agent says hi" }],
|
payloads: [{ text: "agent says hi" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -315,7 +235,7 @@ describe("trigger handling", () => {
|
|||||||
expect(String(blockReplies[0]?.text ?? "")).toContain("Model:");
|
expect(String(blockReplies[0]?.text ?? "")).toContain("Model:");
|
||||||
expect(replies.length).toBe(1);
|
expect(replies.length).toBe(1);
|
||||||
expect(replies[0]?.text).toBe("agent says hi");
|
expect(replies[0]?.text).toBe("agent says hi");
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).not.toContain("/status");
|
expect(prompt).not.toContain("/status");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -333,7 +253,7 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("⚙️ Agent was aborted.");
|
expect(text).toBe("⚙️ Agent was aborted.");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("handles /stop without invoking the agent", async () => {
|
it("handles /stop without invoking the agent", async () => {
|
||||||
@@ -350,7 +270,7 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("⚙️ Agent was aborted.");
|
expect(text).toBe("⚙️ Agent was aborted.");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+37
-113
@@ -1,107 +1,26 @@
|
|||||||
import { join } from "node:path";
|
import { describe, expect, it } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("handles inline /commands and strips it before the agent", async () => {
|
it("handles inline /commands and strips it before the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const blockReplies: Array<{ text?: string }> = [];
|
const blockReplies: Array<{ text?: string }> = [];
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
@@ -117,24 +36,28 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
makeCfg(home),
|
makeCfg(home),
|
||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(blockReplies.length).toBe(1);
|
expect(blockReplies.length).toBe(1);
|
||||||
expect(blockReplies[0]?.text).toContain("Slash commands");
|
expect(blockReplies[0]?.text).toContain("Slash commands");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).not.toContain("/commands");
|
expect(prompt).not.toContain("/commands");
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles inline /whoami and strips it before the agent", async () => {
|
it("handles inline /whoami and strips it before the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const blockReplies: Array<{ text?: string }> = [];
|
const blockReplies: Array<{ text?: string }> = [];
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
@@ -151,31 +74,31 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
makeCfg(home),
|
makeCfg(home),
|
||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(blockReplies.length).toBe(1);
|
expect(blockReplies.length).toBe(1);
|
||||||
expect(blockReplies[0]?.text).toContain("Identity");
|
expect(blockReplies[0]?.text).toContain("Identity");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).not.toContain("/whoami");
|
expect(prompt).not.toContain("/whoami");
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drops /status for unauthorized senders", async () => {
|
it("drops /status for unauthorized senders", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
const baseCfg = makeCfg(home);
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
...baseCfg,
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
channels: {
|
||||||
|
...baseCfg.channels,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/status",
|
Body: "/status",
|
||||||
@@ -187,26 +110,26 @@ describe("trigger handling", () => {
|
|||||||
{},
|
{},
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(res).toBeUndefined();
|
expect(res).toBeUndefined();
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drops /whoami for unauthorized senders", async () => {
|
it("drops /whoami for unauthorized senders", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
const baseCfg = makeCfg(home);
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
...baseCfg,
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
channels: {
|
||||||
|
...baseCfg.channels,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/whoami",
|
Body: "/whoami",
|
||||||
@@ -218,8 +141,9 @@ describe("trigger handling", () => {
|
|||||||
{},
|
{},
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(res).toBeUndefined();
|
expect(res).toBeUndefined();
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-95
@@ -1,102 +1,21 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
MAIN_SESSION_KEY,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("ignores inline elevated directive for unapproved sender", async () => {
|
it("ignores inline elevated directive for unapproved sender", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -136,7 +55,7 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).not.toContain("elevated is not available right now");
|
expect(text).not.toContain("elevated is not available right now");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("uses tools.elevated.allowFrom.discord for elevated approval", async () => {
|
it("uses tools.elevated.allowFrom.discord for elevated approval", async () => {
|
||||||
@@ -204,12 +123,12 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("tools.elevated.allowFrom.discord");
|
expect(text).toContain("tools.elevated.allowFrom.discord");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("returns a context overflow fallback when the embedded agent throws", async () => {
|
it("returns a context overflow fallback when the embedded agent throws", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("Context window exceeded"));
|
getRunEmbeddedPiAgentMock().mockRejectedValue(new Error("Context window exceeded"));
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
@@ -225,7 +144,7 @@ describe("trigger handling", () => {
|
|||||||
expect(text).toBe(
|
expect(text).toBe(
|
||||||
"⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.",
|
"⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.",
|
||||||
);
|
);
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+36
-106
@@ -1,103 +1,22 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { describe, expect, it } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
MAIN_SESSION_KEY,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("includes the error cause when the embedded agent throws", async () => {
|
it("includes the error cause when the embedded agent throws", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("sandbox is not defined."));
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockRejectedValue(new Error("sandbox is not defined."));
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
@@ -113,12 +32,14 @@ describe("trigger handling", () => {
|
|||||||
expect(text).toBe(
|
expect(text).toBe(
|
||||||
"⚠️ Agent failed before reply: sandbox is not defined.\nLogs: openclaw logs --follow",
|
"⚠️ Agent failed before reply: sandbox is not defined.\nLogs: openclaw logs --follow",
|
||||||
);
|
);
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses heartbeat model override for heartbeat runs", async () => {
|
it("uses heartbeat model override for heartbeat runs", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -128,9 +49,9 @@ describe("trigger handling", () => {
|
|||||||
|
|
||||||
const cfg = makeCfg(home);
|
const cfg = makeCfg(home);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
join(home, "sessions.json"),
|
cfg.session.store,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
[_MAIN_SESSION_KEY]: {
|
[MAIN_SESSION_KEY]: {
|
||||||
sessionId: "main",
|
sessionId: "main",
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
providerOverride: "openai",
|
providerOverride: "openai",
|
||||||
@@ -157,14 +78,16 @@ describe("trigger handling", () => {
|
|||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
|
|
||||||
const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0];
|
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
|
||||||
expect(call?.provider).toBe("anthropic");
|
expect(call?.provider).toBe("anthropic");
|
||||||
expect(call?.model).toBe("claude-haiku-4-5-20251001");
|
expect(call?.model).toBe("claude-haiku-4-5-20251001");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps stored model override for heartbeat runs when heartbeat model is not configured", async () => {
|
it("keeps stored model override for heartbeat runs when heartbeat model is not configured", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -172,10 +95,11 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cfg = makeCfg(home);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
join(home, "sessions.json"),
|
cfg.session.store,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
[_MAIN_SESSION_KEY]: {
|
[MAIN_SESSION_KEY]: {
|
||||||
sessionId: "main",
|
sessionId: "main",
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
providerOverride: "openai",
|
providerOverride: "openai",
|
||||||
@@ -192,17 +116,19 @@ describe("trigger handling", () => {
|
|||||||
To: "+2000",
|
To: "+2000",
|
||||||
},
|
},
|
||||||
{ isHeartbeat: true },
|
{ isHeartbeat: true },
|
||||||
makeCfg(home),
|
cfg,
|
||||||
);
|
);
|
||||||
|
|
||||||
const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0];
|
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
|
||||||
expect(call?.provider).toBe("openai");
|
expect(call?.provider).toBe("openai");
|
||||||
expect(call?.model).toBe("gpt-5.2");
|
expect(call?.model).toBe("gpt-5.2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("suppresses HEARTBEAT_OK replies outside heartbeat runs", async () => {
|
it("suppresses HEARTBEAT_OK replies outside heartbeat runs", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: HEARTBEAT_TOKEN }],
|
payloads: [{ text: HEARTBEAT_TOKEN }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -221,12 +147,14 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(res).toBeUndefined();
|
expect(res).toBeUndefined();
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips HEARTBEAT_OK at edges outside heartbeat runs", async () => {
|
it("strips HEARTBEAT_OK at edges outside heartbeat runs", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: `${HEARTBEAT_TOKEN} hello` }],
|
payloads: [{ text: `${HEARTBEAT_TOKEN} hello` }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -248,8 +176,10 @@ describe("trigger handling", () => {
|
|||||||
expect(text).toBe("hello");
|
expect(text).toBe("hello");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates group activation when the owner sends /activation", async () => {
|
it("updates group activation when the owner sends /activation", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
const cfg = makeCfg(home);
|
const cfg = makeCfg(home);
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
@@ -271,7 +201,7 @@ describe("trigger handling", () => {
|
|||||||
{ groupActivation?: string }
|
{ groupActivation?: string }
|
||||||
>;
|
>;
|
||||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe("always");
|
expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe("always");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+35
-119
@@ -1,122 +1,39 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { describe, expect, it } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
MAIN_SESSION_KEY,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("keeps inline /status for unauthorized senders", async () => {
|
it("keeps inline /status for unauthorized senders", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const baseCfg = makeCfg(home);
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
...baseCfg,
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
channels: {
|
||||||
|
...baseCfg.channels,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "please /status now",
|
Body: "please /status now",
|
||||||
@@ -130,35 +47,35 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
// Not allowlisted: inline /status is treated as plain text and is not stripped.
|
// Not allowlisted: inline /status is treated as plain text and is not stripped.
|
||||||
expect(prompt).toContain("/status");
|
expect(prompt).toContain("/status");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps inline /help for unauthorized senders", async () => {
|
it("keeps inline /help for unauthorized senders", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const baseCfg = makeCfg(home);
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
...baseCfg,
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
channels: {
|
||||||
|
...baseCfg.channels,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "please /help now",
|
Body: "please /help now",
|
||||||
@@ -172,13 +89,15 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).toContain("/help");
|
expect(prompt).toContain("/help");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns help without invoking the agent", async () => {
|
it("returns help without invoking the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/help",
|
Body: "/help",
|
||||||
@@ -193,24 +112,21 @@ describe("trigger handling", () => {
|
|||||||
expect(text).toContain("Help");
|
expect(text).toContain("Help");
|
||||||
expect(text).toContain("Session");
|
expect(text).toContain("Session");
|
||||||
expect(text).toContain("More: /commands for full list");
|
expect(text).toContain("More: /commands for full list");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows owner to set send policy", async () => {
|
it("allows owner to set send policy", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const baseCfg = makeCfg(home);
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
...baseCfg,
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
channels: {
|
||||||
|
...baseCfg.channels,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
|
|||||||
+22
-98
@@ -1,102 +1,21 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { resolveSessionKey } from "../config/sessions.js";
|
import { resolveSessionKey } from "../config/sessions.js";
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("reports active auth profile and key snippet in status", async () => {
|
it("reports active auth profile and key snippet in status", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
const cfg = makeCfg(home);
|
const cfg = makeCfg(home);
|
||||||
const agentDir = join(home, ".openclaw", "agents", "main", "agent");
|
const agentDir = join(home, ".openclaw", "agents", "main", "agent");
|
||||||
await fs.mkdir(agentDir, { recursive: true });
|
await fs.mkdir(agentDir, { recursive: true });
|
||||||
@@ -153,21 +72,24 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("api-key");
|
expect(text).toContain("api-key");
|
||||||
expect(text).toMatch(/…|\.{3}/);
|
expect(text).toMatch(/\u2026|\.{3}/);
|
||||||
expect(text).toContain("(anthropic:work)");
|
expect(text).toContain("(anthropic:work)");
|
||||||
expect(text).not.toContain("mixed");
|
expect(text).not.toContain("mixed");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips inline /status and still runs the agent", async () => {
|
it("strips inline /status and still runs the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const blockReplies: Array<{ text?: string }> = [];
|
const blockReplies: Array<{ text?: string }> = [];
|
||||||
await getReplyFromConfig(
|
await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
@@ -186,18 +108,20 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
makeCfg(home),
|
makeCfg(home),
|
||||||
);
|
);
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
||||||
// Allowlisted senders: inline /status runs immediately (like /help) and is
|
// Allowlisted senders: inline /status runs immediately (like /help) and is
|
||||||
// stripped from the prompt; the remaining text continues through the agent.
|
// stripped from the prompt; the remaining text continues through the agent.
|
||||||
expect(blockReplies.length).toBe(1);
|
expect(blockReplies.length).toBe(1);
|
||||||
expect(String(blockReplies[0]?.text ?? "").length).toBeGreaterThan(0);
|
expect(String(blockReplies[0]?.text ?? "").length).toBeGreaterThan(0);
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).not.toContain("/status");
|
expect(prompt).not.toContain("/status");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles inline /help and strips it before the agent", async () => {
|
it("handles inline /help and strips it before the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -222,8 +146,8 @@ describe("trigger handling", () => {
|
|||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(blockReplies.length).toBe(1);
|
expect(blockReplies.length).toBe(1);
|
||||||
expect(blockReplies[0]?.text).toContain("Help");
|
expect(blockReplies[0]?.text).toContain("Help");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).not.toContain("/help");
|
expect(prompt).not.toContain("/help");
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
});
|
});
|
||||||
|
|||||||
+22
-107
@@ -1,108 +1,23 @@
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import {
|
|
||||||
abortEmbeddedPiRun,
|
|
||||||
compactEmbeddedPiSession,
|
|
||||||
runEmbeddedPiAgent,
|
|
||||||
} from "../agents/pi-embedded.js";
|
|
||||||
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getCompactEmbeddedPiSessionMock,
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("runs /compact as a gated command", async () => {
|
it("runs /compact as a gated command", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const storePath = join(tmpdir(), `openclaw-session-test-${Date.now()}.json`);
|
const storePath = join(tmpdir(), `openclaw-session-test-${Date.now()}.json`);
|
||||||
vi.mocked(compactEmbeddedPiSession).mockResolvedValue({
|
getCompactEmbeddedPiSessionMock().mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
compacted: true,
|
compacted: true,
|
||||||
result: {
|
result: {
|
||||||
@@ -139,8 +54,8 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
|
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
|
||||||
expect(compactEmbeddedPiSession).toHaveBeenCalledOnce();
|
expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce();
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
const store = loadSessionStore(storePath);
|
const store = loadSessionStore(storePath);
|
||||||
const sessionKey = resolveSessionKey("per-sender", {
|
const sessionKey = resolveSessionKey("per-sender", {
|
||||||
Body: "/compact focus on decisions",
|
Body: "/compact focus on decisions",
|
||||||
@@ -152,8 +67,8 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
it("runs /compact for non-default agents without transcript path validation failures", async () => {
|
it("runs /compact for non-default agents without transcript path validation failures", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(compactEmbeddedPiSession).mockClear();
|
getCompactEmbeddedPiSessionMock().mockClear();
|
||||||
vi.mocked(compactEmbeddedPiSession).mockResolvedValue({
|
getCompactEmbeddedPiSessionMock().mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
compacted: true,
|
compacted: true,
|
||||||
result: {
|
result: {
|
||||||
@@ -177,16 +92,16 @@ describe("trigger handling", () => {
|
|||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
|
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
|
||||||
expect(compactEmbeddedPiSession).toHaveBeenCalledOnce();
|
expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce();
|
||||||
expect(vi.mocked(compactEmbeddedPiSession).mock.calls[0]?.[0]?.sessionFile).toContain(
|
expect(getCompactEmbeddedPiSessionMock().mock.calls[0]?.[0]?.sessionFile).toContain(
|
||||||
join("agents", "worker1", "sessions"),
|
join("agents", "worker1", "sessions"),
|
||||||
);
|
);
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("ignores think directives that only appear in the context wrapper", async () => {
|
it("ignores think directives that only appear in the context wrapper", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -212,8 +127,8 @@ describe("trigger handling", () => {
|
|||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).toContain("Give me the status");
|
expect(prompt).toContain("Give me the status");
|
||||||
expect(prompt).not.toContain("/thinking high");
|
expect(prompt).not.toContain("/thinking high");
|
||||||
expect(prompt).not.toContain("/think high");
|
expect(prompt).not.toContain("/think high");
|
||||||
@@ -221,7 +136,7 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
it("does not emit directive acks for heartbeats with /think", async () => {
|
it("does not emit directive acks for heartbeats with /think", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -242,7 +157,7 @@ describe("trigger handling", () => {
|
|||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
expect(text).not.toMatch(/Thinking level set/i);
|
expect(text).not.toMatch(/Thinking level set/i);
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-95
@@ -1,102 +1,19 @@
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("runs a greeting prompt for a bare /reset", async () => {
|
it("runs a greeting prompt for a bare /reset", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "hello" }],
|
payloads: [{ text: "hello" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -131,8 +48,8 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("hello");
|
expect(text).toBe("hello");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
|
||||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).toContain("A new session was started via /new or /reset");
|
expect(prompt).toContain("A new session was started via /new or /reset");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -164,7 +81,7 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(res).toBeUndefined();
|
expect(res).toBeUndefined();
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("blocks /reset for non-owner senders", async () => {
|
it("blocks /reset for non-owner senders", async () => {
|
||||||
@@ -195,7 +112,7 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(res).toBeUndefined();
|
expect(res).toBeUndefined();
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-94
@@ -1,97 +1,14 @@
|
|||||||
import { join } from "node:path";
|
import { describe, expect, it } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("shows endpoint default in /model status when not configured", async () => {
|
it("shows endpoint default in /model status when not configured", async () => {
|
||||||
@@ -153,6 +70,7 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
it("rejects /restart by default", async () => {
|
it("rejects /restart by default", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: " [Dec 5] /restart",
|
Body: " [Dec 5] /restart",
|
||||||
@@ -165,11 +83,12 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("/restart is disabled");
|
expect(text).toContain("/restart is disabled");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("restarts when enabled", async () => {
|
it("restarts when enabled", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
const cfg = { ...makeCfg(home), commands: { restart: true } };
|
const cfg = { ...makeCfg(home), commands: { restart: true } };
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
@@ -183,11 +102,12 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text?.startsWith("⚙️ Restarting") || text?.startsWith("⚠️ Restart failed")).toBe(true);
|
expect(text?.startsWith("⚙️ Restarting") || text?.startsWith("⚠️ Restart failed")).toBe(true);
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("reports status without invoking the agent", async () => {
|
it("reports status without invoking the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/status",
|
Body: "/status",
|
||||||
@@ -200,7 +120,7 @@ describe("trigger handling", () => {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("OpenClaw");
|
expect(text).toContain("OpenClaw");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-91
@@ -1,98 +1,14 @@
|
|||||||
import { join } from "node:path";
|
import { describe, expect, it } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { loadSessionStore } from "../config/sessions.js";
|
import { loadSessionStore } from "../config/sessions.js";
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("shows a /model summary and points to /models", async () => {
|
it("shows a /model summary and points to /models", async () => {
|
||||||
|
|||||||
+14
-94
@@ -1,99 +1,19 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
|
||||||
compactEmbeddedPiSession: vi.fn(),
|
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
|
||||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
|
||||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
|
||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usageMocks = vi.hoisted(() => ({
|
|
||||||
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [],
|
|
||||||
}),
|
|
||||||
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
|
||||||
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
|
||||||
|
|
||||||
const modelCatalogMocks = vi.hoisted(() => ({
|
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue([
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provider: "openrouter",
|
|
||||||
id: "anthropic/claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5 (OpenRouter)",
|
|
||||||
contextWindow: 200000,
|
|
||||||
},
|
|
||||||
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
||||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
|
||||||
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
|
||||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
|
||||||
]),
|
|
||||||
resetModelCatalogCacheForTest: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
|
||||||
|
|
||||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
|
||||||
import { loadSessionStore } from "../config/sessions.js";
|
import { loadSessionStore } from "../config/sessions.js";
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
import {
|
||||||
|
getAbortEmbeddedPiRunMock,
|
||||||
|
getRunEmbeddedPiAgentMock,
|
||||||
|
installTriggerHandlingE2eTestHooks,
|
||||||
|
MAIN_SESSION_KEY,
|
||||||
|
makeCfg,
|
||||||
|
withTempHome,
|
||||||
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./reply/queue.js";
|
import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./reply/queue.js";
|
||||||
|
|
||||||
const MAIN_SESSION_KEY = "agent:main:main";
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
|
||||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../web/session.js", () => webMocks);
|
|
||||||
|
|
||||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|
||||||
return withTempHomeBase(
|
|
||||||
async (home) => {
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
|
||||||
vi.mocked(abortEmbeddedPiRun).mockClear();
|
|
||||||
return await fn(home);
|
|
||||||
},
|
|
||||||
{ prefix: "openclaw-triggers-" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
|
||||||
return {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: join(home, "openclaw"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
allowFrom: ["*"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: join(home, "sessions.json") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("targets the active session for native /stop", async () => {
|
it("targets the active session for native /stop", async () => {
|
||||||
@@ -160,7 +80,7 @@ describe("trigger handling", () => {
|
|||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("⚙️ Agent was aborted.");
|
expect(text).toBe("⚙️ Agent was aborted.");
|
||||||
expect(vi.mocked(abortEmbeddedPiRun)).toHaveBeenCalledWith(targetSessionId);
|
expect(getAbortEmbeddedPiRunMock()).toHaveBeenCalledWith(targetSessionId);
|
||||||
const store = loadSessionStore(cfg.session.store);
|
const store = loadSessionStore(cfg.session.store);
|
||||||
expect(store[targetSessionKey]?.abortedLastRun).toBe(true);
|
expect(store[targetSessionKey]?.abortedLastRun).toBe(true);
|
||||||
expect(getFollowupQueueDepth(targetSessionKey)).toBe(0);
|
expect(getFollowupQueueDepth(targetSessionKey)).toBe(0);
|
||||||
@@ -212,7 +132,7 @@ describe("trigger handling", () => {
|
|||||||
expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini");
|
expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini");
|
||||||
expect(store[slashSessionKey]).toBeUndefined();
|
expect(store[slashSessionKey]).toBeUndefined();
|
||||||
|
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 5,
|
durationMs: 5,
|
||||||
@@ -233,8 +153,8 @@ describe("trigger handling", () => {
|
|||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
|
||||||
expect(vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]).toEqual(
|
expect(getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "gpt-4.1-mini",
|
model: "gpt-4.1-mini",
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, vi } from "vitest";
|
||||||
|
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||||
|
|
||||||
|
const piEmbeddedMocks = vi.hoisted(() => ({
|
||||||
|
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||||
|
compactEmbeddedPiSession: vi.fn(),
|
||||||
|
runEmbeddedPiAgent: vi.fn(),
|
||||||
|
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||||
|
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||||
|
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function getAbortEmbeddedPiRunMock() {
|
||||||
|
return piEmbeddedMocks.abortEmbeddedPiRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCompactEmbeddedPiSessionMock() {
|
||||||
|
return piEmbeddedMocks.compactEmbeddedPiSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRunEmbeddedPiAgentMock() {
|
||||||
|
return piEmbeddedMocks.runEmbeddedPiAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQueueEmbeddedPiMessageMock() {
|
||||||
|
return piEmbeddedMocks.queueEmbeddedPiMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("../agents/pi-embedded.js", () => ({
|
||||||
|
abortEmbeddedPiRun: (...args: unknown[]) => piEmbeddedMocks.abortEmbeddedPiRun(...args),
|
||||||
|
compactEmbeddedPiSession: (...args: unknown[]) =>
|
||||||
|
piEmbeddedMocks.compactEmbeddedPiSession(...args),
|
||||||
|
runEmbeddedPiAgent: (...args: unknown[]) => piEmbeddedMocks.runEmbeddedPiAgent(...args),
|
||||||
|
queueEmbeddedPiMessage: (...args: unknown[]) => piEmbeddedMocks.queueEmbeddedPiMessage(...args),
|
||||||
|
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||||
|
isEmbeddedPiRunActive: (...args: unknown[]) => piEmbeddedMocks.isEmbeddedPiRunActive(...args),
|
||||||
|
isEmbeddedPiRunStreaming: (...args: unknown[]) =>
|
||||||
|
piEmbeddedMocks.isEmbeddedPiRunStreaming(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const providerUsageMocks = vi.hoisted(() => ({
|
||||||
|
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
||||||
|
updatedAt: 0,
|
||||||
|
providers: [],
|
||||||
|
}),
|
||||||
|
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
||||||
|
formatUsageWindowSummary: vi.fn().mockReturnValue("Claude 80% left"),
|
||||||
|
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function getProviderUsageMocks() {
|
||||||
|
return providerUsageMocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("../infra/provider-usage.js", () => providerUsageMocks);
|
||||||
|
|
||||||
|
const modelCatalogMocks = vi.hoisted(() => ({
|
||||||
|
loadModelCatalog: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
provider: "anthropic",
|
||||||
|
id: "claude-opus-4-5",
|
||||||
|
name: "Claude Opus 4.5",
|
||||||
|
contextWindow: 200000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: "openrouter",
|
||||||
|
id: "anthropic/claude-opus-4-5",
|
||||||
|
name: "Claude Opus 4.5 (OpenRouter)",
|
||||||
|
contextWindow: 200000,
|
||||||
|
},
|
||||||
|
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
||||||
|
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
||||||
|
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
||||||
|
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
||||||
|
]),
|
||||||
|
resetModelCatalogCacheForTest: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function getModelCatalogMocks() {
|
||||||
|
return modelCatalogMocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||||
|
|
||||||
|
const webSessionMocks = vi.hoisted(() => ({
|
||||||
|
webAuthExists: vi.fn().mockResolvedValue(true),
|
||||||
|
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
||||||
|
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function getWebSessionMocks() {
|
||||||
|
return webSessionMocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("../web/session.js", () => webSessionMocks);
|
||||||
|
|
||||||
|
export const MAIN_SESSION_KEY = "agent:main:main";
|
||||||
|
|
||||||
|
export async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||||
|
return withTempHomeBase(
|
||||||
|
async (home) => {
|
||||||
|
// Avoid cross-test leakage if a test doesn't touch these mocks.
|
||||||
|
piEmbeddedMocks.runEmbeddedPiAgent.mockClear();
|
||||||
|
piEmbeddedMocks.abortEmbeddedPiRun.mockClear();
|
||||||
|
piEmbeddedMocks.compactEmbeddedPiSession.mockClear();
|
||||||
|
return await fn(home);
|
||||||
|
},
|
||||||
|
{ prefix: "openclaw-triggers-" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeCfg(home: string) {
|
||||||
|
return {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "openclaw"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
channels: {
|
||||||
|
whatsapp: {
|
||||||
|
allowFrom: ["*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: { store: join(home, "sessions.json") },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installTriggerHandlingE2eTestHooks() {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user