From 5196565f197ee1946d647b432af46f08b1f98a64 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Feb 2026 13:41:33 +0000 Subject: [PATCH] test: reduce trigger test redundancy and speed up model coverage --- ...ne-commands-strips-it-before-agent.test.ts | 84 ++++++++---------- ...targets-active-session-native-stop.test.ts | 87 ------------------- ....triggers.trigger-handling.test-harness.ts | 25 ++---- .../reply/directive-handling.model.test.ts | 69 +++++++++++++++ 4 files changed, 113 insertions(+), 152 deletions(-) diff --git a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts index 6072608aeb7..45fda184b47 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts @@ -4,7 +4,6 @@ import { join } from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; import { expectInlineCommandHandledAndStripped, - expectDirectElevatedToggleOn, getRunEmbeddedPiAgentMock, installTriggerHandlingE2eTestHooks, loadGetReplyFromConfig, @@ -178,15 +177,11 @@ describe("trigger handling", () => { }); }); - it("runs a greeting prompt for a bare /reset", async () => { + it("runs a greeting prompt for bare /reset and /new", async () => { await withTempHome(async (home) => { - await runGreetingPromptForBareNewOrReset({ home, body: "/reset", getReplyFromConfig }); - }); - }); - - it("runs a greeting prompt for a bare /new", async () => { - await withTempHome(async (home) => { - await runGreetingPromptForBareNewOrReset({ home, body: "/new", getReplyFromConfig }); + for (const body of ["/reset", "/new"] as const) { + await runGreetingPromptForBareNewOrReset({ home, body, getReplyFromConfig }); + } }); }); @@ -201,42 +196,41 @@ describe("trigger handling", () => { }); }); - it("handles inline /commands and strips it before the agent", async () => { + it("handles inline help/whoami/commands and strips directives before the agent", async () => { await withTempHome(async (home) => { - await expectInlineCommandHandledAndStripped({ - home, - getReplyFromConfig, - body: "please /commands now", - stripToken: "/commands", - blockReplyContains: "Slash commands", - }); - }); - }); - - it("handles inline /whoami and strips it before the agent", async () => { - await withTempHome(async (home) => { - await expectInlineCommandHandledAndStripped({ - home, - getReplyFromConfig, - body: "please /whoami now", - stripToken: "/whoami", - blockReplyContains: "Identity", - requestOverrides: { - SenderId: "12345", + const cases: Array<{ + body: string; + stripToken: string; + blockReplyContains: string; + requestOverrides?: Record; + }> = [ + { + body: "please /commands now", + stripToken: "/commands", + blockReplyContains: "Slash commands", }, - }); - }); - }); - - it("handles inline /help and strips it before the agent", async () => { - await withTempHome(async (home) => { - await expectInlineCommandHandledAndStripped({ - home, - getReplyFromConfig, - body: "please /help now", - stripToken: "/help", - blockReplyContains: "Help", - }); + { + body: "please /whoami now", + stripToken: "/whoami", + blockReplyContains: "Identity", + requestOverrides: { SenderId: "12345" }, + }, + { + body: "please /help now", + stripToken: "/help", + blockReplyContains: "Help", + }, + ]; + for (const testCase of cases) { + await expectInlineCommandHandledAndStripped({ + home, + getReplyFromConfig, + body: testCase.body, + stripToken: testCase.stripToken, + blockReplyContains: testCase.blockReplyContains, + requestOverrides: testCase.requestOverrides, + }); + } }); }); @@ -311,10 +305,6 @@ describe("trigger handling", () => { }); }); - it("allows approved sender to toggle elevated mode", async () => { - await expectDirectElevatedToggleOn({ getReplyFromConfig }); - }); - it("rejects elevated toggles when disabled", async () => { await withTempHome(async (home) => { const cfg = makeWhatsAppElevatedCfg(home, { elevatedEnabled: false }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts index 61b9be19d43..4c40064b269 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { normalizeTestText } from "../../test/helpers/normalize-text.js"; import type { OpenClawConfig } from "../config/config.js"; import { loadSessionStore, resolveSessionKey } from "../config/sessions.js"; import { @@ -36,42 +35,12 @@ afterAll(() => { installTriggerHandlingE2eTestHooks(); -const DEFAULT_SESSION_KEY = "telegram:slash:111"; const BASE_MESSAGE = { Body: "hello", From: "+1002", To: "+2000", } as const; -function makeTelegramModelCommand(body: string, sessionKey = DEFAULT_SESSION_KEY) { - return { - Body: body, - From: "telegram:111", - To: "telegram:111", - ChatType: "direct" as const, - Provider: "telegram" as const, - Surface: "telegram" as const, - SessionKey: sessionKey, - CommandAuthorized: true, - }; -} - -function firstReplyText(reply: Awaited>) { - return Array.isArray(reply) ? (reply[0]?.text ?? "") : (reply?.text ?? ""); -} - -async function runModelCommand(home: string, body: string, sessionKey = DEFAULT_SESSION_KEY) { - const cfg = makeCfg(home); - const res = await getReplyFromConfig(makeTelegramModelCommand(body, sessionKey), {}, cfg); - const text = firstReplyText(res); - return { - cfg, - sessionKey, - text, - normalized: normalizeTestText(text), - }; -} - function maybeReplyText(reply: Awaited>) { return Array.isArray(reply) ? reply[0]?.text : reply?.text; } @@ -326,62 +295,6 @@ describe("trigger handling", () => { }); }); - it("selects the exact provider/model pair for openrouter", async () => { - await withTempHome(async (home) => { - const { cfg, sessionKey, normalized } = await runModelCommand( - home, - "/model openrouter/anthropic/claude-opus-4-5", - ); - - expect(normalized).toContain("Model set to openrouter/anthropic/claude-opus-4-5"); - - const store = loadSessionStore(requireSessionStorePath(cfg)); - expect(store[sessionKey]?.providerOverride).toBe("openrouter"); - expect(store[sessionKey]?.modelOverride).toBe("anthropic/claude-opus-4-5"); - }); - }); - - it("rejects invalid /model <#> selections", async () => { - await withTempHome(async (home) => { - const { cfg, sessionKey, normalized } = await runModelCommand(home, "/model 99"); - - expect(normalized).toContain("Numeric model selection is not supported in chat."); - expect(normalized).toContain("Browse: /models or /models "); - expect(normalized).toContain("Switch: /model "); - - const store = loadSessionStore(requireSessionStorePath(cfg)); - expect(store[sessionKey]?.providerOverride).toBeUndefined(); - expect(store[sessionKey]?.modelOverride).toBeUndefined(); - }); - }); - - it("resets to the default model via /model ", async () => { - await withTempHome(async (home) => { - const { cfg, sessionKey, normalized } = await runModelCommand( - home, - "/model anthropic/claude-opus-4-5", - ); - - expect(normalized).toContain("Model reset to default (anthropic/claude-opus-4-5)"); - - const store = loadSessionStore(requireSessionStorePath(cfg)); - expect(store[sessionKey]?.providerOverride).toBeUndefined(); - expect(store[sessionKey]?.modelOverride).toBeUndefined(); - }); - }); - - it("selects a model via /model ", async () => { - await withTempHome(async (home) => { - const { cfg, sessionKey, normalized } = await runModelCommand(home, "/model openai/gpt-5.2"); - - expect(normalized).toContain("Model set to openai/gpt-5.2"); - - const store = loadSessionStore(requireSessionStorePath(cfg)); - expect(store[sessionKey]?.providerOverride).toBe("openai"); - expect(store[sessionKey]?.modelOverride).toBe("gpt-5.2"); - }); - }); - it("targets the active session for native /stop", async () => { await withTempHome(async (home) => { const cfg = makeCfg(home); diff --git a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts b/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts index a0d538a501b..2d567de6ea8 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts @@ -301,20 +301,6 @@ export async function runDirectElevatedToggleAndLoadStore(params: { return { text, store }; } -export async function expectDirectElevatedToggleOn(params: { - getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; -}) { - await withTempHome(async (home) => { - const cfg = makeWhatsAppElevatedCfg(home); - const { text, store } = await runDirectElevatedToggleAndLoadStore({ - cfg, - getReplyFromConfig: params.getReplyFromConfig, - }); - expect(text).toContain("Elevated mode set to ask"); - expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on"); - }); -} - export async function expectInlineCommandHandledAndStripped(params: { home: string; getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; @@ -324,6 +310,7 @@ export async function expectInlineCommandHandledAndStripped(params: { requestOverrides?: Record; }) { const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentOk(); + runEmbeddedPiAgentMock.mockClear(); const { blockReplies, handlers } = createBlockReplyCollector(); const res = await params.getReplyFromConfig( { @@ -341,7 +328,7 @@ export async function expectInlineCommandHandledAndStripped(params: { expect(blockReplies.length).toBe(1); expect(blockReplies[0]?.text).toContain(params.blockReplyContains); expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); - const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? ""; expect(prompt).not.toContain(params.stripToken); expect(text).toBe("ok"); } @@ -351,7 +338,9 @@ export async function runGreetingPromptForBareNewOrReset(params: { body: "/new" | "/reset"; getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; }) { - getRunEmbeddedPiAgentMock().mockResolvedValue({ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockClear(); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "hello" }], meta: { durationMs: 1, @@ -371,8 +360,8 @@ export async function runGreetingPromptForBareNewOrReset(params: { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("hello"); - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? ""; + expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + const prompt = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? ""; expect(prompt).toContain("A new session was started via /new or /reset"); expect(prompt).toContain("Execute your Session Startup sequence now"); } diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index 9e47d5dffcc..d8e198184e0 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -112,6 +112,75 @@ describe("/model chat UX", () => { }); expect(resolved.errorText).toBeUndefined(); }); + + it("rejects numeric /model selections with a guided error", () => { + const directives = parseInlineDirectives("/model 99"); + const cfg = { commands: { text: true } } as unknown as OpenClawConfig; + + const resolved = resolveModelSelectionFromDirective({ + directives, + cfg, + agentDir: "/tmp/agent", + defaultProvider: "anthropic", + defaultModel: "claude-opus-4-5", + aliasIndex: baseAliasIndex(), + allowedModelKeys: new Set(["anthropic/claude-opus-4-5", "openai/gpt-4o"]), + allowedModelCatalog: [], + provider: "anthropic", + }); + + expect(resolved.modelSelection).toBeUndefined(); + expect(resolved.errorText).toContain("Numeric model selection is not supported in chat."); + expect(resolved.errorText).toContain("Browse: /models or /models "); + }); + + it("treats explicit default /model selection as resettable default", () => { + const directives = parseInlineDirectives("/model anthropic/claude-opus-4-5"); + const cfg = { commands: { text: true } } as unknown as OpenClawConfig; + + const resolved = resolveModelSelectionFromDirective({ + directives, + cfg, + agentDir: "/tmp/agent", + defaultProvider: "anthropic", + defaultModel: "claude-opus-4-5", + aliasIndex: baseAliasIndex(), + allowedModelKeys: new Set(["anthropic/claude-opus-4-5", "openai/gpt-4o"]), + allowedModelCatalog: [], + provider: "anthropic", + }); + + expect(resolved.errorText).toBeUndefined(); + expect(resolved.modelSelection).toEqual({ + provider: "anthropic", + model: "claude-opus-4-5", + isDefault: true, + }); + }); + + it("keeps openrouter provider/model split for exact selections", () => { + const directives = parseInlineDirectives("/model openrouter/anthropic/claude-opus-4-5"); + const cfg = { commands: { text: true } } as unknown as OpenClawConfig; + + const resolved = resolveModelSelectionFromDirective({ + directives, + cfg, + agentDir: "/tmp/agent", + defaultProvider: "anthropic", + defaultModel: "claude-opus-4-5", + aliasIndex: baseAliasIndex(), + allowedModelKeys: new Set(["openrouter/anthropic/claude-opus-4-5"]), + allowedModelCatalog: [], + provider: "anthropic", + }); + + expect(resolved.errorText).toBeUndefined(); + expect(resolved.modelSelection).toEqual({ + provider: "openrouter", + model: "anthropic/claude-opus-4-5", + isDefault: false, + }); + }); }); describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => {