Download src/hooks/message-hook-mappers.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 33.3 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/hooks/message-hook-mappers.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/hooks/message-hook-mappers.test.ts
-
curl -L -o message-hook-mappers.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/hooks/message-hook-mappers.test.ts
33.3 kB
| // Message hook mapper tests cover mapping runtime messages into hook payloads. | |
| import { beforeEach, describe, expect, it } from "vitest"; | |
| import { finalizeInboundContextForSdk } from "../auto-reply/reply/inbound-context.js"; | |
| import type { FinalizedMsgContext } from "../auto-reply/templating.js"; | |
| import type { ChannelMessagingAdapter } from "../channels/plugins/types.core.js"; | |
| import type { OpenClawConfig } from "../config/config.js"; | |
| import type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js"; | |
| import { setActivePluginRegistry } from "../plugins/runtime.js"; | |
| import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js"; | |
| import { | |
| buildCanonicalSentMessageHookContext, | |
| deriveInboundMessageHookContext, | |
| toPluginInboundClaimPair, | |
| toInternalMessagePreprocessedContext, | |
| toInternalMessageReceivedContext, | |
| toInternalMessageSentContext, | |
| toInternalMessageTranscribedContext, | |
| toPluginMessageContext, | |
| toPluginMessageReceivedEvent, | |
| toPluginMessageSentEvent, | |
| } from "./message-hook-mappers.js"; | |
| type ResolveInboundConversationParams = Parameters< | |
| NonNullable<ChannelMessagingAdapter["resolveInboundConversation"]> | |
| >[0]; | |
| interface MinimalInboundHookContext { | |
| from: string; | |
| content: string; | |
| channelId: string; | |
| isGroup: boolean; | |
| } | |
| 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", | |
| SessionKey: "session-1", | |
| AccountId: "acc-1", | |
| MessageSid: "msg-1", | |
| SenderId: "sender-1", | |
| SenderName: "User One", | |
| SenderUsername: "userone", | |
| SenderE164: "+15551234567", | |
| MessageThreadId: 42, | |
| media: [ | |
| { | |
| path: "/tmp/audio.ogg", | |
| url: "https://cdn.example.com/audio.ogg", | |
| contentType: "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; | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| pluginId: "thread-claim-chat", | |
| source: "test", | |
| plugin: { | |
| ...createChannelTestPluginBase({ id: "thread-claim-chat", label: "Thread claim chat" }), | |
| messaging: { | |
| resolveInboundConversation: ({ | |
| to, | |
| threadId, | |
| threadParentId, | |
| }: ResolveInboundConversationParams) => { | |
| if (threadId) { | |
| return { | |
| conversationId: String(threadId), | |
| ...(threadParentId | |
| ? { parentConversationId: `channel:${threadParentId}` } | |
| : {}), | |
| }; | |
| } | |
| return to ? { conversationId: to } : null; | |
| }, | |
| }, | |
| }, | |
| }, | |
| ]), | |
| ); | |
| }); | |
| it("preserves producer enrichment and minimal mapper input compatibility", () => { | |
| const derived = deriveInboundMessageHookContext(makeInboundCtx()); | |
| derived.runId = "run-1"; | |
| derived.trace = { | |
| traceId: "11111111111111111111111111111111", | |
| spanId: "2222222222222222", | |
| }; | |
| derived.callDepth = 2; | |
| derived.mediaStagingPending = true; | |
| derived.originalMedia = []; | |
| const minimal: MinimalInboundHookContext = { | |
| from: "sender", | |
| content: "hello", | |
| channelId: "demo-chat", | |
| isGroup: false, | |
| }; | |
| expect(toPluginMessageContext(minimal)).toMatchObject({ channelId: "demo-chat" }); | |
| }); | |
| 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("normalizes canonical inbound message id precedence", () => { | |
| expect( | |
| deriveInboundMessageHookContext( | |
| makeInboundCtx({ MessageSidFull: "full-message-id", MessageSid: "short-message-id" }), | |
| ).messageId, | |
| ).toBe("full-message-id"); | |
| expect( | |
| deriveInboundMessageHookContext( | |
| makeInboundCtx({ MessageSidFull: " ", MessageSid: "short-message-id" }), | |
| ).messageId, | |
| ).toBe("short-message-id"); | |
| }); | |
| it("uses the session key as the Control UI conversation id", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| From: undefined, | |
| To: undefined, | |
| OriginatingTo: undefined, | |
| Provider: "webchat", | |
| Surface: "webchat", | |
| OriginatingChannel: "webchat", | |
| SessionKey: "agent:main:adopted", | |
| }), | |
| ); | |
| expect(canonical.channelId).toBe("webchat"); | |
| expect(canonical.conversationId).toBe("agent:main:adopted"); | |
| }); | |
| it("maps inbound reply metadata into canonical and plugin payloads", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| ReplyToId: "discord-message-42", | |
| ReplyToIdFull: "discord:channel-1:discord-message-42", | |
| ReplyToBody: "quoted Discord reply body", | |
| ReplyToSender: "Ada", | |
| ReplyToIsQuote: true, | |
| }), | |
| ); | |
| expect(canonical.replyToId).toBe("discord-message-42"); | |
| expect(canonical.replyToIdFull).toBe("discord:channel-1:discord-message-42"); | |
| expect(canonical.replyToBody).toBe("quoted Discord reply body"); | |
| expect(canonical.replyToSender).toBe("Ada"); | |
| expect(canonical.replyToIsQuote).toBe(true); | |
| expect(toPluginMessageContext(canonical)).toMatchObject({ | |
| replyToId: "discord-message-42", | |
| replyToIdFull: "discord:channel-1:discord-message-42", | |
| replyToBody: "quoted Discord reply body", | |
| replyToSender: "Ada", | |
| replyToIsQuote: true, | |
| }); | |
| const { context: claimContext, event: claimEvent } = toPluginInboundClaimPair(canonical); | |
| expect(claimContext).toMatchObject({ | |
| replyToId: "discord-message-42", | |
| replyToIdFull: "discord:channel-1:discord-message-42", | |
| replyToBody: "quoted Discord reply body", | |
| replyToSender: "Ada", | |
| replyToIsQuote: true, | |
| }); | |
| expect(claimEvent).toMatchObject({ | |
| replyToId: "discord-message-42", | |
| replyToIdFull: "discord:channel-1:discord-message-42", | |
| replyToBody: "quoted Discord reply body", | |
| replyToSender: "Ada", | |
| replyToIsQuote: true, | |
| }); | |
| expect(claimEvent.metadata).toMatchObject({ | |
| replyToId: "discord-message-42", | |
| replyToIdFull: "discord:channel-1:discord-message-42", | |
| replyToBody: "quoted Discord reply body", | |
| replyToSender: "Ada", | |
| replyToIsQuote: true, | |
| }); | |
| const receivedEvent = toPluginMessageReceivedEvent(canonical); | |
| expect(receivedEvent).toMatchObject({ | |
| replyToId: "discord-message-42", | |
| replyToIdFull: "discord:channel-1:discord-message-42", | |
| replyToBody: "quoted Discord reply body", | |
| replyToSender: "Ada", | |
| replyToIsQuote: true, | |
| }); | |
| expect(receivedEvent.metadata).toMatchObject({ | |
| replyToId: "discord-message-42", | |
| replyToIdFull: "discord:channel-1:discord-message-42", | |
| replyToBody: "quoted Discord reply body", | |
| replyToSender: "Ada", | |
| replyToIsQuote: true, | |
| }); | |
| }); | |
| it.each([ | |
| { name: "absent", fields: {}, expected: {} }, | |
| { | |
| name: "undefined", | |
| fields: { | |
| replyToId: undefined, | |
| replyToIdFull: undefined, | |
| replyToBody: undefined, | |
| replyToSender: undefined, | |
| replyToIsQuote: undefined, | |
| }, | |
| expected: {}, | |
| }, | |
| { | |
| name: "empty and false", | |
| fields: { | |
| replyToId: "", | |
| replyToIdFull: "", | |
| replyToBody: "", | |
| replyToSender: "", | |
| replyToIsQuote: false, | |
| }, | |
| expected: { | |
| replyToId: "", | |
| replyToIdFull: "", | |
| replyToBody: "", | |
| replyToSender: "", | |
| replyToIsQuote: false, | |
| }, | |
| }, | |
| ])("preserves $name optional reply fields across hook projections", ({ fields, expected }) => { | |
| const canonical = { | |
| from: "sender", | |
| content: "hello", | |
| channelId: "demo-chat", | |
| isGroup: false, | |
| ...fields, | |
| }; | |
| const { context, event } = toPluginInboundClaimPair(canonical); | |
| const received = toPluginMessageReceivedEvent(canonical); | |
| for (const output of [toPluginMessageContext(canonical), context, event, received]) { | |
| const entries = Object.entries(output).filter(([key]) => key.startsWith("replyTo")); | |
| expect(entries).toEqual(Object.entries(expected)); | |
| } | |
| for (const metadata of [event.metadata, received.metadata]) { | |
| expect(metadata).toMatchObject({ | |
| replyToId: undefined, | |
| replyToIdFull: undefined, | |
| replyToBody: undefined, | |
| replyToSender: undefined, | |
| replyToIsQuote: undefined, | |
| ...expected, | |
| }); | |
| expect(Object.keys(metadata ?? {}).filter((key) => key.startsWith("replyTo"))).toEqual([ | |
| "replyToId", | |
| "replyToIdFull", | |
| "replyToBody", | |
| "replyToSender", | |
| "replyToIsQuote", | |
| ]); | |
| } | |
| }); | |
| it("checks reply-field presence before reading a sent context", () => { | |
| const reads: PropertyKey[] = []; | |
| const canonical = new Proxy( | |
| buildCanonicalSentMessageHookContext({ | |
| to: "target", | |
| content: "reply", | |
| success: true, | |
| channelId: "demo-chat", | |
| }), | |
| { | |
| get(target, key, receiver) { | |
| if (typeof key === "string" && key.startsWith("replyTo")) { | |
| reads.push(key); | |
| } | |
| return Reflect.get(target, key, receiver); | |
| }, | |
| }, | |
| ); | |
| expect(toPluginMessageContext(canonical)).toEqual({ | |
| channelId: "demo-chat", | |
| accountId: undefined, | |
| conversationId: "target", | |
| }); | |
| expect(reads).toEqual([]); | |
| }); | |
| it("falls back to raw body when command body is blank", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| BodyForCommands: " \n\t", | |
| RawBody: "Readiness probe failed", | |
| }), | |
| ); | |
| expect(canonical.content).toBe("Readiness probe failed"); | |
| expect(toPluginMessageReceivedEvent(canonical).content).toBe("Readiness probe failed"); | |
| expect(toInternalMessageReceivedContext(canonical).content).toBe("Readiness probe failed"); | |
| }); | |
| it("keeps nonblank command body ahead of raw body for hook content", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| BodyForCommands: "/status", | |
| RawBody: "Readiness probe failed", | |
| }), | |
| ); | |
| expect(canonical.content).toBe("/status"); | |
| }); | |
| 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("uses OriginatingTo as the canonical conversation when To is flat", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| To: "demo-chat:chat:456", | |
| OriginatingTo: "demo-chat:chat:456:topic:42", | |
| MessageThreadId: 42, | |
| }), | |
| ); | |
| expect(canonical.to).toBe("demo-chat:chat:456"); | |
| expect(canonical.conversationId).toBe("demo-chat:chat:456:topic:42"); | |
| expect(canonical.originatingTo).toBe("demo-chat:chat:456:topic:42"); | |
| const pluginContext = toPluginMessageContext(canonical); | |
| expect(pluginContext.conversationId).toBe("demo-chat:chat:456:topic:42"); | |
| const receivedEvent = toPluginMessageReceivedEvent(canonical); | |
| expect(receivedEvent.metadata?.to).toBe("demo-chat:chat:456"); | |
| expect(receivedEvent.metadata?.originatingTo).toBe("demo-chat:chat:456:topic:42"); | |
| expect(receivedEvent.metadata?.threadId).toBe(42); | |
| const internalReceived = toInternalMessageReceivedContext(canonical); | |
| expect(internalReceived.conversationId).toBe("demo-chat:chat:456:topic:42"); | |
| expect(internalReceived.metadata?.to).toBe("demo-chat:chat:456"); | |
| expect(internalReceived.metadata?.threadId).toBe(42); | |
| }); | |
| it("preserves multi-attachment arrays for inbound claim metadata", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| media: [ | |
| { | |
| path: "/tmp/tree.jpg", | |
| url: "https://example.test/tree.jpg", | |
| contentType: "image/jpeg", | |
| }, | |
| { | |
| path: "/tmp/ramp.jpg", | |
| url: "https://example.test/ramp.jpg", | |
| contentType: "image/jpeg", | |
| }, | |
| ], | |
| }), | |
| ); | |
| expect(canonical.mediaPath).toBe("/tmp/tree.jpg"); | |
| expect(canonical.mediaUrl).toBe("https://example.test/tree.jpg"); | |
| expect(canonical.mediaType).toBe("image/jpeg"); | |
| expect(canonical.mediaPaths).toEqual(["/tmp/tree.jpg", "/tmp/ramp.jpg"]); | |
| expect(canonical.mediaUrls).toEqual([ | |
| "https://example.test/tree.jpg", | |
| "https://example.test/ramp.jpg", | |
| ]); | |
| expect(canonical.mediaTypes).toEqual(["image/jpeg", "image/jpeg"]); | |
| const { event: claimEvent } = toPluginInboundClaimPair(canonical); | |
| expect(claimEvent.metadata?.mediaPath).toBe("/tmp/tree.jpg"); | |
| expect(claimEvent.metadata?.mediaUrl).toBe("https://example.test/tree.jpg"); | |
| expect(claimEvent.metadata?.mediaType).toBe("image/jpeg"); | |
| expect(claimEvent.metadata?.mediaPaths).toEqual(["/tmp/tree.jpg", "/tmp/ramp.jpg"]); | |
| expect(claimEvent.metadata?.mediaUrls).toEqual([ | |
| "https://example.test/tree.jpg", | |
| "https://example.test/ramp.jpg", | |
| ]); | |
| expect(claimEvent.metadata?.mediaTypes).toEqual(["image/jpeg", "image/jpeg"]); | |
| expect(claimEvent.media).toEqual([ | |
| { | |
| path: "/tmp/tree.jpg", | |
| url: "https://example.test/tree.jpg", | |
| contentType: "image/jpeg", | |
| kind: "image", | |
| }, | |
| { | |
| path: "/tmp/ramp.jpg", | |
| url: "https://example.test/ramp.jpg", | |
| contentType: "image/jpeg", | |
| kind: "image", | |
| }, | |
| ]); | |
| }); | |
| it.each([ | |
| { | |
| name: "legacy-only", | |
| input: { media: undefined, MediaPath: "/tmp/legacy.png", MediaType: "image/png" }, | |
| expectedPath: "/tmp/legacy.png", | |
| }, | |
| { | |
| name: "facts-only", | |
| input: { media: [{ path: "/tmp/fact.png", contentType: "image/png" }] }, | |
| expectedPath: "/tmp/fact.png", | |
| }, | |
| { | |
| name: "both-equal", | |
| input: { | |
| MediaPath: "/tmp/equal.png", | |
| media: [{ path: "/tmp/equal.png", contentType: "image/png" }], | |
| }, | |
| expectedPath: "/tmp/equal.png", | |
| }, | |
| { | |
| name: "both-conflict", | |
| input: { | |
| MediaPath: "/tmp/legacy-conflict.png", | |
| media: [{ path: "/tmp/canonical.png", contentType: "image/png" }], | |
| }, | |
| expectedPath: "/tmp/canonical.png", | |
| }, | |
| { | |
| name: "sparse", | |
| input: { media: [{}, { path: "/tmp/sparse.png", contentType: "image/png" }] }, | |
| expectedPath: "/tmp/sparse.png", | |
| expectedIndex: 1, | |
| }, | |
| { | |
| name: "type-only", | |
| input: { media: [{ contentType: "image/png" }] }, | |
| expectedPath: undefined, | |
| }, | |
| { | |
| name: "media-only", | |
| input: { Body: "", media: [{ path: "/tmp/media-only.png", kind: "image" as const }] }, | |
| expectedPath: "/tmp/media-only.png", | |
| }, | |
| ])("dual-emits $name hook media from canonical facts", (testCase) => { | |
| const ctx = finalizeInboundContextForSdk(makeInboundCtx(testCase.input)); | |
| const { event } = toPluginInboundClaimPair(deriveInboundMessageHookContext(ctx)); | |
| const expectedIndex = "expectedIndex" in testCase ? (testCase.expectedIndex ?? 0) : 0; | |
| expect(event.media?.[expectedIndex]?.path).toBe(testCase.expectedPath); | |
| expect(event.metadata?.mediaPath).toBe(testCase.expectedPath); | |
| }); | |
| it("withholds pending remote media from every inbound hook event", () => { | |
| const canonical = { | |
| ...deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| media: [ | |
| { | |
| path: "/remote-host/photo.jpg", | |
| url: "media://remote/photo.jpg", | |
| contentType: "image/jpeg", | |
| }, | |
| ], | |
| }), | |
| ), | |
| mediaStagingPending: true, | |
| }; | |
| const expectedOriginalMedia = [ | |
| { | |
| path: "/remote-host/photo.jpg", | |
| url: "media://remote/photo.jpg", | |
| contentType: "image/jpeg", | |
| kind: "image", | |
| }, | |
| ]; | |
| for (const event of [ | |
| toPluginInboundClaimPair(canonical).event, | |
| toPluginMessageReceivedEvent(canonical), | |
| toInternalMessageReceivedContext(canonical), | |
| toInternalMessageTranscribedContext(canonical, {}), | |
| toInternalMessagePreprocessedContext(canonical, {}), | |
| ]) { | |
| expect(event.media).toBeUndefined(); | |
| expect(event.originalMedia).toEqual(expectedOriginalMedia); | |
| expect(event.mediaStagingPending).toBe(true); | |
| } | |
| expect(toPluginInboundClaimPair(canonical).event.metadata?.mediaPath).toBeUndefined(); | |
| expect(toPluginMessageReceivedEvent(canonical).metadata?.mediaPath).toBeUndefined(); | |
| expect(toInternalMessageReceivedContext(canonical).metadata?.mediaPath).toBeUndefined(); | |
| }); | |
| it("projects retained facts into hook media metadata", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| media: [ | |
| { path: "/tmp/tree.jpg", contentType: "image/jpeg" }, | |
| { url: "https://example.test/ramp.jpg", kind: "image" }, | |
| ], | |
| }), | |
| ); | |
| expect(canonical).toMatchObject({ | |
| media: [ | |
| { path: "/tmp/tree.jpg", contentType: "image/jpeg", kind: "image" }, | |
| { url: "https://example.test/ramp.jpg", kind: "image" }, | |
| ], | |
| mediaPath: "/tmp/tree.jpg", | |
| mediaUrl: "/tmp/tree.jpg", | |
| mediaType: "image/jpeg", | |
| mediaPaths: ["/tmp/tree.jpg"], | |
| mediaUrls: ["/tmp/tree.jpg", "https://example.test/ramp.jpg"], | |
| mediaTypes: ["image/jpeg", "image"], | |
| }); | |
| const staged = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| media: [ | |
| { | |
| path: "/tmp/staged/tree.jpg", | |
| url: "/tmp/staged/tree.jpg", | |
| contentType: "image/jpeg", | |
| workspaceDir: "/tmp/staged", | |
| }, | |
| ], | |
| }), | |
| ); | |
| expect(staged).toMatchObject({ | |
| mediaPath: "/tmp/staged/tree.jpg", | |
| mediaUrl: "/tmp/staged/tree.jpg", | |
| mediaPaths: ["/tmp/staged/tree.jpg"], | |
| mediaUrls: ["/tmp/staged/tree.jpg"], | |
| }); | |
| }); | |
| it("uses complete staged compatibility projections in hook media metadata", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| finalizeInboundContextForSdk( | |
| makeInboundCtx({ | |
| media: [ | |
| { path: "/remote/tree.jpg", contentType: "image/jpeg", messageId: "tree" }, | |
| { path: "/remote/ramp.jpg", contentType: "image/jpeg", messageId: "ramp" }, | |
| ], | |
| MediaPaths: ["/tmp/staged/tree.jpg", "/tmp/staged/ramp.jpg"], | |
| MediaUrls: ["file:///tmp/staged/tree.jpg", "file:///tmp/staged/ramp.jpg"], | |
| MediaTypes: ["image/jpeg", "image/jpeg"], | |
| MediaStaged: true, | |
| }), | |
| ), | |
| ); | |
| expect(canonical).toMatchObject({ | |
| mediaPath: "/tmp/staged/tree.jpg", | |
| mediaUrl: "file:///tmp/staged/tree.jpg", | |
| mediaType: "image/jpeg", | |
| mediaPaths: ["/tmp/staged/tree.jpg", "/tmp/staged/ramp.jpg"], | |
| mediaUrls: ["file:///tmp/staged/tree.jpg", "file:///tmp/staged/ramp.jpg"], | |
| mediaTypes: ["image/jpeg", "image/jpeg"], | |
| }); | |
| }); | |
| it("maps canonical inbound context to plugin/internal received payloads", () => { | |
| const trace: DiagnosticTraceContext = { | |
| traceId: "11111111111111111111111111111111", | |
| spanId: "2222222222222222", | |
| parentSpanId: "3333333333333333", | |
| }; | |
| const canonical = { | |
| ...deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| TopicName: "Deployments", | |
| media: [ | |
| { | |
| path: "/tmp/audio.ogg", | |
| url: "https://cdn.example.com/audio.ogg", | |
| contentType: "audio/ogg", | |
| }, | |
| { | |
| path: "/tmp/photo.jpg", | |
| url: "https://cdn.example.com/photo.jpg", | |
| contentType: "image/jpeg", | |
| }, | |
| ], | |
| }), | |
| ), | |
| runId: "run-1", | |
| trace, | |
| callDepth: 2, | |
| }; | |
| const pluginContext = toPluginMessageContext(canonical); | |
| const receivedEvent = toPluginMessageReceivedEvent(canonical); | |
| const { metadata: receivedMetadata, ...receivedEventBase } = receivedEvent; | |
| expect(pluginContext).toEqual({ | |
| channelId: "demo-chat", | |
| accountId: "acc-1", | |
| conversationId: "demo-chat:chat:456", | |
| sessionKey: "session-1", | |
| runId: "run-1", | |
| messageId: "msg-1", | |
| senderId: "sender-1", | |
| trace, | |
| traceId: "11111111111111111111111111111111", | |
| spanId: "2222222222222222", | |
| parentSpanId: "3333333333333333", | |
| callDepth: 2, | |
| }); | |
| expect(pluginContext.trace).not.toBe(trace); | |
| expect(pluginContext.trace).toEqual(trace); | |
| expect(Object.isFrozen(pluginContext.trace)).toBe(true); | |
| expect(receivedEvent.trace).not.toBe(trace); | |
| expect(receivedEvent.trace).toEqual(trace); | |
| expect(Object.isFrozen(receivedEvent.trace)).toBe(true); | |
| expect(receivedEventBase).toEqual({ | |
| from: "demo-chat:user:123", | |
| content: "commands-body", | |
| timestamp: 1710000000, | |
| threadId: 42, | |
| messageId: "msg-1", | |
| senderId: "sender-1", | |
| sessionKey: "session-1", | |
| runId: "run-1", | |
| media: [ | |
| { | |
| path: "/tmp/audio.ogg", | |
| url: "https://cdn.example.com/audio.ogg", | |
| contentType: "audio/ogg", | |
| kind: "audio", | |
| }, | |
| { | |
| path: "/tmp/photo.jpg", | |
| url: "https://cdn.example.com/photo.jpg", | |
| contentType: "image/jpeg", | |
| kind: "image", | |
| }, | |
| ], | |
| trace: receivedEvent.trace, | |
| traceId: "11111111111111111111111111111111", | |
| spanId: "2222222222222222", | |
| parentSpanId: "3333333333333333", | |
| }); | |
| expect(receivedMetadata?.messageId).toBe("msg-1"); | |
| expect(receivedMetadata?.senderName).toBe("User One"); | |
| expect(receivedMetadata?.threadId).toBe(42); | |
| expect(receivedMetadata?.topicName).toBe("Deployments"); | |
| expect(receivedMetadata?.mediaPath).toBe("/tmp/audio.ogg"); | |
| expect(receivedMetadata?.mediaUrl).toBe("https://cdn.example.com/audio.ogg"); | |
| expect(receivedMetadata?.mediaType).toBe("audio/ogg"); | |
| expect(receivedMetadata?.mediaPaths).toEqual(["/tmp/audio.ogg", "/tmp/photo.jpg"]); | |
| expect(receivedMetadata?.mediaUrls).toEqual([ | |
| "https://cdn.example.com/audio.ogg", | |
| "https://cdn.example.com/photo.jpg", | |
| ]); | |
| expect(receivedMetadata?.mediaTypes).toEqual(["audio/ogg", "image/jpeg"]); | |
| const internalReceived = toInternalMessageReceivedContext(canonical); | |
| const { metadata: internalMetadata, ...internalReceivedBase } = internalReceived; | |
| expect(internalReceivedBase).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", | |
| media: receivedEvent.media, | |
| }); | |
| expect(internalMetadata?.senderUsername).toBe("userone"); | |
| expect(internalMetadata?.senderE164).toBe("+15551234567"); | |
| expect(internalMetadata?.topicName).toBe("Deployments"); | |
| expect(internalMetadata?.mediaPath).toBe("/tmp/audio.ogg"); | |
| expect(internalMetadata?.mediaUrl).toBe("https://cdn.example.com/audio.ogg"); | |
| expect(internalMetadata?.mediaType).toBe("audio/ogg"); | |
| expect(internalMetadata?.mediaPaths).toEqual(["/tmp/audio.ogg", "/tmp/photo.jpg"]); | |
| expect(internalMetadata?.mediaUrls).toEqual([ | |
| "https://cdn.example.com/audio.ogg", | |
| "https://cdn.example.com/photo.jpg", | |
| ]); | |
| expect(internalMetadata?.mediaTypes).toEqual(["audio/ogg", "image/jpeg"]); | |
| }); | |
| it("passes frozen trace copies to inbound claim and sent plugin hooks", () => { | |
| const trace: DiagnosticTraceContext = { | |
| traceId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | |
| spanId: "bbbbbbbbbbbbbbbb", | |
| parentSpanId: "cccccccccccccccc", | |
| traceFlags: "01", | |
| }; | |
| const inbound = { | |
| ...deriveInboundMessageHookContext(makeInboundCtx()), | |
| trace, | |
| }; | |
| const { context: inboundContext, event: inboundEvent } = toPluginInboundClaimPair(inbound); | |
| expect(inboundContext.trace).not.toBe(trace); | |
| expect(inboundContext.trace).toEqual(trace); | |
| expect(Object.isFrozen(inboundContext.trace)).toBe(true); | |
| expect(inboundEvent.trace).not.toBe(trace); | |
| expect(inboundEvent.trace).toEqual(trace); | |
| expect(Object.isFrozen(inboundEvent.trace)).toBe(true); | |
| const sent = buildCanonicalSentMessageHookContext({ | |
| to: "demo-chat:chat:456", | |
| content: "reply", | |
| success: true, | |
| channelId: "demo-chat", | |
| trace, | |
| }); | |
| const sentEvent = toPluginMessageSentEvent(sent); | |
| expect(sentEvent.trace).not.toBe(trace); | |
| expect(sentEvent.trace).toEqual(trace); | |
| expect(Object.isFrozen(sentEvent.trace)).toBe(true); | |
| }); | |
| 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(toPluginInboundClaimPair(canonical).context).toEqual({ | |
| channelId: "claim-chat", | |
| accountId: "acc-1", | |
| conversationId: "channel:123456789012345678", | |
| sessionKey: "session-1", | |
| parentConversationId: undefined, | |
| senderId: "sender-1", | |
| messageId: "msg-1", | |
| runId: undefined, | |
| trace: undefined, | |
| traceId: undefined, | |
| spanId: undefined, | |
| parentSpanId: undefined, | |
| callDepth: undefined, | |
| }); | |
| }); | |
| it("does not fall back when a channel rejects inbound claim resolution", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| Provider: "claim-chat", | |
| Surface: "claim-chat", | |
| OriginatingChannel: "claim-chat", | |
| From: undefined, | |
| To: "channel:room-123", | |
| OriginatingTo: "channel:room-123", | |
| GroupChannel: undefined, | |
| GroupSubject: undefined, | |
| }), | |
| ); | |
| const { context, event } = toPluginInboundClaimPair(canonical); | |
| expect(context.conversationId).toBeUndefined(); | |
| expect(event.conversationId).toBeUndefined(); | |
| }); | |
| it("passes thread parent ids to channel plugin claim resolvers", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| Provider: "thread-claim-chat", | |
| Surface: "thread-claim-chat", | |
| OriginatingChannel: "thread-claim-chat", | |
| To: "channel:1510164477642014740", | |
| OriginatingTo: "channel:1510164477642014740", | |
| MessageThreadId: "1510164477642014740", | |
| ThreadParentId: "1510164477642014999", | |
| GroupChannel: "thread", | |
| GroupSubject: "guild", | |
| }), | |
| ); | |
| expect(toPluginInboundClaimPair(canonical).context).toMatchObject({ | |
| channelId: "thread-claim-chat", | |
| conversationId: "1510164477642014740", | |
| parentConversationId: "channel:1510164477642014999", | |
| }); | |
| }); | |
| 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(toPluginInboundClaimPair(canonical).context).toEqual({ | |
| channelId: "claim-chat", | |
| accountId: "acc-1", | |
| conversationId: "user:1177378744822943744", | |
| sessionKey: "session-1", | |
| parentConversationId: undefined, | |
| senderId: "sender-1", | |
| messageId: "msg-1", | |
| runId: undefined, | |
| trace: undefined, | |
| traceId: undefined, | |
| spanId: undefined, | |
| parentSpanId: undefined, | |
| callDepth: undefined, | |
| }); | |
| }); | |
| 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); | |
| expect(transcribed.media).toEqual([ | |
| expect.objectContaining({ path: "/tmp/audio.ogg", contentType: "audio/ogg" }), | |
| ]); | |
| 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); | |
| expect(preprocessed.media).toEqual(transcribed.media); | |
| }); | |
| 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", | |
| sessionKey: "session-1", | |
| messageId: "out-1", | |
| runId: "run-out-1", | |
| isGroup: true, | |
| groupId: "demo-chat:chat:456", | |
| }); | |
| expect(toPluginMessageContext(canonical)).toEqual({ | |
| channelId: "demo-chat", | |
| accountId: "acc-1", | |
| conversationId: "demo-chat:chat:456", | |
| sessionKey: "session-1", | |
| runId: "run-out-1", | |
| messageId: "out-1", | |
| }); | |
| expect(toPluginMessageSentEvent(canonical)).toEqual({ | |
| to: "demo-chat:chat:456", | |
| content: "reply", | |
| success: false, | |
| messageId: "out-1", | |
| sessionKey: "session-1", | |
| runId: "run-out-1", | |
| 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", | |
| }); | |
| }); | |
| it("projects normalized location and stable provider update identity", () => { | |
| const canonical = deriveInboundMessageHookContext( | |
| makeInboundCtx({ | |
| LocationLat: 43.8376, | |
| LocationLon: 18.4534, | |
| LocationAccuracy: 12, | |
| LocationSource: "live", | |
| LocationIsLive: true, | |
| LocationLivePeriodSeconds: 900, | |
| ProviderUpdateId: "9002", | |
| ProviderUpdateKind: "edited_message", | |
| ProviderMessageTimestamp: 1_786_094_460_000, | |
| ProviderEditTimestamp: 1_786_094_520_000, | |
| }), | |
| ); | |
| const { event } = toPluginInboundClaimPair(canonical); | |
| expect(event.location).toEqual({ | |
| latitude: 43.8376, | |
| longitude: 18.4534, | |
| accuracy: 12, | |
| source: "live", | |
| isLive: true, | |
| livePeriodSeconds: 900, | |
| }); | |
| expect(event.providerUpdate).toEqual({ | |
| id: "9002", | |
| kind: "edited_message", | |
| messageId: "msg-1", | |
| messageTimestamp: 1_786_094_460_000, | |
| editedTimestamp: 1_786_094_520_000, | |
| }); | |
| expect(toPluginMessageReceivedEvent(canonical)).toMatchObject({ | |
| location: event.location, | |
| providerUpdate: event.providerUpdate, | |
| }); | |
| }); | |
| }); | |