Files
openclaw/src/hooks/message-hook-mappers.test.ts
2026-04-03 19:09:21 +01:00

267 lines
8.6 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import type { FinalizedMsgContext } from "../auto-reply/templating.js";
import type { OpenClawConfig } from "../config/config.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
import {
buildCanonicalSentMessageHookContext,
deriveInboundMessageHookContext,
toPluginInboundClaimEvent,
toPluginInboundClaimContext,
toInternalMessagePreprocessedContext,
toInternalMessageReceivedContext,
toInternalMessageSentContext,
toInternalMessageTranscribedContext,
toPluginMessageContext,
toPluginMessageReceivedEvent,
toPluginMessageSentEvent,
} from "./message-hook-mappers.js";
function makeInboundCtx(overrides: Partial<FinalizedMsgContext> = {}): FinalizedMsgContext {
return {
From: "demo-chat:user:123",
To: "demo-chat:chat:456",
Body: "body",
BodyForAgent: "body-for-agent",
BodyForCommands: "commands-body",
RawBody: "raw-body",
Transcript: "hello transcript",
Timestamp: 1710000000,
Provider: "demo-chat",
Surface: "demo-chat",
OriginatingChannel: "demo-chat",
OriginatingTo: "demo-chat:chat:456",
AccountId: "acc-1",
MessageSid: "msg-1",
SenderId: "sender-1",
SenderName: "User One",
SenderUsername: "userone",
SenderE164: "+15551234567",
MessageThreadId: 42,
MediaPath: "/tmp/audio.ogg",
MediaType: "audio/ogg",
GroupSubject: "ops",
GroupChannel: "ops-room",
GroupSpace: "guild-1",
...overrides,
} as FinalizedMsgContext;
}
describe("message hook mappers", () => {
beforeEach(() => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "claim-chat",
source: "test",
plugin: {
...createChannelTestPluginBase({ id: "claim-chat", label: "Claim chat" }),
messaging: {
resolveInboundConversation: ({
from,
to,
isGroup,
}: {
from?: string;
to?: string;
isGroup?: boolean;
}) => {
const normalizedTo = to?.replace(/^channel:/i, "").trim();
const normalizedFrom = from?.replace(/^claim-chat:/i, "").trim();
if (isGroup && normalizedTo) {
return { conversationId: `channel:${normalizedTo}` };
}
if (normalizedFrom) {
return { conversationId: `user:${normalizedFrom}` };
}
return null;
},
},
},
},
]),
);
});
it("derives canonical inbound context with body precedence and group metadata", () => {
const canonical = deriveInboundMessageHookContext(makeInboundCtx());
expect(canonical.content).toBe("commands-body");
expect(canonical.channelId).toBe("demo-chat");
expect(canonical.conversationId).toBe("demo-chat:chat:456");
expect(canonical.messageId).toBe("msg-1");
expect(canonical.isGroup).toBe(true);
expect(canonical.groupId).toBe("demo-chat:chat:456");
expect(canonical.guildId).toBe("guild-1");
});
it("supports explicit content/messageId overrides", () => {
const canonical = deriveInboundMessageHookContext(makeInboundCtx(), {
content: "override-content",
messageId: "override-msg",
});
expect(canonical.content).toBe("override-content");
expect(canonical.messageId).toBe("override-msg");
});
it("preserves multi-attachment arrays for inbound claim metadata", () => {
const canonical = deriveInboundMessageHookContext(
makeInboundCtx({
MediaPath: undefined,
MediaType: undefined,
MediaPaths: ["/tmp/tree.jpg", "/tmp/ramp.jpg"],
MediaTypes: ["image/jpeg", "image/jpeg"],
}),
);
expect(canonical.mediaPath).toBe("/tmp/tree.jpg");
expect(canonical.mediaType).toBe("image/jpeg");
expect(canonical.mediaPaths).toEqual(["/tmp/tree.jpg", "/tmp/ramp.jpg"]);
expect(canonical.mediaTypes).toEqual(["image/jpeg", "image/jpeg"]);
expect(toPluginInboundClaimEvent(canonical)).toEqual(
expect.objectContaining({
metadata: expect.objectContaining({
mediaPath: "/tmp/tree.jpg",
mediaType: "image/jpeg",
mediaPaths: ["/tmp/tree.jpg", "/tmp/ramp.jpg"],
mediaTypes: ["image/jpeg", "image/jpeg"],
}),
}),
);
});
it("maps canonical inbound context to plugin/internal received payloads", () => {
const canonical = deriveInboundMessageHookContext(makeInboundCtx());
expect(toPluginMessageContext(canonical)).toEqual({
channelId: "demo-chat",
accountId: "acc-1",
conversationId: "demo-chat:chat:456",
});
expect(toPluginMessageReceivedEvent(canonical)).toEqual({
from: "demo-chat:user:123",
content: "commands-body",
timestamp: 1710000000,
metadata: expect.objectContaining({
messageId: "msg-1",
senderName: "User One",
threadId: 42,
}),
});
expect(toInternalMessageReceivedContext(canonical)).toEqual({
from: "demo-chat:user:123",
content: "commands-body",
timestamp: 1710000000,
channelId: "demo-chat",
accountId: "acc-1",
conversationId: "demo-chat:chat:456",
messageId: "msg-1",
metadata: expect.objectContaining({
senderUsername: "userone",
senderE164: "+15551234567",
}),
});
});
it("uses channel plugin claim resolvers for grouped conversations", () => {
const canonical = deriveInboundMessageHookContext(
makeInboundCtx({
Provider: "claim-chat",
Surface: "claim-chat",
OriginatingChannel: "claim-chat",
To: "channel:123456789012345678",
OriginatingTo: "channel:123456789012345678",
GroupChannel: "general",
GroupSubject: "guild",
}),
);
expect(toPluginInboundClaimContext(canonical)).toEqual({
channelId: "claim-chat",
accountId: "acc-1",
conversationId: "channel:123456789012345678",
parentConversationId: undefined,
senderId: "sender-1",
messageId: "msg-1",
});
});
it("uses channel plugin claim resolvers for direct-message conversations", () => {
const canonical = deriveInboundMessageHookContext(
makeInboundCtx({
Provider: "claim-chat",
Surface: "claim-chat",
OriginatingChannel: "claim-chat",
From: "claim-chat:1177378744822943744",
To: "channel:1480574946919846079",
OriginatingTo: "channel:1480574946919846079",
GroupChannel: undefined,
GroupSubject: undefined,
}),
);
expect(toPluginInboundClaimContext(canonical)).toEqual({
channelId: "claim-chat",
accountId: "acc-1",
conversationId: "user:1177378744822943744",
parentConversationId: undefined,
senderId: "sender-1",
messageId: "msg-1",
});
});
it("maps transcribed and preprocessed internal payloads", () => {
const cfg = {} as OpenClawConfig;
const canonical = deriveInboundMessageHookContext(makeInboundCtx({ Transcript: undefined }));
const transcribed = toInternalMessageTranscribedContext(canonical, cfg);
expect(transcribed.transcript).toBe("");
expect(transcribed.cfg).toBe(cfg);
const preprocessed = toInternalMessagePreprocessedContext(canonical, cfg);
expect(preprocessed.transcript).toBeUndefined();
expect(preprocessed.isGroup).toBe(true);
expect(preprocessed.groupId).toBe("demo-chat:chat:456");
expect(preprocessed.cfg).toBe(cfg);
});
it("maps sent context consistently for plugin/internal hooks", () => {
const canonical = buildCanonicalSentMessageHookContext({
to: "demo-chat:chat:456",
content: "reply",
success: false,
error: "network error",
channelId: "demo-chat",
accountId: "acc-1",
messageId: "out-1",
isGroup: true,
groupId: "demo-chat:chat:456",
});
expect(toPluginMessageContext(canonical)).toEqual({
channelId: "demo-chat",
accountId: "acc-1",
conversationId: "demo-chat:chat:456",
});
expect(toPluginMessageSentEvent(canonical)).toEqual({
to: "demo-chat:chat:456",
content: "reply",
success: false,
error: "network error",
});
expect(toInternalMessageSentContext(canonical)).toEqual({
to: "demo-chat:chat:456",
content: "reply",
success: false,
error: "network error",
channelId: "demo-chat",
accountId: "acc-1",
conversationId: "demo-chat:chat:456",
messageId: "out-1",
isGroup: true,
groupId: "demo-chat:chat:456",
});
});
});