From 1603577dfdd16a61efa24838adec8a283d415151 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 19:53:41 +0100 Subject: [PATCH] test: share get-reply hook fixtures --- .../get-reply.before-agent-reply.test.ts | 127 ++++-------------- .../reply/get-reply.message-hooks.test.ts | 53 +++----- .../get-reply.reset-hooks-fallback.test.ts | 120 ++++------------- .../reply/get-reply.test-fixtures.ts | 103 +++++++++++++- 4 files changed, 172 insertions(+), 231 deletions(-) diff --git a/src/auto-reply/reply/get-reply.before-agent-reply.test.ts b/src/auto-reply/reply/get-reply.before-agent-reply.test.ts index 704b80972c6..e786bab89ec 100644 --- a/src/auto-reply/reply/get-reply.before-agent-reply.test.ts +++ b/src/auto-reply/reply/get-reply.before-agent-reply.test.ts @@ -1,7 +1,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { HookRunner } from "../../plugins/hooks.js"; -import type { MsgContext } from "../templating.js"; import { SILENT_REPLY_TOKEN } from "../tokens.js"; +import { + buildGetReplyGroupCtx, + createGetReplyContinueDirectivesResult, + createGetReplySessionState, + registerGetReplyRuntimeOverrides, +} from "./get-reply.test-fixtures.js"; import { loadGetReplyModuleForTest } from "./get-reply.test-loader.js"; import "./get-reply.test-runtime-mocks.js"; @@ -20,15 +25,7 @@ vi.mock("../../plugins/hook-runner-global.js", () => ({ runBeforeAgentReply: mocks.runBeforeAgentReply, }) as unknown as HookRunner, })); -vi.mock("./get-reply-directives.js", () => ({ - resolveReplyDirectives: (...args: unknown[]) => mocks.resolveReplyDirectives(...args), -})); -vi.mock("./get-reply-inline-actions.js", () => ({ - handleInlineActions: (...args: unknown[]) => mocks.handleInlineActions(...args), -})); -vi.mock("./session.js", () => ({ - initSessionState: (...args: unknown[]) => mocks.initSessionState(...args), -})); +registerGetReplyRuntimeOverrides(mocks); let getReplyFromConfig: typeof import("./get-reply.js").getReplyFromConfig; @@ -36,74 +33,17 @@ async function loadGetReplyRuntimeForTest() { ({ getReplyFromConfig } = await loadGetReplyModuleForTest({ cacheKey: import.meta.url })); } -function buildCtx(overrides: Partial = {}): MsgContext { - return { - Provider: "telegram", - Surface: "telegram", - OriginatingChannel: "telegram", - OriginatingTo: "telegram:-100123", - ChatType: "group", - Body: "hello world", - BodyForAgent: "hello world", - RawBody: "hello world", - CommandBody: "hello world", - BodyForCommands: "hello world", - SessionKey: "agent:main:telegram:-100123", - From: "telegram:user:42", - To: "telegram:-100123", - Timestamp: 1710000000000, - ...overrides, - }; -} - function createContinueDirectivesResult() { - return { - kind: "continue" as const, - result: { - commandSource: "text", - command: { - surface: "telegram", - channel: "telegram", - channelId: "telegram", - ownerList: [], - senderIsOwner: false, - isAuthorizedSender: true, - senderId: "42", - abortKey: "agent:main:telegram:-100123", - rawBodyNormalized: "hello world", - commandBodyNormalized: "hello world", - from: "telegram:user:42", - to: "telegram:-100123", - resetHookTriggered: false, - }, - allowTextCommands: true, - skillCommands: [], - directives: {}, - cleanedBody: "hello world", - elevatedEnabled: false, - elevatedAllowed: false, - elevatedFailures: [], - defaultActivation: "always", - resolvedThinkLevel: undefined, - resolvedVerboseLevel: "off", - resolvedReasoningLevel: "off", - resolvedElevatedLevel: "off", - execOverrides: undefined, - blockStreamingEnabled: false, - blockReplyChunking: undefined, - resolvedBlockStreamingBreak: undefined, - provider: "openai", - model: "gpt-4o-mini", - modelState: { - resolveDefaultThinkingLevel: async () => undefined, - }, - contextTokens: 0, - inlineStatusRequested: false, - directiveAck: undefined, - perMessageQueueMode: undefined, - perMessageQueueOptions: undefined, - }, - }; + return createGetReplyContinueDirectivesResult({ + body: "hello world", + abortKey: "agent:main:telegram:-100123", + from: "telegram:user:42", + to: "telegram:-100123", + senderId: "42", + commandSource: "text", + senderIsOwner: false, + resetHookTriggered: false, + }); } describe("getReplyFromConfig before_agent_reply wiring", () => { @@ -116,27 +56,16 @@ describe("getReplyFromConfig before_agent_reply wiring", () => { mocks.hasHooks.mockReset(); mocks.runBeforeAgentReply.mockReset(); - mocks.initSessionState.mockResolvedValue({ - sessionCtx: buildCtx({ - OriginatingChannel: "Telegram", - Provider: "telegram", + mocks.initSessionState.mockResolvedValue( + createGetReplySessionState({ + sessionCtx: buildGetReplyGroupCtx({ OriginatingChannel: "Telegram", Provider: "telegram" }), + sessionKey: "agent:main:telegram:-100123", + sessionScope: "per-chat", + isGroup: true, + triggerBodyNormalized: "hello world", + bodyStripped: "hello world", }), - sessionEntry: {}, - previousSessionEntry: {}, - sessionStore: {}, - sessionKey: "agent:main:telegram:-100123", - sessionId: "session-1", - isNewSession: false, - resetTriggered: false, - systemSent: false, - abortedLastRun: false, - storePath: "/tmp/sessions.json", - sessionScope: "per-chat", - groupResolution: undefined, - isGroup: true, - triggerBodyNormalized: "hello world", - bodyStripped: "hello world", - }); + ); mocks.resolveReplyDirectives.mockResolvedValue(createContinueDirectivesResult()); mocks.handleInlineActions.mockResolvedValue({ kind: "continue", @@ -152,7 +81,7 @@ describe("getReplyFromConfig before_agent_reply wiring", () => { reply: { text: "plugin reply" }, }); - const result = await getReplyFromConfig(buildCtx(), undefined, {}); + const result = await getReplyFromConfig(buildGetReplyGroupCtx(), undefined, {}); expect(result).toEqual({ text: "plugin reply" }); expect(mocks.runBeforeAgentReply).toHaveBeenCalledWith( @@ -175,7 +104,7 @@ describe("getReplyFromConfig before_agent_reply wiring", () => { it("falls back to NO_REPLY when the hook claims without a reply payload", async () => { mocks.runBeforeAgentReply.mockResolvedValue({ handled: true }); - const result = await getReplyFromConfig(buildCtx(), undefined, {}); + const result = await getReplyFromConfig(buildGetReplyGroupCtx(), undefined, {}); expect(result).toEqual({ text: SILENT_REPLY_TOKEN }); }); diff --git a/src/auto-reply/reply/get-reply.message-hooks.test.ts b/src/auto-reply/reply/get-reply.message-hooks.test.ts index a1f548196a4..5f6de6745ad 100644 --- a/src/auto-reply/reply/get-reply.message-hooks.test.ts +++ b/src/auto-reply/reply/get-reply.message-hooks.test.ts @@ -1,6 +1,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MsgContext } from "../templating.js"; import { withFastReplyConfig } from "./get-reply-fast-path.js"; +import { + buildGetReplyGroupCtx, + createGetReplySessionState, + registerGetReplyRuntimeOverrides, +} from "./get-reply.test-fixtures.js"; import { loadGetReplyModuleForTest } from "./get-reply.test-loader.js"; import { registerGetReplyCommonMocks } from "./get-reply.test-mocks.js"; @@ -37,15 +42,7 @@ vi.mock("../../media-understanding/apply.runtime.js", () => ({ vi.mock("./commands-core.js", () => ({ emitResetCommandHooks: vi.fn(async () => undefined), })); -vi.mock("./get-reply-directives.js", () => ({ - resolveReplyDirectives: mocks.resolveReplyDirectives, -})); -vi.mock("./get-reply-inline-actions.js", () => ({ - handleInlineActions: vi.fn(async () => ({ kind: "reply", reply: { text: "ok" } })), -})); -vi.mock("./session.js", () => ({ - initSessionState: mocks.initSessionState, -})); +registerGetReplyRuntimeOverrides(mocks); let getReplyFromConfig: typeof import("./get-reply.js").getReplyFromConfig; @@ -54,26 +51,17 @@ async function loadGetReplyRuntimeForTest() { } function buildCtx(overrides: Partial = {}): MsgContext { - return { - Provider: "telegram", - Surface: "telegram", - OriginatingChannel: "telegram", - OriginatingTo: "telegram:-100123", - ChatType: "group", + return buildGetReplyGroupCtx({ Body: "", BodyForAgent: "", RawBody: "", CommandBody: "", - SessionKey: "agent:main:telegram:-100123", - From: "telegram:user:42", - To: "telegram:-100123", GroupChannel: "ops", - Timestamp: 1710000000000, MediaPath: "/tmp/voice.ogg", MediaUrl: "https://example.test/voice.ogg", MediaType: "audio/ogg", ...overrides, - }; + }); } describe("getReplyFromConfig message hooks", () => { @@ -106,24 +94,13 @@ describe("getReplyFromConfig message hooks", () => { ); mocks.triggerInternalHook.mockResolvedValue(undefined); mocks.resolveReplyDirectives.mockResolvedValue({ kind: "reply", reply: { text: "ok" } }); - mocks.initSessionState.mockResolvedValue({ - sessionCtx: {}, - sessionEntry: {}, - previousSessionEntry: {}, - sessionStore: {}, - sessionKey: "agent:main:telegram:-100123", - sessionId: "session-1", - isNewSession: false, - resetTriggered: false, - systemSent: false, - abortedLastRun: false, - storePath: "/tmp/sessions.json", - sessionScope: "per-chat", - groupResolution: undefined, - isGroup: true, - triggerBodyNormalized: "", - bodyStripped: "", - }); + mocks.initSessionState.mockResolvedValue( + createGetReplySessionState({ + sessionKey: "agent:main:telegram:-100123", + sessionScope: "per-chat", + isGroup: true, + }), + ); }); it("emits transcribed + preprocessed hooks with enriched context", async () => { diff --git a/src/auto-reply/reply/get-reply.reset-hooks-fallback.test.ts b/src/auto-reply/reply/get-reply.reset-hooks-fallback.test.ts index dc6c36e4a3f..8f66a95637a 100644 --- a/src/auto-reply/reply/get-reply.reset-hooks-fallback.test.ts +++ b/src/auto-reply/reply/get-reply.reset-hooks-fallback.test.ts @@ -1,5 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { MsgContext } from "../templating.js"; +import { + buildNativeResetContext, + createGetReplyContinueDirectivesResult, + createGetReplySessionState, + registerGetReplyRuntimeOverrides, +} from "./get-reply.test-fixtures.js"; import { loadGetReplyModuleForTest } from "./get-reply.test-loader.js"; import "./get-reply.test-runtime-mocks.js"; @@ -15,15 +20,7 @@ vi.mock("./commands-core.js", () => ({ vi.mock("./commands-core.runtime.js", () => ({ emitResetCommandHooks: (...args: unknown[]) => mocks.emitResetCommandHooks(...args), })); -vi.mock("./get-reply-directives.js", () => ({ - resolveReplyDirectives: (...args: unknown[]) => mocks.resolveReplyDirectives(...args), -})); -vi.mock("./get-reply-inline-actions.js", () => ({ - handleInlineActions: (...args: unknown[]) => mocks.handleInlineActions(...args), -})); -vi.mock("./session.js", () => ({ - initSessionState: (...args: unknown[]) => mocks.initSessionState(...args), -})); +registerGetReplyRuntimeOverrides(mocks); let getReplyFromConfig: typeof import("./get-reply.js").getReplyFromConfig; @@ -31,71 +28,17 @@ async function loadGetReplyRuntimeForTest() { ({ getReplyFromConfig } = await loadGetReplyModuleForTest({ cacheKey: import.meta.url })); } -function buildNativeResetContext(): MsgContext { - return { - Provider: "telegram", - Surface: "telegram", - ChatType: "direct", - Body: "/new", - RawBody: "/new", - CommandBody: "/new", - CommandSource: "native", - CommandAuthorized: true, - SessionKey: "telegram:slash:123", - CommandTargetSessionKey: "agent:main:telegram:direct:123", - From: "telegram:123", - To: "slash:123", - }; -} - function createContinueDirectivesResult(resetHookTriggered: boolean) { - return { - kind: "continue" as const, - result: { - commandSource: "/new", - command: { - surface: "telegram", - channel: "telegram", - channelId: "telegram", - ownerList: [], - senderIsOwner: true, - isAuthorizedSender: true, - senderId: "123", - abortKey: "telegram:slash:123", - rawBodyNormalized: "/new", - commandBodyNormalized: "/new", - from: "telegram:123", - to: "slash:123", - resetHookTriggered, - }, - allowTextCommands: true, - skillCommands: [], - directives: {}, - cleanedBody: "/new", - elevatedEnabled: false, - elevatedAllowed: false, - elevatedFailures: [], - defaultActivation: "always", - resolvedThinkLevel: undefined, - resolvedVerboseLevel: "off", - resolvedReasoningLevel: "off", - resolvedElevatedLevel: "off", - execOverrides: undefined, - blockStreamingEnabled: false, - blockReplyChunking: undefined, - resolvedBlockStreamingBreak: undefined, - provider: "openai", - model: "gpt-4o-mini", - modelState: { - resolveDefaultThinkingLevel: async () => undefined, - }, - contextTokens: 0, - inlineStatusRequested: false, - directiveAck: undefined, - perMessageQueueMode: undefined, - perMessageQueueOptions: undefined, - }, - }; + return createGetReplyContinueDirectivesResult({ + body: "/new", + abortKey: "telegram:slash:123", + from: "telegram:123", + to: "slash:123", + senderId: "123", + commandSource: "/new", + senderIsOwner: true, + resetHookTriggered, + }); } describe("getReplyFromConfig reset-hook fallback", () => { @@ -107,24 +50,17 @@ describe("getReplyFromConfig reset-hook fallback", () => { mocks.emitResetCommandHooks.mockReset(); mocks.initSessionState.mockReset(); - mocks.initSessionState.mockResolvedValue({ - sessionCtx: buildNativeResetContext(), - sessionEntry: {}, - previousSessionEntry: {}, - sessionStore: {}, - sessionKey: "agent:main:telegram:direct:123", - sessionId: "session-1", - isNewSession: true, - resetTriggered: true, - systemSent: false, - abortedLastRun: false, - storePath: "/tmp/sessions.json", - sessionScope: "per-sender", - groupResolution: undefined, - isGroup: false, - triggerBodyNormalized: "/new", - bodyStripped: "", - }); + mocks.initSessionState.mockResolvedValue( + createGetReplySessionState({ + sessionCtx: buildNativeResetContext(), + sessionKey: "agent:main:telegram:direct:123", + isNewSession: true, + resetTriggered: true, + sessionScope: "per-sender", + triggerBodyNormalized: "/new", + bodyStripped: "", + }), + ); mocks.resolveReplyDirectives.mockResolvedValue(createContinueDirectivesResult(false)); }); diff --git a/src/auto-reply/reply/get-reply.test-fixtures.ts b/src/auto-reply/reply/get-reply.test-fixtures.ts index 72de4098bdb..e588e8838df 100644 --- a/src/auto-reply/reply/get-reply.test-fixtures.ts +++ b/src/auto-reply/reply/get-reply.test-fixtures.ts @@ -18,7 +18,44 @@ export function buildGetReplyCtx(overrides: Partial = {}): MsgContex }; } -export function createGetReplySessionState() { +export function buildGetReplyGroupCtx(overrides: Partial = {}): MsgContext { + return { + Provider: "telegram", + Surface: "telegram", + OriginatingChannel: "telegram", + OriginatingTo: "telegram:-100123", + ChatType: "group", + Body: "hello world", + BodyForAgent: "hello world", + RawBody: "hello world", + CommandBody: "hello world", + BodyForCommands: "hello world", + SessionKey: "agent:main:telegram:-100123", + From: "telegram:user:42", + To: "telegram:-100123", + Timestamp: 1710000000000, + ...overrides, + }; +} + +export function buildNativeResetContext(): MsgContext { + return { + Provider: "telegram", + Surface: "telegram", + ChatType: "direct", + Body: "/new", + RawBody: "/new", + CommandBody: "/new", + CommandSource: "native", + CommandAuthorized: true, + SessionKey: "telegram:slash:123", + CommandTargetSessionKey: "agent:main:telegram:direct:123", + From: "telegram:123", + To: "slash:123", + }; +} + +export function createGetReplySessionState(overrides: Record = {}) { return { sessionCtx: {}, sessionEntry: {}, @@ -36,18 +73,80 @@ export function createGetReplySessionState() { isGroup: false, triggerBodyNormalized: "", bodyStripped: "", + ...overrides, + }; +} + +export function createGetReplyContinueDirectivesResult(params: { + body: string; + abortKey: string; + from: string; + to: string; + senderId: string; + commandSource: string; + senderIsOwner: boolean; + resetHookTriggered: boolean; +}) { + return { + kind: "continue" as const, + result: { + commandSource: params.commandSource, + command: { + surface: "telegram", + channel: "telegram", + channelId: "telegram", + ownerList: [], + senderIsOwner: params.senderIsOwner, + isAuthorizedSender: true, + senderId: params.senderId, + abortKey: params.abortKey, + rawBodyNormalized: params.body, + commandBodyNormalized: params.body, + from: params.from, + to: params.to, + resetHookTriggered: params.resetHookTriggered, + }, + allowTextCommands: true, + skillCommands: [], + directives: {}, + cleanedBody: params.body, + elevatedEnabled: false, + elevatedAllowed: false, + elevatedFailures: [], + defaultActivation: "always", + resolvedThinkLevel: undefined, + resolvedVerboseLevel: "off", + resolvedReasoningLevel: "off", + resolvedElevatedLevel: "off", + execOverrides: undefined, + blockStreamingEnabled: false, + blockReplyChunking: undefined, + resolvedBlockStreamingBreak: undefined, + provider: "openai", + model: "gpt-4o-mini", + modelState: { + resolveDefaultThinkingLevel: async () => undefined, + }, + contextTokens: 0, + inlineStatusRequested: false, + directiveAck: undefined, + perMessageQueueMode: undefined, + perMessageQueueOptions: undefined, + }, }; } export function registerGetReplyRuntimeOverrides(handles: { resolveReplyDirectives: (...args: unknown[]) => unknown; initSessionState: (...args: unknown[]) => unknown; + handleInlineActions?: (...args: unknown[]) => unknown; }): void { vi.doMock("./get-reply-directives.js", () => ({ resolveReplyDirectives: (...args: unknown[]) => handles.resolveReplyDirectives(...args), })); vi.doMock("./get-reply-inline-actions.js", () => ({ - handleInlineActions: vi.fn(async () => ({ kind: "reply", reply: { text: "ok" } })), + handleInlineActions: + handles.handleInlineActions ?? vi.fn(async () => ({ kind: "reply", reply: { text: "ok" } })), })); vi.doMock("./session.js", () => ({ initSessionState: (...args: unknown[]) => handles.initSessionState(...args),