@moltzap/client 2026.407.0 → 2026.413.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel-core.d.ts +98 -0
- package/dist/channel-core.d.ts.map +1 -0
- package/dist/channel-core.js +131 -0
- package/dist/channel-core.js.map +1 -0
- package/dist/channel-core.test.d.ts +2 -0
- package/dist/channel-core.test.d.ts.map +1 -0
- package/dist/channel-core.test.js +425 -0
- package/dist/channel-core.test.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/service.d.ts +43 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +106 -14
- package/dist/service.js.map +1 -1
- package/dist/service.test.d.ts +2 -0
- package/dist/service.test.d.ts.map +1 -0
- package/dist/service.test.js +311 -0
- package/dist/service.test.js.map +1 -0
- package/dist/test-utils/channel-service-fixture.d.ts +43 -0
- package/dist/test-utils/channel-service-fixture.d.ts.map +1 -0
- package/dist/test-utils/channel-service-fixture.js +106 -0
- package/dist/test-utils/channel-service-fixture.js.map +1 -0
- package/dist/test-utils/index.d.ts +5 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/index.js +18 -0
- package/dist/test-utils/index.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared message-enrichment helper for MoltZap channel adapters.
|
|
3
|
+
*/
|
|
4
|
+
import type { Message } from "@moltzap/protocol";
|
|
5
|
+
import type { CrossConversationEntry } from "./service.js";
|
|
6
|
+
import type { WsClientLogger } from "./ws-client.js";
|
|
7
|
+
export interface EnrichedSender {
|
|
8
|
+
type: "agent" | "user";
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
export interface EnrichedConversationMeta {
|
|
13
|
+
type: "dm" | "group";
|
|
14
|
+
name?: string;
|
|
15
|
+
/** "type:id" strings (e.g. "agent:uuid"). */
|
|
16
|
+
participants: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ContextBlocks {
|
|
19
|
+
groupMetadata?: EnrichedConversationMeta;
|
|
20
|
+
crossConversation?: CrossConversationEntry[];
|
|
21
|
+
}
|
|
22
|
+
export interface EnrichedInboundMessage {
|
|
23
|
+
id: string;
|
|
24
|
+
conversationId: string;
|
|
25
|
+
sender: EnrichedSender;
|
|
26
|
+
/** Text parts joined with newlines. Non-text parts dropped. */
|
|
27
|
+
text: string;
|
|
28
|
+
isFromMe: boolean;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
replyToId?: string;
|
|
31
|
+
conversationMeta?: EnrichedConversationMeta;
|
|
32
|
+
contextBlocks: ContextBlocks;
|
|
33
|
+
}
|
|
34
|
+
/** The subset of MoltZapService that MoltZapChannelCore needs. */
|
|
35
|
+
export interface ChannelService {
|
|
36
|
+
readonly ownAgentId: string | undefined;
|
|
37
|
+
on(event: "message", handler: (msg: Message) => void): void;
|
|
38
|
+
on(event: "disconnect", handler: () => void): void;
|
|
39
|
+
on(event: "reconnect", handler: () => void): void;
|
|
40
|
+
connect(): Promise<unknown>;
|
|
41
|
+
close(): void;
|
|
42
|
+
send(conversationId: string, text: string): Promise<void>;
|
|
43
|
+
getConversation(convId: string): {
|
|
44
|
+
type: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
participants: string[];
|
|
47
|
+
} | undefined;
|
|
48
|
+
getAgentName(agentId: string): string | undefined;
|
|
49
|
+
resolveAgentName(agentId: string): Promise<string>;
|
|
50
|
+
peekContextEntries(currentConvId: string, opts?: {
|
|
51
|
+
maxConversations?: number;
|
|
52
|
+
maxMessagesPerConv?: number;
|
|
53
|
+
}): {
|
|
54
|
+
entries: CrossConversationEntry[];
|
|
55
|
+
commit: () => void;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export interface ChannelCoreOptions {
|
|
59
|
+
service: ChannelService;
|
|
60
|
+
logger?: WsClientLogger;
|
|
61
|
+
}
|
|
62
|
+
export type InboundHandler = (msg: EnrichedInboundMessage) => Promise<void> | void;
|
|
63
|
+
/**
|
|
64
|
+
* Wraps a MoltZapService with message enrichment, dispatch-chain ordering,
|
|
65
|
+
* and a send helper.
|
|
66
|
+
*
|
|
67
|
+
* Do NOT construct multiple cores over the same service — getContextEntries()
|
|
68
|
+
* is side-effectful (advances per-conversation markers), so a second core
|
|
69
|
+
* would consume entries the first expected.
|
|
70
|
+
*/
|
|
71
|
+
export declare class MoltZapChannelCore {
|
|
72
|
+
private readonly service;
|
|
73
|
+
private readonly logger;
|
|
74
|
+
private connected;
|
|
75
|
+
private inboundHandler;
|
|
76
|
+
private dispatchChain;
|
|
77
|
+
private disconnectHandlers;
|
|
78
|
+
private reconnectHandlers;
|
|
79
|
+
constructor(opts: ChannelCoreOptions);
|
|
80
|
+
/** Replaces any previous handler. */
|
|
81
|
+
onInbound(handler: InboundHandler): void;
|
|
82
|
+
onDisconnect(handler: () => void): void;
|
|
83
|
+
onReconnect(handler: () => void): void;
|
|
84
|
+
connect(): Promise<void>;
|
|
85
|
+
disconnect(): Promise<void>;
|
|
86
|
+
isConnected(): boolean;
|
|
87
|
+
sendReply(conversationId: string, text: string): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Stateless enrichment helper. Falls back to `sender.id` if
|
|
90
|
+
* `resolveAgentName` throws (e.g. service not yet connected).
|
|
91
|
+
*/
|
|
92
|
+
static enrichMessage(service: ChannelService, message: Message): Promise<{
|
|
93
|
+
enriched: EnrichedInboundMessage;
|
|
94
|
+
commitContext?: () => void;
|
|
95
|
+
}>;
|
|
96
|
+
private handleInbound;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=channel-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-core.d.ts","sourceRoot":"","sources":["../src/channel-core.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,iBAAiB,CAAC,EAAE,sBAAsB,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,wBAAwB,CAAC;IAC5C,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IAC5D,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACnD,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAClD,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,eAAe,CACb,MAAM,EAAE,MAAM,GACb;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,SAAS,CAAC;IACvE,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAClD,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,kBAAkB,CAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,GAChE;QAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CAC9D;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,MAAM,MAAM,cAAc,GAAG,CAC3B,GAAG,EAAE,sBAAsB,KACxB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAkB1B;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,iBAAiB,CAAyB;gBAEtC,IAAI,EAAE,kBAAkB;IA0BpC,qCAAqC;IACrC,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAIxC,YAAY,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIvC,WAAW,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIhC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAKxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAKjC,WAAW,IAAI,OAAO;IAIhB,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE;;;OAGG;WACU,aAAa,CACxB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;QACT,QAAQ,EAAE,sBAAsB,CAAC;QACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;KAC5B,CAAC;YAwDY,aAAa;CAS5B"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared message-enrichment helper for MoltZap channel adapters.
|
|
3
|
+
*/
|
|
4
|
+
const noopLogger = {
|
|
5
|
+
info: () => { },
|
|
6
|
+
warn: () => { },
|
|
7
|
+
error: () => { },
|
|
8
|
+
};
|
|
9
|
+
function extractTextContent(parts) {
|
|
10
|
+
return parts
|
|
11
|
+
.filter((p) => p.type === "text")
|
|
12
|
+
.map((p) => p.text)
|
|
13
|
+
.join("\n");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a MoltZapService with message enrichment, dispatch-chain ordering,
|
|
17
|
+
* and a send helper.
|
|
18
|
+
*
|
|
19
|
+
* Do NOT construct multiple cores over the same service — getContextEntries()
|
|
20
|
+
* is side-effectful (advances per-conversation markers), so a second core
|
|
21
|
+
* would consume entries the first expected.
|
|
22
|
+
*/
|
|
23
|
+
export class MoltZapChannelCore {
|
|
24
|
+
service;
|
|
25
|
+
logger;
|
|
26
|
+
connected = false;
|
|
27
|
+
inboundHandler = null;
|
|
28
|
+
dispatchChain = Promise.resolve();
|
|
29
|
+
disconnectHandlers = [];
|
|
30
|
+
reconnectHandlers = [];
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
this.service = opts.service;
|
|
33
|
+
this.logger = opts.logger ?? noopLogger;
|
|
34
|
+
this.service.on("message", (message) => {
|
|
35
|
+
this.dispatchChain = this.dispatchChain
|
|
36
|
+
.then(() => this.handleInbound(message))
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
this.logger.error({ messageId: message.id, err }, "MoltZapChannelCore: inbound handler threw");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
this.service.on("disconnect", () => {
|
|
42
|
+
this.connected = false;
|
|
43
|
+
for (const h of this.disconnectHandlers)
|
|
44
|
+
h();
|
|
45
|
+
});
|
|
46
|
+
this.service.on("reconnect", () => {
|
|
47
|
+
this.connected = true;
|
|
48
|
+
for (const h of this.reconnectHandlers)
|
|
49
|
+
h();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Replaces any previous handler. */
|
|
53
|
+
onInbound(handler) {
|
|
54
|
+
this.inboundHandler = handler;
|
|
55
|
+
}
|
|
56
|
+
onDisconnect(handler) {
|
|
57
|
+
this.disconnectHandlers.push(handler);
|
|
58
|
+
}
|
|
59
|
+
onReconnect(handler) {
|
|
60
|
+
this.reconnectHandlers.push(handler);
|
|
61
|
+
}
|
|
62
|
+
async connect() {
|
|
63
|
+
await this.service.connect();
|
|
64
|
+
this.connected = true;
|
|
65
|
+
}
|
|
66
|
+
async disconnect() {
|
|
67
|
+
this.service.close();
|
|
68
|
+
this.connected = false;
|
|
69
|
+
}
|
|
70
|
+
isConnected() {
|
|
71
|
+
return this.connected;
|
|
72
|
+
}
|
|
73
|
+
async sendReply(conversationId, text) {
|
|
74
|
+
await this.service.send(conversationId, text);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Stateless enrichment helper. Falls back to `sender.id` if
|
|
78
|
+
* `resolveAgentName` throws (e.g. service not yet connected).
|
|
79
|
+
*/
|
|
80
|
+
static async enrichMessage(service, message) {
|
|
81
|
+
const convMeta = service.getConversation(message.conversationId);
|
|
82
|
+
const senderName = service.getAgentName(message.sender.id) ??
|
|
83
|
+
(await service
|
|
84
|
+
.resolveAgentName(message.sender.id)
|
|
85
|
+
.catch(() => message.sender.id));
|
|
86
|
+
const text = extractTextContent(message.parts);
|
|
87
|
+
const isFromMe = service.ownAgentId !== undefined &&
|
|
88
|
+
message.sender.id === service.ownAgentId;
|
|
89
|
+
const conversationMeta = convMeta
|
|
90
|
+
? {
|
|
91
|
+
type: convMeta.type === "group" ? "group" : "dm",
|
|
92
|
+
name: convMeta.name,
|
|
93
|
+
participants: convMeta.participants,
|
|
94
|
+
}
|
|
95
|
+
: undefined;
|
|
96
|
+
const contextBlocks = {};
|
|
97
|
+
if (conversationMeta?.type === "group") {
|
|
98
|
+
contextBlocks.groupMetadata = conversationMeta;
|
|
99
|
+
}
|
|
100
|
+
const { entries, commit } = service.peekContextEntries(message.conversationId);
|
|
101
|
+
if (entries.length > 0) {
|
|
102
|
+
contextBlocks.crossConversation = entries;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
enriched: {
|
|
106
|
+
id: message.id,
|
|
107
|
+
conversationId: message.conversationId,
|
|
108
|
+
sender: {
|
|
109
|
+
type: message.sender.type === "user" ? "user" : "agent",
|
|
110
|
+
id: message.sender.id,
|
|
111
|
+
name: senderName,
|
|
112
|
+
},
|
|
113
|
+
text,
|
|
114
|
+
isFromMe,
|
|
115
|
+
createdAt: message.createdAt,
|
|
116
|
+
replyToId: message.replyToId,
|
|
117
|
+
conversationMeta,
|
|
118
|
+
contextBlocks,
|
|
119
|
+
},
|
|
120
|
+
commitContext: entries.length > 0 ? commit : undefined,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async handleInbound(message) {
|
|
124
|
+
if (!this.inboundHandler)
|
|
125
|
+
return;
|
|
126
|
+
const { enriched, commitContext } = await MoltZapChannelCore.enrichMessage(this.service, message);
|
|
127
|
+
await this.inboundHandler(enriched);
|
|
128
|
+
commitContext?.();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=channel-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-core.js","sourceRoot":"","sources":["../src/channel-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-core.test.d.ts","sourceRoot":"","sources":["../src/channel-core.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { MoltZapChannelCore, } from "./index.js";
|
|
3
|
+
import { createFakeChannelService, buildMessage, flushDispatchChain, } from "./test-utils/index.js";
|
|
4
|
+
function customSetup() {
|
|
5
|
+
const fake = createFakeChannelService({ ownAgentId: "agent-self" });
|
|
6
|
+
const received = [];
|
|
7
|
+
const errorSpy = vi.fn();
|
|
8
|
+
const core = new MoltZapChannelCore({
|
|
9
|
+
service: fake.service,
|
|
10
|
+
logger: { info: () => { }, warn: () => { }, error: errorSpy },
|
|
11
|
+
});
|
|
12
|
+
core.onInbound((m) => {
|
|
13
|
+
received.push(m);
|
|
14
|
+
});
|
|
15
|
+
return { fake, core, received, errorSpy };
|
|
16
|
+
}
|
|
17
|
+
/** Stub out getAgentName on the fixture's service so resolveAgentName is the only path. */
|
|
18
|
+
function forceResolveAgentNamePath(fake) {
|
|
19
|
+
fake.service.getAgentName = () => undefined;
|
|
20
|
+
}
|
|
21
|
+
describe("MoltZapChannelCore", () => {
|
|
22
|
+
let fake;
|
|
23
|
+
let service;
|
|
24
|
+
let core;
|
|
25
|
+
let inbound;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
fake = createFakeChannelService({ ownAgentId: "agent-self" });
|
|
28
|
+
service = fake.service;
|
|
29
|
+
core = new MoltZapChannelCore({ service });
|
|
30
|
+
inbound = [];
|
|
31
|
+
core.onInbound((msg) => {
|
|
32
|
+
inbound.push(msg);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("lifecycle", () => {
|
|
36
|
+
it("connect() delegates to service and sets connected", async () => {
|
|
37
|
+
expect(core.isConnected()).toBe(false);
|
|
38
|
+
await core.connect();
|
|
39
|
+
expect(fake.state.connectCalls.count).toBe(1);
|
|
40
|
+
expect(core.isConnected()).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it("disconnect() closes the service and clears the connected flag", async () => {
|
|
43
|
+
await core.connect();
|
|
44
|
+
await core.disconnect();
|
|
45
|
+
expect(fake.state.closeCalls.count).toBe(1);
|
|
46
|
+
expect(core.isConnected()).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
it("disconnect event from the service clears the connected flag", async () => {
|
|
49
|
+
await core.connect();
|
|
50
|
+
fake.emit.disconnect();
|
|
51
|
+
expect(core.isConnected()).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it("reconnect event from the service sets the connected flag", () => {
|
|
54
|
+
fake.emit.reconnect();
|
|
55
|
+
expect(core.isConnected()).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it("onDisconnect handlers fire on disconnect event", async () => {
|
|
58
|
+
const spy = vi.fn();
|
|
59
|
+
core.onDisconnect(spy);
|
|
60
|
+
await core.connect();
|
|
61
|
+
fake.emit.disconnect();
|
|
62
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
63
|
+
});
|
|
64
|
+
it("onReconnect handlers fire on reconnect event", () => {
|
|
65
|
+
const spy = vi.fn();
|
|
66
|
+
core.onReconnect(spy);
|
|
67
|
+
fake.emit.reconnect();
|
|
68
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("inbound message enrichment", () => {
|
|
72
|
+
it("maps a MoltZap Message to EnrichedInboundMessage", async () => {
|
|
73
|
+
fake.state.setConversation("conv-1", {
|
|
74
|
+
type: "dm",
|
|
75
|
+
name: "alice-dm",
|
|
76
|
+
participants: ["agent:agent-alice", "agent:agent-self"],
|
|
77
|
+
});
|
|
78
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
79
|
+
fake.emit.message(buildMessage({
|
|
80
|
+
id: "msg-abc",
|
|
81
|
+
conversationId: "conv-1",
|
|
82
|
+
sender: { type: "agent", id: "agent-alice" },
|
|
83
|
+
parts: [{ type: "text", text: "hi there" }],
|
|
84
|
+
createdAt: "2026-04-10T13:00:00.000Z",
|
|
85
|
+
}));
|
|
86
|
+
await flushDispatchChain();
|
|
87
|
+
expect(inbound).toHaveLength(1);
|
|
88
|
+
const enriched = inbound[0];
|
|
89
|
+
expect(enriched).toMatchObject({
|
|
90
|
+
id: "msg-abc",
|
|
91
|
+
conversationId: "conv-1",
|
|
92
|
+
sender: { type: "agent", id: "agent-alice", name: "Alice" },
|
|
93
|
+
text: "hi there",
|
|
94
|
+
isFromMe: false,
|
|
95
|
+
createdAt: "2026-04-10T13:00:00.000Z",
|
|
96
|
+
});
|
|
97
|
+
expect(enriched.conversationMeta).toMatchObject({
|
|
98
|
+
type: "dm",
|
|
99
|
+
name: "alice-dm",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it("resolves sender name from getAgentName cache when present", async () => {
|
|
103
|
+
fake.state.setAgentName("agent-alice", "Alice (cached)");
|
|
104
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
105
|
+
fake.emit.message(buildMessage());
|
|
106
|
+
await flushDispatchChain();
|
|
107
|
+
expect(inbound[0].sender.name).toBe("Alice (cached)");
|
|
108
|
+
expect(fake.state.resolveAgentNameCallCount("agent-alice")).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
it("falls back to resolveAgentName when getAgentName returns undefined", async () => {
|
|
111
|
+
const { fake, received } = customSetup();
|
|
112
|
+
forceResolveAgentNamePath(fake);
|
|
113
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
114
|
+
fake.state.setAgentName("agent-alice", "Alice (via resolve)");
|
|
115
|
+
fake.emit.message(buildMessage());
|
|
116
|
+
await flushDispatchChain();
|
|
117
|
+
expect(received[0].sender.name).toBe("Alice (via resolve)");
|
|
118
|
+
expect(fake.state.resolveAgentNameCallCount("agent-alice")).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
it("falls back to sender.id when both name lookups fail", async () => {
|
|
121
|
+
const { fake, received } = customSetup();
|
|
122
|
+
forceResolveAgentNamePath(fake);
|
|
123
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
124
|
+
fake.emit.message(buildMessage({ sender: { type: "agent", id: "agent-unknown" } }));
|
|
125
|
+
await flushDispatchChain();
|
|
126
|
+
expect(received[0].sender.name).toBe("agent-unknown");
|
|
127
|
+
});
|
|
128
|
+
it("swallows resolveAgentName errors and falls back to sender.id", async () => {
|
|
129
|
+
const { fake, received } = customSetup();
|
|
130
|
+
forceResolveAgentNamePath(fake);
|
|
131
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
132
|
+
fake.state.setResolveAgentNameFailure("agent-broken", new Error("network down"));
|
|
133
|
+
fake.emit.message(buildMessage({ sender: { type: "agent", id: "agent-broken" } }));
|
|
134
|
+
await flushDispatchChain();
|
|
135
|
+
expect(received[0].sender.name).toBe("agent-broken");
|
|
136
|
+
});
|
|
137
|
+
it("concatenates multi-text-part messages with newlines", async () => {
|
|
138
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
139
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
140
|
+
fake.emit.message(buildMessage({
|
|
141
|
+
parts: [
|
|
142
|
+
{ type: "text", text: "line one" },
|
|
143
|
+
{ type: "text", text: "line two" },
|
|
144
|
+
],
|
|
145
|
+
}));
|
|
146
|
+
await flushDispatchChain();
|
|
147
|
+
expect(inbound[0].text).toBe("line one\nline two");
|
|
148
|
+
});
|
|
149
|
+
it("ignores non-text parts when building text", async () => {
|
|
150
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
151
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
152
|
+
fake.emit.message(buildMessage({
|
|
153
|
+
parts: [
|
|
154
|
+
{ type: "text", text: "caption" },
|
|
155
|
+
{ type: "image", url: "https://example.com/pic.png" },
|
|
156
|
+
],
|
|
157
|
+
}));
|
|
158
|
+
await flushDispatchChain();
|
|
159
|
+
expect(inbound[0].text).toBe("caption");
|
|
160
|
+
});
|
|
161
|
+
it("sets isFromMe=true when sender matches ownAgentId", async () => {
|
|
162
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
163
|
+
fake.emit.message(buildMessage({ sender: { type: "agent", id: "agent-self" } }));
|
|
164
|
+
await flushDispatchChain();
|
|
165
|
+
expect(inbound[0].isFromMe).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it("forwards replyToId from the message frame", async () => {
|
|
168
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
169
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
170
|
+
fake.emit.message(buildMessage({ replyToId: "msg-parent-123" }));
|
|
171
|
+
await flushDispatchChain();
|
|
172
|
+
expect(inbound[0].replyToId).toBe("msg-parent-123");
|
|
173
|
+
});
|
|
174
|
+
it("logs and swallows errors from the inbound handler", async () => {
|
|
175
|
+
const { fake, errorSpy, core } = customSetup();
|
|
176
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
177
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
178
|
+
let handlerShouldThrow = true;
|
|
179
|
+
const received = [];
|
|
180
|
+
// Replace the setup's default capture handler with one that can throw.
|
|
181
|
+
core.onInbound((m) => {
|
|
182
|
+
if (handlerShouldThrow) {
|
|
183
|
+
throw new Error("handler boom");
|
|
184
|
+
}
|
|
185
|
+
received.push(m);
|
|
186
|
+
});
|
|
187
|
+
fake.emit.message(buildMessage({ id: "msg-1" }));
|
|
188
|
+
await flushDispatchChain();
|
|
189
|
+
expect(received).toHaveLength(0);
|
|
190
|
+
expect(errorSpy).toHaveBeenCalledOnce();
|
|
191
|
+
// Recovery: subsequent message lands cleanly.
|
|
192
|
+
handlerShouldThrow = false;
|
|
193
|
+
fake.emit.message(buildMessage({ id: "msg-2" }));
|
|
194
|
+
await flushDispatchChain();
|
|
195
|
+
expect(received).toHaveLength(1);
|
|
196
|
+
expect(received[0].id).toBe("msg-2");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
describe("dispatch chain ordering", () => {
|
|
200
|
+
it("serializes handlers so message order is preserved across async resolution", async () => {
|
|
201
|
+
const { fake, received } = customSetup();
|
|
202
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
203
|
+
forceResolveAgentNamePath(fake);
|
|
204
|
+
// Hold the resolveAgentName promises so we can control timing.
|
|
205
|
+
const resolvers = [];
|
|
206
|
+
fake.service.resolveAgentName = (id) => new Promise((resolve) => {
|
|
207
|
+
resolvers.push(() => resolve(id));
|
|
208
|
+
});
|
|
209
|
+
fake.emit.message(buildMessage({ id: "msg-1" }));
|
|
210
|
+
fake.emit.message(buildMessage({ id: "msg-2" }));
|
|
211
|
+
// Neither has been delivered to the handler yet — first message is
|
|
212
|
+
// still awaiting resolveAgentName; second is queued behind it.
|
|
213
|
+
await flushDispatchChain();
|
|
214
|
+
expect(received).toHaveLength(0);
|
|
215
|
+
expect(resolvers).toHaveLength(1);
|
|
216
|
+
// Resolve the first, chain advances.
|
|
217
|
+
resolvers[0]("agent-alice");
|
|
218
|
+
await flushDispatchChain();
|
|
219
|
+
expect(received.map((r) => r.id)).toEqual(["msg-1"]);
|
|
220
|
+
expect(resolvers).toHaveLength(2);
|
|
221
|
+
// Resolve the second.
|
|
222
|
+
resolvers[1]("agent-bob");
|
|
223
|
+
await flushDispatchChain();
|
|
224
|
+
expect(received.map((r) => r.id)).toEqual(["msg-1", "msg-2"]);
|
|
225
|
+
});
|
|
226
|
+
it("awaits async handler fully before processing the next message", async () => {
|
|
227
|
+
const { fake, core } = customSetup();
|
|
228
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
229
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
230
|
+
const handlerBarriers = [];
|
|
231
|
+
const order = [];
|
|
232
|
+
core.onInbound(async (m) => {
|
|
233
|
+
order.push(`enter:${m.id}`);
|
|
234
|
+
await new Promise((resolve) => handlerBarriers.push(resolve));
|
|
235
|
+
order.push(`exit:${m.id}`);
|
|
236
|
+
});
|
|
237
|
+
fake.emit.message(buildMessage({ id: "msg-1" }));
|
|
238
|
+
fake.emit.message(buildMessage({ id: "msg-2" }));
|
|
239
|
+
await flushDispatchChain();
|
|
240
|
+
// Handler started for msg-1, hasn't returned yet. msg-2 has NOT entered.
|
|
241
|
+
expect(order).toEqual(["enter:msg-1"]);
|
|
242
|
+
handlerBarriers[0]();
|
|
243
|
+
await flushDispatchChain();
|
|
244
|
+
// msg-1 fully processed; msg-2 has entered.
|
|
245
|
+
expect(order).toEqual(["enter:msg-1", "exit:msg-1", "enter:msg-2"]);
|
|
246
|
+
handlerBarriers[1]();
|
|
247
|
+
await flushDispatchChain();
|
|
248
|
+
expect(order).toEqual([
|
|
249
|
+
"enter:msg-1",
|
|
250
|
+
"exit:msg-1",
|
|
251
|
+
"enter:msg-2",
|
|
252
|
+
"exit:msg-2",
|
|
253
|
+
]);
|
|
254
|
+
});
|
|
255
|
+
it("onInbound replaces the previous handler instead of adding", async () => {
|
|
256
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
257
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
258
|
+
const firstHandler = vi.fn();
|
|
259
|
+
const secondHandler = vi.fn();
|
|
260
|
+
core.onInbound(firstHandler);
|
|
261
|
+
core.onInbound(secondHandler);
|
|
262
|
+
fake.emit.message(buildMessage());
|
|
263
|
+
await flushDispatchChain();
|
|
264
|
+
expect(firstHandler).not.toHaveBeenCalled();
|
|
265
|
+
expect(secondHandler).toHaveBeenCalledOnce();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe("context blocks enrichment", () => {
|
|
269
|
+
it("attaches groupMetadata when conversation is a group", async () => {
|
|
270
|
+
fake.state.setConversation("conv-1", {
|
|
271
|
+
type: "group",
|
|
272
|
+
name: "devs",
|
|
273
|
+
participants: [
|
|
274
|
+
"agent:agent-alice",
|
|
275
|
+
"agent:agent-bob",
|
|
276
|
+
"agent:agent-self",
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
280
|
+
fake.emit.message(buildMessage());
|
|
281
|
+
await flushDispatchChain();
|
|
282
|
+
const msg = inbound[0];
|
|
283
|
+
expect(msg.contextBlocks.groupMetadata).toEqual({
|
|
284
|
+
type: "group",
|
|
285
|
+
name: "devs",
|
|
286
|
+
participants: [
|
|
287
|
+
"agent:agent-alice",
|
|
288
|
+
"agent:agent-bob",
|
|
289
|
+
"agent:agent-self",
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
it("does NOT attach groupMetadata for DM conversations", async () => {
|
|
294
|
+
fake.state.setConversation("conv-1", {
|
|
295
|
+
type: "dm",
|
|
296
|
+
name: "alice-dm",
|
|
297
|
+
participants: ["agent:agent-alice", "agent:agent-self"],
|
|
298
|
+
});
|
|
299
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
300
|
+
fake.emit.message(buildMessage());
|
|
301
|
+
await flushDispatchChain();
|
|
302
|
+
expect(inbound[0].contextBlocks.groupMetadata).toBeUndefined();
|
|
303
|
+
});
|
|
304
|
+
it("attaches crossConversation entries when getContextEntries returns non-empty", async () => {
|
|
305
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
306
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
307
|
+
const entries = [
|
|
308
|
+
{
|
|
309
|
+
conversationId: "conv-other",
|
|
310
|
+
conversationName: "other-dm",
|
|
311
|
+
senderName: "Bob",
|
|
312
|
+
text: "hello from the other side",
|
|
313
|
+
minutesAgo: 3,
|
|
314
|
+
count: 1,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
fake.state.setContextEntries("conv-1", entries);
|
|
318
|
+
fake.emit.message(buildMessage());
|
|
319
|
+
await flushDispatchChain();
|
|
320
|
+
expect(inbound[0].contextBlocks.crossConversation).toEqual(entries);
|
|
321
|
+
});
|
|
322
|
+
it("does NOT attach crossConversation when getContextEntries returns empty", async () => {
|
|
323
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
324
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
325
|
+
// Fixture default: returns [] for unknown convs
|
|
326
|
+
fake.emit.message(buildMessage());
|
|
327
|
+
await flushDispatchChain();
|
|
328
|
+
expect(inbound[0].contextBlocks.crossConversation).toBeUndefined();
|
|
329
|
+
});
|
|
330
|
+
it("handles groups with zero participants gracefully", async () => {
|
|
331
|
+
fake.state.setConversation("conv-1", {
|
|
332
|
+
type: "group",
|
|
333
|
+
name: "empty-group",
|
|
334
|
+
participants: [],
|
|
335
|
+
});
|
|
336
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
337
|
+
fake.emit.message(buildMessage());
|
|
338
|
+
await flushDispatchChain();
|
|
339
|
+
const meta = inbound[0].contextBlocks.groupMetadata;
|
|
340
|
+
expect(meta).toBeDefined();
|
|
341
|
+
expect(meta.participants).toEqual([]);
|
|
342
|
+
});
|
|
343
|
+
it("commits context markers after enrichment so a second inbound message does not re-see the same entries", async () => {
|
|
344
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
345
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
346
|
+
fake.state.setContextEntries("conv-1", [
|
|
347
|
+
{
|
|
348
|
+
conversationId: "conv-other",
|
|
349
|
+
senderName: "Bob",
|
|
350
|
+
text: "first visit",
|
|
351
|
+
minutesAgo: 1,
|
|
352
|
+
count: 1,
|
|
353
|
+
},
|
|
354
|
+
]);
|
|
355
|
+
fake.emit.message(buildMessage({ id: "msg-1" }));
|
|
356
|
+
await flushDispatchChain();
|
|
357
|
+
expect(inbound[0].contextBlocks.crossConversation).toHaveLength(1);
|
|
358
|
+
fake.emit.message(buildMessage({ id: "msg-2" }));
|
|
359
|
+
await flushDispatchChain();
|
|
360
|
+
expect(inbound[1].contextBlocks.crossConversation).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
it("does not commit when there are no context entries", async () => {
|
|
363
|
+
const commitSpy = vi.fn();
|
|
364
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
365
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
366
|
+
// Install a peekContextEntries that records commit calls.
|
|
367
|
+
fake.service.peekContextEntries = () => ({ entries: [], commit: commitSpy });
|
|
368
|
+
fake.emit.message(buildMessage());
|
|
369
|
+
await flushDispatchChain();
|
|
370
|
+
expect(commitSpy).not.toHaveBeenCalled();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
describe("sendReply", () => {
|
|
374
|
+
it("delegates to service.send with conversationId and text", async () => {
|
|
375
|
+
await core.sendReply("conv-42", "hello there");
|
|
376
|
+
expect(fake.state.sent).toEqual([
|
|
377
|
+
{ convId: "conv-42", text: "hello there" },
|
|
378
|
+
]);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
describe("static enrichMessage", () => {
|
|
382
|
+
it("returns the same shape as the instance handler path", async () => {
|
|
383
|
+
fake.state.setConversation("conv-1", {
|
|
384
|
+
type: "group",
|
|
385
|
+
name: "devs",
|
|
386
|
+
participants: ["agent:agent-alice", "agent:agent-self"],
|
|
387
|
+
});
|
|
388
|
+
fake.state.setAgentName("agent-alice", "Alice");
|
|
389
|
+
fake.state.setContextEntries("conv-1", [
|
|
390
|
+
{
|
|
391
|
+
conversationId: "conv-other",
|
|
392
|
+
senderName: "Bob",
|
|
393
|
+
text: "bonjour",
|
|
394
|
+
minutesAgo: 1,
|
|
395
|
+
count: 1,
|
|
396
|
+
},
|
|
397
|
+
]);
|
|
398
|
+
const msg = buildMessage({
|
|
399
|
+
id: "msg-static",
|
|
400
|
+
conversationId: "conv-1",
|
|
401
|
+
parts: [{ type: "text", text: "static enrichment" }],
|
|
402
|
+
});
|
|
403
|
+
const { enriched: staticResult, commitContext } = await MoltZapChannelCore.enrichMessage(service, msg);
|
|
404
|
+
expect(staticResult).toMatchObject({
|
|
405
|
+
id: "msg-static",
|
|
406
|
+
conversationId: "conv-1",
|
|
407
|
+
sender: { type: "agent", id: "agent-alice", name: "Alice" },
|
|
408
|
+
text: "static enrichment",
|
|
409
|
+
isFromMe: false,
|
|
410
|
+
});
|
|
411
|
+
expect(staticResult.contextBlocks.groupMetadata?.name).toBe("devs");
|
|
412
|
+
expect(staticResult.contextBlocks.crossConversation).toHaveLength(1);
|
|
413
|
+
expect(commitContext).toBeTypeOf("function");
|
|
414
|
+
});
|
|
415
|
+
it("static helper tolerates resolveAgentName throwing (disconnected service)", async () => {
|
|
416
|
+
const { fake } = customSetup();
|
|
417
|
+
fake.state.setConversation("conv-1", { type: "dm", participants: [] });
|
|
418
|
+
forceResolveAgentNamePath(fake);
|
|
419
|
+
fake.state.setResolveAgentNameFailure("agent-unknown", new Error("Not connected"));
|
|
420
|
+
const { enriched: result } = await MoltZapChannelCore.enrichMessage(fake.service, buildMessage({ sender: { type: "agent", id: "agent-unknown" } }));
|
|
421
|
+
expect(result.sender.name).toBe("agent-unknown");
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
//# sourceMappingURL=channel-core.test.js.map
|