mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
146 lines
5.5 KiB
TypeScript
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 },
|
|
});
|
|
});
|
|
});
|