test: share get-reply hook fixtures

This commit is contained in:
Peter Steinberger
2026-04-20 19:53:41 +01:00
parent a74ba90196
commit 1603577dfd
4 changed files with 172 additions and 231 deletions

View File

@@ -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> = {}): 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 });
});

View File

@@ -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> = {}): MsgContext {
return {
Provider: "telegram",
Surface: "telegram",
OriginatingChannel: "telegram",
OriginatingTo: "telegram:-100123",
ChatType: "group",
return buildGetReplyGroupCtx({
Body: "<media:audio>",
BodyForAgent: "<media:audio>",
RawBody: "<media:audio>",
CommandBody: "<media:audio>",
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 () => {

View File

@@ -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));
});

View File

@@ -18,7 +18,44 @@ export function buildGetReplyCtx(overrides: Partial<MsgContext> = {}): MsgContex
};
}
export function createGetReplySessionState() {
export function buildGetReplyGroupCtx(overrides: Partial<MsgContext> = {}): 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<string, unknown> = {}) {
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),