import { afterEach, beforeEach, describe, expect, it, vi } from
"vitest";
import {
clearInternalHooks,
createInternalHookEvent,
registerInternalHook,
triggerInternalHook,
type InternalHookEvent,
} from
"./internal-hooks.js";
type ActionCase = {
label: string;
key: string;
action:
"received" |
"transcribed" |
"preprocessed" |
"sent";
context: Record<string, unknown>;
assertContext: (context: Record<string, unknown>) =>
void;
};
const actionCases: ActionCase[] = [
{
label:
"message:received",
key:
"message:received",
action:
"received",
context: {
from:
"signal:+15551234567",
to:
"bot:+15559876543",
content:
"Test message",
channelId:
"signal",
conversationId:
"conv-abc",
messageId:
"msg-xyz",
senderId:
"sender-1",
senderName:
"Test User",
senderUsername:
"testuser",
senderE164:
"+15551234567",
provider:
"signal",
surface:
"signal",
threadId:
"thread-1",
originatingChannel:
"signal",
originatingTo:
"bot:+15559876543",
timestamp:
1707600000,
},
assertContext: (context) => {
expect(context.content).toBe(
"Test message");
expect(context.channelId).toBe(
"signal");
expect(context.senderE164).toBe(
"+15551234567");
expect(context.threadId).toBe(
"thread-1");
},
},
{
label:
"message:transcribed",
key:
"message:transcribed",
action:
"transcribed",
context: {
body:
" [Audio]",
bodyForAgent:
"[Audio] Transcript: Hello from voice",
transcript:
"Hello from voice",
channelId:
"telegram",
mediaType:
"audio/ogg",
},
assertContext: (context) => {
expect(context.body).toBe(
" [Audio]");
expect(context.bodyForAgent).toContain(
"Transcript:");
expect(context.transcript).toBe(
"Hello from voice");
expect(context.mediaType).toBe(
"audio/ogg");
},
},
{
label:
"message:preprocessed",
key:
"message:preprocessed",
action:
"preprocessed",
context: {
body:
" [Audio]",
bodyForAgent:
"[Audio] Transcript: Check https://example.com\n[Link summary: Example site]",
transcript:
"Check https://example.com",
channelId:
"telegram",
mediaType:
"audio/ogg",
isGroup:
false,
},
assertContext: (context) => {
expect(context.transcript).toBe(
"Check https://example.com");
expect(String(context.bodyForAgent)).toContain(
"Link summary");
expect(String(context.bodyForAgent)).toContain(
"Transcript:");
},
},
{
label:
"message:sent",
key:
"message:sent",
action:
"sent",
context: {
from:
"bot:456",
to:
"user:123",
content:
"Reply text",
channelId:
"discord",
conversationId:
"channel:C123",
provider:
"discord",
surface:
"discord",
threadId:
"thread-abc",
originatingChannel:
"discord",
originatingTo:
"channel:C123",
},
assertContext: (context) => {
expect(context.content).toBe(
"Reply text");
expect(context.channelId).toBe(
"discord");
expect(context.conversationId).toBe(
"channel:C123");
expect(context.threadId).toBe(
"thread-abc");
},
},
];
describe(
"message hooks", () => {
beforeEach(() => {
clearInternalHooks();
});
afterEach(() => {
clearInternalHooks();
});
describe(
"action handlers", () => {
for (
const testCase of actionCases) {
it(`triggers handler
for ${testCase.label}`, async () => {
const handler = vi.fn();
registerInternalHook(testCase.key, handler);
await triggerInternalHook(
createInternalHookEvent(
"message", testCase.action,
"session-1", testCase.context),
);
expect(handler).toHaveBeenCalledOnce();
const event = handler.mock.calls[
0][
0] as InternalHookEvent;
expect(event.type).toBe(
"message");
expect(event.action).toBe(testCase.action);
testCase.assertContext(event.context);
});
}
it(
"does not trigger action-specific handlers for other actions", async () => {
const sentHandler = vi.fn();
registerInternalHook(
"message:sent", sentHandler);
await triggerInternalHook(
createInternalHookEvent(
"message",
"received",
"session-1", { content:
"hello" }),
);
expect(sentHandler).not.toHaveBeenCalled();
});
});
describe(
"general handler", () => {
it(
"receives full message lifecycle in order", async () => {
const events: InternalHookEvent[] = [];
registerInternalHook(
"message", (event) => {
events.push(event);
});
const lifecycleFixtures: Array<{
action:
"received" |
"transcribed" |
"preprocessed" |
"sent";
context: Record<string, unknown>;
}> = [
{ action:
"received", context: { content:
"hi" } },
{ action:
"transcribed", context: { transcript:
"hello" } },
{ action:
"preprocessed", context: { body:
"hello", bodyForAgent:
"hello" } },
{ action:
"sent", context: { content:
"reply" } },
];
for (
const fixture of lifecycleFixtures) {
await triggerInternalHook(
createInternalHookEvent(
"message", fixture.action,
"s1", fixture.context),
);
}
expect(events.map((event) => event.action)).toEqual([
"received",
"transcribed",
"preprocessed",
"sent",
]);
});
it(
"triggers both general and specific handlers", async () => {
const generalHandler = vi.fn();
const specificHandler = vi.fn();
registerInternalHook(
"message", generalHandler);
registerInternalHook(
"message:received", specificHandler);
await triggerInternalHook(
createInternalHookEvent(
"message",
"received",
"s1", { content:
"test" }),
);
expect(generalHandler).toHaveBeenCalledOnce();
expect(specificHandler).toHaveBeenCalledOnce();
});
});
describe(
"error isolation", () => {
it(
"does not propagate handler errors", async () => {
const badHandler = vi.fn(() => {
throw new Error(
"Hook exploded");
});
registerInternalHook(
"message:received", badHandler);
await expect(
triggerInternalHook(
createInternalHookEvent(
"message",
"received",
"s1", { content:
"test" }),
),
).resolves.not.toThrow();
expect(badHandler).toHaveBeenCalledOnce();
});
it(
"continues with later handlers when one fails", async () => {
const failHandler = vi.fn(() => {
throw new Error(
"First handler fails");
});
const successHandler = vi.fn();
registerInternalHook(
"message:received", failHandler);
registerInternalHook(
"message:received", successHandler);
await triggerInternalHook(
createInternalHookEvent(
"message",
"received",
"s1", { content:
"test" }),
);
expect(failHandler).toHaveBeenCalledOnce();
expect(successHandler).toHaveBeenCalledOnce();
});
it(
"isolates async handler errors", async () => {
const asyncFailHandler = vi.fn(async () => {
throw new Error(
"Async hook failed");
});
registerInternalHook(
"message:sent", asyncFailHandler);
await expect(
triggerInternalHook(createInternalHookEvent(
"message",
"sent",
"s1", { content:
"reply" })),
).resolves.not.toThrow();
expect(asyncFailHandler).toHaveBeenCalledOnce();
});
});
describe(
"event structure", () => {
it(
"includes timestamps on message events", async () => {
const handler = vi.fn();
registerInternalHook(
"message", handler);
const before =
new Date();
await triggerInternalHook(
createInternalHookEvent(
"message",
"received",
"s1", { content:
"hi" }),
);
const after =
new Date();
const event = handler.mock.calls[
0][
0] as InternalHookEvent;
expect(event.timestamp).toBeInstanceOf(Date);
expect(event.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
expect(event.timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
});
it(
"preserves mutable messages and sessionKey", async () => {
const events: InternalHookEvent[] = [];
registerInternalHook(
"message", (event) => {
event.messages.push(
"Echo");
events.push(event);
});
const sessionKey =
"agent:main:telegram:abc";
const received = createInternalHookEvent(
"message",
"received", sessionKey, {
content:
"hi",
});
await triggerInternalHook(received);
await triggerInternalHook(
createInternalHookEvent(
"message",
"sent", sessionKey, { content:
"reply" }),
);
expect(received.messages).toContain(
"Echo");
expect(events[
0]?.sessionKey).toBe(sessionKey);
expect(events[
1]?.sessionKey).toBe(sessionKey);
});
});
});