diff --git a/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts b/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts index e7ba0e50f..24d101ea6 100644 --- a/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts @@ -171,24 +171,18 @@ describe("directive behavior", () => { expect(blockReplies.length).toBe(0); }); }); - it("acks verbose directive immediately with system marker", async () => { + it("handles standalone verbose directives and persistence", async () => { await withTempHome(async (home) => { - const res = await getReplyFromConfig( + const storePath = sessionStorePath(home); + + const enabledRes = await getReplyFromConfig( { Body: "/verbose on", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, makeWhatsAppDirectiveConfig(home, { model: "anthropic/claude-opus-4-5" }), ); + expect(replyText(enabledRes)).toMatch(/^⚙️ Verbose logging enabled\./); - const text = replyText(res); - expect(text).toMatch(/^⚙️ Verbose logging enabled\./); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("persists verbose off when directive is standalone", async () => { - await withTempHome(async (home) => { - const storePath = sessionStorePath(home); - - const res = await getReplyFromConfig( + const disabledRes = await getReplyFromConfig( { Body: "/verbose off", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, makeWhatsAppDirectiveConfig( @@ -200,7 +194,7 @@ describe("directive behavior", () => { ), ); - const text = replyText(res); + const text = replyText(disabledRes); expect(text).toMatch(/Verbose logging disabled\./); const store = loadSessionStore(storePath); const entry = Object.values(store)[0]; @@ -208,59 +202,46 @@ describe("directive behavior", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); - it("updates tool verbose during an in-flight run (toggle on)", async () => { + it("updates tool verbose during in-flight runs for toggle on/off", async () => { await withTempHome(async (home) => { - const { res } = await runInFlightVerboseToggleCase({ - home, - shouldEmitBefore: false, - toggledVerboseLevel: "on", - }); - - const texts = replyTexts(res); - expect(texts).toContain("done"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + for (const testCase of [ + { + shouldEmitBefore: false, + toggledVerboseLevel: "on" as const, + }, + { + shouldEmitBefore: true, + toggledVerboseLevel: "off" as const, + seedVerboseOn: true, + }, + ]) { + vi.mocked(runEmbeddedPiAgent).mockClear(); + const { res } = await runInFlightVerboseToggleCase({ + home, + ...testCase, + }); + const texts = replyTexts(res); + expect(texts).toContain("done"); + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + } }); }); - it("updates tool verbose during an in-flight run (toggle off)", async () => { - await withTempHome(async (home) => { - const { res } = await runInFlightVerboseToggleCase({ - home, - shouldEmitBefore: true, - toggledVerboseLevel: "off", - seedVerboseOn: true, - }); - - const texts = replyTexts(res); - expect(texts).toContain("done"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - }); - }); - it("shows current think level when /think has no argument", async () => { + it("covers think status and /thinking xhigh support matrix", async () => { await withTempHome(async (home) => { const text = await runThinkDirectiveAndGetText(home); expect(text).toContain("Current thinking level: high"); expect(text).toContain("Options: off, minimal, low, medium, high."); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("accepts /thinking xhigh for codex models", async () => { - await withTempHome(async (home) => { - const texts = await runThinkingDirective(home, "openai-codex/gpt-5.2-codex"); - expect(texts).toContain("Thinking level set to xhigh."); - }); - }); - it("accepts /thinking xhigh for openai gpt-5.2", async () => { - await withTempHome(async (home) => { - const texts = await runThinkingDirective(home, "openai/gpt-5.2"); - expect(texts).toContain("Thinking level set to xhigh."); - }); - }); - it("rejects /thinking xhigh for non-codex models", async () => { - await withTempHome(async (home) => { - const texts = await runThinkingDirective(home, "openai/gpt-4.1-mini"); - expect(texts).toContain( + + for (const model of ["openai-codex/gpt-5.2-codex", "openai/gpt-5.2"]) { + const texts = await runThinkingDirective(home, model); + expect(texts).toContain("Thinking level set to xhigh."); + } + + const unsupportedModelTexts = await runThinkingDirective(home, "openai/gpt-4.1-mini"); + expect(unsupportedModelTexts).toContain( 'Thinking level "xhigh" is only supported for openai/gpt-5.2, openai-codex/gpt-5.3-codex, openai-codex/gpt-5.3-codex-spark, openai-codex/gpt-5.2-codex, openai-codex/gpt-5.1-codex, github-copilot/gpt-5.2-codex or github-copilot/gpt-5.2.', ); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); it("keeps reserved command aliases from matching after trimming", async () => { @@ -325,9 +306,9 @@ describe("directive behavior", () => { expect(prompt).toContain('Use the "demo-skill" skill'); }); }); - it("errors on invalid queue options", async () => { + it("reports invalid queue options and current queue settings", async () => { await withTempHome(async (home) => { - const res = await getReplyFromConfig( + const invalidRes = await getReplyFromConfig( { Body: "/queue collect debounce:bogus cap:zero drop:maybe", From: "+1222", @@ -344,16 +325,12 @@ describe("directive behavior", () => { ), ); - const text = replyText(res); - expect(text).toContain("Invalid debounce"); - expect(text).toContain("Invalid cap"); - expect(text).toContain("Invalid drop policy"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("shows current queue settings when /queue has no arguments", async () => { - await withTempHome(async (home) => { - const res = await getReplyFromConfig( + const invalidText = replyText(invalidRes); + expect(invalidText).toContain("Invalid debounce"); + expect(invalidText).toContain("Invalid cap"); + expect(invalidText).toContain("Invalid drop policy"); + + const currentRes = await getReplyFromConfig( { Body: "/queue", From: "+1222", @@ -379,7 +356,7 @@ describe("directive behavior", () => { ), ); - const text = replyText(res); + const text = replyText(currentRes); expect(text).toContain( "Current queue settings: mode=collect, debounce=1500ms, cap=9, drop=summarize.", ); 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 3de33b8b9..27e41f414 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 @@ -86,6 +86,7 @@ async function runReasoningDefaultCase(params: { expectedReasoningLevel: "off" | "on"; thinkingDefault?: "off" | "low" | "medium" | "high"; }) { + vi.mocked(runEmbeddedPiAgent).mockClear(); mockEmbeddedTextResult("done"); mockReasoningCapableCatalog(); @@ -244,11 +245,11 @@ describe("directive behavior", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); - it("ignores inline /model and uses the default model", async () => { + it("ignores inline /model and /think directives while still running agent content", async () => { await withTempHome(async (home) => { mockEmbeddedTextResult("done"); - const res = await getReplyFromConfig( + const inlineModelRes = await getReplyFromConfig( { Body: "please sync /model openai/gpt-4.1-mini now", From: "+1004", @@ -258,31 +259,47 @@ describe("directive behavior", () => { makeDefaultModelConfig(home), ); - const texts = replyTexts(res); + const texts = replyTexts(inlineModelRes); 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"); + vi.mocked(runEmbeddedPiAgent).mockClear(); + + mockEmbeddedTextResult("done"); + const inlineThinkRes = await getReplyFromConfig( + { + Body: "please sync /think:high now", + From: "+1004", + To: "+2000", + }, + {}, + makeWhatsAppDirectiveConfig(home, { model: { primary: "anthropic/claude-opus-4-5" } }), + ); + + expect(replyTexts(inlineThinkRes)).toContain("done"); + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); }); }); - it("defaults thinking to low for reasoning-capable models without auto-enabling reasoning", async () => { + it("applies reasoning defaults based on thinkingDefault configuration", async () => { await withTempHome(async (home) => { - await runReasoningDefaultCase({ - home, - expectedThinkLevel: "low", - expectedReasoningLevel: "off", - }); - }); - }); - it("keeps auto-reasoning enabled when thinking is explicitly off", async () => { - await withTempHome(async (home) => { - await runReasoningDefaultCase({ - home, - expectedThinkLevel: "off", - expectedReasoningLevel: "on", - thinkingDefault: "off", - }); + for (const scenario of [ + { + expectedThinkLevel: "low" as const, + expectedReasoningLevel: "off" as const, + }, + { + expectedThinkLevel: "off" as const, + expectedReasoningLevel: "on" as const, + thinkingDefault: "off" as const, + }, + ]) { + await runReasoningDefaultCase({ + home, + ...scenario, + }); + } }); }); it("passes elevated defaults when sender is approved", async () => { @@ -384,17 +401,14 @@ describe("directive behavior", () => { expect(call?.reasoningLevel).toBe("off"); }); }); - for (const replyTag of ["[[reply_to_current]]", "[[ reply_to_current ]]"]) { - it(`strips ${replyTag} and maps reply_to_current to MessageSid`, async () => { - await withTempHome(async (home) => { + it("handles reply_to_current tags and explicit reply_to precedence", async () => { + await withTempHome(async (home) => { + for (const replyTag of ["[[reply_to_current]]", "[[ reply_to_current ]]"]) { const payload = await runReplyToCurrentCase(home, `hello ${replyTag}`); expect(payload?.text).toBe("hello"); expect(payload?.replyToId).toBe("msg-123"); - }); - }); - } - it("prefers explicit reply_to id over reply_to_current", async () => { - await withTempHome(async (home) => { + } + vi.mocked(runEmbeddedPiAgent).mockResolvedValue( makeEmbeddedTextResult("hi [[reply_to_current]] [[reply_to:abc-456]]"), ); @@ -415,23 +429,4 @@ describe("directive behavior", () => { expect(payload?.replyToId).toBe("abc-456"); }); }); - it("applies inline think and still runs agent content", async () => { - await withTempHome(async (home) => { - mockEmbeddedTextResult("done"); - - const res = await getReplyFromConfig( - { - Body: "please sync /think:high now", - From: "+1004", - To: "+2000", - }, - {}, - makeWhatsAppDirectiveConfig(home, { model: { primary: "anthropic/claude-opus-4-5" } }), - ); - - const texts = replyTexts(res); - expect(texts).toContain("done"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - }); - }); }); diff --git a/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts b/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts index 19403fd64..781965858 100644 --- a/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts @@ -110,87 +110,84 @@ describe("directive behavior", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); - it("picks the best fuzzy match when multiple models match", async () => { + it("picks the best fuzzy match for global and provider-scoped minimax queries", async () => { await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - - await getReplyFromConfig( - { Body: "/model minimax", From: "+1222", To: "+1222", CommandAuthorized: true }, - {}, + for (const testCase of [ { - agents: { - defaults: { - model: { primary: "minimax/MiniMax-M2.1" }, - workspace: path.join(home, "openclaw"), - models: { - "minimax/MiniMax-M2.1": {}, - "minimax/MiniMax-M2.1-lightning": {}, - "lmstudio/minimax-m2.1-gs32": {}, + body: "/model minimax", + storePath: path.join(home, "sessions-global-fuzzy.json"), + config: { + agents: { + defaults: { + model: { primary: "minimax/MiniMax-M2.1" }, + workspace: path.join(home, "openclaw"), + models: { + "minimax/MiniMax-M2.1": {}, + "minimax/MiniMax-M2.1-lightning": {}, + "lmstudio/minimax-m2.1-gs32": {}, + }, + }, + }, + models: { + mode: "merge", + providers: { + minimax: { + baseUrl: "https://api.minimax.io/anthropic", + apiKey: "sk-test", + api: "anthropic-messages", + models: [makeModelDefinition("MiniMax-M2.1", "MiniMax M2.1")], + }, + lmstudio: { + baseUrl: "http://127.0.0.1:1234/v1", + apiKey: "lmstudio", + api: "openai-responses", + models: [makeModelDefinition("minimax-m2.1-gs32", "MiniMax M2.1 GS32")], + }, }, }, }, - models: { - mode: "merge", - providers: { - minimax: { - baseUrl: "https://api.minimax.io/anthropic", - apiKey: "sk-test", - api: "anthropic-messages", - models: [makeModelDefinition("MiniMax-M2.1", "MiniMax M2.1")], - }, - lmstudio: { - baseUrl: "http://127.0.0.1:1234/v1", - apiKey: "lmstudio", - api: "openai-responses", - models: [makeModelDefinition("minimax-m2.1-gs32", "MiniMax M2.1 GS32")], - }, - }, - }, - session: { store: storePath }, - } as unknown as OpenClawConfig, - ); - - assertModelSelection(storePath); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("picks the best fuzzy match within a provider", async () => { - await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - - await getReplyFromConfig( - { Body: "/model minimax/m2.1", From: "+1222", To: "+1222", CommandAuthorized: true }, - {}, + }, { - agents: { - defaults: { - model: { primary: "minimax/MiniMax-M2.1" }, - workspace: path.join(home, "openclaw"), - models: { - "minimax/MiniMax-M2.1": {}, - "minimax/MiniMax-M2.1-lightning": {}, + body: "/model minimax/m2.1", + storePath: path.join(home, "sessions-provider-fuzzy.json"), + config: { + agents: { + defaults: { + model: { primary: "minimax/MiniMax-M2.1" }, + workspace: path.join(home, "openclaw"), + models: { + "minimax/MiniMax-M2.1": {}, + "minimax/MiniMax-M2.1-lightning": {}, + }, + }, + }, + models: { + mode: "merge", + providers: { + minimax: { + baseUrl: "https://api.minimax.io/anthropic", + apiKey: "sk-test", + api: "anthropic-messages", + models: [ + makeModelDefinition("MiniMax-M2.1", "MiniMax M2.1"), + makeModelDefinition("MiniMax-M2.1-lightning", "MiniMax M2.1 Lightning"), + ], + }, }, }, }, - models: { - mode: "merge", - providers: { - minimax: { - baseUrl: "https://api.minimax.io/anthropic", - apiKey: "sk-test", - api: "anthropic-messages", - models: [ - makeModelDefinition("MiniMax-M2.1", "MiniMax M2.1"), - makeModelDefinition("MiniMax-M2.1-lightning", "MiniMax M2.1 Lightning"), - ], - }, - }, - }, - session: { store: storePath }, - } as unknown as OpenClawConfig, - ); - - assertModelSelection(storePath); + }, + ]) { + await getReplyFromConfig( + { Body: testCase.body, From: "+1222", To: "+1222", CommandAuthorized: true }, + {}, + { + ...testCase.config, + session: { store: testCase.storePath }, + } as unknown as OpenClawConfig, + ); + assertModelSelection(testCase.storePath); + } 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 4c9a4e3f1..2e6f63df2 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 @@ -184,9 +184,9 @@ describe("directive behavior", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); - it("rejects per-agent elevated when disabled", async () => { + it("enforces per-agent elevated restrictions and status visibility", async () => { await withTempHome(async (home) => { - const res = await getReplyFromConfig( + const deniedRes = await getReplyFromConfig( { Body: "/elevated on", From: "+1222", @@ -199,93 +199,10 @@ describe("directive behavior", () => { {}, makeRestrictedElevatedDisabledConfig(home) as unknown as OpenClawConfig, ); + const deniedText = replyText(deniedRes); + expect(deniedText).toContain("agents.list[].tools.elevated.enabled"); - const text = replyText(res); - expect(text).toContain("agents.list[].tools.elevated.enabled"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("requires per-agent allowlist in addition to global", async () => { - await withTempHome(async (home) => { - const res = await getReplyFromConfig( - { - Body: "/elevated on", - From: "+1222", - To: "+1222", - Provider: "whatsapp", - SenderE164: "+1222", - SessionKey: "agent:work:main", - CommandAuthorized: true, - }, - {}, - makeWorkElevatedAllowlistConfig(home), - ); - - const text = replyText(res); - expect(text).toContain("agents.list[].tools.elevated.allowFrom.whatsapp"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("allows elevated when both global and per-agent allowlists match", async () => { - await withTempHome(async (home) => { - const res = await getReplyFromConfig( - { - ...makeCommandMessage("/elevated on", "+1333"), - SessionKey: "agent:work:main", - }, - {}, - makeWorkElevatedAllowlistConfig(home), - ); - - const text = replyText(res); - expect(text).toContain("Elevated mode set to ask"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("warns when elevated is used in direct runtime", async () => { - await withTempHome(async (home) => { - const res = await getReplyFromConfig( - makeCommandMessage("/elevated off"), - {}, - makeAllowlistedElevatedConfig(home, { sandbox: { mode: "off" } }), - ); - - const text = replyText(res); - expect(text).toContain("Elevated mode disabled."); - expect(text).toContain("Runtime is direct; sandboxing does not apply."); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("rejects invalid elevated level", async () => { - await withTempHome(async (home) => { - const res = await getReplyFromConfig( - makeCommandMessage("/elevated maybe"), - {}, - makeAllowlistedElevatedConfig(home), - ); - - const text = replyText(res); - expect(text).toContain("Unrecognized elevated level"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("handles multiple directives in a single message", async () => { - await withTempHome(async (home) => { - const res = await getReplyFromConfig( - makeCommandMessage("/elevated off\n/verbose on"), - {}, - makeAllowlistedElevatedConfig(home), - ); - - const text = replyText(res); - expect(text).toContain("Elevated mode disabled."); - expect(text).toContain("Verbose logging enabled."); - 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( + const statusRes = await getReplyFromConfig( { Body: "/status", From: "+1222", @@ -298,9 +215,71 @@ describe("directive behavior", () => { {}, makeRestrictedElevatedDisabledConfig(home) as unknown as OpenClawConfig, ); + const statusText = replyText(statusRes); + expect(statusText).not.toContain("elevated"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("applies per-agent allowlist requirements before allowing elevated", async () => { + await withTempHome(async (home) => { + const deniedRes = await getReplyFromConfig( + { + ...makeCommandMessage("/elevated on", "+1222"), + SessionKey: "agent:work:main", + }, + {}, + makeWorkElevatedAllowlistConfig(home), + ); - const text = replyText(res); - expect(text).not.toContain("elevated"); + const deniedText = replyText(deniedRes); + expect(deniedText).toContain("agents.list[].tools.elevated.allowFrom.whatsapp"); + + const allowedRes = await getReplyFromConfig( + { + ...makeCommandMessage("/elevated on", "+1333"), + SessionKey: "agent:work:main", + }, + {}, + makeWorkElevatedAllowlistConfig(home), + ); + + const allowedText = replyText(allowedRes); + expect(allowedText).toContain("Elevated mode set to ask"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("handles runtime warning, invalid level, and multi-directive elevated inputs", async () => { + await withTempHome(async (home) => { + for (const scenario of [ + { + body: "/elevated off", + config: makeAllowlistedElevatedConfig(home, { sandbox: { mode: "off" } }), + expectedSnippets: [ + "Elevated mode disabled.", + "Runtime is direct; sandboxing does not apply.", + ], + }, + { + body: "/elevated maybe", + config: makeAllowlistedElevatedConfig(home), + expectedSnippets: ["Unrecognized elevated level"], + }, + { + body: "/elevated off\n/verbose on", + config: makeAllowlistedElevatedConfig(home), + expectedSnippets: ["Elevated mode disabled.", "Verbose logging enabled."], + }, + ]) { + const res = await getReplyFromConfig( + makeCommandMessage(scenario.body), + {}, + scenario.config, + ); + const text = replyText(res); + for (const snippet of scenario.expectedSnippets) { + expect(text).toContain(snippet); + } + } expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); });