test(auto-reply): move directive event coverage lower

This commit is contained in:
Peter Steinberger
2026-04-11 02:13:50 +01:00
parent 48a66a647d
commit 54cb10e79a
2 changed files with 104 additions and 110 deletions

View File

@@ -1,16 +1,11 @@
import "./reply.directive.directive-behavior.e2e-mocks.js";
import fs from "node:fs/promises";
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 type { ModelDefinitionConfig } from "../config/types.models.js";
import { drainSystemEvents } from "../infra/system-events.js";
import {
assertModelSelection,
installDirectiveBehaviorE2EHooks,
MAIN_SESSION_KEY,
makeWhatsAppDirectiveConfig,
replyText,
sessionStorePath,
withTempHome,
@@ -31,18 +26,6 @@ function makeModelDefinition(id: string, name: string): ModelDefinitionConfig {
};
}
function makeModelSwitchConfig(home: string) {
return withFullRuntimeReplyConfig(
makeWhatsAppDirectiveConfig(home, {
model: { primary: "openai/gpt-4.1-mini" },
models: {
"openai/gpt-4.1-mini": {},
"anthropic/claude-opus-4-6": { alias: "Opus" },
},
}),
);
}
function makeMoonshotConfig(home: string, storePath: string) {
return withFullRuntimeReplyConfig({
agents: {
@@ -251,97 +234,4 @@ describe("directive behavior", () => {
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
});
});
it("stores auth profile overrides on /model directive", async () => {
await withTempHome(async (home) => {
const storePath = sessionStorePath(home);
const authDir = path.join(home, ".openclaw", "agents", "main", "agent");
await fs.mkdir(authDir, { recursive: true, mode: 0o700 });
await fs.writeFile(
path.join(authDir, "auth-profiles.json"),
JSON.stringify(
{
version: 1,
profiles: {
"anthropic:work": {
type: "api_key",
provider: "anthropic",
key: "sk-test-1234567890",
},
},
},
null,
2,
),
);
const res = await getReplyFromConfig(
{ Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
makeModelSwitchConfig(home),
);
const text = replyText(res);
expect(text).toContain("Auth profile set to anthropic:work");
const store = loadSessionStore(storePath);
const entry = store["agent:main:main"];
expect(entry.authProfileOverride).toBe("anthropic:work");
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
});
});
it("queues system events for model, elevated, and reasoning directives", async () => {
await withTempHome(async (home) => {
drainSystemEvents(MAIN_SESSION_KEY);
await getReplyFromConfig(
{ Body: "/model Opus", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
makeModelSwitchConfig(home),
);
let events = drainSystemEvents(MAIN_SESSION_KEY);
expect(events).toContain("Model switched to Opus (anthropic/claude-opus-4-6).");
drainSystemEvents(MAIN_SESSION_KEY);
await getReplyFromConfig(
{
Body: "/elevated on",
From: "+1222",
To: "+1222",
Provider: "whatsapp",
CommandAuthorized: true,
},
{},
withFullRuntimeReplyConfig(
makeWhatsAppDirectiveConfig(
home,
{ model: { primary: "openai/gpt-4.1-mini" } },
{ tools: { elevated: { allowFrom: { whatsapp: ["*"] } } } },
),
),
);
events = drainSystemEvents(MAIN_SESSION_KEY);
expect(events.some((e) => e.includes("Elevated ASK"))).toBe(true);
drainSystemEvents(MAIN_SESSION_KEY);
await getReplyFromConfig(
{
Body: "/reasoning stream",
From: "+1222",
To: "+1222",
Provider: "whatsapp",
CommandAuthorized: true,
},
{},
withFullRuntimeReplyConfig(
makeWhatsAppDirectiveConfig(home, { model: { primary: "openai/gpt-4.1-mini" } }),
),
);
events = drainSystemEvents(MAIN_SESSION_KEY);
expect(events.some((e) => e.includes("Reasoning STREAM"))).toBe(true);
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -32,6 +32,7 @@ import {
import type { ModelAliasIndex } from "../../agents/model-selection.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import type { ElevatedLevel } from "../thinking.js";
import { handleDirectiveOnly } from "./directive-handling.impl.js";
import {
@@ -119,6 +120,7 @@ beforeEach(() => {
]);
vi.mocked(resolveAgentDir).mockReset().mockReturnValue(TEST_AGENT_DIR);
vi.mocked(resolveSessionAgentId).mockReset().mockReturnValue("main");
vi.mocked(enqueueSystemEvent).mockClear();
liveModelSwitchMocks.requestLiveSessionModelSwitch.mockReset().mockReturnValue(false);
queueMocks.refreshQueuedFollowupSession.mockReset();
});
@@ -153,6 +155,21 @@ function createGptAliasIndex(): ModelAliasIndex {
};
}
function createOpusAliasIndex(): ModelAliasIndex {
return {
byAlias: new Map([
[
"opus",
{
alias: "Opus",
ref: { provider: "anthropic", model: "claude-opus-4-6" },
},
],
]),
byKey: new Map([["anthropic/claude-opus-4-6", ["Opus"]]]),
};
}
function resolveModelSelectionForCommand(params: {
command: string;
allowedModelKeys: Set<string>;
@@ -685,6 +702,55 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => {
});
});
it("persists auth profile overrides for alias model directives", async () => {
setAuthProfiles({
"anthropic:work": {
type: "api_key",
provider: "anthropic",
key: "sk-test",
},
});
const sessionEntry = createSessionEntry();
const sessionStore = { [sessionKey]: sessionEntry };
const result = await handleDirectiveOnly(
createHandleParams({
directives: parseInlineDirectives("/model Opus@anthropic:work"),
aliasIndex: createOpusAliasIndex(),
defaultProvider: "openai",
defaultModel: "gpt-4o",
provider: "openai",
model: "gpt-4o",
initialModelLabel: "openai/gpt-4o",
sessionEntry,
sessionStore,
formatModelSwitchEvent: (label, alias) =>
alias ? `Model switched to ${alias} (${label}).` : `Model switched to ${label}.`,
}),
);
expect(result?.text).toContain("Model set to Opus (anthropic/claude-opus-4-6).");
expect(result?.text).toContain("Auth profile set to anthropic:work.");
expect(sessionEntry.providerOverride).toBe("anthropic");
expect(sessionEntry.modelOverride).toBe("claude-opus-4-6");
expect(sessionEntry.authProfileOverride).toBe("anthropic:work");
expect(sessionEntry.authProfileOverrideSource).toBe("user");
expect(queueMocks.refreshQueuedFollowupSession).toHaveBeenCalledWith({
key: sessionKey,
nextProvider: "anthropic",
nextModel: "claude-opus-4-6",
nextAuthProfileId: "anthropic:work",
nextAuthProfileIdSource: "user",
});
expect(enqueueSystemEvent).toHaveBeenCalledWith(
"Model switched to Opus (anthropic/claude-opus-4-6).",
{
sessionKey,
contextKey: "model:anthropic/claude-opus-4-6",
},
);
});
it("shows no model message when no /model directive", async () => {
const directives = parseInlineDirectives("hello world");
const sessionEntry = createSessionEntry();
@@ -799,6 +865,44 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => {
expect(sessionEntry.elevatedLevel).toBe("off");
});
it("queues system events for elevated and reasoning mode directives", async () => {
const sessionEntry = createSessionEntry();
const sessionStore = { [sessionKey]: sessionEntry };
await handleDirectiveOnly(
createHandleParams({
directives: parseInlineDirectives("/elevated on"),
elevatedAllowed: true,
elevatedEnabled: true,
sessionEntry,
sessionStore,
}),
);
expect(enqueueSystemEvent).toHaveBeenCalledWith(
"Elevated ASK - exec runs on host; approvals may still apply.",
{
sessionKey,
contextKey: "mode:elevated",
},
);
vi.mocked(enqueueSystemEvent).mockClear();
await handleDirectiveOnly(
createHandleParams({
directives: parseInlineDirectives("/reasoning stream"),
sessionEntry,
sessionStore,
}),
);
expect(enqueueSystemEvent).toHaveBeenCalledWith("Reasoning STREAM - emit live <think>.", {
sessionKey,
contextKey: "mode:reasoning",
});
});
it("blocks internal operator.write exec persistence in directive-only handling", async () => {
const directives = parseInlineDirectives(
"/exec host=node security=allowlist ask=always node=worker-1",