gogcli-mcp-gmail 2.4.0 → 2.4.1
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/index.js +32 -10
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/src/tools/gmail-extra.ts +48 -9
- package/tests/tools/gmail-extra.test.ts +80 -0
package/dist/index.js
CHANGED
|
@@ -31297,7 +31297,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
|
|
|
31297
31297
|
);
|
|
31298
31298
|
|
|
31299
31299
|
// ../gogcli-mcp/src/server.ts
|
|
31300
|
-
var VERSION = true ? "2.4.
|
|
31300
|
+
var VERSION = true ? "2.4.1" : "0.0.0";
|
|
31301
31301
|
function createServer(options) {
|
|
31302
31302
|
return new McpServer({
|
|
31303
31303
|
name: options?.name ?? "gogcli",
|
|
@@ -31478,7 +31478,7 @@ function registerExtraGmailTools(server2) {
|
|
|
31478
31478
|
return runOrDiagnose(args, { account });
|
|
31479
31479
|
});
|
|
31480
31480
|
server2.registerTool("gog_gmail_thread_get", {
|
|
31481
|
-
description: "Get a Gmail thread with all messages. For long threads that overflow context, use latestN to fetch only the most recent messages and/or snippetsOnly for a lightweight per-message headers+snippet view; sanitizeContent strips raw payloads/HTML and is the biggest size reducer when you do need bodies.",
|
|
31481
|
+
description: "Get a Gmail thread with all messages. For long threads that overflow context, use latestN to fetch only the most recent messages and/or snippetsOnly for a lightweight per-message headers+snippet view; sanitizeContent strips raw payloads/HTML and is the biggest size reducer when you do need bodies. Note each message carries two distinct id concepts: the top-level `id` (the Gmail short hex message id \u2014 pass THIS as replyToMessageId to reply) and the `Message-Id` header (the RFC822 `<\u2026@host>` value used in In-Reply-To/References) \u2014 don't confuse either with the `threadId`. To reply to the thread itself, pass the thread's id as replyToThreadId on gog_gmail_drafts_create.",
|
|
31482
31482
|
annotations: { readOnlyHint: true },
|
|
31483
31483
|
inputSchema: {
|
|
31484
31484
|
threadId: external_exports.string().describe("Gmail thread ID"),
|
|
@@ -31630,9 +31630,10 @@ function registerExtraGmailTools(server2) {
|
|
|
31630
31630
|
subject: external_exports.string().describe("Subject"),
|
|
31631
31631
|
body: external_exports.string().describe("Body (plain text)"),
|
|
31632
31632
|
bodyHtml: external_exports.string().optional().describe("Body (HTML; optional)"),
|
|
31633
|
-
replyToMessageId: external_exports.string().optional().describe("Reply to Gmail
|
|
31633
|
+
replyToMessageId: external_exports.string().optional().describe("Reply to a specific Gmail MESSAGE id \u2014 the short hex `id` field from gog_gmail_get / _search / _thread_get (e.g. 19e7593d77fd9636), NOT a thread id and NOT the RFC822 `<\u2026@host>` Message-Id header. Anchors In-Reply-To/References to that exact message. To reply to a thread when you don't know the latest message, use replyToThreadId instead. If both are given, replyToMessageId wins."),
|
|
31634
|
+
replyToThreadId: external_exports.string().optional().describe(`Reply to a Gmail THREAD id \u2014 the wrapper resolves the thread's most recent message and anchors the reply (In-Reply-To/References) to it. This is what "reply to this thread" almost always means. Mutually exclusive with replyToMessageId (which wins if both are set). Thread ids and message ids are both 16-hex strings and easy to confuse \u2014 use this param, not replyToMessageId, when the id came from a thread.`),
|
|
31634
31635
|
replyTo: external_exports.string().optional().describe("Reply-To header address"),
|
|
31635
|
-
quote: external_exports.boolean().optional().describe("Include quoted original message in reply (requires replyToMessageId)"),
|
|
31636
|
+
quote: external_exports.boolean().optional().describe("Include quoted original message in reply (requires replyToMessageId or replyToThreadId)"),
|
|
31636
31637
|
attach: external_exports.array(external_exports.string()).optional().describe("Attachment file paths (repeatable)"),
|
|
31637
31638
|
from: external_exports.string().optional().describe("Send from this email address (must be a verified send-as alias)"),
|
|
31638
31639
|
omitRecipients: external_exports.boolean().optional().describe("Create the draft with no recipients even if to/cc/bcc are supplied \u2014 an accidental-send guard. Populate recipients in a later update before sending."),
|
|
@@ -31654,6 +31655,23 @@ function registerExtraGmailTools(server2) {
|
|
|
31654
31655
|
if (f.attach) for (const path of f.attach) args.push(`--attach=${path}`);
|
|
31655
31656
|
if (f.from) args.push(`--from=${f.from}`);
|
|
31656
31657
|
}
|
|
31658
|
+
async function resolveReplyTarget(replyToMessageId, replyToThreadId, account) {
|
|
31659
|
+
if (replyToMessageId) return { messageId: replyToMessageId };
|
|
31660
|
+
if (!replyToThreadId) return { messageId: void 0 };
|
|
31661
|
+
const threadResult = await runOrDiagnose(["gmail", "thread", "get", replyToThreadId], { account });
|
|
31662
|
+
let parsed;
|
|
31663
|
+
try {
|
|
31664
|
+
parsed = JSON.parse(threadResult.content[0].text);
|
|
31665
|
+
} catch {
|
|
31666
|
+
return { error: threadResult };
|
|
31667
|
+
}
|
|
31668
|
+
const messages = parsed.thread?.messages;
|
|
31669
|
+
const latest = Array.isArray(messages) ? messages[messages.length - 1] : void 0;
|
|
31670
|
+
if (!latest?.id) {
|
|
31671
|
+
return { error: toText(`Error: thread ${replyToThreadId} has no message to reply to`) };
|
|
31672
|
+
}
|
|
31673
|
+
return { messageId: latest.id };
|
|
31674
|
+
}
|
|
31657
31675
|
async function writeDraft(args, account, returnFull, knownDraftId) {
|
|
31658
31676
|
const result = await runOrDiagnose(args, { account });
|
|
31659
31677
|
if (!returnFull) return result;
|
|
@@ -31668,23 +31686,27 @@ function registerExtraGmailTools(server2) {
|
|
|
31668
31686
|
return runOrDiagnose(["gmail", "drafts", "get", draftId], { account });
|
|
31669
31687
|
}
|
|
31670
31688
|
server2.registerTool("gog_gmail_drafts_create", {
|
|
31671
|
-
description: "Create a new Gmail draft. Recipients (to/cc/bcc) are optional; omit them (or set omitRecipients) to create a recipient-less draft as an accidental-send guard.",
|
|
31689
|
+
description: "Create a new Gmail draft. Recipients (to/cc/bcc) are optional; omit them (or set omitRecipients) to create a recipient-less draft as an accidental-send guard. For replies, prefer replyToThreadId (anchors to the thread's latest message) or replyToMessageId (a specific message) \u2014 don't pass a thread id into replyToMessageId, which mis-threads silently.",
|
|
31672
31690
|
inputSchema: draftWriteSchema
|
|
31673
|
-
}, async ({ account, returnFull, ...flags }) => {
|
|
31691
|
+
}, async ({ account, returnFull, replyToThreadId, ...flags }) => {
|
|
31692
|
+
const resolved = await resolveReplyTarget(flags.replyToMessageId, replyToThreadId, account);
|
|
31693
|
+
if ("error" in resolved) return resolved.error;
|
|
31674
31694
|
const args = ["gmail", "drafts", "create"];
|
|
31675
|
-
appendDraftFlags(args, flags);
|
|
31695
|
+
appendDraftFlags(args, { ...flags, replyToMessageId: resolved.messageId });
|
|
31676
31696
|
return writeDraft(args, account, returnFull);
|
|
31677
31697
|
});
|
|
31678
31698
|
server2.registerTool("gog_gmail_drafts_update", {
|
|
31679
|
-
description: "Update an existing Gmail draft.",
|
|
31699
|
+
description: "Update an existing Gmail draft. For replies, prefer replyToThreadId (anchors to the thread's latest message) or replyToMessageId (a specific message) over passing a thread id into replyToMessageId.",
|
|
31680
31700
|
annotations: { destructiveHint: true },
|
|
31681
31701
|
inputSchema: {
|
|
31682
31702
|
draftId: external_exports.string().describe("Draft ID"),
|
|
31683
31703
|
...draftWriteSchema
|
|
31684
31704
|
}
|
|
31685
|
-
}, async ({ draftId, account, returnFull, ...flags }) => {
|
|
31705
|
+
}, async ({ draftId, account, returnFull, replyToThreadId, ...flags }) => {
|
|
31706
|
+
const resolved = await resolveReplyTarget(flags.replyToMessageId, replyToThreadId, account);
|
|
31707
|
+
if ("error" in resolved) return resolved.error;
|
|
31686
31708
|
const args = ["gmail", "drafts", "update", draftId];
|
|
31687
|
-
appendDraftFlags(args, flags);
|
|
31709
|
+
appendDraftFlags(args, { ...flags, replyToMessageId: resolved.messageId });
|
|
31688
31710
|
return writeDraft(args, account, returnFull, draftId);
|
|
31689
31711
|
});
|
|
31690
31712
|
server2.registerTool("gog_gmail_drafts_delete", {
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-gmail",
|
|
5
5
|
"display_name": "gogcli (Gmail)",
|
|
6
|
-
"version": "2.4.
|
|
6
|
+
"version": "2.4.1",
|
|
7
7
|
"description": "Extended Gmail for Claude via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-gmail",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-gmail",
|
|
5
5
|
"description": "Extended Gmail MCP server via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/src/tools/gmail-extra.ts
CHANGED
|
@@ -208,7 +208,7 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
server.registerTool('gog_gmail_thread_get', {
|
|
211
|
-
description: 'Get a Gmail thread with all messages. For long threads that overflow context, use latestN to fetch only the most recent messages and/or snippetsOnly for a lightweight per-message headers+snippet view; sanitizeContent strips raw payloads/HTML and is the biggest size reducer when you do need bodies.',
|
|
211
|
+
description: 'Get a Gmail thread with all messages. For long threads that overflow context, use latestN to fetch only the most recent messages and/or snippetsOnly for a lightweight per-message headers+snippet view; sanitizeContent strips raw payloads/HTML and is the biggest size reducer when you do need bodies. Note each message carries two distinct id concepts: the top-level `id` (the Gmail short hex message id — pass THIS as replyToMessageId to reply) and the `Message-Id` header (the RFC822 `<…@host>` value used in In-Reply-To/References) — don\'t confuse either with the `threadId`. To reply to the thread itself, pass the thread\'s id as replyToThreadId on gog_gmail_drafts_create.',
|
|
212
212
|
annotations: { readOnlyHint: true },
|
|
213
213
|
inputSchema: {
|
|
214
214
|
threadId: z.string().describe('Gmail thread ID'),
|
|
@@ -371,9 +371,10 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
371
371
|
subject: z.string().describe('Subject'),
|
|
372
372
|
body: z.string().describe('Body (plain text)'),
|
|
373
373
|
bodyHtml: z.string().optional().describe('Body (HTML; optional)'),
|
|
374
|
-
replyToMessageId: z.string().optional().describe('Reply to Gmail
|
|
374
|
+
replyToMessageId: z.string().optional().describe('Reply to a specific Gmail MESSAGE id — the short hex `id` field from gog_gmail_get / _search / _thread_get (e.g. 19e7593d77fd9636), NOT a thread id and NOT the RFC822 `<…@host>` Message-Id header. Anchors In-Reply-To/References to that exact message. To reply to a thread when you don\'t know the latest message, use replyToThreadId instead. If both are given, replyToMessageId wins.'),
|
|
375
|
+
replyToThreadId: z.string().optional().describe('Reply to a Gmail THREAD id — the wrapper resolves the thread\'s most recent message and anchors the reply (In-Reply-To/References) to it. This is what "reply to this thread" almost always means. Mutually exclusive with replyToMessageId (which wins if both are set). Thread ids and message ids are both 16-hex strings and easy to confuse — use this param, not replyToMessageId, when the id came from a thread.'),
|
|
375
376
|
replyTo: z.string().optional().describe('Reply-To header address'),
|
|
376
|
-
quote: z.boolean().optional().describe('Include quoted original message in reply (requires replyToMessageId)'),
|
|
377
|
+
quote: z.boolean().optional().describe('Include quoted original message in reply (requires replyToMessageId or replyToThreadId)'),
|
|
377
378
|
attach: z.array(z.string()).optional().describe('Attachment file paths (repeatable)'),
|
|
378
379
|
from: z.string().optional().describe('Send from this email address (must be a verified send-as alias)'),
|
|
379
380
|
omitRecipients: z.boolean().optional().describe('Create the draft with no recipients even if to/cc/bcc are supplied — an accidental-send guard. Populate recipients in a later update before sending.'),
|
|
@@ -412,6 +413,40 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
412
413
|
if (f.from) args.push(`--from=${f.from}`);
|
|
413
414
|
}
|
|
414
415
|
|
|
416
|
+
// Resolve the effective reply target for a draft write. A draft can only
|
|
417
|
+
// thread off a specific MESSAGE id, but callers usually have a THREAD id and
|
|
418
|
+
// mean "reply to this thread". gog's `drafts create/update` has no --thread-id
|
|
419
|
+
// (unlike `gmail send`), so we resolve thread -> latest message here and pass
|
|
420
|
+
// that message id — gog then sets In-Reply-To/References from its RFC822
|
|
421
|
+
// Message-Id. replyToMessageId always wins; when only a thread id is given we
|
|
422
|
+
// fetch the thread and reply to its most recent message. Returns either the
|
|
423
|
+
// resolved message id (possibly undefined when neither was supplied) or an
|
|
424
|
+
// error ToolResult that the handler surfaces instead of writing a
|
|
425
|
+
// mis-threaded draft. (When upstream adds --thread-id to drafts — openclaw/
|
|
426
|
+
// gogcli#673 — this can pass it through directly with no MCP surface change.)
|
|
427
|
+
async function resolveReplyTarget(
|
|
428
|
+
replyToMessageId: string | undefined,
|
|
429
|
+
replyToThreadId: string | undefined,
|
|
430
|
+
account: string | undefined,
|
|
431
|
+
): Promise<{ messageId?: string } | { error: ToolResult }> {
|
|
432
|
+
if (replyToMessageId) return { messageId: replyToMessageId };
|
|
433
|
+
if (!replyToThreadId) return { messageId: undefined };
|
|
434
|
+
const threadResult = await runOrDiagnose(['gmail', 'thread', 'get', replyToThreadId], { account });
|
|
435
|
+
let parsed: { thread?: { messages?: GmailMessage[] } };
|
|
436
|
+
try {
|
|
437
|
+
parsed = JSON.parse(threadResult.content[0].text) as { thread?: { messages?: GmailMessage[] } };
|
|
438
|
+
} catch {
|
|
439
|
+
// Non-JSON output means the thread fetch itself errored — surface it.
|
|
440
|
+
return { error: threadResult };
|
|
441
|
+
}
|
|
442
|
+
const messages = parsed.thread?.messages;
|
|
443
|
+
const latest = Array.isArray(messages) ? messages[messages.length - 1] : undefined;
|
|
444
|
+
if (!latest?.id) {
|
|
445
|
+
return { error: toText(`Error: thread ${replyToThreadId} has no message to reply to`) };
|
|
446
|
+
}
|
|
447
|
+
return { messageId: latest.id };
|
|
448
|
+
}
|
|
449
|
+
|
|
415
450
|
// Run a draft write, then — when returnFull is set — re-fetch the stored
|
|
416
451
|
// draft so the caller can verify subject/body/recipients persisted without a
|
|
417
452
|
// separate gog_gmail_drafts_get round trip. For updates the id is known up
|
|
@@ -441,24 +476,28 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
441
476
|
}
|
|
442
477
|
|
|
443
478
|
server.registerTool('gog_gmail_drafts_create', {
|
|
444
|
-
description: 'Create a new Gmail draft. Recipients (to/cc/bcc) are optional; omit them (or set omitRecipients) to create a recipient-less draft as an accidental-send guard.',
|
|
479
|
+
description: 'Create a new Gmail draft. Recipients (to/cc/bcc) are optional; omit them (or set omitRecipients) to create a recipient-less draft as an accidental-send guard. For replies, prefer replyToThreadId (anchors to the thread\'s latest message) or replyToMessageId (a specific message) — don\'t pass a thread id into replyToMessageId, which mis-threads silently.',
|
|
445
480
|
inputSchema: draftWriteSchema,
|
|
446
|
-
}, async ({ account, returnFull, ...flags }) => {
|
|
481
|
+
}, async ({ account, returnFull, replyToThreadId, ...flags }) => {
|
|
482
|
+
const resolved = await resolveReplyTarget(flags.replyToMessageId, replyToThreadId, account);
|
|
483
|
+
if ('error' in resolved) return resolved.error;
|
|
447
484
|
const args = ['gmail', 'drafts', 'create'];
|
|
448
|
-
appendDraftFlags(args, flags);
|
|
485
|
+
appendDraftFlags(args, { ...flags, replyToMessageId: resolved.messageId });
|
|
449
486
|
return writeDraft(args, account, returnFull);
|
|
450
487
|
});
|
|
451
488
|
|
|
452
489
|
server.registerTool('gog_gmail_drafts_update', {
|
|
453
|
-
description: 'Update an existing Gmail draft.',
|
|
490
|
+
description: 'Update an existing Gmail draft. For replies, prefer replyToThreadId (anchors to the thread\'s latest message) or replyToMessageId (a specific message) over passing a thread id into replyToMessageId.',
|
|
454
491
|
annotations: { destructiveHint: true },
|
|
455
492
|
inputSchema: {
|
|
456
493
|
draftId: z.string().describe('Draft ID'),
|
|
457
494
|
...draftWriteSchema,
|
|
458
495
|
},
|
|
459
|
-
}, async ({ draftId, account, returnFull, ...flags }) => {
|
|
496
|
+
}, async ({ draftId, account, returnFull, replyToThreadId, ...flags }) => {
|
|
497
|
+
const resolved = await resolveReplyTarget(flags.replyToMessageId, replyToThreadId, account);
|
|
498
|
+
if ('error' in resolved) return resolved.error;
|
|
460
499
|
const args = ['gmail', 'drafts', 'update', draftId];
|
|
461
|
-
appendDraftFlags(args, flags);
|
|
500
|
+
appendDraftFlags(args, { ...flags, replyToMessageId: resolved.messageId });
|
|
462
501
|
return writeDraft(args, account, returnFull, draftId);
|
|
463
502
|
});
|
|
464
503
|
|
|
@@ -545,6 +545,86 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
545
545
|
});
|
|
546
546
|
});
|
|
547
547
|
|
|
548
|
+
describe('gmail draft reply threading (replyToThreadId resolution)', () => {
|
|
549
|
+
const threadJson = (ids: (string | undefined)[]) =>
|
|
550
|
+
toText(JSON.stringify({ thread: { messages: ids.map((id) => (id ? { id } : {})) } }));
|
|
551
|
+
|
|
552
|
+
it('resolves replyToThreadId to the thread\'s latest message id on create', async () => {
|
|
553
|
+
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
|
|
554
|
+
if (args[1] === 'thread' && args[2] === 'get') return threadJson(['m1', 'm2', 'mLatest']);
|
|
555
|
+
return toText('{"draftId":"d1"}');
|
|
556
|
+
});
|
|
557
|
+
await handlers.get('gog_gmail_drafts_create')!({
|
|
558
|
+
subject: 'Re: roof', body: 'Sounds good', replyToThreadId: '19dffe06f9668b28', account: 'me@x.com',
|
|
559
|
+
});
|
|
560
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'thread', 'get', '19dffe06f9668b28'], { account: 'me@x.com' });
|
|
561
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
562
|
+
['gmail', 'drafts', 'create', '--subject=Re: roof', '--body=Sounds good', '--reply-to-message-id=mLatest'],
|
|
563
|
+
{ account: 'me@x.com' },
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('resolves replyToThreadId on update too', async () => {
|
|
568
|
+
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
|
|
569
|
+
if (args[1] === 'thread' && args[2] === 'get') return threadJson(['a', 'b']);
|
|
570
|
+
return toText('{"draftId":"d1"}');
|
|
571
|
+
});
|
|
572
|
+
await handlers.get('gog_gmail_drafts_update')!({
|
|
573
|
+
draftId: 'd1', subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
574
|
+
});
|
|
575
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
576
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--reply-to-message-id=b'],
|
|
577
|
+
{ account: undefined },
|
|
578
|
+
);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('replyToMessageId wins when both ids are supplied (no thread fetch)', async () => {
|
|
582
|
+
await handlers.get('gog_gmail_drafts_create')!({
|
|
583
|
+
subject: 'S', body: 'B', replyToMessageId: 'mExplicit', replyToThreadId: 't1',
|
|
584
|
+
});
|
|
585
|
+
// Only the create call — the thread was never fetched.
|
|
586
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
587
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
588
|
+
['gmail', 'drafts', 'create', '--subject=S', '--body=B', '--reply-to-message-id=mExplicit'],
|
|
589
|
+
{ account: undefined },
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('surfaces the thread-fetch error and does not write a mis-threaded draft', async () => {
|
|
594
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(toText('Error: thread t1 not found'));
|
|
595
|
+
const result = await handlers.get('gog_gmail_drafts_create')!({
|
|
596
|
+
subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
597
|
+
});
|
|
598
|
+
expect(result.content[0].text).toBe('Error: thread t1 not found');
|
|
599
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1); // only the thread fetch; no create
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('errors when the thread has no message to reply to', async () => {
|
|
603
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(toText(JSON.stringify({ thread: { messages: [] } })));
|
|
604
|
+
const result = await handlers.get('gog_gmail_drafts_create')!({
|
|
605
|
+
subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
606
|
+
});
|
|
607
|
+
expect(result.content[0].text).toBe('Error: thread t1 has no message to reply to');
|
|
608
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('errors when the thread payload has no messages array', async () => {
|
|
612
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(toText('{}'));
|
|
613
|
+
const result = await handlers.get('gog_gmail_drafts_create')!({
|
|
614
|
+
subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
615
|
+
});
|
|
616
|
+
expect(result.content[0].text).toBe('Error: thread t1 has no message to reply to');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('errors when the latest message has no id', async () => {
|
|
620
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(threadJson(['m1', undefined]));
|
|
621
|
+
const result = await handlers.get('gog_gmail_drafts_create')!({
|
|
622
|
+
subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
623
|
+
});
|
|
624
|
+
expect(result.content[0].text).toBe('Error: thread t1 has no message to reply to');
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
548
628
|
describe('gog_gmail_drafts_update', () => {
|
|
549
629
|
it('calls runOrDiagnose with draftId and updated fields', async () => {
|
|
550
630
|
await handlers.get('gog_gmail_drafts_update')!({
|