mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 22:14:34 +00:00
test: reduce trigger test redundancy and speed up model coverage
This commit is contained in:
@@ -4,7 +4,6 @@ import { join } from "node:path";
|
|||||||
import { beforeAll, describe, expect, it } from "vitest";
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
expectInlineCommandHandledAndStripped,
|
expectInlineCommandHandledAndStripped,
|
||||||
expectDirectElevatedToggleOn,
|
|
||||||
getRunEmbeddedPiAgentMock,
|
getRunEmbeddedPiAgentMock,
|
||||||
installTriggerHandlingE2eTestHooks,
|
installTriggerHandlingE2eTestHooks,
|
||||||
loadGetReplyFromConfig,
|
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 withTempHome(async (home) => {
|
||||||
await runGreetingPromptForBareNewOrReset({ home, body: "/reset", getReplyFromConfig });
|
for (const body of ["/reset", "/new"] as const) {
|
||||||
});
|
await runGreetingPromptForBareNewOrReset({ home, body, getReplyFromConfig });
|
||||||
});
|
}
|
||||||
|
|
||||||
it("runs a greeting prompt for a bare /new", async () => {
|
|
||||||
await withTempHome(async (home) => {
|
|
||||||
await runGreetingPromptForBareNewOrReset({ home, body: "/new", 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 withTempHome(async (home) => {
|
||||||
await expectInlineCommandHandledAndStripped({
|
const cases: Array<{
|
||||||
home,
|
body: string;
|
||||||
getReplyFromConfig,
|
stripToken: string;
|
||||||
body: "please /commands now",
|
blockReplyContains: string;
|
||||||
stripToken: "/commands",
|
requestOverrides?: Record<string, unknown>;
|
||||||
blockReplyContains: "Slash commands",
|
}> = [
|
||||||
});
|
{
|
||||||
});
|
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",
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
});
|
body: "please /whoami now",
|
||||||
});
|
stripToken: "/whoami",
|
||||||
|
blockReplyContains: "Identity",
|
||||||
it("handles inline /help and strips it before the agent", async () => {
|
requestOverrides: { SenderId: "12345" },
|
||||||
await withTempHome(async (home) => {
|
},
|
||||||
await expectInlineCommandHandledAndStripped({
|
{
|
||||||
home,
|
body: "please /help now",
|
||||||
getReplyFromConfig,
|
stripToken: "/help",
|
||||||
body: "please /help now",
|
blockReplyContains: "Help",
|
||||||
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 () => {
|
it("rejects elevated toggles when disabled", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = makeWhatsAppElevatedCfg(home, { elevatedEnabled: false });
|
const cfg = makeWhatsAppElevatedCfg(home, { elevatedEnabled: false });
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
||||||
import {
|
import {
|
||||||
@@ -36,42 +35,12 @@ afterAll(() => {
|
|||||||
|
|
||||||
installTriggerHandlingE2eTestHooks();
|
installTriggerHandlingE2eTestHooks();
|
||||||
|
|
||||||
const DEFAULT_SESSION_KEY = "telegram:slash:111";
|
|
||||||
const BASE_MESSAGE = {
|
const BASE_MESSAGE = {
|
||||||
Body: "hello",
|
Body: "hello",
|
||||||
From: "+1002",
|
From: "+1002",
|
||||||
To: "+2000",
|
To: "+2000",
|
||||||
} as const;
|
} 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<ReturnType<typeof getReplyFromConfig>>) {
|
|
||||||
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<ReturnType<typeof getReplyFromConfig>>) {
|
function maybeReplyText(reply: Awaited<ReturnType<typeof getReplyFromConfig>>) {
|
||||||
return Array.isArray(reply) ? reply[0]?.text : reply?.text;
|
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 <provider>");
|
|
||||||
expect(normalized).toContain("Switch: /model <provider/model>");
|
|
||||||
|
|
||||||
const store = loadSessionStore(requireSessionStorePath(cfg));
|
|
||||||
expect(store[sessionKey]?.providerOverride).toBeUndefined();
|
|
||||||
expect(store[sessionKey]?.modelOverride).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets to the default model via /model <provider/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 <provider/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 () => {
|
it("targets the active session for native /stop", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = makeCfg(home);
|
const cfg = makeCfg(home);
|
||||||
|
|||||||
@@ -301,20 +301,6 @@ export async function runDirectElevatedToggleAndLoadStore(params: {
|
|||||||
return { text, store };
|
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: {
|
export async function expectInlineCommandHandledAndStripped(params: {
|
||||||
home: string;
|
home: string;
|
||||||
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
||||||
@@ -324,6 +310,7 @@ export async function expectInlineCommandHandledAndStripped(params: {
|
|||||||
requestOverrides?: Record<string, unknown>;
|
requestOverrides?: Record<string, unknown>;
|
||||||
}) {
|
}) {
|
||||||
const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentOk();
|
const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentOk();
|
||||||
|
runEmbeddedPiAgentMock.mockClear();
|
||||||
const { blockReplies, handlers } = createBlockReplyCollector();
|
const { blockReplies, handlers } = createBlockReplyCollector();
|
||||||
const res = await params.getReplyFromConfig(
|
const res = await params.getReplyFromConfig(
|
||||||
{
|
{
|
||||||
@@ -341,7 +328,7 @@ export async function expectInlineCommandHandledAndStripped(params: {
|
|||||||
expect(blockReplies.length).toBe(1);
|
expect(blockReplies.length).toBe(1);
|
||||||
expect(blockReplies[0]?.text).toContain(params.blockReplyContains);
|
expect(blockReplies[0]?.text).toContain(params.blockReplyContains);
|
||||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
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(prompt).not.toContain(params.stripToken);
|
||||||
expect(text).toBe("ok");
|
expect(text).toBe("ok");
|
||||||
}
|
}
|
||||||
@@ -351,7 +338,9 @@ export async function runGreetingPromptForBareNewOrReset(params: {
|
|||||||
body: "/new" | "/reset";
|
body: "/new" | "/reset";
|
||||||
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
||||||
}) {
|
}) {
|
||||||
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
|
runEmbeddedPiAgentMock.mockClear();
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
payloads: [{ text: "hello" }],
|
payloads: [{ text: "hello" }],
|
||||||
meta: {
|
meta: {
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
@@ -371,8 +360,8 @@ export async function runGreetingPromptForBareNewOrReset(params: {
|
|||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("hello");
|
expect(text).toBe("hello");
|
||||||
expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce();
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
|
||||||
const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? "";
|
const prompt = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? "";
|
||||||
expect(prompt).toContain("A new session was started via /new or /reset");
|
expect(prompt).toContain("A new session was started via /new or /reset");
|
||||||
expect(prompt).toContain("Execute your Session Startup sequence now");
|
expect(prompt).toContain("Execute your Session Startup sequence now");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,75 @@ describe("/model chat UX", () => {
|
|||||||
});
|
});
|
||||||
expect(resolved.errorText).toBeUndefined();
|
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 <provider>");
|
||||||
|
});
|
||||||
|
|
||||||
|
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)", () => {
|
describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user