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 492cfdfbf..3de33b8b9 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 @@ -45,30 +45,28 @@ async function runReplyToCurrentCase(home: string, text: string) { } async function expectThinkStatusForReasoningModel(params: { + home: string; reasoning: boolean; expectedLevel: "low" | "off"; }): Promise { - await withTempHome(async (home) => { - vi.mocked(loadModelCatalog).mockResolvedValueOnce([ - { - id: "claude-opus-4-5", - name: "Opus 4.5", - provider: "anthropic", - reasoning: params.reasoning, - }, - ]); + vi.mocked(loadModelCatalog).mockResolvedValueOnce([ + { + id: "claude-opus-4-5", + name: "Opus 4.5", + provider: "anthropic", + reasoning: params.reasoning, + }, + ]); - const res = await getReplyFromConfig( - { Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true }, - {}, - makeWhatsAppDirectiveConfig(home, { model: "anthropic/claude-opus-4-5" }), - ); + const res = await getReplyFromConfig( + { Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true }, + {}, + makeWhatsAppDirectiveConfig(params.home, { model: "anthropic/claude-opus-4-5" }), + ); - const text = replyText(res); - expect(text).toContain(`Current thinking level: ${params.expectedLevel}`); - expect(text).toContain("Options: off, minimal, low, medium, high."); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); + const text = replyText(res); + expect(text).toContain(`Current thinking level: ${params.expectedLevel}`); + expect(text).toContain("Options: off, minimal, low, medium, high."); } function mockReasoningCapableCatalog() { @@ -113,60 +111,53 @@ async function runReasoningDefaultCase(params: { describe("directive behavior", () => { installDirectiveBehaviorE2EHooks(); - it("defaults /think to low for reasoning-capable models when no default set", async () => { - await expectThinkStatusForReasoningModel({ - reasoning: true, - expectedLevel: "low", - }); - }); - it("shows off when /think has no argument and model lacks reasoning", async () => { - await expectThinkStatusForReasoningModel({ - reasoning: false, - expectedLevel: "off", - }); - }); - it("aliases /model list to /models", async () => { + it("shows /think defaults for reasoning and non-reasoning models", async () => { await withTempHome(async (home) => { - const text = await runModelDirectiveText(home, "/model list"); - expect(text).toContain("Providers:"); - expect(text).toContain("- anthropic"); - expect(text).toContain("- openai"); - expect(text).toContain("Use: /models "); - expect(text).toContain("Switch: /model "); + await expectThinkStatusForReasoningModel({ + home, + reasoning: true, + expectedLevel: "low", + }); + await expectThinkStatusForReasoningModel({ + home, + reasoning: false, + expectedLevel: "off", + }); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); - it("shows current model when catalog is unavailable", async () => { + it("renders model list and status variants across catalog/config combinations", async () => { await withTempHome(async (home) => { + const aliasText = await runModelDirectiveText(home, "/model list"); + expect(aliasText).toContain("Providers:"); + expect(aliasText).toContain("- anthropic"); + expect(aliasText).toContain("- openai"); + expect(aliasText).toContain("Use: /models "); + expect(aliasText).toContain("Switch: /model "); + vi.mocked(loadModelCatalog).mockResolvedValueOnce([]); - const text = await runModelDirectiveText(home, "/model"); - expect(text).toContain("Current: anthropic/claude-opus-4-5"); - expect(text).toContain("Switch: /model "); - expect(text).toContain("Browse: /models (providers) or /models (models)"); - expect(text).toContain("More: /model status"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("lists allowlisted models on /model status", async () => { - await withTempHome(async (home) => { - const text = await runModelDirectiveText(home, "/model status", { + const unavailableCatalogText = await runModelDirectiveText(home, "/model"); + expect(unavailableCatalogText).toContain("Current: anthropic/claude-opus-4-5"); + expect(unavailableCatalogText).toContain("Switch: /model "); + expect(unavailableCatalogText).toContain( + "Browse: /models (providers) or /models (models)", + ); + expect(unavailableCatalogText).toContain("More: /model status"); + + const allowlistedStatusText = await runModelDirectiveText(home, "/model status", { includeSessionStore: false, }); - expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("openai/gpt-4.1-mini"); - expect(text).not.toContain("claude-sonnet-4-1"); - expect(text).toContain("auth:"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("includes catalog providers when no allowlist is set", async () => { - await withTempHome(async (home) => { + expect(allowlistedStatusText).toContain("anthropic/claude-opus-4-5"); + expect(allowlistedStatusText).toContain("openai/gpt-4.1-mini"); + expect(allowlistedStatusText).not.toContain("claude-sonnet-4-1"); + expect(allowlistedStatusText).toContain("auth:"); + vi.mocked(loadModelCatalog).mockResolvedValue([ { id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" }, { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" }, { id: "grok-4", name: "Grok 4", provider: "xai" }, ]); - const text = await runModelDirectiveText(home, "/model list", { + const noAllowlistText = await runModelDirectiveText(home, "/model list", { defaults: { model: { primary: "anthropic/claude-opus-4-5", @@ -176,16 +167,12 @@ describe("directive behavior", () => { models: undefined, }, }); - expect(text).toContain("Providers:"); - expect(text).toContain("- anthropic"); - expect(text).toContain("- openai"); - expect(text).toContain("- xai"); - expect(text).toContain("Use: /models "); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("lists config-only providers when catalog is present", async () => { - await withTempHome(async (home) => { + expect(noAllowlistText).toContain("Providers:"); + expect(noAllowlistText).toContain("- anthropic"); + expect(noAllowlistText).toContain("- openai"); + expect(noAllowlistText).toContain("- xai"); + expect(noAllowlistText).toContain("Use: /models "); + vi.mocked(loadModelCatalog).mockResolvedValueOnce([ { provider: "anthropic", @@ -194,7 +181,7 @@ describe("directive behavior", () => { }, { provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" }, ]); - const text = await runModelDirectiveText(home, "/models minimax", { + const configOnlyProviderText = await runModelDirectiveText(home, "/models minimax", { defaults: { models: { "anthropic/claude-opus-4-5": {}, @@ -215,22 +202,18 @@ describe("directive behavior", () => { }, }, }); - expect(text).toContain("Models (minimax"); - expect(text).toContain("minimax/MiniMax-M2.1"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("does not repeat missing auth labels on /model list", async () => { - await withTempHome(async (home) => { - const text = await runModelDirectiveText(home, "/model list", { + expect(configOnlyProviderText).toContain("Models (minimax"); + expect(configOnlyProviderText).toContain("minimax/MiniMax-M2.1"); + + const missingAuthText = await runModelDirectiveText(home, "/model list", { defaults: { models: { "anthropic/claude-opus-4-5": {}, }, }, }); - expect(text).toContain("Providers:"); - expect(text).not.toContain("missing (missing)"); + expect(missingAuthText).toContain("Providers:"); + expect(missingAuthText).not.toContain("missing (missing)"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); 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 ca691e4e2..19403fd64 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 @@ -95,43 +95,19 @@ describe("directive behavior", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); } - it("supports fuzzy model matches on /model directive", async () => { + it("supports unambiguous fuzzy model matches across /model forms", async () => { await withTempHome(async (home) => { const storePath = path.join(home, "sessions.json"); - const res = await runMoonshotModelDirective({ - home, - storePath, - body: "/model kimi", - }); - - expectMoonshotSelectionFromResponse({ response: res, storePath }); - }); - }); - it("resolves provider-less exact model ids via fuzzy matching when unambiguous", async () => { - await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - - const res = await runMoonshotModelDirective({ - home, - storePath, - body: "/model kimi-k2-0905-preview", - }); - - expectMoonshotSelectionFromResponse({ response: res, storePath }); - }); - }); - it("supports fuzzy matches within a provider on /model provider/model", async () => { - await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - - const res = await runMoonshotModelDirective({ - home, - storePath, - body: "/model moonshot/kimi", - }); - - expectMoonshotSelectionFromResponse({ response: res, storePath }); + for (const body of ["/model kimi", "/model kimi-k2-0905-preview", "/model moonshot/kimi"]) { + const res = await runMoonshotModelDirective({ + home, + storePath, + body, + }); + expectMoonshotSelectionFromResponse({ response: res, storePath }); + } + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); it("picks the best fuzzy match when multiple models match", async () => { @@ -304,7 +280,7 @@ describe("directive behavior", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); - it("queues a system event when switching models", async () => { + it("queues system events for model, elevated, and reasoning directives", async () => { await withTempHome(async (home) => { drainSystemEvents(MAIN_SESSION_KEY); await getReplyFromConfig( @@ -313,13 +289,9 @@ describe("directive behavior", () => { makeModelSwitchConfig(home), ); - const events = drainSystemEvents(MAIN_SESSION_KEY); + let events = drainSystemEvents(MAIN_SESSION_KEY); expect(events).toContain("Model switched to Opus (anthropic/claude-opus-4-5)."); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); - }); - }); - it("queues a system event when toggling elevated", async () => { - await withTempHome(async (home) => { + drainSystemEvents(MAIN_SESSION_KEY); await getReplyFromConfig( @@ -338,12 +310,9 @@ describe("directive behavior", () => { ), ); - const events = drainSystemEvents(MAIN_SESSION_KEY); + events = drainSystemEvents(MAIN_SESSION_KEY); expect(events.some((e) => e.includes("Elevated ASK"))).toBe(true); - }); - }); - it("queues a system event when toggling reasoning", async () => { - await withTempHome(async (home) => { + drainSystemEvents(MAIN_SESSION_KEY); await getReplyFromConfig( @@ -358,8 +327,9 @@ describe("directive behavior", () => { makeWhatsAppDirectiveConfig(home, { model: { primary: "openai/gpt-4.1-mini" } }), ); - const events = drainSystemEvents(MAIN_SESSION_KEY); + events = drainSystemEvents(MAIN_SESSION_KEY); expect(events.some((e) => e.includes("Reasoning STREAM"))).toBe(true); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); });