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.
@@ -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
+ });