test(auto-reply): move mixed reasoning coverage to directive seam

This commit is contained in:
Peter Steinberger
2026-04-07 12:56:11 +01:00
parent 9d358d557d
commit c084630f9e
2 changed files with 123 additions and 68 deletions

View File

@@ -20,8 +20,6 @@ import {
} from "./reply.directive.directive-behavior.e2e-mocks.js";
let getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
let actualRunPreparedReply: typeof import("./reply/get-reply-run.js").runPreparedReply;
const runPreparedReplyMock = vi.hoisted(() => vi.fn());
async function writeSkill(params: { workspaceDir: string; name: string; description: string }) {
const { workspaceDir, name, description } = params;
@@ -60,36 +58,6 @@ async function runThinkDirectiveAndGetText(home: string): Promise<string | undef
return replyText(res);
}
async function runInlineReasoningMessage(params: {
home: string;
body: string;
storePath: string;
blockReplies: string[];
}) {
return await getReplyFromConfig(
{
Body: params.body,
From: "+1222",
To: "+1222",
Provider: "whatsapp",
},
{
onBlockReply: (payload) => {
if (payload.text) {
params.blockReplies.push(payload.text);
}
},
},
makeWhatsAppDirectiveConfig(
params.home,
{ model: "anthropic/claude-opus-4-6" },
{
session: { store: params.storePath },
},
),
);
}
function makeRunConfig(home: string, storePath: string) {
return makeWhatsAppDirectiveConfig(
home,
@@ -153,45 +121,10 @@ describe("directive behavior", () => {
vi.resetModules();
loadModelCatalogMock.mockReset();
loadModelCatalogMock.mockResolvedValue(DEFAULT_TEST_MODEL_CATALOG);
installFreshDirectiveBehaviorReplyMocks({
onActualRunPreparedReply: (runPreparedReply) => {
actualRunPreparedReply = runPreparedReply;
},
runPreparedReply: (...args) => runPreparedReplyMock(...args),
});
installFreshDirectiveBehaviorReplyMocks();
({ getReplyFromConfig } = await import("./reply.js"));
runPreparedReplyMock.mockReset();
runPreparedReplyMock.mockImplementation((...args: Parameters<typeof actualRunPreparedReply>) =>
actualRunPreparedReply(...args),
);
});
it("keeps reasoning acks out of mixed messages, including rapid repeats", async () => {
await withTempHome(async (home) => {
runPreparedReplyMock.mockResolvedValue({ text: "done" });
const blockReplies: string[] = [];
const storePath = sessionStorePath(home);
const firstRes = await runInlineReasoningMessage({
home,
body: "please reply\n/reasoning on",
storePath,
blockReplies,
});
expect(replyTexts(firstRes)).toContain("done");
await runInlineReasoningMessage({
home,
body: "again\n/reasoning on",
storePath,
blockReplies,
});
expect(runPreparedReplyMock).toHaveBeenCalledTimes(2);
expect(blockReplies.length).toBe(0);
});
});
it("handles standalone verbose directives and persistence", async () => {
await withTempHome(async (home) => {
const storePath = sessionStorePath(home);

View File

@@ -0,0 +1,122 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { applyInlineDirectivesFastLane } from "./directive-handling.fast-lane.js";
import { parseInlineDirectives } from "./directive-handling.parse.js";
import { persistInlineDirectives } from "./directive-handling.persist.js";
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentConfig: vi.fn(() => ({})),
resolveAgentDir: vi.fn(() => "/tmp/agent"),
resolveSessionAgentId: vi.fn(() => "main"),
resolveDefaultAgentId: vi.fn(() => "main"),
}));
vi.mock("../../agents/sandbox.js", () => ({
resolveSandboxRuntimeStatus: vi.fn(() => ({ sandboxed: false })),
}));
vi.mock("../../config/sessions/store.js", () => ({
updateSessionStore: vi.fn(async () => {}),
}));
vi.mock("../../infra/system-events.js", () => ({
enqueueSystemEvent: vi.fn(),
}));
vi.mock("./queue.js", () => ({
refreshQueuedFollowupSession: vi.fn(),
}));
function createSessionEntry(overrides?: Partial<SessionEntry>): SessionEntry {
return {
sessionId: "session-1",
updatedAt: Date.now(),
...overrides,
};
}
function createConfig(): OpenClawConfig {
return {
commands: { text: true },
agents: { defaults: {} },
} as unknown as OpenClawConfig;
}
describe("mixed inline directives", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("emits directive ack while persisting inline reasoning in mixed messages", async () => {
const directives = parseInlineDirectives("please reply\n/reasoning on");
const cfg = createConfig();
const sessionEntry = createSessionEntry();
const sessionStore = { "agent:main:dm:1": sessionEntry };
const fastLane = await applyInlineDirectivesFastLane({
directives,
commandAuthorized: true,
ctx: { Surface: "whatsapp" } as never,
cfg,
agentId: "main",
isGroup: false,
sessionEntry,
sessionStore,
sessionKey: "agent:main:dm:1",
storePath: undefined,
elevatedEnabled: false,
elevatedAllowed: false,
elevatedFailures: [],
messageProviderKey: "whatsapp",
defaultProvider: "anthropic",
defaultModel: "claude-opus-4-6",
aliasIndex: { byAlias: new Map(), byKey: new Map() },
allowedModelKeys: new Set(),
allowedModelCatalog: [],
resetModelOverride: false,
provider: "anthropic",
model: "claude-opus-4-6",
initialModelLabel: "anthropic/claude-opus-4-6",
formatModelSwitchEvent: (label) => label,
agentCfg: cfg.agents?.defaults,
modelState: {
resolveDefaultThinkingLevel: async () => "off",
allowedModelKeys: new Set(),
allowedModelCatalog: [],
resetModelOverride: false,
},
});
expect(fastLane.directiveAck).toEqual({
text: "⚙️ Reasoning visibility enabled.",
});
const persisted = await persistInlineDirectives({
directives,
cfg,
sessionEntry,
sessionStore,
sessionKey: "agent:main:dm:1",
storePath: undefined,
elevatedEnabled: false,
elevatedAllowed: false,
defaultProvider: "anthropic",
defaultModel: "claude-opus-4-6",
aliasIndex: { byAlias: new Map(), byKey: new Map() },
allowedModelKeys: new Set(),
provider: "anthropic",
model: "claude-opus-4-6",
initialModelLabel: "anthropic/claude-opus-4-6",
formatModelSwitchEvent: (label) => label,
agentCfg: cfg.agents?.defaults,
messageProvider: "whatsapp",
surface: "whatsapp",
gatewayClientScopes: [],
});
expect(sessionEntry.reasoningLevel).toBe("on");
expect(persisted.provider).toBe("anthropic");
expect(persisted.model).toBe("claude-opus-4-6");
});
});