gogcli-mcp-gmail 2.21.0 → 2.22.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/README.md +26 -4
- package/SKILL.md +24 -3
- package/dist/index.js +522 -75
- package/manifest.json +6 -2
- package/package.json +1 -1
- package/src/tools/gmail-extra.ts +201 -29
- package/tests/tools/attachment-index-resolve.test.ts +76 -0
- package/tests/tools/gmail-extra.test.ts +382 -46
|
@@ -96,7 +96,7 @@ describe('gog_gmail_attachment', () => {
|
|
|
96
96
|
stubGog({ meta: PDF_LIST, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 99723, contentBase64: PDF_B64 } });
|
|
97
97
|
const res = await call({});
|
|
98
98
|
// download to the temp path first, then the metadata read to resolve the name.
|
|
99
|
-
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--inline', '--out=/tmp/gog-attachments/m1/attachment', '--name=attachment']);
|
|
99
|
+
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--use-indexed-attachment-ids=false', '--inline', '--inline-max-bytes=3145728', '--out=/tmp/gog-attachments/m1/attachment', '--name=attachment']);
|
|
100
100
|
expect(gotGet()).toBe(true);
|
|
101
101
|
const payload = JSON.parse(textOf(res));
|
|
102
102
|
expect(payload).toMatchObject({
|
|
@@ -135,14 +135,14 @@ describe('gog_gmail_attachment', () => {
|
|
|
135
135
|
stubGog({ download: { path: '/tmp/gog-attachments/m1/report.pdf', bytes: 12, contentBase64: PDF_B64 } });
|
|
136
136
|
await call({ name: 'report.pdf' });
|
|
137
137
|
expect(gotGet()).toBe(false);
|
|
138
|
-
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--inline', '--out=/tmp/gog-attachments/m1/report.pdf', '--name=report.pdf']);
|
|
138
|
+
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--use-indexed-attachment-ids=false', '--inline', '--inline-max-bytes=3145728', '--out=/tmp/gog-attachments/m1/report.pdf', '--name=report.pdf']);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
it('a named non-image on the connector skips --inline (headed straight to Drive)', async () => {
|
|
142
142
|
stubGog({ download: { path: '/tmp/gog-attachments/m1/report.pdf', bytes: 12 }, drive: { file: { id: 'F9' } } });
|
|
143
143
|
await asConnector(() => call({ name: 'report.pdf' }));
|
|
144
144
|
expect(gotGet()).toBe(false);
|
|
145
|
-
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--out=/tmp/gog-attachments/m1/report.pdf', '--name=report.pdf']);
|
|
145
|
+
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--use-indexed-attachment-ids=false', '--inline-max-bytes=3145728', '--out=/tmp/gog-attachments/m1/report.pdf', '--name=report.pdf']);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
it('resolves the real filename by size and sanitizes path separators (no traversal)', async () => {
|
|
@@ -235,7 +235,7 @@ describe('gog_gmail_attachment', () => {
|
|
|
235
235
|
it('deliver=drive skips --inline and uploads, honoring driveFolder and name', async () => {
|
|
236
236
|
stubGog({ download: { path: '/tmp/gog-attachments/m1/renamed.png', bytes: 24 }, drive: { file: { id: 'F2', webViewLink: 'https://drive.google.com/file/d/F2/view' } } });
|
|
237
237
|
const res = await call({ deliver: 'drive', driveFolder: 'DIR9', name: 'renamed.png', account: 'me@x.com' });
|
|
238
|
-
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--out=/tmp/gog-attachments/m1/renamed.png', '--name=renamed.png']);
|
|
238
|
+
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--use-indexed-attachment-ids=false', '--inline-max-bytes=3145728', '--out=/tmp/gog-attachments/m1/renamed.png', '--name=renamed.png']);
|
|
239
239
|
expect(lib.run).toHaveBeenCalledWith(
|
|
240
240
|
['drive', 'upload', '/tmp/gog-attachments/m1/renamed.png', '--json', '--parent=DIR9', '--name=renamed.png'], { account: 'me@x.com' });
|
|
241
241
|
expect(JSON.parse(textOf(res))).toMatchObject({ deliveredVia: 'drive', id: 'F2' });
|
|
@@ -268,14 +268,14 @@ describe('gog_gmail_attachment', () => {
|
|
|
268
268
|
it('honors a caller out on stdio', async () => {
|
|
269
269
|
stubGog({ download: { path: '/home/me/x.png', bytes: 24, contentBase64: PNG_B64 } });
|
|
270
270
|
await call({ out: '/home/me/x.png', name: 'x.png' });
|
|
271
|
-
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--inline', '--out=/home/me/x.png', '--name=x.png']);
|
|
271
|
+
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--use-indexed-attachment-ids=false', '--inline', '--inline-max-bytes=3145728', '--out=/home/me/x.png', '--name=x.png']);
|
|
272
272
|
});
|
|
273
273
|
|
|
274
274
|
it('ignores a caller out on the connector and notes it', async () => {
|
|
275
275
|
stubGog({ download: { path: '/tmp/gog-attachments/m1/report.pdf', bytes: 12 }, drive: { file: { id: 'F3' } } });
|
|
276
276
|
const res = await asConnector(() => call({ out: '/home/claude/report.pdf', name: 'report.pdf' }));
|
|
277
277
|
// download used the temp path, NOT the caller's /home/claude path.
|
|
278
|
-
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--out=/tmp/gog-attachments/m1/report.pdf', '--name=report.pdf']);
|
|
278
|
+
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--use-indexed-attachment-ids=false', '--inline-max-bytes=3145728', '--out=/tmp/gog-attachments/m1/report.pdf', '--name=report.pdf']);
|
|
279
279
|
expect(textOf(res)).toContain('`out` was ignored');
|
|
280
280
|
});
|
|
281
281
|
|
|
@@ -473,7 +473,7 @@ describe('gog_gmail_thread_get', () => {
|
|
|
473
473
|
it('calls runOrDiagnose with threadId', async () => {
|
|
474
474
|
await harness.callTool('gog_gmail_thread_get', { threadId: 't1' });
|
|
475
475
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
476
|
-
['gmail', 'thread', 'get', 't1'],
|
|
476
|
+
['gmail', 'thread', 'get', 't1', '--use-indexed-attachment-ids=false'],
|
|
477
477
|
{ account: undefined },
|
|
478
478
|
);
|
|
479
479
|
});
|
|
@@ -487,7 +487,7 @@ describe('gog_gmail_thread_get', () => {
|
|
|
487
487
|
outDir: '/tmp/atts',
|
|
488
488
|
});
|
|
489
489
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
490
|
-
['gmail', 'thread', 'get', 't1', '--download', '--full', '--sanitize-content', '--out-dir=/tmp/atts'],
|
|
490
|
+
['gmail', 'thread', 'get', 't1', '--download', '--full', '--sanitize-content', '--out-dir=/tmp/atts', '--use-indexed-attachment-ids=false'],
|
|
491
491
|
{ account: undefined },
|
|
492
492
|
);
|
|
493
493
|
});
|
|
@@ -500,7 +500,7 @@ describe('gog_gmail_thread_get', () => {
|
|
|
500
500
|
sanitizeContent: false,
|
|
501
501
|
});
|
|
502
502
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
503
|
-
['gmail', 'thread', 'get', 't1'],
|
|
503
|
+
['gmail', 'thread', 'get', 't1', '--use-indexed-attachment-ids=false'],
|
|
504
504
|
{ account: undefined },
|
|
505
505
|
);
|
|
506
506
|
});
|
|
@@ -527,7 +527,7 @@ describe('gog_gmail_thread_get', () => {
|
|
|
527
527
|
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult(THREAD));
|
|
528
528
|
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', latestN: 2 });
|
|
529
529
|
// latestN is wrapper-side; no CLI flag is added
|
|
530
|
-
expect(vi.mocked(lib.runOrDiagnose).mock.calls[0]![0]).toEqual(['gmail', 'thread', 'get', 't1']);
|
|
530
|
+
expect(vi.mocked(lib.runOrDiagnose).mock.calls[0]![0]).toEqual(['gmail', 'thread', 'get', 't1', '--use-indexed-attachment-ids=false']);
|
|
531
531
|
const parsed = JSON.parse(result.content[0].text);
|
|
532
532
|
expect(parsed.thread.messages.map((m: { id: string }) => m.id)).toEqual(['m2', 'm3']);
|
|
533
533
|
});
|
|
@@ -598,7 +598,7 @@ describe('gog_gmail_thread_attachments', () => {
|
|
|
598
598
|
it('calls runOrDiagnose with threadId', async () => {
|
|
599
599
|
await harness.callTool('gog_gmail_thread_attachments', { threadId: 't1' });
|
|
600
600
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
601
|
-
['gmail', 'thread', 'attachments', 't1'],
|
|
601
|
+
['gmail', 'thread', 'attachments', 't1', '--use-indexed-attachment-ids=false'],
|
|
602
602
|
{ account: undefined },
|
|
603
603
|
);
|
|
604
604
|
});
|
|
@@ -610,7 +610,7 @@ describe('gog_gmail_thread_attachments', () => {
|
|
|
610
610
|
outDir: '/tmp/atts',
|
|
611
611
|
});
|
|
612
612
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
613
|
-
['gmail', 'thread', 'attachments', 't1', '--download', '--out-dir=/tmp/atts'],
|
|
613
|
+
['gmail', 'thread', 'attachments', 't1', '--download', '--out-dir=/tmp/atts', '--use-indexed-attachment-ids=false'],
|
|
614
614
|
{ account: undefined },
|
|
615
615
|
);
|
|
616
616
|
});
|
|
@@ -718,7 +718,7 @@ describe('gog_gmail_drafts_get', () => {
|
|
|
718
718
|
it('calls runOrDiagnose with draftId', async () => {
|
|
719
719
|
await harness.callTool('gog_gmail_drafts_get', { draftId: 'd1' });
|
|
720
720
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
721
|
-
['gmail', 'drafts', 'get', 'd1'],
|
|
721
|
+
['gmail', 'drafts', 'get', 'd1', '--use-indexed-attachment-ids=false'],
|
|
722
722
|
{ account: undefined },
|
|
723
723
|
);
|
|
724
724
|
});
|
|
@@ -726,7 +726,7 @@ describe('gog_gmail_drafts_get', () => {
|
|
|
726
726
|
it('passes --download when true', async () => {
|
|
727
727
|
await harness.callTool('gog_gmail_drafts_get', { draftId: 'd1', download: true });
|
|
728
728
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
729
|
-
['gmail', 'drafts', 'get', 'd1', '--download'],
|
|
729
|
+
['gmail', 'drafts', 'get', 'd1', '--download', '--use-indexed-attachment-ids=false'],
|
|
730
730
|
{ account: undefined },
|
|
731
731
|
);
|
|
732
732
|
});
|
|
@@ -739,7 +739,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
739
739
|
body: 'Hello',
|
|
740
740
|
});
|
|
741
741
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
742
|
-
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello'],
|
|
742
|
+
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello', '--auto-from-addressed-alias=false'],
|
|
743
743
|
{ account: undefined },
|
|
744
744
|
);
|
|
745
745
|
});
|
|
@@ -772,7 +772,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
772
772
|
'--quote',
|
|
773
773
|
'--attach=/tmp/a.pdf',
|
|
774
774
|
'--attach=/tmp/b.pdf',
|
|
775
|
-
'--from=me@x.com',
|
|
775
|
+
'--from=me@x.com', '--auto-from-addressed-alias=false'
|
|
776
776
|
],
|
|
777
777
|
{ account: undefined },
|
|
778
778
|
);
|
|
@@ -785,7 +785,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
785
785
|
bodyHtmlFile: '/tmp/body.html',
|
|
786
786
|
});
|
|
787
787
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
788
|
-
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello', '--body-html-file=/tmp/body.html'],
|
|
788
|
+
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello', '--body-html-file=/tmp/body.html', '--auto-from-addressed-alias=false'],
|
|
789
789
|
{ account: undefined },
|
|
790
790
|
);
|
|
791
791
|
});
|
|
@@ -798,7 +798,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
798
798
|
replyAll: true,
|
|
799
799
|
});
|
|
800
800
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
801
|
-
['gmail', 'drafts', 'create', '--subject=Re: Hi', '--body=Hello all', '--thread-id=t1', '--reply-all'],
|
|
801
|
+
['gmail', 'drafts', 'create', '--subject=Re: Hi', '--body=Hello all', '--thread-id=t1', '--reply-all', '--auto-from-addressed-alias=false'],
|
|
802
802
|
{ account: undefined },
|
|
803
803
|
);
|
|
804
804
|
});
|
|
@@ -809,7 +809,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
809
809
|
subject: 'Hi', body: 'Hello', omitRecipients: true,
|
|
810
810
|
});
|
|
811
811
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
812
|
-
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello'],
|
|
812
|
+
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello', '--auto-from-addressed-alias=false'],
|
|
813
813
|
{ account: undefined },
|
|
814
814
|
);
|
|
815
815
|
});
|
|
@@ -822,9 +822,9 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
822
822
|
subject: 'Hi', body: 'Hello', returnFull: true,
|
|
823
823
|
});
|
|
824
824
|
expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(1,
|
|
825
|
-
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello'], { account: undefined });
|
|
825
|
+
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello', '--auto-from-addressed-alias=false'], { account: undefined });
|
|
826
826
|
expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(2,
|
|
827
|
-
['gmail', 'drafts', 'get', 'd9'], { account: undefined });
|
|
827
|
+
['gmail', 'drafts', 'get', 'd9', '--use-indexed-attachment-ids=false'], { account: undefined });
|
|
828
828
|
expect(result.content[0].text).toContain('"subject":"Hi"');
|
|
829
829
|
});
|
|
830
830
|
|
|
@@ -859,7 +859,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
859
859
|
// gog resolves the thread's latest-message headers itself — no extra fetch.
|
|
860
860
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
861
861
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
862
|
-
['gmail', 'drafts', 'create', '--subject=Re: roof', '--body=Sounds good', '--thread-id=19dffe06f9668b28'],
|
|
862
|
+
['gmail', 'drafts', 'create', '--subject=Re: roof', '--body=Sounds good', '--thread-id=19dffe06f9668b28', '--auto-from-addressed-alias=false'],
|
|
863
863
|
{ account: 'me@x.com' },
|
|
864
864
|
);
|
|
865
865
|
});
|
|
@@ -869,7 +869,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
869
869
|
draftId: 'd1', subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
870
870
|
});
|
|
871
871
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
872
|
-
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--thread-id=t1'],
|
|
872
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--thread-id=t1', '--auto-from-addressed-alias=false'],
|
|
873
873
|
{ account: undefined },
|
|
874
874
|
);
|
|
875
875
|
});
|
|
@@ -880,7 +880,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
880
880
|
});
|
|
881
881
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
882
882
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
883
|
-
['gmail', 'drafts', 'create', '--subject=S', '--body=B', '--reply-to-message-id=mExplicit'],
|
|
883
|
+
['gmail', 'drafts', 'create', '--subject=S', '--body=B', '--reply-to-message-id=mExplicit', '--auto-from-addressed-alias=false'],
|
|
884
884
|
{ account: undefined },
|
|
885
885
|
);
|
|
886
886
|
});
|
|
@@ -894,7 +894,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
894
894
|
body: 'New body',
|
|
895
895
|
});
|
|
896
896
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
897
|
-
['gmail', 'drafts', 'update', 'd1', '--subject=New subject', '--body=New body'],
|
|
897
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=New subject', '--body=New body', '--auto-from-addressed-alias=false'],
|
|
898
898
|
{ account: undefined },
|
|
899
899
|
);
|
|
900
900
|
});
|
|
@@ -907,7 +907,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
907
907
|
attach: ['/tmp/x.pdf'],
|
|
908
908
|
});
|
|
909
909
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
910
|
-
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--attach=/tmp/x.pdf'],
|
|
910
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--attach=/tmp/x.pdf', '--auto-from-addressed-alias=false'],
|
|
911
911
|
{ account: undefined },
|
|
912
912
|
);
|
|
913
913
|
});
|
|
@@ -917,7 +917,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
917
917
|
draftId: 'd1', to: 'a@b.com', subject: 'S', body: 'B', omitRecipients: true,
|
|
918
918
|
});
|
|
919
919
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
920
|
-
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B'],
|
|
920
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--auto-from-addressed-alias=false'],
|
|
921
921
|
{ account: undefined },
|
|
922
922
|
);
|
|
923
923
|
});
|
|
@@ -927,7 +927,41 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
927
927
|
draftId: 'd1', subject: 'S', body: 'B', clearAttachments: true,
|
|
928
928
|
});
|
|
929
929
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
930
|
-
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--clear-attachments'],
|
|
930
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--auto-from-addressed-alias=false', '--clear-attachments'],
|
|
931
|
+
{ account: undefined },
|
|
932
|
+
);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('passes --clear-reply-context when clearReplyContext is true', async () => {
|
|
936
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
937
|
+
draftId: 'd1', subject: 'S', body: 'B', clearReplyContext: true,
|
|
938
|
+
});
|
|
939
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
940
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--auto-from-addressed-alias=false', '--clear-reply-context'],
|
|
941
|
+
{ account: undefined },
|
|
942
|
+
);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// A plain update carries no reply flags at all: gog preserves the draft's own
|
|
946
|
+
// reply context and threadId. Passing a reply target here would re-anchor the
|
|
947
|
+
// draft, so the wrapper must stay silent when the caller says nothing.
|
|
948
|
+
it('sends no reply or thread flags when no reply target is supplied', async () => {
|
|
949
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
950
|
+
draftId: 'd1', subject: 'S', body: 'B',
|
|
951
|
+
});
|
|
952
|
+
const args = vi.mocked(lib.runOrDiagnose).mock.calls[0]?.[0] as string[];
|
|
953
|
+
expect(args.some((a) => a.startsWith('--reply-to-message-id'))).toBe(false);
|
|
954
|
+
expect(args.some((a) => a.startsWith('--thread-id'))).toBe(false);
|
|
955
|
+
expect(args).not.toContain('--clear-reply-context');
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it('combines clearAttachments and clearReplyContext', async () => {
|
|
959
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
960
|
+
draftId: 'd1', subject: 'S', body: 'B', clearAttachments: true, clearReplyContext: true,
|
|
961
|
+
});
|
|
962
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
963
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B',
|
|
964
|
+
'--auto-from-addressed-alias=false', '--clear-attachments', '--clear-reply-context'],
|
|
931
965
|
{ account: undefined },
|
|
932
966
|
);
|
|
933
967
|
});
|
|
@@ -940,7 +974,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
940
974
|
draftId: 'd1', subject: 'S', body: 'B', returnFull: true,
|
|
941
975
|
});
|
|
942
976
|
expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(2,
|
|
943
|
-
['gmail', 'drafts', 'get', 'd1'], { account: undefined });
|
|
977
|
+
['gmail', 'drafts', 'get', 'd1', '--use-indexed-attachment-ids=false'], { account: undefined });
|
|
944
978
|
expect(result.content[0].text).toContain('"subject":"S"');
|
|
945
979
|
});
|
|
946
980
|
|
|
@@ -1037,7 +1071,7 @@ describe('gog_gmail_reply', () => {
|
|
|
1037
1071
|
it('calls runOrDiagnose with messageId and --body', async () => {
|
|
1038
1072
|
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Thanks' });
|
|
1039
1073
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1040
|
-
['gmail', 'reply', 'm1', '--body=Thanks'],
|
|
1074
|
+
['gmail', 'reply', 'm1', '--body=Thanks', '--auto-from-addressed-alias=false'],
|
|
1041
1075
|
{ account: undefined },
|
|
1042
1076
|
);
|
|
1043
1077
|
});
|
|
@@ -1077,7 +1111,7 @@ describe('gog_gmail_reply', () => {
|
|
|
1077
1111
|
'--from=me@x.com',
|
|
1078
1112
|
'--signature',
|
|
1079
1113
|
'--signature-from=alias@x.com',
|
|
1080
|
-
'--signature-file=/tmp/sig.txt',
|
|
1114
|
+
'--signature-file=/tmp/sig.txt', '--auto-from-addressed-alias=false'
|
|
1081
1115
|
],
|
|
1082
1116
|
{ account: 'me@gmail.com' },
|
|
1083
1117
|
);
|
|
@@ -1086,7 +1120,7 @@ describe('gog_gmail_reply', () => {
|
|
|
1086
1120
|
it('omits --no-quote and --signature when false', async () => {
|
|
1087
1121
|
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', noQuote: false, signature: false });
|
|
1088
1122
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1089
|
-
['gmail', 'reply', 'm1', '--body=Hi'],
|
|
1123
|
+
['gmail', 'reply', 'm1', '--body=Hi', '--auto-from-addressed-alias=false'],
|
|
1090
1124
|
{ account: undefined },
|
|
1091
1125
|
);
|
|
1092
1126
|
});
|
|
@@ -1096,7 +1130,7 @@ describe('gog_gmail_reply_all', () => {
|
|
|
1096
1130
|
it('uses the reply-all subcommand', async () => {
|
|
1097
1131
|
await harness.callTool('gog_gmail_reply_all', { messageId: 'm1', body: 'Thanks all' });
|
|
1098
1132
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1099
|
-
['gmail', 'reply-all', 'm1', '--body=Thanks all'],
|
|
1133
|
+
['gmail', 'reply-all', 'm1', '--body=Thanks all', '--auto-from-addressed-alias=false'],
|
|
1100
1134
|
{ account: undefined },
|
|
1101
1135
|
);
|
|
1102
1136
|
});
|
|
@@ -1116,7 +1150,7 @@ describe('gog_gmail_reply_all', () => {
|
|
|
1116
1150
|
'--cc=x@y.com',
|
|
1117
1151
|
'--cc=z@y.com',
|
|
1118
1152
|
'--remove=drop@y.com',
|
|
1119
|
-
'--signature-file=/tmp/sig.html',
|
|
1153
|
+
'--signature-file=/tmp/sig.html', '--auto-from-addressed-alias=false'
|
|
1120
1154
|
],
|
|
1121
1155
|
{ account: undefined },
|
|
1122
1156
|
);
|
|
@@ -1194,7 +1228,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
1194
1228
|
it('calls runOrDiagnose with just the query', async () => {
|
|
1195
1229
|
await harness.callTool('gog_gmail_messages_search', { query: 'from:alice' });
|
|
1196
1230
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1197
|
-
['gmail', 'messages', 'search', 'from:alice'],
|
|
1231
|
+
['gmail', 'messages', 'search', 'from:alice', '--include-attachments=false', '--use-indexed-attachment-ids=false'],
|
|
1198
1232
|
{ account: undefined },
|
|
1199
1233
|
);
|
|
1200
1234
|
});
|
|
@@ -1211,7 +1245,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
1211
1245
|
account: 'me@x.com',
|
|
1212
1246
|
});
|
|
1213
1247
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1214
|
-
['gmail', 'messages', 'search', 'is:unread', '--max=10', '--page=tok', '--all', '--include-body', '--full', '--body-format=html'],
|
|
1248
|
+
['gmail', 'messages', 'search', 'is:unread', '--max=10', '--page=tok', '--all', '--include-body', '--full', '--body-format=html', '--include-attachments=false', '--use-indexed-attachment-ids=false'],
|
|
1215
1249
|
{ account: 'me@x.com' },
|
|
1216
1250
|
);
|
|
1217
1251
|
});
|
|
@@ -1219,7 +1253,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
1219
1253
|
it('omits flags when false/absent', async () => {
|
|
1220
1254
|
await harness.callTool('gog_gmail_messages_search', { query: 'x', all: false, includeBody: false, full: false });
|
|
1221
1255
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1222
|
-
['gmail', 'messages', 'search', 'x'],
|
|
1256
|
+
['gmail', 'messages', 'search', 'x', '--include-attachments=false', '--use-indexed-attachment-ids=false'],
|
|
1223
1257
|
{ account: undefined },
|
|
1224
1258
|
);
|
|
1225
1259
|
});
|
|
@@ -1516,7 +1550,7 @@ describe('large payloads route to file args', () => {
|
|
|
1516
1550
|
it('keeps a body at exactly the threshold inline', async () => {
|
|
1517
1551
|
const atLimit = 'x'.repeat(lib.PAYLOAD_INLINE_MAX);
|
|
1518
1552
|
await harness.callTool('gog_gmail_drafts_create', { subject: 'S', body: atLimit });
|
|
1519
|
-
expect(args()).toEqual(['gmail', 'drafts', 'create', '--subject=S', `--body=${atLimit}
|
|
1553
|
+
expect(args()).toEqual(['gmail', 'drafts', 'create', '--subject=S', `--body=${atLimit}`, '--auto-from-addressed-alias=false']);
|
|
1520
1554
|
});
|
|
1521
1555
|
|
|
1522
1556
|
it('measures bytes, not characters, so a multibyte body crosses earlier', async () => {
|
|
@@ -1534,7 +1568,7 @@ describe('large payloads route to file args', () => {
|
|
|
1534
1568
|
expect(args()).toEqual([
|
|
1535
1569
|
'gmail', 'drafts', 'create',
|
|
1536
1570
|
'--subject=S',
|
|
1537
|
-
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1571
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined }, '--auto-from-addressed-alias=false'
|
|
1538
1572
|
]);
|
|
1539
1573
|
});
|
|
1540
1574
|
|
|
@@ -1545,7 +1579,7 @@ describe('large payloads route to file args', () => {
|
|
|
1545
1579
|
'gmail', 'drafts', 'create',
|
|
1546
1580
|
'--subject=S',
|
|
1547
1581
|
'--body=plain',
|
|
1548
|
-
{ kind: 'file', flag: 'body-html-file', contents: bigHtml, ext: 'html' },
|
|
1582
|
+
{ kind: 'file', flag: 'body-html-file', contents: bigHtml, ext: 'html' }, '--auto-from-addressed-alias=false'
|
|
1549
1583
|
]);
|
|
1550
1584
|
});
|
|
1551
1585
|
|
|
@@ -1554,7 +1588,7 @@ describe('large payloads route to file args', () => {
|
|
|
1554
1588
|
expect(args()).toEqual([
|
|
1555
1589
|
'gmail', 'drafts', 'update', 'd1',
|
|
1556
1590
|
'--subject=S',
|
|
1557
|
-
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1591
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined }, '--auto-from-addressed-alias=false'
|
|
1558
1592
|
]);
|
|
1559
1593
|
});
|
|
1560
1594
|
|
|
@@ -1565,7 +1599,7 @@ describe('large payloads route to file args', () => {
|
|
|
1565
1599
|
expect(args()).toEqual([
|
|
1566
1600
|
'gmail', 'drafts', 'create',
|
|
1567
1601
|
'--subject=S',
|
|
1568
|
-
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1602
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined }, '--auto-from-addressed-alias=false'
|
|
1569
1603
|
]);
|
|
1570
1604
|
});
|
|
1571
1605
|
|
|
@@ -1578,7 +1612,7 @@ describe('large payloads route to file args', () => {
|
|
|
1578
1612
|
'--subject=S',
|
|
1579
1613
|
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1580
1614
|
'--thread-id=t1',
|
|
1581
|
-
'--attach=/tmp/a.pdf',
|
|
1615
|
+
'--attach=/tmp/a.pdf', '--auto-from-addressed-alias=false'
|
|
1582
1616
|
]);
|
|
1583
1617
|
});
|
|
1584
1618
|
|
|
@@ -1595,7 +1629,7 @@ describe('large payloads route to file args', () => {
|
|
|
1595
1629
|
expect(args()).toEqual([
|
|
1596
1630
|
'gmail', 'reply', 'm1',
|
|
1597
1631
|
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1598
|
-
{ kind: 'file', flag: 'body-html-file', contents: bigHtml, ext: 'html' },
|
|
1632
|
+
{ kind: 'file', flag: 'body-html-file', contents: bigHtml, ext: 'html' }, '--auto-from-addressed-alias=false'
|
|
1599
1633
|
]);
|
|
1600
1634
|
});
|
|
1601
1635
|
|
|
@@ -1604,7 +1638,7 @@ describe('large payloads route to file args', () => {
|
|
|
1604
1638
|
expect(args()).toEqual([
|
|
1605
1639
|
'gmail', 'reply-all', 'm1',
|
|
1606
1640
|
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1607
|
-
'--signature',
|
|
1641
|
+
'--signature', '--auto-from-addressed-alias=false'
|
|
1608
1642
|
]);
|
|
1609
1643
|
});
|
|
1610
1644
|
|
|
@@ -1676,8 +1710,310 @@ describe('inline/file param conflicts are rejected before gog runs', () => {
|
|
|
1676
1710
|
it('bodyHtmlFile alone still passes through as --body-html-file', async () => {
|
|
1677
1711
|
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', bodyHtmlFile: '/tmp/b.html' });
|
|
1678
1712
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1679
|
-
['gmail', 'reply', 'm1', '--body=Hi', '--body-html-file=/tmp/b.html'],
|
|
1713
|
+
['gmail', 'reply', 'm1', '--body=Hi', '--body-html-file=/tmp/b.html', '--auto-from-addressed-alias=false'],
|
|
1680
1714
|
{ account: undefined },
|
|
1681
1715
|
);
|
|
1682
1716
|
});
|
|
1683
1717
|
});
|
|
1718
|
+
|
|
1719
|
+
// ---------------------------------------------------------------------------
|
|
1720
|
+
// gog 0.35.0 capabilities (MIN_GOG_VERSION floor).
|
|
1721
|
+
//
|
|
1722
|
+
// Every flag below is env-bound in gog, so an ambient GOG_GMAIL_* var on the
|
|
1723
|
+
// host silently changes the SHAPE of the output (or, for the attachment
|
|
1724
|
+
// download, makes gog reject the caller's own argument). runner.ts strips only
|
|
1725
|
+
// *_TOKEN/*_SECRET/*_API_KEY/*_PRIVATE_KEY, and on the remote runner the child
|
|
1726
|
+
// env belongs to a backend this wrapper does not control — so the tests below
|
|
1727
|
+
// assert the flag is PINNED on every call, not merely pushed when true.
|
|
1728
|
+
// ---------------------------------------------------------------------------
|
|
1729
|
+
|
|
1730
|
+
describe('gog 0.35.0 — indexed attachment ids (gog_gmail_attachment)', () => {
|
|
1731
|
+
const PDF_B64 = 'JVBERi0xLjUKJVBFRgo=';
|
|
1732
|
+
const INDEXED_LIST = {
|
|
1733
|
+
attachments: [
|
|
1734
|
+
{ filename: 'cover.png', mimeType: 'image/png', attachmentIndex: 0, size: 11 },
|
|
1735
|
+
{ filename: 'Guest_Copy.pdf', mimeType: 'application/pdf', attachmentIndex: 1, size: 99723 },
|
|
1736
|
+
],
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
function stub(opts: { meta?: unknown; metaError?: Error; download?: unknown; downloadError?: unknown }): void {
|
|
1740
|
+
vi.mocked(lib.run).mockImplementation(async (args) => {
|
|
1741
|
+
const a = args as string[];
|
|
1742
|
+
if (a[0] === 'gmail' && a[1] === 'get') {
|
|
1743
|
+
if (opts.metaError) throw opts.metaError;
|
|
1744
|
+
return JSON.stringify(opts.meta ?? { attachments: [] });
|
|
1745
|
+
}
|
|
1746
|
+
if (a[0] === 'gmail' && a[1] === 'attachment') {
|
|
1747
|
+
if (opts.downloadError) throw opts.downloadError;
|
|
1748
|
+
return JSON.stringify(opts.download ?? {});
|
|
1749
|
+
}
|
|
1750
|
+
return '{}';
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
const runArgs = () => vi.mocked(lib.run).mock.calls.map((c) => c[0] as string[]);
|
|
1754
|
+
const dlArgs = () => runArgs().find((a) => a[1] === 'attachment')!;
|
|
1755
|
+
const textOf = (res: { content: unknown[] }) => (res.content[0] as { text: string }).text;
|
|
1756
|
+
|
|
1757
|
+
it('resolves the real filename BEFORE the download and never runs the size heuristic', async () => {
|
|
1758
|
+
stub({ meta: INDEXED_LIST, download: { path: '/tmp/gog-attachments/m1/Guest_Copy.pdf', bytes: 99723, contentBase64: PDF_B64 } });
|
|
1759
|
+
const res = await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentIndex: 1 });
|
|
1760
|
+
// exactly one metadata read, and it happens BEFORE the download.
|
|
1761
|
+
expect(runArgs().map((a) => a.slice(0, 2))).toEqual([['gmail', 'get'], ['gmail', 'attachment']]);
|
|
1762
|
+
expect(runArgs()[0]).toEqual(['gmail', 'get', 'm1', '--use-indexed-attachment-ids']);
|
|
1763
|
+
// the index rides in the positional slot, and gog is told to read it as one.
|
|
1764
|
+
expect(dlArgs()).toEqual([
|
|
1765
|
+
'gmail', 'attachment', 'm1', '1', '--use-indexed-attachment-ids', '--inline', '--inline-max-bytes=3145728',
|
|
1766
|
+
'--out=/tmp/gog-attachments/m1/Guest_Copy.pdf', '--name=Guest_Copy.pdf',
|
|
1767
|
+
]);
|
|
1768
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ fileName: 'Guest_Copy.pdf', mimeType: 'application/pdf' });
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
it('pins the mode OFF on the legacy attachmentId path', async () => {
|
|
1772
|
+
stub({ download: { path: '/tmp/gog-attachments/m1/x.pdf', bytes: 12, contentBase64: PDF_B64 } });
|
|
1773
|
+
await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentId: 'OPAQUE1', name: 'x.pdf' });
|
|
1774
|
+
expect(dlArgs()).toEqual([
|
|
1775
|
+
'gmail', 'attachment', 'm1', 'OPAQUE1', '--use-indexed-attachment-ids=false', '--inline', '--inline-max-bytes=3145728',
|
|
1776
|
+
'--out=/tmp/gog-attachments/m1/x.pdf', '--name=x.pdf',
|
|
1777
|
+
]);
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
it('rejects both attachmentId and attachmentIndex', async () => {
|
|
1781
|
+
const res = await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentId: 'a1', attachmentIndex: 0 });
|
|
1782
|
+
expect(res.isError).toBe(true);
|
|
1783
|
+
expect(textOf(res)).toContain('exactly one');
|
|
1784
|
+
expect(lib.run).not.toHaveBeenCalled();
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
it('rejects neither attachmentId nor attachmentIndex', async () => {
|
|
1788
|
+
const res = await harness.callTool('gog_gmail_attachment', { messageId: 'm1' });
|
|
1789
|
+
expect(res.isError).toBe(true);
|
|
1790
|
+
expect(textOf(res)).toContain('exactly one');
|
|
1791
|
+
expect(lib.run).not.toHaveBeenCalled();
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
it('a caller-supplied name skips the index lookup', async () => {
|
|
1795
|
+
stub({ meta: INDEXED_LIST, download: { path: '/tmp/gog-attachments/m1/mine.pdf', bytes: 5, contentBase64: PDF_B64 } });
|
|
1796
|
+
await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentIndex: 1, name: 'mine.pdf' });
|
|
1797
|
+
expect(runArgs().some((a) => a[1] === 'get')).toBe(false);
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
it('survives an index lookup failure without falling back to the size heuristic', async () => {
|
|
1801
|
+
stub({ metaError: new Error('get failed'), download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 12, contentBase64: PDF_B64 } });
|
|
1802
|
+
const res = await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentIndex: 0 });
|
|
1803
|
+
// only the (failed) pre-download lookup — resolveBySize must not run after it.
|
|
1804
|
+
expect(runArgs().filter((a) => a[1] === 'get')).toHaveLength(1);
|
|
1805
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ fileName: 'attachment.pdf', mimeType: 'application/pdf' });
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
it('tolerates a message whose listing carries no attachments array', async () => {
|
|
1809
|
+
stub({ meta: {}, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 12, contentBase64: PDF_B64 } });
|
|
1810
|
+
const res = await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentIndex: 3 });
|
|
1811
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ fileName: 'attachment.pdf' });
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
it('uses the filename/mimeType gog itself reports on an --inline download', async () => {
|
|
1815
|
+
// gog >= 0.34 returns the part metadata alongside the bytes; prefer it over
|
|
1816
|
+
// any wrapper-side guess.
|
|
1817
|
+
stub({ meta: { attachments: [{}] }, download: {
|
|
1818
|
+
path: '/tmp/gog-attachments/m1/attachment', bytes: 12, contentBase64: PDF_B64,
|
|
1819
|
+
filename: 'From_Gog.pdf', mimeType: 'application/pdf',
|
|
1820
|
+
} });
|
|
1821
|
+
const res = await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentIndex: 0 });
|
|
1822
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ fileName: 'From_Gog.pdf', mimeType: 'application/pdf' });
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
it('prefers a caller name over the one gog reports', async () => {
|
|
1826
|
+
stub({ download: {
|
|
1827
|
+
path: '/tmp/gog-attachments/m1/mine.pdf', bytes: 12, contentBase64: PDF_B64,
|
|
1828
|
+
filename: 'From_Gog.pdf', mimeType: 'application/pdf',
|
|
1829
|
+
} });
|
|
1830
|
+
const res = await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentIndex: 0, name: 'mine.pdf' });
|
|
1831
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ fileName: 'mine.pdf', mimeType: 'application/pdf' });
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
it('passes inlineMaxBytes through as --inline-max-bytes', async () => {
|
|
1835
|
+
stub({ meta: INDEXED_LIST, download: { path: '/tmp/gog-attachments/m1/cover.png', bytes: 11 } });
|
|
1836
|
+
await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentIndex: 0, inlineMaxBytes: 1048576 });
|
|
1837
|
+
expect(dlArgs()).toEqual([
|
|
1838
|
+
'gmail', 'attachment', 'm1', '0', '--use-indexed-attachment-ids', '--inline', '--inline-max-bytes=1048576',
|
|
1839
|
+
'--out=/tmp/gog-attachments/m1/cover.png', '--name=cover.png',
|
|
1840
|
+
]);
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
it('does not mangle an indexed failure message while redacting (no bare-digit substitution)', async () => {
|
|
1844
|
+
stub({ downloadError: new Error('Command failed: gog gmail attachment m1 0\nquota exceeded: 1000 requests') });
|
|
1845
|
+
await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentIndex: 0 });
|
|
1846
|
+
const passed = (vi.mocked(lib.diagnose).mock.calls[0]![0] as Error).message;
|
|
1847
|
+
expect(passed).toBe('quota exceeded: 1000 requests');
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
describe('gog 0.35.0 — indexed ids are pinned on every listing that emits attachments', () => {
|
|
1852
|
+
const args = () => vi.mocked(lib.runOrDiagnose).mock.calls[0]![0] as string[];
|
|
1853
|
+
|
|
1854
|
+
it('gog_gmail_thread_get pins the mode off by default and on when asked', async () => {
|
|
1855
|
+
await harness.callTool('gog_gmail_thread_get', { threadId: 't1' });
|
|
1856
|
+
expect(args()).toEqual(['gmail', 'thread', 'get', 't1', '--use-indexed-attachment-ids=false']);
|
|
1857
|
+
vi.clearAllMocks();
|
|
1858
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1859
|
+
await harness.callTool('gog_gmail_thread_get', { threadId: 't1', useIndexedAttachmentIds: true });
|
|
1860
|
+
expect(args()).toEqual(['gmail', 'thread', 'get', 't1', '--use-indexed-attachment-ids']);
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
it('gog_gmail_thread_attachments pins the mode — a flat list needs per-message indexes', async () => {
|
|
1864
|
+
await harness.callTool('gog_gmail_thread_attachments', { threadId: 't1' });
|
|
1865
|
+
expect(args()).toEqual(['gmail', 'thread', 'attachments', 't1', '--use-indexed-attachment-ids=false']);
|
|
1866
|
+
vi.clearAllMocks();
|
|
1867
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1868
|
+
await harness.callTool('gog_gmail_thread_attachments', { threadId: 't1', useIndexedAttachmentIds: true, download: true });
|
|
1869
|
+
expect(args()).toEqual(['gmail', 'thread', 'attachments', 't1', '--download', '--use-indexed-attachment-ids']);
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
it('gog_gmail_drafts_get pins the mode', async () => {
|
|
1873
|
+
await harness.callTool('gog_gmail_drafts_get', { draftId: 'd1' });
|
|
1874
|
+
expect(args()).toEqual(['gmail', 'drafts', 'get', 'd1', '--use-indexed-attachment-ids=false']);
|
|
1875
|
+
vi.clearAllMocks();
|
|
1876
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1877
|
+
await harness.callTool('gog_gmail_drafts_get', { draftId: 'd1', useIndexedAttachmentIds: true, download: true });
|
|
1878
|
+
expect(args()).toEqual(['gmail', 'drafts', 'get', 'd1', '--download', '--use-indexed-attachment-ids']);
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
it('gog_gmail_messages_search pins BOTH attachment-shaping flags', async () => {
|
|
1882
|
+
await harness.callTool('gog_gmail_messages_search', { query: 'x' });
|
|
1883
|
+
expect(args()).toEqual([
|
|
1884
|
+
'gmail', 'messages', 'search', 'x', '--include-attachments=false', '--use-indexed-attachment-ids=false',
|
|
1885
|
+
]);
|
|
1886
|
+
vi.clearAllMocks();
|
|
1887
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1888
|
+
await harness.callTool('gog_gmail_messages_search', { query: 'x', includeAttachments: true, useIndexedAttachmentIds: true });
|
|
1889
|
+
expect(args()).toEqual([
|
|
1890
|
+
'gmail', 'messages', 'search', 'x', '--include-attachments', '--use-indexed-attachment-ids',
|
|
1891
|
+
]);
|
|
1892
|
+
});
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
describe('gog 0.35.0 — --auto-from-addressed-alias is pinned on every send-shaped write', () => {
|
|
1896
|
+
const args = () => vi.mocked(lib.runOrDiagnose).mock.calls[0]![0] as string[];
|
|
1897
|
+
|
|
1898
|
+
it('gog_gmail_drafts_create pins it off, and sets it when asked', async () => {
|
|
1899
|
+
await harness.callTool('gog_gmail_drafts_create', { subject: 'S', body: 'B' });
|
|
1900
|
+
expect(args()).toEqual(['gmail', 'drafts', 'create', '--subject=S', '--body=B', '--auto-from-addressed-alias=false']);
|
|
1901
|
+
vi.clearAllMocks();
|
|
1902
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1903
|
+
await harness.callTool('gog_gmail_drafts_create', { subject: 'S', body: 'B', autoFromAddressedAlias: true });
|
|
1904
|
+
expect(args()).toEqual(['gmail', 'drafts', 'create', '--subject=S', '--body=B', '--auto-from-addressed-alias']);
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
it('gog_gmail_drafts_update pins it', async () => {
|
|
1908
|
+
await harness.callTool('gog_gmail_drafts_update', { draftId: 'd1', subject: 'S', body: 'B', autoFromAddressedAlias: true });
|
|
1909
|
+
expect(args()).toEqual(['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--auto-from-addressed-alias']);
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
it('gog_gmail_reply and gog_gmail_reply_all pin it', async () => {
|
|
1913
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi' });
|
|
1914
|
+
expect(args()).toEqual(['gmail', 'reply', 'm1', '--body=Hi', '--auto-from-addressed-alias=false']);
|
|
1915
|
+
vi.clearAllMocks();
|
|
1916
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1917
|
+
await harness.callTool('gog_gmail_reply_all', { messageId: 'm1', body: 'Hi', autoFromAddressedAlias: true });
|
|
1918
|
+
expect(args()).toEqual(['gmail', 'reply-all', 'm1', '--body=Hi', '--auto-from-addressed-alias']);
|
|
1919
|
+
});
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
describe('gog 0.35.0 — gog_gmail_import', () => {
|
|
1923
|
+
const args = () => vi.mocked(lib.runOrDiagnose).mock.calls[0]![0] as string[];
|
|
1924
|
+
|
|
1925
|
+
it('imports a file with no options', async () => {
|
|
1926
|
+
await harness.callTool('gog_gmail_import', { file: '/tmp/msg.eml' });
|
|
1927
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'import', '/tmp/msg.eml'], { account: undefined });
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
it('passes every option, repeating --label', async () => {
|
|
1931
|
+
await harness.callTool('gog_gmail_import', {
|
|
1932
|
+
file: '/srv/exports/archived.eml',
|
|
1933
|
+
labels: ['INBOX', 'Archive/2026'],
|
|
1934
|
+
internalDateSource: 'receivedTime',
|
|
1935
|
+
neverMarkSpam: true,
|
|
1936
|
+
processForCalendar: true,
|
|
1937
|
+
account: 'me@x.com',
|
|
1938
|
+
});
|
|
1939
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1940
|
+
['gmail', 'import', '/srv/exports/archived.eml', '--label=INBOX', '--label=Archive/2026', '--internal-date-source=receivedTime', '--never-mark-spam', '--process-for-calendar'],
|
|
1941
|
+
{ account: 'me@x.com' },
|
|
1942
|
+
);
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
it('omits flags that are false or absent', async () => {
|
|
1946
|
+
await harness.callTool('gog_gmail_import', { file: '/tmp/m.eml', neverMarkSpam: false, processForCalendar: false });
|
|
1947
|
+
expect(args()).toEqual(['gmail', 'import', '/tmp/m.eml']);
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
it('rejects an internalDateSource outside gog\'s enum', async () => {
|
|
1951
|
+
const res = await harness.callTool('gog_gmail_import', { file: '/tmp/m.eml', internalDateSource: 'yesterday' });
|
|
1952
|
+
expect(res.isError).toBe(true);
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
it('never appends --force — gog gates no confirmation on import', async () => {
|
|
1956
|
+
await harness.callTool('gog_gmail_import', { file: '/tmp/m.eml' });
|
|
1957
|
+
expect(args()).not.toContain('--force');
|
|
1958
|
+
});
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
// gog reads a "-" path argument with io.ReadAll(stdinReader(ctx)) — os.Stdin,
|
|
1962
|
+
// unaffected by --no-input (gmail_import.go:102, gmail_body_input.go:45 at
|
|
1963
|
+
// upstream-v0.35.0). runner.ts's spawnGog spawns with default stdio and never
|
|
1964
|
+
// writes to or ends child.stdin, so gog blocks forever on a read that never
|
|
1965
|
+
// EOFs: `spawn('gog', ['gmail','import','-','--dry-run','--json','--no-input'])`
|
|
1966
|
+
// was still running with no output after 5s. Under run() that burns the whole
|
|
1967
|
+
// 30s timeout and returns "gog timed out after 30s". No description may offer
|
|
1968
|
+
// it as a usable option.
|
|
1969
|
+
describe('server-side file params never advertise stdin as usable', () => {
|
|
1970
|
+
const STDIN_PARAMS: Array<[tool: string, param: string]> = [
|
|
1971
|
+
['gog_gmail_import', 'file'],
|
|
1972
|
+
['gog_gmail_drafts_create', 'bodyHtmlFile'],
|
|
1973
|
+
['gog_gmail_drafts_update', 'bodyHtmlFile'],
|
|
1974
|
+
['gog_gmail_reply', 'bodyHtmlFile'],
|
|
1975
|
+
['gog_gmail_reply_all', 'bodyHtmlFile'],
|
|
1976
|
+
];
|
|
1977
|
+
|
|
1978
|
+
async function paramDescriptions(): Promise<Map<string, Record<string, { description?: string }>>> {
|
|
1979
|
+
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
1980
|
+
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
1981
|
+
const configs = new Map<string, Record<string, { description?: string }>>();
|
|
1982
|
+
vi.spyOn(server, 'registerTool').mockImplementation((name, config) => {
|
|
1983
|
+
configs.set(name, (config as { inputSchema: Record<string, { description?: string }> }).inputSchema);
|
|
1984
|
+
return undefined as never;
|
|
1985
|
+
});
|
|
1986
|
+
registerExtraGmailTools(server);
|
|
1987
|
+
return configs;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
it.each(STDIN_PARAMS)('%s.%s warns that stdin hangs instead of offering it', async (tool, param) => {
|
|
1991
|
+
const schema = (await paramDescriptions()).get(tool);
|
|
1992
|
+
const desc = schema?.[param]?.description ?? '';
|
|
1993
|
+
expect(desc).not.toBe('');
|
|
1994
|
+
expect(desc).not.toMatch(/(?:or|use)\s+"?-"?\s+(?:for|to read)/i);
|
|
1995
|
+
expect(desc).toMatch(/stdin/i);
|
|
1996
|
+
expect(desc).toMatch(/hang|never writes/i);
|
|
1997
|
+
});
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
// Every env-bound gog flag this tool depends on is pinned on the call, because
|
|
2001
|
+
// the child env on the remote runner belongs to a backend we do not control.
|
|
2002
|
+
// --inline-max-bytes is declared env:"GOG_GMAIL_INLINE_MAX_BYTES"
|
|
2003
|
+
// (gmail_attachment.go:27 at upstream-v0.35.0), so an ambient value would
|
|
2004
|
+
// silently decide whether contentBase64 comes back at all.
|
|
2005
|
+
describe('gog_gmail_attachment pins --inline-max-bytes', () => {
|
|
2006
|
+
const args = () => vi.mocked(lib.run).mock.calls[0]![0] as string[];
|
|
2007
|
+
|
|
2008
|
+
it('pins gog\'s default when the caller supplies nothing', async () => {
|
|
2009
|
+
vi.mocked(lib.run).mockResolvedValue(JSON.stringify({ bytes: 10, contentBase64: 'AAAA', mimeType: 'image/png', filename: 'a.png' }));
|
|
2010
|
+
await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentId: 'a1', deliver: 'inline' });
|
|
2011
|
+
expect(args()).toContain('--inline-max-bytes=3145728');
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
it('pins the caller\'s value when supplied', async () => {
|
|
2015
|
+
vi.mocked(lib.run).mockResolvedValue(JSON.stringify({ bytes: 10, contentBase64: 'AAAA', mimeType: 'image/png', filename: 'a.png' }));
|
|
2016
|
+
await harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentId: 'a1', deliver: 'inline', inlineMaxBytes: 99 });
|
|
2017
|
+
expect(args()).toContain('--inline-max-bytes=99');
|
|
2018
|
+
});
|
|
2019
|
+
});
|