diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.e2e.test.ts index 959295807b4..46fbba9ff3f 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.e2e.test.ts @@ -1,97 +1,15 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; 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"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { 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; 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 () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, @@ -159,15 +77,15 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const extra = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.extraSystemPrompt ?? ""; + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); + const extra = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.extraSystemPrompt ?? ""; expect(extra).toContain('"chat_type": "group"'); expect(extra).toContain("Activation: always-on"); }); }); it("runs a greeting prompt for a bare /new", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "hello" }], meta: { durationMs: 1, @@ -202,8 +120,8 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("hello"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); + const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain("A new session was started via /new or /reset"); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.e2e.test.ts index 05a61712740..dcfbd2f2b9a 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.e2e.test.ts @@ -1,97 +1,15 @@ import fs from "node:fs/promises"; import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; 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"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { 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 () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, @@ -223,7 +141,7 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBeUndefined(); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.e2e.test.ts index fc723b4b8d2..baa4ac2ec18 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.e2e.test.ts @@ -1,128 +1,34 @@ import fs from "node:fs/promises"; -import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; import { loadSessionStore } from "../config/sessions.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"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("allows elevated off in groups without mention", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ - payloads: [{ text: "ok" }], - meta: { - durationMs: 1, - agentMeta: { sessionId: "s", provider: "p", model: "m" }, - }, - }); + const baseCfg = makeCfg(home); const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, + ...baseCfg, tools: { elevated: { allowFrom: { whatsapp: ["+1000"] }, }, }, channels: { + ...baseCfg.channels, whatsapp: { allowFrom: ["+1000"], groups: { "*": { requireMention: false } }, }, }, - session: { store: join(home, "sessions.json") }, }; const res = await getReplyFromConfig( @@ -146,27 +52,24 @@ describe("trigger handling", () => { expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("off"); }); }); + it("allows elevated directive in groups when mentioned", async () => { await withTempHome(async (home) => { + const baseCfg = makeCfg(home); const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, + ...baseCfg, tools: { elevated: { allowFrom: { whatsapp: ["+1000"] }, }, }, channels: { + ...baseCfg.channels, whatsapp: { allowFrom: ["+1000"], groups: { "*": { requireMention: true } }, }, }, - session: { store: join(home, "sessions.json") }, }; const res = await getReplyFromConfig( @@ -191,26 +94,23 @@ describe("trigger handling", () => { expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on"); }); }); + it("allows elevated directive in direct chats without mentions", async () => { await withTempHome(async (home) => { + const baseCfg = makeCfg(home); const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, + ...baseCfg, tools: { elevated: { allowFrom: { whatsapp: ["+1000"] }, }, }, channels: { + ...baseCfg.channels, whatsapp: { allowFrom: ["+1000"], }, }, - session: { store: join(home, "sessions.json") }, }; const res = await getReplyFromConfig( diff --git a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.e2e.test.ts index 92e6b15df8c..4f4bff874f2 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.e2e.test.ts @@ -1,95 +1,19 @@ import { readFile } from "node:fs/promises"; 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 { 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 { + 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(() => ({ - 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(fn: (home: string) => Promise): Promise { - 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") }, - }; -} +const usageMocks = getProviderUsageMocks(); async function readSessionStore(home: string): Promise> { const raw = await readFile(join(home, "sessions.json"), "utf-8"); @@ -101,10 +25,6 @@ function pickFirstStoreEntry(store: Record): T | undefined { return entries[0]; } -afterEach(() => { - vi.restoreAllMocks(); -}); - describe("trigger handling", () => { it("filters usage summary to the current model provider", async () => { await withTempHome(async (home) => { @@ -193,7 +113,7 @@ describe("trigger handling", () => { expect(blockReplies.length).toBe(0); expect(replies.length).toBe(1); 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); 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); 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 () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "agent says hi" }], meta: { durationMs: 1, @@ -315,7 +235,7 @@ describe("trigger handling", () => { expect(String(blockReplies[0]?.text ?? "")).toContain("Model:"); expect(replies.length).toBe(1); 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"); }); }); @@ -333,7 +253,7 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("⚙️ Agent was aborted."); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); }); }); 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; expect(text).toBe("⚙️ Agent was aborted."); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.e2e.test.ts index 418f517b598..9f1a79b6369 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.e2e.test.ts @@ -1,107 +1,26 @@ -import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; 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"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("handles inline /commands and strips it before the agent", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); + const blockReplies: Array<{ text?: string }> = []; const res = await getReplyFromConfig( { @@ -117,24 +36,28 @@ describe("trigger handling", () => { }, makeCfg(home), ); + const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(blockReplies.length).toBe(1); expect(blockReplies[0]?.text).toContain("Slash commands"); - expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); + const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).not.toContain("/commands"); expect(text).toBe("ok"); }); }); + it("handles inline /whoami and strips it before the agent", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); + const blockReplies: Array<{ text?: string }> = []; const res = await getReplyFromConfig( { @@ -151,31 +74,31 @@ describe("trigger handling", () => { }, makeCfg(home), ); + const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(blockReplies.length).toBe(1); expect(blockReplies[0]?.text).toContain("Identity"); - expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); + const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).not.toContain("/whoami"); expect(text).toBe("ok"); }); }); + it("drops /status for unauthorized senders", async () => { await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const baseCfg = makeCfg(home); const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, + ...baseCfg, channels: { + ...baseCfg.channels, whatsapp: { allowFrom: ["+1000"], }, }, - session: { store: join(home, "sessions.json") }, }; + const res = await getReplyFromConfig( { Body: "/status", @@ -187,26 +110,26 @@ describe("trigger handling", () => { {}, cfg, ); + expect(res).toBeUndefined(); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); }); + it("drops /whoami for unauthorized senders", async () => { await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const baseCfg = makeCfg(home); const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, + ...baseCfg, channels: { + ...baseCfg.channels, whatsapp: { allowFrom: ["+1000"], }, }, - session: { store: join(home, "sessions.json") }, }; + const res = await getReplyFromConfig( { Body: "/whoami", @@ -218,8 +141,9 @@ describe("trigger handling", () => { {}, cfg, ); + expect(res).toBeUndefined(); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.e2e.test.ts index 2969c2407db..f3c0add19ed 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.e2e.test.ts @@ -1,102 +1,21 @@ import fs from "node:fs/promises"; import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; 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"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("ignores inline elevated directive for unapproved sender", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, @@ -136,7 +55,7 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; 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 () => { @@ -204,12 +123,12 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; 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 () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("Context window exceeded")); + getRunEmbeddedPiAgentMock().mockRejectedValue(new Error("Context window exceeded")); const res = await getReplyFromConfig( { @@ -225,7 +144,7 @@ describe("trigger handling", () => { expect(text).toBe( "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.", ); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.e2e.test.ts index b96319d5be5..a4c7d447eb9 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.e2e.test.ts @@ -1,103 +1,22 @@ import fs from "node:fs/promises"; -import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; 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"; -const _MAIN_SESSION_KEY = "agent:main:main"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("includes the error cause when the embedded agent throws", async () => { 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( { @@ -113,12 +32,14 @@ describe("trigger handling", () => { expect(text).toBe( "⚠️ 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 () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, @@ -128,9 +49,9 @@ describe("trigger handling", () => { const cfg = makeCfg(home); await fs.writeFile( - join(home, "sessions.json"), + cfg.session.store, JSON.stringify({ - [_MAIN_SESSION_KEY]: { + [MAIN_SESSION_KEY]: { sessionId: "main", updatedAt: Date.now(), providerOverride: "openai", @@ -157,14 +78,16 @@ describe("trigger handling", () => { cfg, ); - const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; + const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0]; expect(call?.provider).toBe("anthropic"); expect(call?.model).toBe("claude-haiku-4-5-20251001"); }); }); + it("keeps stored model override for heartbeat runs when heartbeat model is not configured", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, @@ -172,10 +95,11 @@ describe("trigger handling", () => { }, }); + const cfg = makeCfg(home); await fs.writeFile( - join(home, "sessions.json"), + cfg.session.store, JSON.stringify({ - [_MAIN_SESSION_KEY]: { + [MAIN_SESSION_KEY]: { sessionId: "main", updatedAt: Date.now(), providerOverride: "openai", @@ -192,17 +116,19 @@ describe("trigger handling", () => { To: "+2000", }, { 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?.model).toBe("gpt-5.2"); }); }); + it("suppresses HEARTBEAT_OK replies outside heartbeat runs", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: HEARTBEAT_TOKEN }], meta: { durationMs: 1, @@ -221,12 +147,14 @@ describe("trigger handling", () => { ); expect(res).toBeUndefined(); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); }); }); + it("strips HEARTBEAT_OK at edges outside heartbeat runs", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: `${HEARTBEAT_TOKEN} hello` }], meta: { durationMs: 1, @@ -248,8 +176,10 @@ describe("trigger handling", () => { expect(text).toBe("hello"); }); }); + it("updates group activation when the owner sends /activation", async () => { await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); const cfg = makeCfg(home); const res = await getReplyFromConfig( { @@ -271,7 +201,7 @@ describe("trigger handling", () => { { groupActivation?: string } >; expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe("always"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.e2e.test.ts index 9d82efd14b2..f5a0f9b8588 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.e2e.test.ts @@ -1,122 +1,39 @@ import fs from "node:fs/promises"; -import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; 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"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("keeps inline /status for unauthorized senders", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); + + const baseCfg = makeCfg(home); const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, + ...baseCfg, channels: { + ...baseCfg.channels, whatsapp: { allowFrom: ["+1000"], }, }, - session: { store: join(home, "sessions.json") }, }; + const res = await getReplyFromConfig( { Body: "please /status now", @@ -130,35 +47,35 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); - expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); + const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? ""; // Not allowlisted: inline /status is treated as plain text and is not stripped. expect(prompt).toContain("/status"); }); }); + it("keeps inline /help for unauthorized senders", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); + + const baseCfg = makeCfg(home); const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, + ...baseCfg, channels: { + ...baseCfg.channels, whatsapp: { allowFrom: ["+1000"], }, }, - session: { store: join(home, "sessions.json") }, }; + const res = await getReplyFromConfig( { Body: "please /help now", @@ -172,13 +89,15 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); - expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); + const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain("/help"); }); }); + it("returns help without invoking the agent", async () => { await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); const res = await getReplyFromConfig( { Body: "/help", @@ -193,24 +112,21 @@ describe("trigger handling", () => { expect(text).toContain("Help"); expect(text).toContain("Session"); expect(text).toContain("More: /commands for full list"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); }); + it("allows owner to set send policy", async () => { await withTempHome(async (home) => { + const baseCfg = makeCfg(home); const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, + ...baseCfg, channels: { + ...baseCfg.channels, whatsapp: { allowFrom: ["+1000"], }, }, - session: { store: join(home, "sessions.json") }, }; const res = await getReplyFromConfig( diff --git a/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.e2e.test.ts index bb56bc3a52d..1f7d9f43f43 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.e2e.test.ts @@ -1,102 +1,21 @@ import fs from "node:fs/promises"; import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; import { resolveSessionKey } from "../config/sessions.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"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("reports active auth profile and key snippet in status", async () => { await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); const cfg = makeCfg(home); const agentDir = join(home, ".openclaw", "agents", "main", "agent"); await fs.mkdir(agentDir, { recursive: true }); @@ -153,21 +72,24 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("api-key"); - expect(text).toMatch(/…|\.{3}/); + expect(text).toMatch(/\u2026|\.{3}/); expect(text).toContain("(anthropic:work)"); expect(text).not.toContain("mixed"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); }); + it("strips inline /status and still runs the agent", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); + const blockReplies: Array<{ text?: string }> = []; await getReplyFromConfig( { @@ -186,18 +108,20 @@ describe("trigger handling", () => { }, makeCfg(home), ); - expect(runEmbeddedPiAgent).toHaveBeenCalled(); + expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); // Allowlisted senders: inline /status runs immediately (like /help) and is // stripped from the prompt; the remaining text continues through the agent. expect(blockReplies.length).toBe(1); 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"); }); }); + it("handles inline /help and strips it before the agent", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, @@ -222,8 +146,8 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(blockReplies.length).toBe(1); expect(blockReplies[0]?.text).toContain("Help"); - expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); + const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).not.toContain("/help"); expect(text).toBe("ok"); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.e2e.test.ts index c1d4b1a6ada..13f5815c4f7 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.e2e.test.ts @@ -1,108 +1,23 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; -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, - compactEmbeddedPiSession, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { describe, expect, it } from "vitest"; import { loadSessionStore, resolveSessionKey } from "../config/sessions.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"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("runs /compact as a gated command", async () => { await withTempHome(async (home) => { const storePath = join(tmpdir(), `openclaw-session-test-${Date.now()}.json`); - vi.mocked(compactEmbeddedPiSession).mockResolvedValue({ + getCompactEmbeddedPiSessionMock().mockResolvedValue({ ok: true, compacted: true, result: { @@ -139,8 +54,8 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text?.startsWith("⚙️ Compacted")).toBe(true); - expect(compactEmbeddedPiSession).toHaveBeenCalledOnce(); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); const store = loadSessionStore(storePath); const sessionKey = resolveSessionKey("per-sender", { 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 () => { await withTempHome(async (home) => { - vi.mocked(compactEmbeddedPiSession).mockClear(); - vi.mocked(compactEmbeddedPiSession).mockResolvedValue({ + getCompactEmbeddedPiSessionMock().mockClear(); + getCompactEmbeddedPiSessionMock().mockResolvedValue({ ok: true, compacted: true, result: { @@ -177,16 +92,16 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text?.startsWith("⚙️ Compacted")).toBe(true); - expect(compactEmbeddedPiSession).toHaveBeenCalledOnce(); - expect(vi.mocked(compactEmbeddedPiSession).mock.calls[0]?.[0]?.sessionFile).toContain( + expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce(); + expect(getCompactEmbeddedPiSessionMock().mock.calls[0]?.[0]?.sessionFile).toContain( join("agents", "worker1", "sessions"), ); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); }); }); it("ignores think directives that only appear in the context wrapper", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, @@ -212,8 +127,8 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); + const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain("Give me the status"); expect(prompt).not.toContain("/thinking high"); expect(prompt).not.toContain("/think high"); @@ -221,7 +136,7 @@ describe("trigger handling", () => { }); it("does not emit directive acks for heartbeats with /think", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, @@ -242,7 +157,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); expect(text).not.toMatch(/Thinking level set/i); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.e2e.test.ts index f08a3093fce..3beb427788b 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.e2e.test.ts @@ -1,102 +1,19 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; import { getReplyFromConfig } from "./reply.js"; +import { + getRunEmbeddedPiAgentMock, + installTriggerHandlingE2eTestHooks, + withTempHome, +} from "./reply.triggers.trigger-handling.test-harness.js"; -const _MAIN_SESSION_KEY = "agent:main:main"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("runs a greeting prompt for a bare /reset", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "hello" }], meta: { durationMs: 1, @@ -131,8 +48,8 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("hello"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); + const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain("A new session was started via /new or /reset"); }); }); @@ -164,7 +81,7 @@ describe("trigger handling", () => { }, ); expect(res).toBeUndefined(); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); }); }); it("blocks /reset for non-owner senders", async () => { @@ -195,7 +112,7 @@ describe("trigger handling", () => { }, ); expect(res).toBeUndefined(); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.e2e.test.ts index d634f5f6478..832dc2a0929 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.e2e.test.ts @@ -1,97 +1,14 @@ -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 { 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 { + getRunEmbeddedPiAgentMock, + installTriggerHandlingE2eTestHooks, + makeCfg, + withTempHome, +} from "./reply.triggers.trigger-handling.test-harness.js"; -const _MAIN_SESSION_KEY = "agent:main:main"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("shows endpoint default in /model status when not configured", async () => { @@ -153,6 +70,7 @@ describe("trigger handling", () => { }); it("rejects /restart by default", async () => { await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); const res = await getReplyFromConfig( { Body: " [Dec 5] /restart", @@ -165,11 +83,12 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("/restart is disabled"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); }); it("restarts when enabled", async () => { await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); const cfg = { ...makeCfg(home), commands: { restart: true } }; const res = await getReplyFromConfig( { @@ -183,11 +102,12 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; 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 () => { await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); const res = await getReplyFromConfig( { Body: "/status", @@ -200,7 +120,7 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("OpenClaw"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.e2e.test.ts index 3fa07253d89..97a3186413e 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.e2e.test.ts @@ -1,98 +1,14 @@ -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 { 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 { getReplyFromConfig } from "./reply.js"; +import { + installTriggerHandlingE2eTestHooks, + makeCfg, + withTempHome, +} from "./reply.triggers.trigger-handling.test-harness.js"; -const _MAIN_SESSION_KEY = "agent:main:main"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { it("shows a /model summary and points to /models", async () => { diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts index a6511f9e1e6..21452d854cc 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts @@ -1,99 +1,19 @@ import fs from "node:fs/promises"; import { join } from "node:path"; -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 { describe, expect, it } from "vitest"; import { loadSessionStore } from "../config/sessions.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"; -const MAIN_SESSION_KEY = "agent:main:main"; - -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(fn: (home: string) => Promise): Promise { - 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(); -}); +installTriggerHandlingE2eTestHooks(); describe("trigger handling", () => { 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; expect(text).toBe("⚙️ Agent was aborted."); - expect(vi.mocked(abortEmbeddedPiRun)).toHaveBeenCalledWith(targetSessionId); + expect(getAbortEmbeddedPiRunMock()).toHaveBeenCalledWith(targetSessionId); const store = loadSessionStore(cfg.session.store); expect(store[targetSessionKey]?.abortedLastRun).toBe(true); expect(getFollowupQueueDepth(targetSessionKey)).toBe(0); @@ -212,7 +132,7 @@ describe("trigger handling", () => { expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini"); expect(store[slashSessionKey]).toBeUndefined(); - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + getRunEmbeddedPiAgentMock().mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 5, @@ -233,8 +153,8 @@ describe("trigger handling", () => { cfg, ); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - expect(vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]).toEqual( + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); + expect(getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]).toEqual( expect.objectContaining({ provider: "openai", model: "gpt-4.1-mini", diff --git a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts b/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts new file mode 100644 index 00000000000..944caa7b258 --- /dev/null +++ b/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts @@ -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(fn: (home: string) => Promise): Promise { + 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(); + }); +}