@intx/tools-mail 0.1.2 → 0.3.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.
@@ -1,587 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import type {
3
- BodyStructure,
4
- InboundMessage,
5
- Mailbox,
6
- MailboxEvent,
7
- MailboxStatus,
8
- MessageHeaders,
9
- MessagePart,
10
- MessageRef,
11
- MessageTransport,
12
- OutboundMessage,
13
- SearchQuery,
14
- SendReceipt,
15
- SyncResult,
16
- SyncState,
17
- Thread,
18
- ListInfo,
19
- ToolCall,
20
- Unsubscribe,
21
- } from "@intx/types/runtime";
22
- import {
23
- createRuntimeCapabilities,
24
- type RuntimeCapabilities,
25
- } from "@intx/types/runtime-capabilities";
26
-
27
- import { createMailTools } from "./index";
28
- import {
29
- makeMailReadHandler,
30
- makeMailReplyHandler,
31
- makeMailSearchHandler,
32
- makeMailSendHandler,
33
- makeMailWaitHandler,
34
- } from "./handlers";
35
-
36
- // ---------------------------------------------------------------------------
37
- // Mock transport — minimal MessageTransport with hooks for sent-message
38
- // inspection, watch firing, and message enqueueing.
39
- // ---------------------------------------------------------------------------
40
-
41
- type WatchCallback = (event: MailboxEvent) => void;
42
-
43
- type MockTransport = MessageTransport & {
44
- getSentMessages(): OutboundMessage[];
45
- fireWatch(event: MailboxEvent): void;
46
- enqueueMessage(ref: MessageRef, msg: InboundMessage): void;
47
- setSearchResult(refs: MessageRef[]): void;
48
- };
49
-
50
- function makeMockTransport(): MockTransport {
51
- const sentMessages: OutboundMessage[] = [];
52
- const watchCallbacks: WatchCallback[] = [];
53
- const messageStore = new Map<string, InboundMessage>();
54
- let searchResult: MessageRef[] = [];
55
-
56
- function refKey(ref: MessageRef): string {
57
- return `${ref.mailbox}:${String(ref.uid)}`;
58
- }
59
-
60
- const transport: MockTransport = {
61
- getSentMessages() {
62
- return sentMessages;
63
- },
64
- fireWatch(event: MailboxEvent): void {
65
- for (const cb of watchCallbacks) {
66
- cb(event);
67
- }
68
- },
69
- enqueueMessage(ref: MessageRef, msg: InboundMessage): void {
70
- messageStore.set(refKey(ref), msg);
71
- },
72
- setSearchResult(refs: MessageRef[]): void {
73
- searchResult = refs;
74
- },
75
-
76
- async send(message: OutboundMessage): Promise<SendReceipt> {
77
- sentMessages.push(message);
78
- return {
79
- messageId: `<msg-${String(Date.now())}@test>`,
80
- status: "delivered",
81
- };
82
- },
83
-
84
- async append(
85
- mailbox: string,
86
- message: InboundMessage,
87
- ): Promise<MessageRef> {
88
- const ref = { uid: 999, mailbox };
89
- messageStore.set(refKey(ref), message);
90
- return ref;
91
- },
92
-
93
- async listMailboxes(): Promise<Mailbox[]> {
94
- return [{ name: "INBOX", role: "\\Inbox" }];
95
- },
96
-
97
- async createMailbox(name: string): Promise<Mailbox> {
98
- return { name };
99
- },
100
-
101
- async deleteMailbox(): Promise<void> {
102
- /* noop */
103
- },
104
-
105
- async getMailboxStatus(): Promise<MailboxStatus> {
106
- return {
107
- total: 0,
108
- unseen: 0,
109
- recent: 0,
110
- uidNext: 1,
111
- uidValidity: 1,
112
- highestModSeq: 0,
113
- };
114
- },
115
-
116
- async search(_mailbox: string, _query: SearchQuery): Promise<MessageRef[]> {
117
- return searchResult;
118
- },
119
-
120
- async thread(): Promise<Thread[]> {
121
- return [];
122
- },
123
-
124
- async fetchHeaders(ref: MessageRef): Promise<MessageHeaders> {
125
- const msg = messageStore.get(refKey(ref));
126
- if (msg !== undefined) return msg.headers;
127
- return {
128
- from: "sender@test",
129
- to: ["agent@test"],
130
- date: new Date().toISOString(),
131
- messageId: `<${String(ref.uid)}@test>`,
132
- };
133
- },
134
-
135
- async fetchStructure(): Promise<BodyStructure> {
136
- return { contentType: "multipart/signed" };
137
- },
138
-
139
- async fetchPart(): Promise<MessagePart> {
140
- return { contentType: "text/plain", content: new Uint8Array() };
141
- },
142
-
143
- async fetchFull(ref: MessageRef): Promise<InboundMessage> {
144
- const stored = messageStore.get(refKey(ref));
145
- if (stored !== undefined) return stored;
146
- return {
147
- ref,
148
- headers: {
149
- from: "sender@test",
150
- to: ["agent@test"],
151
- date: new Date().toISOString(),
152
- messageId: `<${String(ref.uid)}@test>`,
153
- },
154
- flags: [],
155
- content: "hello",
156
- signatureStatus: "missing",
157
- };
158
- },
159
-
160
- async setFlags(): Promise<void> {
161
- /* noop */
162
- },
163
-
164
- async clearFlags(): Promise<void> {
165
- /* noop */
166
- },
167
-
168
- async move(): Promise<void> {
169
- /* noop */
170
- },
171
-
172
- async copy(): Promise<void> {
173
- /* noop */
174
- },
175
-
176
- async expunge(): Promise<void> {
177
- /* noop */
178
- },
179
-
180
- watch(_mailbox: string, callback: WatchCallback): Unsubscribe {
181
- watchCallbacks.push(callback);
182
- return () => {
183
- const idx = watchCallbacks.indexOf(callback);
184
- if (idx !== -1) watchCallbacks.splice(idx, 1);
185
- };
186
- },
187
-
188
- async sync(_mailbox: string, _state: SyncState): Promise<SyncResult> {
189
- return {
190
- vanished: [],
191
- changed: [],
192
- newMessages: [],
193
- fullResyncRequired: false,
194
- };
195
- },
196
-
197
- async createList(address: string, name: string): Promise<ListInfo> {
198
- return {
199
- address,
200
- name,
201
- memberCount: 0,
202
- createdAt: new Date().toISOString(),
203
- };
204
- },
205
-
206
- async listMembers(): Promise<string[]> {
207
- return [];
208
- },
209
-
210
- async subscribe(): Promise<void> {
211
- /* noop */
212
- },
213
-
214
- async unsubscribe(): Promise<void> {
215
- /* noop */
216
- },
217
- };
218
-
219
- return transport;
220
- }
221
-
222
- function makeInboundMessage(from = "user@test"): InboundMessage {
223
- return {
224
- ref: { uid: 0, mailbox: "INBOX" },
225
- headers: {
226
- from,
227
- to: ["agent@local.interchange"],
228
- date: new Date().toISOString(),
229
- messageId: `<inbound-${String(Date.now())}@test>`,
230
- subject: "Test conversation",
231
- },
232
- flags: [],
233
- content: "Hello, agent!",
234
- signatureStatus: "missing",
235
- };
236
- }
237
-
238
- function makeCapabilities(transport: MessageTransport): RuntimeCapabilities {
239
- return createRuntimeCapabilities({ "mail.transport": transport });
240
- }
241
-
242
- const signal = AbortSignal.timeout(5000);
243
-
244
- // ---------------------------------------------------------------------------
245
- // createMailTools factory surface
246
- // ---------------------------------------------------------------------------
247
-
248
- describe("createMailTools", () => {
249
- test("definitions include all five mail tools in registered order", () => {
250
- const tools = createMailTools({
251
- capabilities: makeCapabilities(makeMockTransport()),
252
- });
253
-
254
- expect(tools.definitions.map((d) => d.name)).toEqual([
255
- "mail_send",
256
- "mail_reply",
257
- "mail_search",
258
- "mail_read",
259
- "mail_wait",
260
- ]);
261
- });
262
-
263
- test("run dispatches each registered tool name", async () => {
264
- const transport = makeMockTransport();
265
- const tools = createMailTools({
266
- capabilities: makeCapabilities(transport),
267
- });
268
-
269
- const result = await tools.run(
270
- {
271
- id: "c1",
272
- name: "mail_send",
273
- arguments: { to: "user@test", content: "hi" },
274
- },
275
- signal,
276
- );
277
-
278
- expect(result.isError).toBeUndefined();
279
- expect(transport.getSentMessages().length).toBe(1);
280
- });
281
-
282
- test("run returns Unknown tool error for an unregistered name", async () => {
283
- const tools = createMailTools({
284
- capabilities: makeCapabilities(makeMockTransport()),
285
- });
286
-
287
- const result = await tools.run(
288
- { id: "c1", name: "not_a_mail_tool", arguments: {} },
289
- signal,
290
- );
291
-
292
- expect(result.callId).toBe("c1");
293
- expect(result.isError).toBe(true);
294
- if (typeof result.content === "string")
295
- throw new Error("expected object content");
296
- expect(result.content["error"]).toBe(`Unknown tool: "not_a_mail_tool"`);
297
- });
298
-
299
- test("run wraps an error thrown from a handler path lacking its own try/catch", async () => {
300
- // mail_wait calls transport.search outside a per-handler try/catch
301
- // (only the inner watch path is guarded). A throw from
302
- // transport.search therefore reaches createMailTools.run, where the
303
- // top-level wrapper turns it into an isError result.
304
- const transport = makeMockTransport();
305
- transport.search = async () => {
306
- throw new Error("synthetic search failure");
307
- };
308
-
309
- const tools = createMailTools({
310
- capabilities: makeCapabilities(transport),
311
- });
312
-
313
- const result = await tools.run(
314
- { id: "c1", name: "mail_wait", arguments: { query: {} } },
315
- signal,
316
- );
317
-
318
- expect(result.callId).toBe("c1");
319
- expect(result.isError).toBe(true);
320
- if (typeof result.content === "string")
321
- throw new Error("expected object content");
322
- expect(result.content["error"]).toBe("synthetic search failure");
323
- });
324
-
325
- test("a resolver that throws on mail.transport propagates from createMailTools", () => {
326
- const capabilities = createRuntimeCapabilities({});
327
-
328
- expect(() => createMailTools({ capabilities })).toThrow(
329
- /"mail\.transport".*not provided by the host/,
330
- );
331
- });
332
-
333
- test("dispose is idempotent", async () => {
334
- const tools = createMailTools({
335
- capabilities: makeCapabilities(makeMockTransport()),
336
- });
337
-
338
- await tools.dispose();
339
- await tools.dispose();
340
- // No throw; reaching here is the assertion.
341
- expect(true).toBe(true);
342
- });
343
- });
344
-
345
- // ---------------------------------------------------------------------------
346
- // mail_send handler
347
- // ---------------------------------------------------------------------------
348
-
349
- describe("mail_send handler", () => {
350
- test("sends a conversation message and returns messageId", async () => {
351
- const transport = makeMockTransport();
352
- const handler = makeMailSendHandler(transport);
353
-
354
- const call: ToolCall = {
355
- id: "s1",
356
- name: "mail_send",
357
- arguments: {
358
- to: "user@test",
359
- content: "Hello from agent",
360
- type: "conversation.message",
361
- },
362
- };
363
-
364
- const result = await handler(call, signal);
365
-
366
- expect(result.isError).toBeUndefined();
367
- if (typeof result.content === "string")
368
- throw new Error("expected object content");
369
- expect(typeof result.content["messageId"]).toBe("string");
370
-
371
- expect(transport.getSentMessages().length).toBe(1);
372
- const sent = transport.getSentMessages()[0];
373
- if (sent === undefined) throw new Error("no sent message");
374
- expect(sent.to).toBe("user@test");
375
- expect(sent.content).toBe("Hello from agent");
376
- expect(sent.type).toBe("conversation.message");
377
- });
378
-
379
- test("returns error when 'to' is missing", async () => {
380
- const handler = makeMailSendHandler(makeMockTransport());
381
-
382
- const result = await handler(
383
- { id: "s2", name: "mail_send", arguments: { content: "No recipient" } },
384
- signal,
385
- );
386
-
387
- expect(result.isError).toBe(true);
388
- });
389
-
390
- test("returns error when both content and payload are provided", async () => {
391
- const handler = makeMailSendHandler(makeMockTransport());
392
-
393
- const result = await handler(
394
- {
395
- id: "s3",
396
- name: "mail_send",
397
- arguments: {
398
- to: "user@test",
399
- content: "text",
400
- payload: { type: "offering.response", version: "1", body: {} },
401
- },
402
- },
403
- signal,
404
- );
405
-
406
- expect(result.isError).toBe(true);
407
- });
408
- });
409
-
410
- // ---------------------------------------------------------------------------
411
- // mail_reply handler
412
- // ---------------------------------------------------------------------------
413
-
414
- describe("mail_reply handler", () => {
415
- test("fetches parent headers and sends reply with inReplyTo", async () => {
416
- const transport = makeMockTransport();
417
-
418
- const parentRef: MessageRef = { uid: 10, mailbox: "INBOX" };
419
- transport.enqueueMessage(parentRef, {
420
- ref: parentRef,
421
- headers: {
422
- from: "user@test",
423
- to: ["agent@local"],
424
- date: new Date().toISOString(),
425
- messageId: "<parent@test>",
426
- subject: "Original subject",
427
- },
428
- flags: [],
429
- content: "original message",
430
- signatureStatus: "missing",
431
- });
432
-
433
- const handler = makeMailReplyHandler(transport);
434
- const result = await handler(
435
- {
436
- id: "r1",
437
- name: "mail_reply",
438
- arguments: { ref: parentRef, content: "This is the reply" },
439
- },
440
- signal,
441
- );
442
-
443
- expect(result.isError).toBeUndefined();
444
- expect(transport.getSentMessages().length).toBe(1);
445
-
446
- const sent = transport.getSentMessages()[0];
447
- if (sent === undefined) throw new Error("no sent message");
448
- expect(sent.to).toBe("user@test");
449
- expect(sent.inReplyTo).toBe("<parent@test>");
450
- expect(sent.subject).toBe("Original subject");
451
- expect(sent.content).toBe("This is the reply");
452
- });
453
-
454
- test("returns error when ref is missing", async () => {
455
- const handler = makeMailReplyHandler(makeMockTransport());
456
-
457
- const result = await handler(
458
- { id: "r2", name: "mail_reply", arguments: { content: "no ref" } },
459
- signal,
460
- );
461
-
462
- expect(result.isError).toBe(true);
463
- });
464
- });
465
-
466
- // ---------------------------------------------------------------------------
467
- // mail_search handler
468
- // ---------------------------------------------------------------------------
469
-
470
- describe("mail_search handler", () => {
471
- test("calls transport.search and returns summaries", async () => {
472
- const handler = makeMailSearchHandler(makeMockTransport());
473
-
474
- const result = await handler(
475
- {
476
- id: "q1",
477
- name: "mail_search",
478
- arguments: {
479
- mailbox: "INBOX",
480
- query: { from: "user@test" },
481
- limit: 5,
482
- },
483
- },
484
- signal,
485
- );
486
-
487
- expect(result.isError).toBeUndefined();
488
- if (typeof result.content === "string")
489
- throw new Error("expected object content");
490
- expect(Array.isArray(result.content["results"])).toBe(true);
491
- });
492
-
493
- test("defaults mailbox to INBOX when not specified", async () => {
494
- const handler = makeMailSearchHandler(makeMockTransport());
495
-
496
- const result = await handler(
497
- { id: "q2", name: "mail_search", arguments: { query: {} } },
498
- signal,
499
- );
500
-
501
- expect(result.isError).toBeUndefined();
502
- });
503
- });
504
-
505
- // ---------------------------------------------------------------------------
506
- // mail_read handler
507
- // ---------------------------------------------------------------------------
508
-
509
- describe("mail_read handler", () => {
510
- test("fetches full message when parts='full'", async () => {
511
- const transport = makeMockTransport();
512
- const ref: MessageRef = { uid: 5, mailbox: "INBOX" };
513
- transport.enqueueMessage(ref, { ...makeInboundMessage(), ref });
514
-
515
- const handler = makeMailReadHandler(transport);
516
- const result = await handler(
517
- { id: "rd1", name: "mail_read", arguments: { ref, parts: "full" } },
518
- signal,
519
- );
520
-
521
- expect(result.isError).toBeUndefined();
522
- if (typeof result.content === "string")
523
- throw new Error("expected object content");
524
- expect(result.content["headers"]).toBeDefined();
525
- expect(result.content["signatureStatus"]).toBe("missing");
526
- });
527
-
528
- test("fetches only headers when parts='headers'", async () => {
529
- const transport = makeMockTransport();
530
- const ref: MessageRef = { uid: 6, mailbox: "INBOX" };
531
- transport.enqueueMessage(ref, { ...makeInboundMessage(), ref });
532
-
533
- const handler = makeMailReadHandler(transport);
534
- const result = await handler(
535
- { id: "rd2", name: "mail_read", arguments: { ref, parts: "headers" } },
536
- signal,
537
- );
538
-
539
- expect(result.isError).toBeUndefined();
540
- if (typeof result.content === "string")
541
- throw new Error("expected object content");
542
- expect(result.content["headers"]).toBeDefined();
543
- });
544
-
545
- test("returns error when ref is missing", async () => {
546
- const handler = makeMailReadHandler(makeMockTransport());
547
-
548
- const result = await handler(
549
- { id: "rd3", name: "mail_read", arguments: { parts: "full" } },
550
- signal,
551
- );
552
-
553
- expect(result.isError).toBe(true);
554
- });
555
- });
556
-
557
- // ---------------------------------------------------------------------------
558
- // mail_wait handler
559
- // ---------------------------------------------------------------------------
560
-
561
- describe("mail_wait handler", () => {
562
- test("returns immediately when initial search yields a match", async () => {
563
- const transport = makeMockTransport();
564
- const ref: MessageRef = { uid: 42, mailbox: "INBOX" };
565
- transport.enqueueMessage(ref, {
566
- ...makeInboundMessage("alice@test"),
567
- ref,
568
- });
569
- transport.setSearchResult([ref]);
570
-
571
- const handler = makeMailWaitHandler(transport);
572
- const result = await handler(
573
- {
574
- id: "w1",
575
- name: "mail_wait",
576
- arguments: { query: { from: "alice@test" } },
577
- },
578
- signal,
579
- );
580
-
581
- expect(result.isError).toBeUndefined();
582
- if (typeof result.content === "string")
583
- throw new Error("expected object content");
584
- expect(result.content["from"]).toBe("alice@test");
585
- expect(result.content["ref"]).toEqual(ref);
586
- });
587
- });
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "include": ["src/**/*.ts"]
4
- }