Files
openclaw/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts
2026-04-08 09:58:22 +01:00

146 lines
5.5 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginHookReplyDispatchResult } from "../../plugins/hooks.js";
import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js";
import {
acpManagerRuntimeMocks,
acpMocks,
agentEventMocks,
createDispatcher,
createHookCtx,
diagnosticMocks,
emptyConfig,
hookMocks,
internalHookMocks,
mocks,
resetPluginTtsAndThreadMocks,
sessionBindingMocks,
sessionStoreMocks,
setDiscordTestRegistry,
} from "./dispatch-from-config.shared.test-harness.js";
let dispatchReplyFromConfig: typeof import("./dispatch-from-config.js").dispatchReplyFromConfig;
let resetInboundDedupe: typeof import("./inbound-dedupe.js").resetInboundDedupe;
describe("dispatchReplyFromConfig reply_dispatch hook", () => {
beforeAll(async () => {
({ dispatchReplyFromConfig } = await import("./dispatch-from-config.js"));
({ resetInboundDedupe } = await import("./inbound-dedupe.js"));
});
beforeEach(() => {
setDiscordTestRegistry();
resetInboundDedupe();
mocks.routeReply.mockReset().mockResolvedValue({ ok: true, messageId: "mock" });
mocks.tryFastAbortFromMessage.mockReset().mockResolvedValue({
handled: false,
aborted: false,
});
hookMocks.runner.hasHooks.mockReset();
hookMocks.runner.hasHooks.mockImplementation(
(hookName?: string) => hookName === "reply_dispatch",
);
hookMocks.runner.runInboundClaim.mockReset().mockResolvedValue(undefined);
hookMocks.runner.runInboundClaimForPlugin.mockReset().mockResolvedValue(undefined);
hookMocks.runner.runInboundClaimForPluginOutcome.mockReset().mockResolvedValue({
status: "no_handler",
});
hookMocks.runner.runMessageReceived.mockReset().mockResolvedValue(undefined);
hookMocks.runner.runBeforeDispatch.mockReset().mockResolvedValue(undefined);
hookMocks.runner.runReplyDispatch.mockReset().mockResolvedValue(undefined);
internalHookMocks.createInternalHookEvent.mockReset();
internalHookMocks.createInternalHookEvent.mockImplementation(createInternalHookEventPayload);
internalHookMocks.triggerInternalHook.mockReset().mockResolvedValue(undefined);
acpMocks.listAcpSessionEntries.mockReset().mockResolvedValue([]);
acpMocks.readAcpSessionEntry.mockReset().mockReturnValue(null);
acpMocks.upsertAcpSessionMeta.mockReset().mockResolvedValue(null);
acpMocks.requireAcpRuntimeBackend.mockReset();
sessionBindingMocks.listBySession.mockReset().mockReturnValue([]);
sessionBindingMocks.resolveByConversation.mockReset().mockReturnValue(null);
sessionBindingMocks.touch.mockReset();
sessionStoreMocks.currentEntry = undefined;
sessionStoreMocks.loadSessionStore.mockReset().mockReturnValue({});
sessionStoreMocks.resolveStorePath.mockReset().mockReturnValue("/tmp/mock-sessions.json");
sessionStoreMocks.resolveSessionStoreEntry.mockReset().mockReturnValue({ existing: undefined });
acpManagerRuntimeMocks.getAcpSessionManager.mockReset();
acpManagerRuntimeMocks.getAcpSessionManager.mockImplementation(() => ({
resolveSession: () => ({ kind: "none" as const }),
getObservabilitySnapshot: () => ({
runtimeCache: { activeSessions: 0, idleTtlMs: 0, evictedTotal: 0 },
turns: {
active: 0,
queueDepth: 0,
completed: 0,
failed: 0,
averageLatencyMs: 0,
maxLatencyMs: 0,
},
errorsByCode: {},
}),
runTurn: vi.fn(),
}));
agentEventMocks.emitAgentEvent.mockReset();
agentEventMocks.onAgentEvent.mockReset().mockImplementation(() => () => {});
diagnosticMocks.logMessageQueued.mockReset();
diagnosticMocks.logMessageProcessed.mockReset();
diagnosticMocks.logSessionStateChange.mockReset();
resetPluginTtsAndThreadMocks();
});
it("returns handled dispatch results from plugins", async () => {
hookMocks.runner.runReplyDispatch.mockResolvedValue({
handled: true,
queuedFinal: true,
counts: { tool: 1, block: 2, final: 3 },
});
const result = await dispatchReplyFromConfig({
ctx: createHookCtx(),
cfg: emptyConfig,
dispatcher: createDispatcher(),
fastAbortResolver: async () => ({ handled: false, aborted: false }),
formatAbortReplyTextResolver: () => "⚙️ Agent was aborted.",
replyResolver: async () => ({ text: "model reply" }),
});
expect(hookMocks.runner.runReplyDispatch).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "agent:test:session",
sendPolicy: "allow",
inboundAudio: false,
}),
expect.objectContaining({
cfg: emptyConfig,
}),
);
expect(result).toEqual({
queuedFinal: true,
counts: { tool: 1, block: 2, final: 3 },
});
});
it("still applies send-policy deny after an unhandled plugin dispatch", async () => {
hookMocks.runner.runReplyDispatch.mockResolvedValue({
handled: false,
queuedFinal: false,
counts: { tool: 0, block: 0, final: 0 },
} satisfies PluginHookReplyDispatchResult);
const result = await dispatchReplyFromConfig({
ctx: createHookCtx(),
cfg: {
...emptyConfig,
session: {
sendPolicy: { default: "deny" },
},
},
dispatcher: createDispatcher(),
replyResolver: async () => ({ text: "model reply" }),
});
expect(hookMocks.runner.runReplyDispatch).toHaveBeenCalled();
expect(result).toEqual({
queuedFinal: false,
counts: { tool: 0, block: 0, final: 0 },
});
});
});