diff --git a/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts index 4696de517ce..e517c244e39 100644 --- a/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts @@ -15,6 +15,16 @@ import { } from "./reply.directive.directive-behavior.e2e-harness.js"; import { getReplyFromConfig } from "./reply.js"; +function makeDefaultModelConfig(home: string) { + return makeWhatsAppDirectiveConfig(home, { + model: { primary: "anthropic/claude-opus-4-5" }, + models: { + "anthropic/claude-opus-4-5": {}, + "openai/gpt-4.1-mini": {}, + }, + }); +} + async function runReplyToCurrentCase(home: string, text: string) { vi.mocked(runEmbeddedPiAgent).mockResolvedValue(makeEmbeddedTextResult(text)); @@ -74,6 +84,90 @@ describe("directive behavior", () => { expectedLevel: "off", }); }); + it("ignores inline /model and uses the default model", async () => { + await withTempHome(async (home) => { + mockEmbeddedTextResult("done"); + + const res = await getReplyFromConfig( + { + Body: "please sync /model openai/gpt-4.1-mini now", + From: "+1004", + To: "+2000", + }, + {}, + makeDefaultModelConfig(home), + ); + + const texts = replyTexts(res); + expect(texts).toContain("done"); + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; + expect(call?.provider).toBe("anthropic"); + expect(call?.model).toBe("claude-opus-4-5"); + }); + }); + it("defaults thinking to low for reasoning-capable models during normal replies", async () => { + await withTempHome(async (home) => { + mockEmbeddedTextResult("done"); + vi.mocked(loadModelCatalog).mockResolvedValueOnce([ + { + id: "claude-opus-4-5", + name: "Opus 4.5", + provider: "anthropic", + reasoning: true, + }, + ]); + + await getReplyFromConfig( + { + Body: "hello", + From: "+1004", + To: "+2000", + }, + {}, + makeWhatsAppDirectiveConfig(home, { model: { primary: "anthropic/claude-opus-4-5" } }), + ); + + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; + expect(call?.thinkLevel).toBe("low"); + }); + }); + it("passes elevated defaults when sender is approved", async () => { + await withTempHome(async (home) => { + mockEmbeddedTextResult("done"); + + await getReplyFromConfig( + { + Body: "hello", + From: "+1004", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1004", + }, + {}, + makeWhatsAppDirectiveConfig( + home, + { model: { primary: "anthropic/claude-opus-4-5" } }, + { + tools: { + elevated: { + allowFrom: { whatsapp: ["+1004"] }, + }, + }, + }, + ), + ); + + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; + expect(call?.bashElevated).toEqual({ + enabled: true, + allowed: true, + defaultLevel: "on", + }); + }); + }); it("persists /reasoning off on discord even when model defaults reasoning on", async () => { await withTempHome(async (home) => { const storePath = sessionStorePath(home); diff --git a/src/auto-reply/reply.directive.directive-behavior.ignores-inline-model-uses-default-model.test.ts b/src/auto-reply/reply.directive.directive-behavior.ignores-inline-model-uses-default-model.test.ts deleted file mode 100644 index 410a5b62fdc..00000000000 --- a/src/auto-reply/reply.directive.directive-behavior.ignores-inline-model-uses-default-model.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import "./reply.directive.directive-behavior.e2e-mocks.js"; -import { describe, expect, it, vi } from "vitest"; -import { - installDirectiveBehaviorE2EHooks, - loadModelCatalog, - makeWhatsAppDirectiveConfig, - mockEmbeddedTextResult, - replyTexts, - runEmbeddedPiAgent, - withTempHome, -} from "./reply.directive.directive-behavior.e2e-harness.js"; -import { getReplyFromConfig } from "./reply.js"; - -function makeDefaultModelConfig(home: string) { - return makeWhatsAppDirectiveConfig(home, { - model: { primary: "anthropic/claude-opus-4-5" }, - models: { - "anthropic/claude-opus-4-5": {}, - "openai/gpt-4.1-mini": {}, - }, - }); -} - -describe("directive behavior", () => { - installDirectiveBehaviorE2EHooks(); - - it("ignores inline /model and uses the default model", async () => { - await withTempHome(async (home) => { - mockEmbeddedTextResult("done"); - - const res = await getReplyFromConfig( - { - Body: "please sync /model openai/gpt-4.1-mini now", - From: "+1004", - To: "+2000", - }, - {}, - makeDefaultModelConfig(home), - ); - - const texts = replyTexts(res); - expect(texts).toContain("done"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; - expect(call?.provider).toBe("anthropic"); - expect(call?.model).toBe("claude-opus-4-5"); - }); - }); - it("defaults thinking to low for reasoning-capable models", async () => { - await withTempHome(async (home) => { - mockEmbeddedTextResult("done"); - vi.mocked(loadModelCatalog).mockResolvedValueOnce([ - { - id: "claude-opus-4-5", - name: "Opus 4.5", - provider: "anthropic", - reasoning: true, - }, - ]); - - await getReplyFromConfig( - { - Body: "hello", - From: "+1004", - To: "+2000", - }, - {}, - makeWhatsAppDirectiveConfig(home, { model: { primary: "anthropic/claude-opus-4-5" } }), - ); - - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; - expect(call?.thinkLevel).toBe("low"); - }); - }); - it("passes elevated defaults when sender is approved", async () => { - await withTempHome(async (home) => { - mockEmbeddedTextResult("done"); - - await getReplyFromConfig( - { - Body: "hello", - From: "+1004", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1004", - }, - {}, - makeWhatsAppDirectiveConfig( - home, - { model: { primary: "anthropic/claude-opus-4-5" } }, - { - tools: { - elevated: { - allowFrom: { whatsapp: ["+1004"] }, - }, - }, - }, - ), - ); - - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; - expect(call?.bashElevated).toEqual({ - enabled: true, - allowed: true, - defaultLevel: "on", - }); - }); - }); -}); diff --git a/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts b/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts deleted file mode 100644 index 8af2f80e3b5..00000000000 --- a/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import "./reply.directive.directive-behavior.e2e-mocks.js"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; -import { loadSessionStore } from "../config/sessions.js"; -import { - assertElevatedOffStatusReply, - installDirectiveBehaviorE2EHooks, - makeRestrictedElevatedDisabledConfig, - runEmbeddedPiAgent, - withTempHome, -} from "./reply.directive.directive-behavior.e2e-harness.js"; -import { getReplyFromConfig } from "./reply.js"; - -describe("directive behavior", () => { - installDirectiveBehaviorE2EHooks(); - - function extractReplyText(res: Awaited>): string { - return (Array.isArray(res) ? res[0]?.text : res?.text) ?? ""; - } - - function makeQueueDirectiveConfig(home: string, storePath: string) { - return { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: path.join(home, "openclaw"), - }, - }, - channels: { whatsapp: { allowFrom: ["*"] } }, - session: { store: storePath }, - } as unknown as OpenClawConfig; - } - - async function runQueueDirective(params: { home: string; storePath: string; body: string }) { - return await getReplyFromConfig( - { Body: params.body, From: "+1222", To: "+1222", CommandAuthorized: true }, - {}, - makeQueueDirectiveConfig(params.home, params.storePath), - ); - } - - it("returns status alongside directive-only acks", async () => { - await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - - const res = await getReplyFromConfig( - { - Body: "/elevated off\n/status", - From: "+1222", - To: "+1222", - Provider: "whatsapp", - SenderE164: "+1222", - CommandAuthorized: true, - }, - {}, - { - agents: { - defaults: { - model: { primary: "anthropic/claude-opus-4-5" }, - workspace: path.join(home, "openclaw"), - }, - }, - tools: { - elevated: { - allowFrom: { whatsapp: ["+1222"] }, - }, - }, - channels: { whatsapp: { allowFrom: ["+1222"] } }, - session: { store: storePath }, - }, - ); - - const text = extractReplyText(res); - expect(text).toContain("Session: agent:main:main"); - assertElevatedOffStatusReply(text); - - const store = loadSessionStore(storePath); - expect(store["agent:main:main"]?.elevatedLevel).toBe("off"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("shows elevated off in status when per-agent elevated is disabled", async () => { - await withTempHome(async (home) => { - const res = await getReplyFromConfig( - { - Body: "/status", - From: "+1222", - To: "+1222", - Provider: "whatsapp", - SenderE164: "+1222", - SessionKey: "agent:restricted:main", - CommandAuthorized: true, - }, - {}, - makeRestrictedElevatedDisabledConfig(home) as unknown as OpenClawConfig, - ); - - const text = extractReplyText(res); - expect(text).not.toContain("elevated"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("acks queue directive and persists override", async () => { - await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - - const res = await runQueueDirective({ - home, - storePath, - body: "/queue interrupt", - }); - - const text = extractReplyText(res); - expect(text).toMatch(/^⚙️ Queue mode set to interrupt\./); - const store = loadSessionStore(storePath); - const entry = Object.values(store)[0]; - expect(entry?.queueMode).toBe("interrupt"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("persists queue options when directive is standalone", async () => { - await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - - const res = await runQueueDirective({ - home, - storePath, - body: "/queue collect debounce:2s cap:5 drop:old", - }); - - const text = extractReplyText(res); - expect(text).toMatch(/^⚙️ Queue mode set to collect\./); - expect(text).toMatch(/Queue debounce set to 2000ms/); - expect(text).toMatch(/Queue cap set to 5/); - expect(text).toMatch(/Queue drop set to old/); - const store = loadSessionStore(storePath); - const entry = Object.values(store)[0]; - expect(entry?.queueMode).toBe("collect"); - expect(entry?.queueDebounceMs).toBe(2000); - expect(entry?.queueCap).toBe(5); - expect(entry?.queueDrop).toBe("old"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("resets queue mode to default", async () => { - await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - - await runQueueDirective({ home, storePath, body: "/queue interrupt" }); - const res = await runQueueDirective({ home, storePath, body: "/queue reset" }); - const text = extractReplyText(res); - expect(text).toMatch(/^⚙️ Queue mode reset to default\./); - const store = loadSessionStore(storePath); - const entry = Object.values(store)[0]; - expect(entry?.queueMode).toBeUndefined(); - expect(entry?.queueDebounceMs).toBeUndefined(); - expect(entry?.queueCap).toBeUndefined(); - expect(entry?.queueDrop).toBeUndefined(); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts index 3ca0bd63f8f..40cb72b48a0 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts @@ -50,6 +50,10 @@ async function runElevatedCommand(home: string, body: string) { ); } +async function runQueueDirective(home: string, body: string) { + return runCommand(home, body); +} + describe("directive behavior", () => { installDirectiveBehaviorE2EHooks(); @@ -106,6 +110,7 @@ describe("directive behavior", () => { const storePath = sessionStorePath(home); const res = await runElevatedCommand(home, "/elevated off\n/status"); const text = replyText(res); + expect(text).toContain("Session: agent:main:main"); assertElevatedOffStatusReply(text); const store = loadSessionStore(storePath); @@ -159,6 +164,75 @@ describe("directive behavior", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); + it("shows elevated off in status when per-agent elevated is disabled", async () => { + await withTempHome(async (home) => { + const res = await getReplyFromConfig( + { + Body: "/status", + From: "+1222", + To: "+1222", + Provider: "whatsapp", + SenderE164: "+1222", + SessionKey: "agent:restricted:main", + CommandAuthorized: true, + }, + {}, + makeRestrictedElevatedDisabledConfig(home) as unknown as OpenClawConfig, + ); + + const text = replyText(res); + expect(text).not.toContain("elevated"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("acks queue directive and persists override", async () => { + await withTempHome(async (home) => { + const storePath = sessionStorePath(home); + + const text = await runQueueDirective(home, "/queue interrupt"); + + expect(text).toMatch(/^⚙️ Queue mode set to interrupt\./); + const store = loadSessionStore(storePath); + const entry = Object.values(store)[0]; + expect(entry?.queueMode).toBe("interrupt"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("persists queue options when directive is standalone", async () => { + await withTempHome(async (home) => { + const storePath = sessionStorePath(home); + + const text = await runQueueDirective(home, "/queue collect debounce:2s cap:5 drop:old"); + + expect(text).toMatch(/^⚙️ Queue mode set to collect\./); + expect(text).toMatch(/Queue debounce set to 2000ms/); + expect(text).toMatch(/Queue cap set to 5/); + expect(text).toMatch(/Queue drop set to old/); + const store = loadSessionStore(storePath); + const entry = Object.values(store)[0]; + expect(entry?.queueMode).toBe("collect"); + expect(entry?.queueDebounceMs).toBe(2000); + expect(entry?.queueCap).toBe(5); + expect(entry?.queueDrop).toBe("old"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("resets queue mode to default", async () => { + await withTempHome(async (home) => { + const storePath = sessionStorePath(home); + + await runQueueDirective(home, "/queue interrupt"); + const text = await runQueueDirective(home, "/queue reset"); + expect(text).toMatch(/^⚙️ Queue mode reset to default\./); + const store = loadSessionStore(storePath); + const entry = Object.values(store)[0]; + expect(entry?.queueMode).toBeUndefined(); + expect(entry?.queueDebounceMs).toBeUndefined(); + expect(entry?.queueCap).toBeUndefined(); + expect(entry?.queueDrop).toBeUndefined(); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); it("strips inline elevated directives from the user text (does not persist session override)", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockResolvedValue({