gogcli-mcp-gmail 2.8.0 → 2.18.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 +1 -1
- package/SKILL.md +1 -1
- package/dist/index.js +20210 -19656
- package/manifest.json +23 -3
- package/package.json +3 -2
- package/src/index.ts +7 -9
- package/src/tools/gmail-extra.ts +498 -39
- package/tests/tools/gmail-extra.test.ts +720 -137
- package/tsconfig.json +1 -1
|
@@ -1,32 +1,37 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { registerExtraGmailTools } from '../../src/tools/gmail-extra.js';
|
|
3
3
|
import * as lib from '../../../gogcli-mcp/src/lib.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
|
|
5
|
+
import { rawTextResult, errorResult } from '@chrischall/mcp-utils';
|
|
5
6
|
|
|
6
7
|
vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
7
8
|
const actual = await importOriginal<typeof lib>();
|
|
8
9
|
return {
|
|
9
10
|
...actual,
|
|
11
|
+
run: vi.fn(),
|
|
10
12
|
runOrDiagnose: vi.fn(),
|
|
13
|
+
diagnose: vi.fn(),
|
|
11
14
|
};
|
|
12
15
|
});
|
|
13
16
|
|
|
14
|
-
let
|
|
17
|
+
let harness: TestHarness;
|
|
15
18
|
|
|
16
|
-
beforeEach(() => {
|
|
19
|
+
beforeEach(async () => {
|
|
17
20
|
vi.clearAllMocks();
|
|
18
|
-
vi.mocked(lib.
|
|
19
|
-
|
|
21
|
+
vi.mocked(lib.run).mockResolvedValue('{}');
|
|
22
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
23
|
+
vi.mocked(lib.diagnose).mockResolvedValue(errorResult('diagnosed'));
|
|
24
|
+
harness = await createTestHarness(registerExtraGmailTools);
|
|
20
25
|
});
|
|
21
26
|
|
|
22
27
|
describe('gog_gmail_raw', () => {
|
|
23
28
|
it('calls runOrDiagnose with messageId', async () => {
|
|
24
|
-
await
|
|
29
|
+
await harness.callTool('gog_gmail_raw', { messageId: 'm1' });
|
|
25
30
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'raw', 'm1'], { account: undefined });
|
|
26
31
|
});
|
|
27
32
|
|
|
28
33
|
it('passes --format and --pretty when provided', async () => {
|
|
29
|
-
await
|
|
34
|
+
await harness.callTool('gog_gmail_raw', { messageId: 'm1', format: 'metadata', pretty: true });
|
|
30
35
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
31
36
|
['gmail', 'raw', 'm1', '--format=metadata', '--pretty'],
|
|
32
37
|
{ account: undefined },
|
|
@@ -34,42 +39,285 @@ describe('gog_gmail_raw', () => {
|
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
it('omits --pretty when false', async () => {
|
|
37
|
-
await
|
|
42
|
+
await harness.callTool('gog_gmail_raw', { messageId: 'm1', pretty: false });
|
|
38
43
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'raw', 'm1'], { account: undefined });
|
|
39
44
|
});
|
|
40
45
|
});
|
|
41
46
|
|
|
42
47
|
describe('gog_gmail_attachment', () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// base64 whose first 16 chars decode to the given ASCII/binary prefix.
|
|
49
|
+
const PNG_B64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB'; // "\x89PNG\r\n\x1a\n..."
|
|
50
|
+
const PDF_B64 = 'JVBERi0xLjUKJVBFRgo='; // "%PDF-1.5\n%PEF\n"
|
|
51
|
+
const OCTET_B64 = 'AAAAAAAAAAAAAAAA'; // decodes to NUL bytes — no magic match
|
|
52
|
+
|
|
53
|
+
// Route the mocked `run` by subcommand: the metadata lookup (`gmail get`), the
|
|
54
|
+
// download (`gmail attachment`), and the Drive upload (`drive upload`).
|
|
55
|
+
function stubGog(opts: {
|
|
56
|
+
meta?: unknown;
|
|
57
|
+
metaError?: Error;
|
|
58
|
+
download?: unknown;
|
|
59
|
+
drive?: unknown;
|
|
60
|
+
downloadError?: unknown;
|
|
61
|
+
}): void {
|
|
62
|
+
vi.mocked(lib.run).mockImplementation(async (args) => {
|
|
63
|
+
const a = args as string[];
|
|
64
|
+
if (a[0] === 'gmail' && a[1] === 'get') {
|
|
65
|
+
if (opts.metaError) throw opts.metaError;
|
|
66
|
+
return JSON.stringify(opts.meta ?? { attachments: [] });
|
|
67
|
+
}
|
|
68
|
+
if (a[0] === 'gmail' && a[1] === 'attachment') {
|
|
69
|
+
if (opts.downloadError) throw opts.downloadError;
|
|
70
|
+
return JSON.stringify(opts.download ?? {});
|
|
71
|
+
}
|
|
72
|
+
if (a[0] === 'drive' && a[1] === 'upload') return JSON.stringify(opts.drive ?? { file: {} });
|
|
73
|
+
return '{}';
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A dummy executor store — its mere presence makes runExecutor.getStore()
|
|
78
|
+
// truthy, which is how the handler detects the remote connector transport.
|
|
79
|
+
const REMOTE = { executor: async () => '{}' };
|
|
80
|
+
const asConnector = <T>(fn: () => Promise<T>): Promise<T> => lib.runExecutor.run(REMOTE, fn);
|
|
81
|
+
|
|
82
|
+
// The part metadata (`gmail get` `.attachments[]`) is matched by SIZE — Gmail's
|
|
83
|
+
// attachmentId isn't stable across calls — so every list entry carries a `size`
|
|
84
|
+
// that the download's `bytes` must equal for the filename/MIME to resolve.
|
|
85
|
+
const PDF_LIST = { attachments: [{ filename: 'Guest_Copy.pdf', mimeType: 'application/pdf', size: 99723 }] };
|
|
86
|
+
const PNG_LIST = { attachments: [{ filename: 'photo.png', mimeType: 'image/png', size: 24 }] };
|
|
87
|
+
|
|
88
|
+
const call = (args: Record<string, unknown>) =>
|
|
89
|
+
harness.callTool('gog_gmail_attachment', { messageId: 'm1', attachmentId: 'a1', ...args });
|
|
90
|
+
const textOf = (res: Awaited<ReturnType<typeof call>>) => (res.content[0] as { text: string }).text;
|
|
91
|
+
const gotGet = () => vi.mocked(lib.run).mock.calls.some((c) => (c[0] as string[])[1] === 'get');
|
|
92
|
+
const dlArgs = () => vi.mocked(lib.run).mock.calls.find((c) => (c[0] as string[])[1] === 'attachment')![0] as string[];
|
|
93
|
+
|
|
94
|
+
it('the repro: a no-name PDF on stdio comes back as a readable file path, named correctly', async () => {
|
|
95
|
+
// download writes to a provisional temp path; the real name resolves by size.
|
|
96
|
+
stubGog({ meta: PDF_LIST, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 99723, contentBase64: PDF_B64 } });
|
|
97
|
+
const res = await call({});
|
|
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']);
|
|
100
|
+
expect(gotGet()).toBe(true);
|
|
101
|
+
const payload = JSON.parse(textOf(res));
|
|
102
|
+
expect(payload).toMatchObject({
|
|
103
|
+
delivery: 'file', path: '/tmp/gog-attachments/m1/attachment', fileName: 'Guest_Copy.pdf', mimeType: 'application/pdf', bytes: 99723,
|
|
104
|
+
});
|
|
105
|
+
// never an embedded-resource blob on auto (the claude.ai host rejects those for PDF).
|
|
106
|
+
expect(res.content.some((c) => c.type === 'resource')).toBe(false);
|
|
49
107
|
});
|
|
50
108
|
|
|
51
|
-
it('
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
name: 'report.pdf',
|
|
109
|
+
it('the repro on the connector: the same PDF is delivered via Drive with the resolved name', async () => {
|
|
110
|
+
stubGog({
|
|
111
|
+
meta: PDF_LIST,
|
|
112
|
+
download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 99723, contentBase64: PDF_B64 },
|
|
113
|
+
drive: { file: { id: 'F1', name: 'Guest_Copy.pdf', webViewLink: 'https://drive.google.com/file/d/F1/view' } },
|
|
57
114
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
115
|
+
const res = await asConnector(() => call({}));
|
|
116
|
+
// uploads the downloaded temp file, but names the Drive copy with the resolved filename.
|
|
117
|
+
expect(lib.run).toHaveBeenCalledWith(
|
|
118
|
+
['drive', 'upload', '/tmp/gog-attachments/m1/attachment', '--json', '--name=Guest_Copy.pdf'], { account: undefined });
|
|
119
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ deliveredVia: 'drive', id: 'F1' });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('an image renders inline (image block), on stdio and connector alike', async () => {
|
|
123
|
+
stubGog({ meta: PNG_LIST, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 24, contentBase64: PNG_B64 } });
|
|
124
|
+
const local = await call({});
|
|
125
|
+
expect(local.content[1]).toEqual({ type: 'image', data: PNG_B64, mimeType: 'image/png' });
|
|
126
|
+
expect(textOf(local)).toContain('photo.png');
|
|
127
|
+
vi.clearAllMocks();
|
|
128
|
+
stubGog({ meta: PNG_LIST, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 24, contentBase64: PNG_B64 } });
|
|
129
|
+
const remote = await asConnector(() => call({}));
|
|
130
|
+
expect(remote.content[1]).toEqual({ type: 'image', data: PNG_B64, mimeType: 'image/png' });
|
|
131
|
+
expect(lib.run).not.toHaveBeenCalledWith(expect.arrayContaining(['drive', 'upload']), expect.anything());
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('a caller-supplied name skips the metadata lookup and names the file directly', async () => {
|
|
135
|
+
stubGog({ download: { path: '/tmp/gog-attachments/m1/report.pdf', bytes: 12, contentBase64: PDF_B64 } });
|
|
136
|
+
await call({ name: 'report.pdf' });
|
|
137
|
+
expect(gotGet()).toBe(false);
|
|
138
|
+
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--inline', '--out=/tmp/gog-attachments/m1/report.pdf', '--name=report.pdf']);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('a named non-image on the connector skips --inline (headed straight to Drive)', async () => {
|
|
142
|
+
stubGog({ download: { path: '/tmp/gog-attachments/m1/report.pdf', bytes: 12 }, drive: { file: { id: 'F9' } } });
|
|
143
|
+
await asConnector(() => call({ name: 'report.pdf' }));
|
|
144
|
+
expect(gotGet()).toBe(false);
|
|
145
|
+
expect(dlArgs()).toEqual(['gmail', 'attachment', 'm1', 'a1', '--out=/tmp/gog-attachments/m1/report.pdf', '--name=report.pdf']);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('resolves the real filename by size and sanitizes path separators (no traversal)', async () => {
|
|
149
|
+
stubGog({
|
|
150
|
+
meta: { attachments: [{ filename: '../../etc/evil.pdf', mimeType: 'application/pdf', size: 10 }] },
|
|
151
|
+
download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 10, contentBase64: PDF_B64 },
|
|
152
|
+
});
|
|
153
|
+
const res = await call({});
|
|
154
|
+
const fileName = JSON.parse(textOf(res)).fileName as string;
|
|
155
|
+
expect(fileName).not.toMatch(/[/\\]/); // single safe segment, no traversal
|
|
156
|
+
expect(fileName).toContain('evil.pdf');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('derives an extension from the MIME type when the part has no filename (never *.bin)', async () => {
|
|
160
|
+
stubGog({
|
|
161
|
+
meta: { attachments: [{ mimeType: 'application/pdf', size: 10 }] },
|
|
162
|
+
download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 10, contentBase64: PDF_B64 },
|
|
163
|
+
});
|
|
164
|
+
const res = await call({});
|
|
165
|
+
expect(JSON.parse(textOf(res)).fileName).toBe('attachment.pdf');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('falls back to a magic-byte sniff when the size is ambiguous (repeated)', async () => {
|
|
169
|
+
stubGog({
|
|
170
|
+
meta: { attachments: [
|
|
171
|
+
{ filename: 'a.pdf', mimeType: 'application/pdf', size: 10 },
|
|
172
|
+
{ filename: 'b.pdf', mimeType: 'application/pdf', size: 10 },
|
|
173
|
+
] },
|
|
174
|
+
download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 10, contentBase64: PDF_B64 },
|
|
175
|
+
});
|
|
176
|
+
const res = await call({});
|
|
177
|
+
// two parts share the size → no unique match → sniff + derived name.
|
|
178
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ fileName: 'attachment.pdf', mimeType: 'application/pdf' });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('survives a metadata-lookup failure and still delivers, sniffing the MIME', async () => {
|
|
182
|
+
stubGog({ metaError: new Error('get failed'), download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 24, contentBase64: PNG_B64 } });
|
|
183
|
+
const res = await call({});
|
|
184
|
+
// resolveBySize catches the failure → sniff → image/png.
|
|
185
|
+
expect(res.content[1]).toEqual({ type: 'image', data: PNG_B64, mimeType: 'image/png' });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('summarizes with "? bytes" when the download reports no size (skips the size lookup)', async () => {
|
|
189
|
+
stubGog({ download: { path: '/tmp/gog-attachments/m1/x.png', contentBase64: PNG_B64 }, meta: PNG_LIST });
|
|
190
|
+
const res = await call({ name: 'x.png' });
|
|
191
|
+
expect(gotGet()).toBe(false); // no bytes → no size match needed; name given anyway
|
|
192
|
+
expect(res.content[1]).toEqual({ type: 'image', data: PNG_B64, mimeType: 'image/png' });
|
|
193
|
+
expect(textOf(res)).toContain('? bytes');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('skips the size lookup entirely when the download reports no bytes and no name', async () => {
|
|
197
|
+
stubGog({ download: { path: '/tmp/gog-attachments/m1/attachment', contentBase64: OCTET_B64 } });
|
|
198
|
+
const res = await call({});
|
|
199
|
+
// info.bytes undefined → resolveBySize short-circuits (no `gmail get`).
|
|
200
|
+
expect(gotGet()).toBe(false);
|
|
201
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ delivery: 'file', fileName: 'attachment', mimeType: 'application/octet-stream' });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('falls back to application/octet-stream when the message has no attachments array', async () => {
|
|
205
|
+
// meta with no `attachments` key exercises the `?? []` guard in resolveBySize.
|
|
206
|
+
stubGog({ meta: {}, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 12, contentBase64: OCTET_B64 } });
|
|
207
|
+
const res = await call({});
|
|
208
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ delivery: 'file', fileName: 'attachment', mimeType: 'application/octet-stream' });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('deliver=inline returns a native image block for an image', async () => {
|
|
212
|
+
stubGog({ meta: PNG_LIST, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 24, contentBase64: PNG_B64 } });
|
|
213
|
+
const res = await call({ deliver: 'inline' });
|
|
214
|
+
expect(res.content[1]).toEqual({ type: 'image', data: PNG_B64, mimeType: 'image/png' });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('deliver=inline forces an embedded resource blob for a non-image', async () => {
|
|
218
|
+
stubGog({ download: { path: '/tmp/gog-attachments/m1/doc.pdf', bytes: 12, contentBase64: PDF_B64 } });
|
|
219
|
+
const res = await call({ deliver: 'inline', name: 'doc.pdf' });
|
|
220
|
+
expect(res.content[1]).toEqual({
|
|
221
|
+
type: 'resource',
|
|
222
|
+
resource: { uri: 'gmail-attachment://m1/doc.pdf', mimeType: 'application/pdf', blob: PDF_B64 },
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('deliver=inline errors when the attachment is too large (no reason field)', async () => {
|
|
227
|
+
// no `path` either → exercises the `info.path ?? outPath` fallback.
|
|
228
|
+
stubGog({ download: { bytes: 9_000_000 } });
|
|
229
|
+
const res = await call({ deliver: 'inline', name: 'big.pdf' });
|
|
230
|
+
expect(res.isError).toBe(true);
|
|
231
|
+
expect(textOf(res)).toContain('too large');
|
|
232
|
+
expect(lib.run).not.toHaveBeenCalledWith(expect.arrayContaining(['drive', 'upload']), expect.anything());
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('deliver=drive skips --inline and uploads, honoring driveFolder and name', async () => {
|
|
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
|
+
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']);
|
|
239
|
+
expect(lib.run).toHaveBeenCalledWith(
|
|
240
|
+
['drive', 'upload', '/tmp/gog-attachments/m1/renamed.png', '--json', '--parent=DIR9', '--name=renamed.png'], { account: 'me@x.com' });
|
|
241
|
+
expect(JSON.parse(textOf(res))).toMatchObject({ deliveredVia: 'drive', id: 'F2' });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('deliver=off returns a structured record with the size-resolved filename + mime', async () => {
|
|
245
|
+
stubGog({ meta: PDF_LIST, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 99723, cached: true } });
|
|
246
|
+
const res = await call({ deliver: 'off' });
|
|
247
|
+
expect(JSON.parse(textOf(res))).toMatchObject({
|
|
248
|
+
delivery: 'file', path: '/tmp/gog-attachments/m1/attachment', fileName: 'Guest_Copy.pdf', mimeType: 'application/pdf', bytes: 99723, cached: true,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('deliver=off on the connector still surfaces the ignored-out note', async () => {
|
|
253
|
+
stubGog({ meta: PDF_LIST, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 99723 } });
|
|
254
|
+
const res = await asConnector(() => call({ deliver: 'off', out: '/home/claude/x.pdf' }));
|
|
255
|
+
expect(res.content[0]).toMatchObject({ type: 'text', text: expect.stringContaining('`out` was ignored') });
|
|
256
|
+
// the structured record still follows the note.
|
|
257
|
+
expect(JSON.parse((res.content[1] as { text: string }).text)).toMatchObject({ delivery: 'file', fileName: 'Guest_Copy.pdf' });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('still reports drive delivery when the upload output lacks a file envelope', async () => {
|
|
261
|
+
stubGog({ meta: PDF_LIST, download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 99723 }, drive: {} });
|
|
262
|
+
const res = await asConnector(() => call({}));
|
|
263
|
+
const payload = JSON.parse(textOf(res));
|
|
264
|
+
expect(payload).toMatchObject({ deliveredVia: 'drive' });
|
|
265
|
+
expect(payload.id).toBeUndefined();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('honors a caller out on stdio', async () => {
|
|
269
|
+
stubGog({ download: { path: '/home/me/x.png', bytes: 24, contentBase64: PNG_B64 } });
|
|
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']);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('ignores a caller out on the connector and notes it', async () => {
|
|
275
|
+
stubGog({ download: { path: '/tmp/gog-attachments/m1/report.pdf', bytes: 12 }, drive: { file: { id: 'F3' } } });
|
|
276
|
+
const res = await asConnector(() => call({ out: '/home/claude/report.pdf', name: 'report.pdf' }));
|
|
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']);
|
|
279
|
+
expect(textOf(res)).toContain('`out` was ignored');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('a caller name that sanitizes to empty falls back to "attachment"', async () => {
|
|
283
|
+
stubGog({ download: { path: '/tmp/gog-attachments/m1/attachment', bytes: 10 } });
|
|
284
|
+
await call({ name: '...' }); // only dots → sanitizes to '' → 'attachment'
|
|
285
|
+
expect(dlArgs()).toEqual(expect.arrayContaining(['--name=attachment', '--out=/tmp/gog-attachments/m1/attachment']));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('wraps a download failure without leaking the command line or the attachment token', async () => {
|
|
289
|
+
stubGog({ downloadError: new Error('Command failed: gog gmail attachment m1 a1 --out=/home/claude/x.pdf\nmkdir /home/claude: permission denied') });
|
|
290
|
+
const res = await call({});
|
|
291
|
+
expect(lib.diagnose).toHaveBeenCalled();
|
|
292
|
+
const passed = (vi.mocked(lib.diagnose).mock.calls[0][0] as Error).message;
|
|
293
|
+
expect(passed).not.toContain('Command failed');
|
|
294
|
+
expect(passed).not.toContain('a1');
|
|
295
|
+
expect(passed).not.toContain('m1');
|
|
296
|
+
expect(passed).toContain('permission denied');
|
|
297
|
+
expect(res.isError).toBe(true);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('wraps a non-Error rejection', async () => {
|
|
301
|
+
stubGog({ downloadError: 'weird string failure' });
|
|
302
|
+
await call({});
|
|
303
|
+
expect((vi.mocked(lib.diagnose).mock.calls[0][0] as Error).message).toBe('weird string failure');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('falls back to a generic message when the error is nothing but the command echo', async () => {
|
|
307
|
+
stubGog({ downloadError: new Error('Command failed: gog gmail attachment m1 a1\n') });
|
|
308
|
+
await call({});
|
|
309
|
+
expect((vi.mocked(lib.diagnose).mock.calls[0][0] as Error).message).toBe('the download failed on the server');
|
|
62
310
|
});
|
|
63
311
|
});
|
|
64
312
|
|
|
65
313
|
describe('gog_gmail_url', () => {
|
|
66
314
|
it('calls runOrDiagnose with a single threadId', async () => {
|
|
67
|
-
await
|
|
315
|
+
await harness.callTool('gog_gmail_url', { threadIds: ['t1'] });
|
|
68
316
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'url', 't1'], { account: undefined });
|
|
69
317
|
});
|
|
70
318
|
|
|
71
319
|
it('calls runOrDiagnose with multiple threadIds', async () => {
|
|
72
|
-
await
|
|
320
|
+
await harness.callTool('gog_gmail_url', { threadIds: ['t1', 't2', 't3'] });
|
|
73
321
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
74
322
|
['gmail', 'url', 't1', 't2', 't3'],
|
|
75
323
|
{ account: undefined },
|
|
@@ -79,12 +327,12 @@ describe('gog_gmail_url', () => {
|
|
|
79
327
|
|
|
80
328
|
describe('gog_gmail_history', () => {
|
|
81
329
|
it('calls runOrDiagnose with no flags', async () => {
|
|
82
|
-
await
|
|
330
|
+
await harness.callTool('gog_gmail_history', {});
|
|
83
331
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'history'], { account: undefined });
|
|
84
332
|
});
|
|
85
333
|
|
|
86
334
|
it('passes all history flags', async () => {
|
|
87
|
-
await
|
|
335
|
+
await harness.callTool('gog_gmail_history', { since: '12345', max: 50, page: 'tok', all: true });
|
|
88
336
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
89
337
|
['gmail', 'history', '--since=12345', '--max=50', '--page=tok', '--all'],
|
|
90
338
|
{ account: undefined },
|
|
@@ -92,7 +340,7 @@ describe('gog_gmail_history', () => {
|
|
|
92
340
|
});
|
|
93
341
|
|
|
94
342
|
it('omits --all when false', async () => {
|
|
95
|
-
await
|
|
343
|
+
await harness.callTool('gog_gmail_history', { all: false });
|
|
96
344
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'history'], { account: undefined });
|
|
97
345
|
});
|
|
98
346
|
});
|
|
@@ -108,7 +356,7 @@ describe('bulk action tools (archive, mark_read, mark_unread, trash)', () => {
|
|
|
108
356
|
for (const { tool, cmd } of bulkTools) {
|
|
109
357
|
describe(tool, () => {
|
|
110
358
|
it('passes messageIds as positional args', async () => {
|
|
111
|
-
await
|
|
359
|
+
await harness.callTool(tool, { messageIds: ['m1', 'm2'] });
|
|
112
360
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
113
361
|
['gmail', cmd, 'm1', 'm2'],
|
|
114
362
|
{ account: undefined },
|
|
@@ -116,7 +364,7 @@ describe('bulk action tools (archive, mark_read, mark_unread, trash)', () => {
|
|
|
116
364
|
});
|
|
117
365
|
|
|
118
366
|
it('passes --query and --max', async () => {
|
|
119
|
-
await
|
|
367
|
+
await harness.callTool(tool, { query: 'is:unread older_than:7d', max: 50 });
|
|
120
368
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
121
369
|
['gmail', cmd, '--query=is:unread older_than:7d', '--max=50'],
|
|
122
370
|
{ account: undefined },
|
|
@@ -124,7 +372,7 @@ describe('bulk action tools (archive, mark_read, mark_unread, trash)', () => {
|
|
|
124
372
|
});
|
|
125
373
|
|
|
126
374
|
it('passes both positional ids and flags together', async () => {
|
|
127
|
-
await
|
|
375
|
+
await harness.callTool(tool, { messageIds: ['m1'], max: 10 });
|
|
128
376
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
129
377
|
['gmail', cmd, 'm1', '--max=10'],
|
|
130
378
|
{ account: undefined },
|
|
@@ -135,7 +383,7 @@ describe('bulk action tools (archive, mark_read, mark_unread, trash)', () => {
|
|
|
135
383
|
|
|
136
384
|
// gog 0.25.0 — --thread is archive-only
|
|
137
385
|
it('gog_gmail_archive passes --thread to archive whole threads by id', async () => {
|
|
138
|
-
await
|
|
386
|
+
await harness.callTool('gog_gmail_archive', { messageIds: ['t1', 't2'], thread: true });
|
|
139
387
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
140
388
|
['gmail', 'archive', 't1', 't2', '--thread'],
|
|
141
389
|
{ account: undefined },
|
|
@@ -143,7 +391,7 @@ describe('bulk action tools (archive, mark_read, mark_unread, trash)', () => {
|
|
|
143
391
|
});
|
|
144
392
|
|
|
145
393
|
it('other bulk tools do not expose a thread param', async () => {
|
|
146
|
-
await
|
|
394
|
+
await harness.callTool('gog_gmail_trash', { messageIds: ['m1'], thread: true });
|
|
147
395
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
148
396
|
['gmail', 'trash', 'm1'],
|
|
149
397
|
{ account: undefined },
|
|
@@ -153,7 +401,7 @@ describe('bulk action tools (archive, mark_read, mark_unread, trash)', () => {
|
|
|
153
401
|
|
|
154
402
|
describe('gog_gmail_message_modify', () => {
|
|
155
403
|
it('calls runOrDiagnose with messageId and label changes', async () => {
|
|
156
|
-
await
|
|
404
|
+
await harness.callTool('gog_gmail_message_modify', {
|
|
157
405
|
messageId: 'm1',
|
|
158
406
|
add: 'STARRED,IMPORTANT',
|
|
159
407
|
remove: 'INBOX',
|
|
@@ -165,7 +413,7 @@ describe('gog_gmail_message_modify', () => {
|
|
|
165
413
|
});
|
|
166
414
|
|
|
167
415
|
it('omits flags when not provided', async () => {
|
|
168
|
-
await
|
|
416
|
+
await harness.callTool('gog_gmail_message_modify', { messageId: 'm1' });
|
|
169
417
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
170
418
|
['gmail', 'messages', 'modify', 'm1'],
|
|
171
419
|
{ account: undefined },
|
|
@@ -175,17 +423,33 @@ describe('gog_gmail_message_modify', () => {
|
|
|
175
423
|
|
|
176
424
|
describe('gog_gmail_batch_delete', () => {
|
|
177
425
|
it('calls runOrDiagnose with messageIds as positional args', async () => {
|
|
178
|
-
await
|
|
426
|
+
await harness.callTool('gog_gmail_batch_delete', { messageIds: ['m1', 'm2', 'm3'] });
|
|
179
427
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
180
428
|
['gmail', 'batch', 'delete', 'm1', 'm2', 'm3'],
|
|
181
429
|
{ account: undefined },
|
|
182
430
|
);
|
|
183
431
|
});
|
|
432
|
+
|
|
433
|
+
it('appends --force when force is true', async () => {
|
|
434
|
+
await harness.callTool('gog_gmail_batch_delete', { messageIds: ['m1'], force: true });
|
|
435
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
436
|
+
['gmail', 'batch', 'delete', 'm1', '--force'],
|
|
437
|
+
{ account: undefined },
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('omits --force when force is false', async () => {
|
|
442
|
+
await harness.callTool('gog_gmail_batch_delete', { messageIds: ['m1'], force: false });
|
|
443
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
444
|
+
['gmail', 'batch', 'delete', 'm1'],
|
|
445
|
+
{ account: undefined },
|
|
446
|
+
);
|
|
447
|
+
});
|
|
184
448
|
});
|
|
185
449
|
|
|
186
450
|
describe('gog_gmail_batch_modify', () => {
|
|
187
451
|
it('calls runOrDiagnose with messageIds and label flags', async () => {
|
|
188
|
-
await
|
|
452
|
+
await harness.callTool('gog_gmail_batch_modify', {
|
|
189
453
|
messageIds: ['m1', 'm2'],
|
|
190
454
|
add: 'STARRED',
|
|
191
455
|
remove: 'INBOX',
|
|
@@ -197,7 +461,7 @@ describe('gog_gmail_batch_modify', () => {
|
|
|
197
461
|
});
|
|
198
462
|
|
|
199
463
|
it('omits label flags when not provided', async () => {
|
|
200
|
-
await
|
|
464
|
+
await harness.callTool('gog_gmail_batch_modify', { messageIds: ['m1'] });
|
|
201
465
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
202
466
|
['gmail', 'batch', 'modify', 'm1'],
|
|
203
467
|
{ account: undefined },
|
|
@@ -207,7 +471,7 @@ describe('gog_gmail_batch_modify', () => {
|
|
|
207
471
|
|
|
208
472
|
describe('gog_gmail_thread_get', () => {
|
|
209
473
|
it('calls runOrDiagnose with threadId', async () => {
|
|
210
|
-
await
|
|
474
|
+
await harness.callTool('gog_gmail_thread_get', { threadId: 't1' });
|
|
211
475
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
212
476
|
['gmail', 'thread', 'get', 't1'],
|
|
213
477
|
{ account: undefined },
|
|
@@ -215,7 +479,7 @@ describe('gog_gmail_thread_get', () => {
|
|
|
215
479
|
});
|
|
216
480
|
|
|
217
481
|
it('passes all flags', async () => {
|
|
218
|
-
await
|
|
482
|
+
await harness.callTool('gog_gmail_thread_get', {
|
|
219
483
|
threadId: 't1',
|
|
220
484
|
download: true,
|
|
221
485
|
full: true,
|
|
@@ -229,7 +493,7 @@ describe('gog_gmail_thread_get', () => {
|
|
|
229
493
|
});
|
|
230
494
|
|
|
231
495
|
it('omits boolean flags when false', async () => {
|
|
232
|
-
await
|
|
496
|
+
await harness.callTool('gog_gmail_thread_get', {
|
|
233
497
|
threadId: 't1',
|
|
234
498
|
download: false,
|
|
235
499
|
full: false,
|
|
@@ -254,14 +518,14 @@ describe('gog_gmail_thread_get', () => {
|
|
|
254
518
|
});
|
|
255
519
|
|
|
256
520
|
it('does not transform the output when no paging params are given', async () => {
|
|
257
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
258
|
-
const result = await
|
|
521
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult(THREAD));
|
|
522
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1' });
|
|
259
523
|
expect(result.content[0].text).toBe(THREAD);
|
|
260
524
|
});
|
|
261
525
|
|
|
262
526
|
it('latestN returns only the last N messages', async () => {
|
|
263
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
264
|
-
const result = await
|
|
527
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult(THREAD));
|
|
528
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', latestN: 2 });
|
|
265
529
|
// latestN is wrapper-side; no CLI flag is added
|
|
266
530
|
expect(vi.mocked(lib.runOrDiagnose).mock.calls[0]![0]).toEqual(['gmail', 'thread', 'get', 't1']);
|
|
267
531
|
const parsed = JSON.parse(result.content[0].text);
|
|
@@ -269,8 +533,8 @@ describe('gog_gmail_thread_get', () => {
|
|
|
269
533
|
});
|
|
270
534
|
|
|
271
535
|
it('snippetsOnly returns per-message headers and snippet without bodies', async () => {
|
|
272
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
273
|
-
const result = await
|
|
536
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult(THREAD));
|
|
537
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', snippetsOnly: true });
|
|
274
538
|
const parsed = JSON.parse(result.content[0].text);
|
|
275
539
|
expect(parsed.thread.messages).toHaveLength(3);
|
|
276
540
|
const m1 = parsed.thread.messages[0];
|
|
@@ -282,35 +546,35 @@ describe('gog_gmail_thread_get', () => {
|
|
|
282
546
|
});
|
|
283
547
|
|
|
284
548
|
it('combines latestN and snippetsOnly', async () => {
|
|
285
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
286
|
-
const result = await
|
|
549
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult(THREAD));
|
|
550
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', latestN: 1, snippetsOnly: true });
|
|
287
551
|
const parsed = JSON.parse(result.content[0].text);
|
|
288
552
|
expect(parsed.thread.messages).toHaveLength(1);
|
|
289
553
|
expect(parsed.thread.messages[0].id).toBe('m3');
|
|
290
554
|
});
|
|
291
555
|
|
|
292
556
|
it('returns the raw result when the payload is not JSON', async () => {
|
|
293
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
294
|
-
const result = await
|
|
557
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult('not json'));
|
|
558
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', latestN: 2 });
|
|
295
559
|
expect(result.content[0].text).toBe('not json');
|
|
296
560
|
});
|
|
297
561
|
|
|
298
562
|
it('returns the raw result when there is no messages array', async () => {
|
|
299
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
300
|
-
const result = await
|
|
563
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult('{"thread":{}}'));
|
|
564
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', snippetsOnly: true });
|
|
301
565
|
expect(result.content[0].text).toBe('{"thread":{}}');
|
|
302
566
|
});
|
|
303
567
|
|
|
304
568
|
it('returns the raw result when there is no thread object', async () => {
|
|
305
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
306
|
-
const result = await
|
|
569
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult('{}'));
|
|
570
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', latestN: 1 });
|
|
307
571
|
expect(result.content[0].text).toBe('{}');
|
|
308
572
|
});
|
|
309
573
|
});
|
|
310
574
|
|
|
311
575
|
describe('gog_gmail_thread_modify', () => {
|
|
312
576
|
it('calls runOrDiagnose with threadId and label flags', async () => {
|
|
313
|
-
await
|
|
577
|
+
await harness.callTool('gog_gmail_thread_modify', {
|
|
314
578
|
threadId: 't1',
|
|
315
579
|
add: 'IMPORTANT',
|
|
316
580
|
remove: 'INBOX',
|
|
@@ -322,7 +586,7 @@ describe('gog_gmail_thread_modify', () => {
|
|
|
322
586
|
});
|
|
323
587
|
|
|
324
588
|
it('omits label flags when not provided', async () => {
|
|
325
|
-
await
|
|
589
|
+
await harness.callTool('gog_gmail_thread_modify', { threadId: 't1' });
|
|
326
590
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
327
591
|
['gmail', 'thread', 'modify', 't1'],
|
|
328
592
|
{ account: undefined },
|
|
@@ -332,7 +596,7 @@ describe('gog_gmail_thread_modify', () => {
|
|
|
332
596
|
|
|
333
597
|
describe('gog_gmail_thread_attachments', () => {
|
|
334
598
|
it('calls runOrDiagnose with threadId', async () => {
|
|
335
|
-
await
|
|
599
|
+
await harness.callTool('gog_gmail_thread_attachments', { threadId: 't1' });
|
|
336
600
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
337
601
|
['gmail', 'thread', 'attachments', 't1'],
|
|
338
602
|
{ account: undefined },
|
|
@@ -340,7 +604,7 @@ describe('gog_gmail_thread_attachments', () => {
|
|
|
340
604
|
});
|
|
341
605
|
|
|
342
606
|
it('passes --download and --out-dir when provided', async () => {
|
|
343
|
-
await
|
|
607
|
+
await harness.callTool('gog_gmail_thread_attachments', {
|
|
344
608
|
threadId: 't1',
|
|
345
609
|
download: true,
|
|
346
610
|
outDir: '/tmp/atts',
|
|
@@ -354,7 +618,7 @@ describe('gog_gmail_thread_attachments', () => {
|
|
|
354
618
|
|
|
355
619
|
describe('gog_gmail_labels_list', () => {
|
|
356
620
|
it('calls runOrDiagnose with no args', async () => {
|
|
357
|
-
await
|
|
621
|
+
await harness.callTool('gog_gmail_labels_list', {});
|
|
358
622
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
359
623
|
['gmail', 'labels', 'list'],
|
|
360
624
|
{ account: undefined },
|
|
@@ -362,7 +626,7 @@ describe('gog_gmail_labels_list', () => {
|
|
|
362
626
|
});
|
|
363
627
|
|
|
364
628
|
it('forwards account', async () => {
|
|
365
|
-
await
|
|
629
|
+
await harness.callTool('gog_gmail_labels_list', { account: 'a@b.com' });
|
|
366
630
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
367
631
|
['gmail', 'labels', 'list'],
|
|
368
632
|
{ account: 'a@b.com' },
|
|
@@ -372,7 +636,7 @@ describe('gog_gmail_labels_list', () => {
|
|
|
372
636
|
|
|
373
637
|
describe('gog_gmail_labels_get', () => {
|
|
374
638
|
it('calls runOrDiagnose with labelIdOrName', async () => {
|
|
375
|
-
await
|
|
639
|
+
await harness.callTool('gog_gmail_labels_get', { labelIdOrName: 'INBOX' });
|
|
376
640
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
377
641
|
['gmail', 'labels', 'get', 'INBOX'],
|
|
378
642
|
{ account: undefined },
|
|
@@ -382,7 +646,7 @@ describe('gog_gmail_labels_get', () => {
|
|
|
382
646
|
|
|
383
647
|
describe('gog_gmail_labels_create', () => {
|
|
384
648
|
it('calls runOrDiagnose with name', async () => {
|
|
385
|
-
await
|
|
649
|
+
await harness.callTool('gog_gmail_labels_create', { name: 'Newsletter' });
|
|
386
650
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
387
651
|
['gmail', 'labels', 'create', 'Newsletter'],
|
|
388
652
|
{ account: undefined },
|
|
@@ -392,7 +656,7 @@ describe('gog_gmail_labels_create', () => {
|
|
|
392
656
|
|
|
393
657
|
describe('gog_gmail_labels_rename', () => {
|
|
394
658
|
it('calls runOrDiagnose with old and new names', async () => {
|
|
395
|
-
await
|
|
659
|
+
await harness.callTool('gog_gmail_labels_rename', { labelIdOrName: 'Old', newName: 'New' });
|
|
396
660
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
397
661
|
['gmail', 'labels', 'rename', 'Old', 'New'],
|
|
398
662
|
{ account: undefined },
|
|
@@ -402,9 +666,9 @@ describe('gog_gmail_labels_rename', () => {
|
|
|
402
666
|
|
|
403
667
|
describe('gog_gmail_labels_delete', () => {
|
|
404
668
|
it('calls runOrDiagnose with labelIdOrName', async () => {
|
|
405
|
-
await
|
|
669
|
+
await harness.callTool('gog_gmail_labels_delete', { labelIdOrName: 'Trash-Me' });
|
|
406
670
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
407
|
-
['gmail', 'labels', 'delete', 'Trash-Me'],
|
|
671
|
+
['gmail', 'labels', 'delete', 'Trash-Me', '--force'],
|
|
408
672
|
{ account: undefined },
|
|
409
673
|
);
|
|
410
674
|
});
|
|
@@ -412,7 +676,7 @@ describe('gog_gmail_labels_delete', () => {
|
|
|
412
676
|
|
|
413
677
|
describe('gog_gmail_labels_modify', () => {
|
|
414
678
|
it('calls runOrDiagnose with threadIds and label flags', async () => {
|
|
415
|
-
await
|
|
679
|
+
await harness.callTool('gog_gmail_labels_modify', {
|
|
416
680
|
threadIds: ['t1', 't2'],
|
|
417
681
|
add: 'Newsletter',
|
|
418
682
|
remove: 'INBOX',
|
|
@@ -424,7 +688,7 @@ describe('gog_gmail_labels_modify', () => {
|
|
|
424
688
|
});
|
|
425
689
|
|
|
426
690
|
it('omits label flags when not provided', async () => {
|
|
427
|
-
await
|
|
691
|
+
await harness.callTool('gog_gmail_labels_modify', { threadIds: ['t1'] });
|
|
428
692
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
429
693
|
['gmail', 'labels', 'modify', 't1'],
|
|
430
694
|
{ account: undefined },
|
|
@@ -434,7 +698,7 @@ describe('gog_gmail_labels_modify', () => {
|
|
|
434
698
|
|
|
435
699
|
describe('gog_gmail_drafts_list', () => {
|
|
436
700
|
it('calls runOrDiagnose with no flags', async () => {
|
|
437
|
-
await
|
|
701
|
+
await harness.callTool('gog_gmail_drafts_list', {});
|
|
438
702
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
439
703
|
['gmail', 'drafts', 'list'],
|
|
440
704
|
{ account: undefined },
|
|
@@ -442,7 +706,7 @@ describe('gog_gmail_drafts_list', () => {
|
|
|
442
706
|
});
|
|
443
707
|
|
|
444
708
|
it('passes pagination flags', async () => {
|
|
445
|
-
await
|
|
709
|
+
await harness.callTool('gog_gmail_drafts_list', { max: 50, page: 'tok', all: true });
|
|
446
710
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
447
711
|
['gmail', 'drafts', 'list', '--max=50', '--page=tok', '--all'],
|
|
448
712
|
{ account: undefined },
|
|
@@ -452,7 +716,7 @@ describe('gog_gmail_drafts_list', () => {
|
|
|
452
716
|
|
|
453
717
|
describe('gog_gmail_drafts_get', () => {
|
|
454
718
|
it('calls runOrDiagnose with draftId', async () => {
|
|
455
|
-
await
|
|
719
|
+
await harness.callTool('gog_gmail_drafts_get', { draftId: 'd1' });
|
|
456
720
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
457
721
|
['gmail', 'drafts', 'get', 'd1'],
|
|
458
722
|
{ account: undefined },
|
|
@@ -460,7 +724,7 @@ describe('gog_gmail_drafts_get', () => {
|
|
|
460
724
|
});
|
|
461
725
|
|
|
462
726
|
it('passes --download when true', async () => {
|
|
463
|
-
await
|
|
727
|
+
await harness.callTool('gog_gmail_drafts_get', { draftId: 'd1', download: true });
|
|
464
728
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
465
729
|
['gmail', 'drafts', 'get', 'd1', '--download'],
|
|
466
730
|
{ account: undefined },
|
|
@@ -470,7 +734,7 @@ describe('gog_gmail_drafts_get', () => {
|
|
|
470
734
|
|
|
471
735
|
describe('gog_gmail_drafts_create', () => {
|
|
472
736
|
it('calls runOrDiagnose with minimal required flags', async () => {
|
|
473
|
-
await
|
|
737
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
474
738
|
subject: 'Hi',
|
|
475
739
|
body: 'Hello',
|
|
476
740
|
});
|
|
@@ -481,7 +745,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
481
745
|
});
|
|
482
746
|
|
|
483
747
|
it('passes all flags including attachments', async () => {
|
|
484
|
-
await
|
|
748
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
485
749
|
to: 'a@b.com,c@d.com',
|
|
486
750
|
cc: 'cc@x.com',
|
|
487
751
|
bcc: 'bcc@x.com',
|
|
@@ -514,8 +778,33 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
514
778
|
);
|
|
515
779
|
});
|
|
516
780
|
|
|
781
|
+
it('passes --body-html-file when bodyHtmlFile is supplied', async () => {
|
|
782
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
783
|
+
subject: 'Hi',
|
|
784
|
+
body: 'Hello',
|
|
785
|
+
bodyHtmlFile: '/tmp/body.html',
|
|
786
|
+
});
|
|
787
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
788
|
+
['gmail', 'drafts', 'create', '--subject=Hi', '--body=Hello', '--body-html-file=/tmp/body.html'],
|
|
789
|
+
{ account: undefined },
|
|
790
|
+
);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('passes --reply-all when replyAll is set', async () => {
|
|
794
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
795
|
+
subject: 'Re: Hi',
|
|
796
|
+
body: 'Hello all',
|
|
797
|
+
replyToThreadId: 't1',
|
|
798
|
+
replyAll: true,
|
|
799
|
+
});
|
|
800
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
801
|
+
['gmail', 'drafts', 'create', '--subject=Re: Hi', '--body=Hello all', '--thread-id=t1', '--reply-all'],
|
|
802
|
+
{ account: undefined },
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
|
|
517
806
|
it('skips recipient flags when omitRecipients is true, even if to/cc/bcc are supplied', async () => {
|
|
518
|
-
await
|
|
807
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
519
808
|
to: 'a@b.com', cc: 'cc@x.com', bcc: 'bcc@x.com',
|
|
520
809
|
subject: 'Hi', body: 'Hello', omitRecipients: true,
|
|
521
810
|
});
|
|
@@ -527,9 +816,9 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
527
816
|
|
|
528
817
|
it('returnFull re-fetches and returns the full stored draft', async () => {
|
|
529
818
|
vi.mocked(lib.runOrDiagnose)
|
|
530
|
-
.mockResolvedValueOnce(
|
|
531
|
-
.mockResolvedValueOnce(
|
|
532
|
-
const result = await
|
|
819
|
+
.mockResolvedValueOnce(rawTextResult('{"draftId":"d9","message":{"id":"m9"}}'))
|
|
820
|
+
.mockResolvedValueOnce(rawTextResult('{"id":"d9","message":{"subject":"Hi","body":"Hello"}}'));
|
|
821
|
+
const result = await harness.callTool('gog_gmail_drafts_create', {
|
|
533
822
|
subject: 'Hi', body: 'Hello', returnFull: true,
|
|
534
823
|
});
|
|
535
824
|
expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(1,
|
|
@@ -541,22 +830,22 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
541
830
|
|
|
542
831
|
it('returnFull does not push --return-full to the CLI', async () => {
|
|
543
832
|
vi.mocked(lib.runOrDiagnose)
|
|
544
|
-
.mockResolvedValueOnce(
|
|
545
|
-
.mockResolvedValueOnce(
|
|
546
|
-
await
|
|
833
|
+
.mockResolvedValueOnce(rawTextResult('{"draftId":"d9"}'))
|
|
834
|
+
.mockResolvedValueOnce(rawTextResult('{}'));
|
|
835
|
+
await harness.callTool('gog_gmail_drafts_create', { subject: 'Hi', body: 'Hello', returnFull: true });
|
|
547
836
|
expect(vi.mocked(lib.runOrDiagnose).mock.calls[0]![0]).not.toContain('--return-full');
|
|
548
837
|
});
|
|
549
838
|
|
|
550
839
|
it('returnFull returns the write result when output is not parseable JSON', async () => {
|
|
551
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
552
|
-
const result = await
|
|
840
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult('not json'));
|
|
841
|
+
const result = await harness.callTool('gog_gmail_drafts_create', { subject: 'Hi', body: 'Hello', returnFull: true });
|
|
553
842
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
554
843
|
expect(result.content[0].text).toBe('not json');
|
|
555
844
|
});
|
|
556
845
|
|
|
557
846
|
it('returnFull returns the write result when no draftId is present', async () => {
|
|
558
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
559
|
-
const result = await
|
|
847
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult('{"message":{"id":"m9"}}'));
|
|
848
|
+
const result = await harness.callTool('gog_gmail_drafts_create', { subject: 'Hi', body: 'Hello', returnFull: true });
|
|
560
849
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
561
850
|
expect(result.content[0].text).toBe('{"message":{"id":"m9"}}');
|
|
562
851
|
});
|
|
@@ -564,7 +853,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
564
853
|
|
|
565
854
|
describe('gmail draft reply threading (native --thread-id)', () => {
|
|
566
855
|
it('passes replyToThreadId straight through as --thread-id on create (no thread fetch)', async () => {
|
|
567
|
-
await
|
|
856
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
568
857
|
subject: 'Re: roof', body: 'Sounds good', replyToThreadId: '19dffe06f9668b28', account: 'me@x.com',
|
|
569
858
|
});
|
|
570
859
|
// gog resolves the thread's latest-message headers itself — no extra fetch.
|
|
@@ -576,7 +865,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
576
865
|
});
|
|
577
866
|
|
|
578
867
|
it('passes replyToThreadId as --thread-id on update', async () => {
|
|
579
|
-
await
|
|
868
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
580
869
|
draftId: 'd1', subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
581
870
|
});
|
|
582
871
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
@@ -586,7 +875,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
586
875
|
});
|
|
587
876
|
|
|
588
877
|
it('replyToMessageId wins when both ids are supplied (no --thread-id)', async () => {
|
|
589
|
-
await
|
|
878
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
590
879
|
subject: 'S', body: 'B', replyToMessageId: 'mExplicit', replyToThreadId: 't1',
|
|
591
880
|
});
|
|
592
881
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
@@ -599,7 +888,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
599
888
|
|
|
600
889
|
describe('gog_gmail_drafts_update', () => {
|
|
601
890
|
it('calls runOrDiagnose with draftId and updated fields', async () => {
|
|
602
|
-
await
|
|
891
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
603
892
|
draftId: 'd1',
|
|
604
893
|
subject: 'New subject',
|
|
605
894
|
body: 'New body',
|
|
@@ -611,7 +900,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
611
900
|
});
|
|
612
901
|
|
|
613
902
|
it('passes attachments as repeatable flags', async () => {
|
|
614
|
-
await
|
|
903
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
615
904
|
draftId: 'd1',
|
|
616
905
|
subject: 'S',
|
|
617
906
|
body: 'B',
|
|
@@ -624,7 +913,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
624
913
|
});
|
|
625
914
|
|
|
626
915
|
it('skips recipient flags when omitRecipients is true', async () => {
|
|
627
|
-
await
|
|
916
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
628
917
|
draftId: 'd1', to: 'a@b.com', subject: 'S', body: 'B', omitRecipients: true,
|
|
629
918
|
});
|
|
630
919
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
@@ -634,7 +923,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
634
923
|
});
|
|
635
924
|
|
|
636
925
|
it('passes --clear-attachments when clearAttachments is true', async () => {
|
|
637
|
-
await
|
|
926
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
638
927
|
draftId: 'd1', subject: 'S', body: 'B', clearAttachments: true,
|
|
639
928
|
});
|
|
640
929
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
@@ -645,9 +934,9 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
645
934
|
|
|
646
935
|
it('returnFull re-fetches the draft by its known id', async () => {
|
|
647
936
|
vi.mocked(lib.runOrDiagnose)
|
|
648
|
-
.mockResolvedValueOnce(
|
|
649
|
-
.mockResolvedValueOnce(
|
|
650
|
-
const result = await
|
|
937
|
+
.mockResolvedValueOnce(rawTextResult('{"draftId":"d1"}'))
|
|
938
|
+
.mockResolvedValueOnce(rawTextResult('{"id":"d1","message":{"subject":"S"}}'));
|
|
939
|
+
const result = await harness.callTool('gog_gmail_drafts_update', {
|
|
651
940
|
draftId: 'd1', subject: 'S', body: 'B', returnFull: true,
|
|
652
941
|
});
|
|
653
942
|
expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(2,
|
|
@@ -656,8 +945,8 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
656
945
|
});
|
|
657
946
|
|
|
658
947
|
it('returnFull surfaces a failed update instead of re-fetching a stale draft', async () => {
|
|
659
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
660
|
-
const result = await
|
|
948
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult('Error: update failed'));
|
|
949
|
+
const result = await harness.callTool('gog_gmail_drafts_update', {
|
|
661
950
|
draftId: 'd1', subject: 'S', body: 'B', returnFull: true,
|
|
662
951
|
});
|
|
663
952
|
// write failed (non-JSON) → no re-fetch; the error is surfaced
|
|
@@ -668,7 +957,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
668
957
|
|
|
669
958
|
describe('gog_gmail_drafts_delete', () => {
|
|
670
959
|
it('calls runOrDiagnose with draftId', async () => {
|
|
671
|
-
await
|
|
960
|
+
await harness.callTool('gog_gmail_drafts_delete', { draftId: 'd1' });
|
|
672
961
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
673
962
|
['gmail', 'drafts', 'delete', 'd1'],
|
|
674
963
|
{ account: undefined },
|
|
@@ -676,7 +965,7 @@ describe('gog_gmail_drafts_delete', () => {
|
|
|
676
965
|
});
|
|
677
966
|
|
|
678
967
|
it('appends --force when force is true', async () => {
|
|
679
|
-
await
|
|
968
|
+
await harness.callTool('gog_gmail_drafts_delete', { draftId: 'd1', force: true });
|
|
680
969
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
681
970
|
['gmail', 'drafts', 'delete', 'd1', '--force'],
|
|
682
971
|
{ account: undefined },
|
|
@@ -684,7 +973,7 @@ describe('gog_gmail_drafts_delete', () => {
|
|
|
684
973
|
});
|
|
685
974
|
|
|
686
975
|
it('omits --force when force is false', async () => {
|
|
687
|
-
await
|
|
976
|
+
await harness.callTool('gog_gmail_drafts_delete', { draftId: 'd1', force: false });
|
|
688
977
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
689
978
|
['gmail', 'drafts', 'delete', 'd1'],
|
|
690
979
|
{ account: undefined },
|
|
@@ -694,7 +983,7 @@ describe('gog_gmail_drafts_delete', () => {
|
|
|
694
983
|
|
|
695
984
|
describe('gog_gmail_drafts_send', () => {
|
|
696
985
|
it('calls runOrDiagnose with draftId', async () => {
|
|
697
|
-
await
|
|
986
|
+
await harness.callTool('gog_gmail_drafts_send', { draftId: 'd1' });
|
|
698
987
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
699
988
|
['gmail', 'drafts', 'send', 'd1'],
|
|
700
989
|
{ account: undefined },
|
|
@@ -704,7 +993,7 @@ describe('gog_gmail_drafts_send', () => {
|
|
|
704
993
|
|
|
705
994
|
describe('gog_gmail_forward', () => {
|
|
706
995
|
it('calls runOrDiagnose with messageId and required --to', async () => {
|
|
707
|
-
await
|
|
996
|
+
await harness.callTool('gog_gmail_forward', { messageId: 'm1', to: 'a@b.com' });
|
|
708
997
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
709
998
|
['gmail', 'forward', 'm1', '--to=a@b.com'],
|
|
710
999
|
{ account: undefined },
|
|
@@ -712,7 +1001,7 @@ describe('gog_gmail_forward', () => {
|
|
|
712
1001
|
});
|
|
713
1002
|
|
|
714
1003
|
it('passes all forward flags', async () => {
|
|
715
|
-
await
|
|
1004
|
+
await harness.callTool('gog_gmail_forward', {
|
|
716
1005
|
messageId: 'm1',
|
|
717
1006
|
to: 'a@b.com',
|
|
718
1007
|
cc: 'cc@x.com',
|
|
@@ -736,7 +1025,7 @@ describe('gog_gmail_forward', () => {
|
|
|
736
1025
|
});
|
|
737
1026
|
|
|
738
1027
|
it('omits --skip-attachments when false', async () => {
|
|
739
|
-
await
|
|
1028
|
+
await harness.callTool('gog_gmail_forward', { messageId: 'm1', to: 'a@b.com', skipAttachments: false });
|
|
740
1029
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
741
1030
|
['gmail', 'forward', 'm1', '--to=a@b.com'],
|
|
742
1031
|
{ account: undefined },
|
|
@@ -744,9 +1033,99 @@ describe('gog_gmail_forward', () => {
|
|
|
744
1033
|
});
|
|
745
1034
|
});
|
|
746
1035
|
|
|
1036
|
+
describe('gog_gmail_reply', () => {
|
|
1037
|
+
it('calls runOrDiagnose with messageId and --body', async () => {
|
|
1038
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Thanks' });
|
|
1039
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1040
|
+
['gmail', 'reply', 'm1', '--body=Thanks'],
|
|
1041
|
+
{ account: undefined },
|
|
1042
|
+
);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it('passes all reply flags including repeatable recipients', async () => {
|
|
1046
|
+
await harness.callTool('gog_gmail_reply', {
|
|
1047
|
+
messageId: 'm1',
|
|
1048
|
+
body: 'Hi',
|
|
1049
|
+
bodyHtml: '<p>Hi</p>',
|
|
1050
|
+
to: ['a@b.com', 'c@d.com'],
|
|
1051
|
+
cc: ['cc@x.com'],
|
|
1052
|
+
bcc: ['bcc@x.com'],
|
|
1053
|
+
remove: ['old@x.com'],
|
|
1054
|
+
subject: 'New subject',
|
|
1055
|
+
noQuote: true,
|
|
1056
|
+
attach: ['/tmp/a.pdf', '/tmp/b.pdf'],
|
|
1057
|
+
from: 'me@x.com',
|
|
1058
|
+
signature: true,
|
|
1059
|
+
signatureFrom: 'alias@x.com',
|
|
1060
|
+
signatureFile: '/tmp/sig.txt',
|
|
1061
|
+
account: 'me@gmail.com',
|
|
1062
|
+
});
|
|
1063
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1064
|
+
[
|
|
1065
|
+
'gmail', 'reply', 'm1',
|
|
1066
|
+
'--body=Hi',
|
|
1067
|
+
'--body-html=<p>Hi</p>',
|
|
1068
|
+
'--to=a@b.com',
|
|
1069
|
+
'--to=c@d.com',
|
|
1070
|
+
'--cc=cc@x.com',
|
|
1071
|
+
'--bcc=bcc@x.com',
|
|
1072
|
+
'--remove=old@x.com',
|
|
1073
|
+
'--subject=New subject',
|
|
1074
|
+
'--no-quote',
|
|
1075
|
+
'--attach=/tmp/a.pdf',
|
|
1076
|
+
'--attach=/tmp/b.pdf',
|
|
1077
|
+
'--from=me@x.com',
|
|
1078
|
+
'--signature',
|
|
1079
|
+
'--signature-from=alias@x.com',
|
|
1080
|
+
'--signature-file=/tmp/sig.txt',
|
|
1081
|
+
],
|
|
1082
|
+
{ account: 'me@gmail.com' },
|
|
1083
|
+
);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it('omits --no-quote and --signature when false', async () => {
|
|
1087
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', noQuote: false, signature: false });
|
|
1088
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1089
|
+
['gmail', 'reply', 'm1', '--body=Hi'],
|
|
1090
|
+
{ account: undefined },
|
|
1091
|
+
);
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
describe('gog_gmail_reply_all', () => {
|
|
1096
|
+
it('uses the reply-all subcommand', async () => {
|
|
1097
|
+
await harness.callTool('gog_gmail_reply_all', { messageId: 'm1', body: 'Thanks all' });
|
|
1098
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1099
|
+
['gmail', 'reply-all', 'm1', '--body=Thanks all'],
|
|
1100
|
+
{ account: undefined },
|
|
1101
|
+
);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('passes repeatable recipient and signature flags', async () => {
|
|
1105
|
+
await harness.callTool('gog_gmail_reply_all', {
|
|
1106
|
+
messageId: 'm1',
|
|
1107
|
+
bodyHtml: '<p>Hi</p>',
|
|
1108
|
+
cc: ['x@y.com', 'z@y.com'],
|
|
1109
|
+
remove: ['drop@y.com'],
|
|
1110
|
+
signatureFile: '/tmp/sig.html',
|
|
1111
|
+
});
|
|
1112
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1113
|
+
[
|
|
1114
|
+
'gmail', 'reply-all', 'm1',
|
|
1115
|
+
'--body-html=<p>Hi</p>',
|
|
1116
|
+
'--cc=x@y.com',
|
|
1117
|
+
'--cc=z@y.com',
|
|
1118
|
+
'--remove=drop@y.com',
|
|
1119
|
+
'--signature-file=/tmp/sig.html',
|
|
1120
|
+
],
|
|
1121
|
+
{ account: undefined },
|
|
1122
|
+
);
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
|
|
747
1126
|
describe('gog_gmail_autoreply', () => {
|
|
748
1127
|
it('calls runOrDiagnose with query and --body', async () => {
|
|
749
|
-
await
|
|
1128
|
+
await harness.callTool('gog_gmail_autoreply', { query: 'is:unread', body: 'Thanks' });
|
|
750
1129
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
751
1130
|
['gmail', 'autoreply', 'is:unread', '--body=Thanks'],
|
|
752
1131
|
{ account: undefined },
|
|
@@ -754,7 +1133,7 @@ describe('gog_gmail_autoreply', () => {
|
|
|
754
1133
|
});
|
|
755
1134
|
|
|
756
1135
|
it('passes all autoreply flags', async () => {
|
|
757
|
-
await
|
|
1136
|
+
await harness.callTool('gog_gmail_autoreply', {
|
|
758
1137
|
query: 'is:unread',
|
|
759
1138
|
max: 50,
|
|
760
1139
|
subject: 'Re: out of office',
|
|
@@ -788,7 +1167,7 @@ describe('gog_gmail_autoreply', () => {
|
|
|
788
1167
|
});
|
|
789
1168
|
|
|
790
1169
|
it('omits boolean flags when false', async () => {
|
|
791
|
-
await
|
|
1170
|
+
await harness.callTool('gog_gmail_autoreply', {
|
|
792
1171
|
query: 'is:unread',
|
|
793
1172
|
body: 'Thanks',
|
|
794
1173
|
archive: false,
|
|
@@ -803,7 +1182,7 @@ describe('gog_gmail_autoreply', () => {
|
|
|
803
1182
|
});
|
|
804
1183
|
|
|
805
1184
|
it('supports HTML-only body (no plain --body)', async () => {
|
|
806
|
-
await
|
|
1185
|
+
await harness.callTool('gog_gmail_autoreply', { query: 'is:unread', bodyHtml: '<p>Hi</p>' });
|
|
807
1186
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
808
1187
|
['gmail', 'autoreply', 'is:unread', '--body-html=<p>Hi</p>'],
|
|
809
1188
|
{ account: undefined },
|
|
@@ -813,7 +1192,7 @@ describe('gog_gmail_autoreply', () => {
|
|
|
813
1192
|
|
|
814
1193
|
describe('gog_gmail_messages_search', () => {
|
|
815
1194
|
it('calls runOrDiagnose with just the query', async () => {
|
|
816
|
-
await
|
|
1195
|
+
await harness.callTool('gog_gmail_messages_search', { query: 'from:alice' });
|
|
817
1196
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
818
1197
|
['gmail', 'messages', 'search', 'from:alice'],
|
|
819
1198
|
{ account: undefined },
|
|
@@ -821,7 +1200,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
821
1200
|
});
|
|
822
1201
|
|
|
823
1202
|
it('passes all flags when provided', async () => {
|
|
824
|
-
await
|
|
1203
|
+
await harness.callTool('gog_gmail_messages_search', {
|
|
825
1204
|
query: 'is:unread',
|
|
826
1205
|
max: 10,
|
|
827
1206
|
page: 'tok',
|
|
@@ -838,7 +1217,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
838
1217
|
});
|
|
839
1218
|
|
|
840
1219
|
it('omits flags when false/absent', async () => {
|
|
841
|
-
await
|
|
1220
|
+
await harness.callTool('gog_gmail_messages_search', { query: 'x', all: false, includeBody: false, full: false });
|
|
842
1221
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
843
1222
|
['gmail', 'messages', 'search', 'x'],
|
|
844
1223
|
{ account: undefined },
|
|
@@ -848,7 +1227,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
848
1227
|
|
|
849
1228
|
describe('gog_gmail_labels_style', () => {
|
|
850
1229
|
it('calls runOrDiagnose with just the label', async () => {
|
|
851
|
-
await
|
|
1230
|
+
await harness.callTool('gog_gmail_labels_style', { labelIdOrName: 'Work' });
|
|
852
1231
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
853
1232
|
['gmail', 'labels', 'style', 'Work'],
|
|
854
1233
|
{ account: undefined },
|
|
@@ -856,7 +1235,7 @@ describe('gog_gmail_labels_style', () => {
|
|
|
856
1235
|
});
|
|
857
1236
|
|
|
858
1237
|
it('passes all style flags when provided', async () => {
|
|
859
|
-
await
|
|
1238
|
+
await harness.callTool('gog_gmail_labels_style', {
|
|
860
1239
|
labelIdOrName: 'Work',
|
|
861
1240
|
backgroundColor: '#000000',
|
|
862
1241
|
textColor: '#ffffff',
|
|
@@ -872,7 +1251,7 @@ describe('gog_gmail_labels_style', () => {
|
|
|
872
1251
|
|
|
873
1252
|
describe('gog_gmail_vacation_get', () => {
|
|
874
1253
|
it('calls runOrDiagnose', async () => {
|
|
875
|
-
await
|
|
1254
|
+
await harness.callTool('gog_gmail_vacation_get', { account: 'me@x.com' });
|
|
876
1255
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
877
1256
|
['gmail', 'settings', 'vacation', 'get'],
|
|
878
1257
|
{ account: 'me@x.com' },
|
|
@@ -882,7 +1261,7 @@ describe('gog_gmail_vacation_get', () => {
|
|
|
882
1261
|
|
|
883
1262
|
describe('gog_gmail_vacation_update', () => {
|
|
884
1263
|
it('calls runOrDiagnose with no flags', async () => {
|
|
885
|
-
await
|
|
1264
|
+
await harness.callTool('gog_gmail_vacation_update', {});
|
|
886
1265
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
887
1266
|
['gmail', 'settings', 'vacation', 'update'],
|
|
888
1267
|
{ account: undefined },
|
|
@@ -890,7 +1269,7 @@ describe('gog_gmail_vacation_update', () => {
|
|
|
890
1269
|
});
|
|
891
1270
|
|
|
892
1271
|
it('enables with subject/body/start/end and scoping', async () => {
|
|
893
|
-
await
|
|
1272
|
+
await harness.callTool('gog_gmail_vacation_update', {
|
|
894
1273
|
enable: true,
|
|
895
1274
|
subject: 'Away',
|
|
896
1275
|
body: '<p>OOO</p>',
|
|
@@ -906,7 +1285,7 @@ describe('gog_gmail_vacation_update', () => {
|
|
|
906
1285
|
});
|
|
907
1286
|
|
|
908
1287
|
it('disables the responder', async () => {
|
|
909
|
-
await
|
|
1288
|
+
await harness.callTool('gog_gmail_vacation_update', { disable: true, enable: false, contactsOnly: false, domainOnly: false });
|
|
910
1289
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
911
1290
|
['gmail', 'settings', 'vacation', 'update', '--disable'],
|
|
912
1291
|
{ account: undefined },
|
|
@@ -916,7 +1295,7 @@ describe('gog_gmail_vacation_update', () => {
|
|
|
916
1295
|
|
|
917
1296
|
describe('gog_gmail_filters_list', () => {
|
|
918
1297
|
it('calls runOrDiagnose', async () => {
|
|
919
|
-
await
|
|
1298
|
+
await harness.callTool('gog_gmail_filters_list', {});
|
|
920
1299
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
921
1300
|
['gmail', 'settings', 'filters', 'list'],
|
|
922
1301
|
{ account: undefined },
|
|
@@ -926,7 +1305,7 @@ describe('gog_gmail_filters_list', () => {
|
|
|
926
1305
|
|
|
927
1306
|
describe('gog_gmail_filters_get', () => {
|
|
928
1307
|
it('calls runOrDiagnose with the filter ID', async () => {
|
|
929
|
-
await
|
|
1308
|
+
await harness.callTool('gog_gmail_filters_get', { filterId: 'f1' });
|
|
930
1309
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
931
1310
|
['gmail', 'settings', 'filters', 'get', 'f1'],
|
|
932
1311
|
{ account: undefined },
|
|
@@ -936,7 +1315,7 @@ describe('gog_gmail_filters_get', () => {
|
|
|
936
1315
|
|
|
937
1316
|
describe('gog_gmail_filters_create', () => {
|
|
938
1317
|
it('calls runOrDiagnose with no flags', async () => {
|
|
939
|
-
await
|
|
1318
|
+
await harness.callTool('gog_gmail_filters_create', {});
|
|
940
1319
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
941
1320
|
['gmail', 'settings', 'filters', 'create'],
|
|
942
1321
|
{ account: undefined },
|
|
@@ -944,7 +1323,7 @@ describe('gog_gmail_filters_create', () => {
|
|
|
944
1323
|
});
|
|
945
1324
|
|
|
946
1325
|
it('passes all criteria and actions when provided', async () => {
|
|
947
|
-
await
|
|
1326
|
+
await harness.callTool('gog_gmail_filters_create', {
|
|
948
1327
|
from: 'alice@x.com',
|
|
949
1328
|
to: 'me@x.com',
|
|
950
1329
|
subject: 'Report',
|
|
@@ -961,13 +1340,13 @@ describe('gog_gmail_filters_create', () => {
|
|
|
961
1340
|
forward: 'fwd@x.com',
|
|
962
1341
|
});
|
|
963
1342
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
964
|
-
['gmail', 'settings', 'filters', 'create', '--from=alice@x.com', '--to=me@x.com', '--subject=Report', '--query=has:attachment', '--has-attachment', '--add-label=Reports', '--remove-label=INBOX', '--archive', '--mark-read', '--star', '--important', '--trash', '--never-spam', '--forward=fwd@x.com'],
|
|
1343
|
+
['gmail', 'settings', 'filters', 'create', '--from=alice@x.com', '--to=me@x.com', '--subject=Report', '--query=has:attachment', '--has-attachment', '--add-label=Reports', '--remove-label=INBOX', '--archive', '--mark-read', '--star', '--important', '--trash', '--never-spam', '--forward=fwd@x.com', '--force'],
|
|
965
1344
|
{ account: undefined },
|
|
966
1345
|
);
|
|
967
1346
|
});
|
|
968
1347
|
|
|
969
1348
|
it('omits boolean flags when false', async () => {
|
|
970
|
-
await
|
|
1349
|
+
await harness.callTool('gog_gmail_filters_create', {
|
|
971
1350
|
from: 'a@x.com',
|
|
972
1351
|
hasAttachment: false,
|
|
973
1352
|
archive: false,
|
|
@@ -986,9 +1365,9 @@ describe('gog_gmail_filters_create', () => {
|
|
|
986
1365
|
|
|
987
1366
|
describe('gog_gmail_filters_delete', () => {
|
|
988
1367
|
it('calls runOrDiagnose with the filter ID', async () => {
|
|
989
|
-
await
|
|
1368
|
+
await harness.callTool('gog_gmail_filters_delete', { filterId: 'f1' });
|
|
990
1369
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
991
|
-
['gmail', 'settings', 'filters', 'delete', 'f1'],
|
|
1370
|
+
['gmail', 'settings', 'filters', 'delete', 'f1', '--force'],
|
|
992
1371
|
{ account: undefined },
|
|
993
1372
|
);
|
|
994
1373
|
});
|
|
@@ -996,7 +1375,7 @@ describe('gog_gmail_filters_delete', () => {
|
|
|
996
1375
|
|
|
997
1376
|
describe('gog_gmail_sendas_list', () => {
|
|
998
1377
|
it('calls runOrDiagnose', async () => {
|
|
999
|
-
await
|
|
1378
|
+
await harness.callTool('gog_gmail_sendas_list', {});
|
|
1000
1379
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1001
1380
|
['gmail', 'settings', 'sendas', 'list'],
|
|
1002
1381
|
{ account: undefined },
|
|
@@ -1006,7 +1385,7 @@ describe('gog_gmail_sendas_list', () => {
|
|
|
1006
1385
|
|
|
1007
1386
|
describe('gog_gmail_sendas_get', () => {
|
|
1008
1387
|
it('calls runOrDiagnose with the email', async () => {
|
|
1009
|
-
await
|
|
1388
|
+
await harness.callTool('gog_gmail_sendas_get', { email: 'alias@x.com' });
|
|
1010
1389
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1011
1390
|
['gmail', 'settings', 'sendas', 'get', 'alias@x.com'],
|
|
1012
1391
|
{ account: undefined },
|
|
@@ -1016,7 +1395,7 @@ describe('gog_gmail_sendas_get', () => {
|
|
|
1016
1395
|
|
|
1017
1396
|
describe('gog_gmail_sendas_create', () => {
|
|
1018
1397
|
it('calls runOrDiagnose with just the email', async () => {
|
|
1019
|
-
await
|
|
1398
|
+
await harness.callTool('gog_gmail_sendas_create', { email: 'alias@x.com' });
|
|
1020
1399
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1021
1400
|
['gmail', 'settings', 'sendas', 'create', 'alias@x.com'],
|
|
1022
1401
|
{ account: undefined },
|
|
@@ -1024,7 +1403,7 @@ describe('gog_gmail_sendas_create', () => {
|
|
|
1024
1403
|
});
|
|
1025
1404
|
|
|
1026
1405
|
it('passes all flags when provided', async () => {
|
|
1027
|
-
await
|
|
1406
|
+
await harness.callTool('gog_gmail_sendas_create', {
|
|
1028
1407
|
email: 'alias@x.com',
|
|
1029
1408
|
displayName: 'Alias',
|
|
1030
1409
|
replyTo: 'reply@x.com',
|
|
@@ -1038,7 +1417,7 @@ describe('gog_gmail_sendas_create', () => {
|
|
|
1038
1417
|
});
|
|
1039
1418
|
|
|
1040
1419
|
it('omits treatAsAlias when false', async () => {
|
|
1041
|
-
await
|
|
1420
|
+
await harness.callTool('gog_gmail_sendas_create', { email: 'alias@x.com', treatAsAlias: false });
|
|
1042
1421
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1043
1422
|
['gmail', 'settings', 'sendas', 'create', 'alias@x.com'],
|
|
1044
1423
|
{ account: undefined },
|
|
@@ -1048,7 +1427,7 @@ describe('gog_gmail_sendas_create', () => {
|
|
|
1048
1427
|
|
|
1049
1428
|
describe('gog_gmail_sendas_update', () => {
|
|
1050
1429
|
it('calls runOrDiagnose with just the email', async () => {
|
|
1051
|
-
await
|
|
1430
|
+
await harness.callTool('gog_gmail_sendas_update', { email: 'alias@x.com' });
|
|
1052
1431
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1053
1432
|
['gmail', 'settings', 'sendas', 'update', 'alias@x.com'],
|
|
1054
1433
|
{ account: undefined },
|
|
@@ -1056,7 +1435,7 @@ describe('gog_gmail_sendas_update', () => {
|
|
|
1056
1435
|
});
|
|
1057
1436
|
|
|
1058
1437
|
it('passes all flags when provided', async () => {
|
|
1059
|
-
await
|
|
1438
|
+
await harness.callTool('gog_gmail_sendas_update', {
|
|
1060
1439
|
email: 'alias@x.com',
|
|
1061
1440
|
displayName: 'Alias',
|
|
1062
1441
|
replyTo: 'reply@x.com',
|
|
@@ -1071,7 +1450,7 @@ describe('gog_gmail_sendas_update', () => {
|
|
|
1071
1450
|
});
|
|
1072
1451
|
|
|
1073
1452
|
it('omits boolean flags when false', async () => {
|
|
1074
|
-
await
|
|
1453
|
+
await harness.callTool('gog_gmail_sendas_update', { email: 'alias@x.com', treatAsAlias: false, makeDefault: false });
|
|
1075
1454
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1076
1455
|
['gmail', 'settings', 'sendas', 'update', 'alias@x.com'],
|
|
1077
1456
|
{ account: undefined },
|
|
@@ -1081,9 +1460,9 @@ describe('gog_gmail_sendas_update', () => {
|
|
|
1081
1460
|
|
|
1082
1461
|
describe('gog_gmail_sendas_delete', () => {
|
|
1083
1462
|
it('calls runOrDiagnose with the email', async () => {
|
|
1084
|
-
await
|
|
1463
|
+
await harness.callTool('gog_gmail_sendas_delete', { email: 'alias@x.com' });
|
|
1085
1464
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1086
|
-
['gmail', 'settings', 'sendas', 'delete', 'alias@x.com'],
|
|
1465
|
+
['gmail', 'settings', 'sendas', 'delete', 'alias@x.com', '--force'],
|
|
1087
1466
|
{ account: undefined },
|
|
1088
1467
|
);
|
|
1089
1468
|
});
|
|
@@ -1091,10 +1470,214 @@ describe('gog_gmail_sendas_delete', () => {
|
|
|
1091
1470
|
|
|
1092
1471
|
describe('gog_gmail_sendas_verify', () => {
|
|
1093
1472
|
it('calls runOrDiagnose with the email', async () => {
|
|
1094
|
-
await
|
|
1473
|
+
await harness.callTool('gog_gmail_sendas_verify', { email: 'alias@x.com' });
|
|
1095
1474
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1096
1475
|
['gmail', 'settings', 'sendas', 'verify', 'alias@x.com'],
|
|
1097
1476
|
{ account: undefined },
|
|
1098
1477
|
);
|
|
1099
1478
|
});
|
|
1100
1479
|
});
|
|
1480
|
+
|
|
1481
|
+
// resultText degradations: a non-text tool result (never produced by
|
|
1482
|
+
// runOrDiagnose today, but allowed by the MCP result shape) is passed
|
|
1483
|
+
// through untouched instead of being post-processed.
|
|
1484
|
+
describe('non-text result passthrough', () => {
|
|
1485
|
+
it('gog_gmail_thread_get returns a non-text result untouched when trimming', async () => {
|
|
1486
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue({ content: [] });
|
|
1487
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', latestN: 1 });
|
|
1488
|
+
expect(result.content).toEqual([]);
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
it('returnFull surfaces a non-text write result without re-fetching', async () => {
|
|
1492
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce({ content: [] });
|
|
1493
|
+
const result = await harness.callTool('gog_gmail_drafts_update', {
|
|
1494
|
+
draftId: 'd1', subject: 'S', body: 'B', returnFull: true,
|
|
1495
|
+
});
|
|
1496
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
1497
|
+
expect(result.content).toEqual([]);
|
|
1498
|
+
});
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
// Large message bodies cannot travel in argv: the hosted Fly runner rejects any
|
|
1502
|
+
// single arg over its cap and the Linux kernel caps MAX_ARG_STRLEN at 128 KiB.
|
|
1503
|
+
// payloadArg swaps an oversize value for a GogFileArg that the executor
|
|
1504
|
+
// materializes as a temp file. These tests pin the boundary behavior at the
|
|
1505
|
+
// tool surface: small bodies stay inline, large ones become file args, and the
|
|
1506
|
+
// rest of the flag set is unaffected either way.
|
|
1507
|
+
describe('large payloads route to file args', () => {
|
|
1508
|
+
const big = 'x'.repeat(lib.PAYLOAD_INLINE_MAX + 1);
|
|
1509
|
+
const bigHtml = `<table>${'<tr><td>cell</td></tr>'.repeat(600)}</table>`;
|
|
1510
|
+
|
|
1511
|
+
// Pull the args array out of the single runOrDiagnose call under test.
|
|
1512
|
+
function args(): lib.GogArg[] {
|
|
1513
|
+
return vi.mocked(lib.runOrDiagnose).mock.calls[0]![0];
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
it('keeps a body at exactly the threshold inline', async () => {
|
|
1517
|
+
const atLimit = 'x'.repeat(lib.PAYLOAD_INLINE_MAX);
|
|
1518
|
+
await harness.callTool('gog_gmail_drafts_create', { subject: 'S', body: atLimit });
|
|
1519
|
+
expect(args()).toEqual(['gmail', 'drafts', 'create', '--subject=S', `--body=${atLimit}`]);
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
it('measures bytes, not characters, so a multibyte body crosses earlier', async () => {
|
|
1523
|
+
// Each emoji is 2 UTF-16 units but 4 UTF-8 bytes, so this sits comfortably
|
|
1524
|
+
// under the threshold by .length yet well over it by byte count.
|
|
1525
|
+
const emoji = '😀'.repeat(1500);
|
|
1526
|
+
expect(emoji.length).toBeLessThan(lib.PAYLOAD_INLINE_MAX);
|
|
1527
|
+
expect(Buffer.byteLength(emoji, 'utf8')).toBeGreaterThan(lib.PAYLOAD_INLINE_MAX);
|
|
1528
|
+
await harness.callTool('gog_gmail_drafts_create', { subject: 'S', body: emoji });
|
|
1529
|
+
expect(args()[4]).toEqual({ kind: 'file', flag: 'body-file', contents: emoji, ext: undefined });
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
it('gog_gmail_drafts_create routes a large body to --body-file', async () => {
|
|
1533
|
+
await harness.callTool('gog_gmail_drafts_create', { subject: 'S', body: big });
|
|
1534
|
+
expect(args()).toEqual([
|
|
1535
|
+
'gmail', 'drafts', 'create',
|
|
1536
|
+
'--subject=S',
|
|
1537
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1538
|
+
]);
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
it('gog_gmail_drafts_create routes a large bodyHtml to --body-html-file with an html ext', async () => {
|
|
1542
|
+
expect(Buffer.byteLength(bigHtml)).toBeGreaterThan(12_000);
|
|
1543
|
+
await harness.callTool('gog_gmail_drafts_create', { subject: 'S', body: 'plain', bodyHtml: bigHtml });
|
|
1544
|
+
expect(args()).toEqual([
|
|
1545
|
+
'gmail', 'drafts', 'create',
|
|
1546
|
+
'--subject=S',
|
|
1547
|
+
'--body=plain',
|
|
1548
|
+
{ kind: 'file', flag: 'body-html-file', contents: bigHtml, ext: 'html' },
|
|
1549
|
+
]);
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
it('gog_gmail_drafts_update routes a large body to --body-file', async () => {
|
|
1553
|
+
await harness.callTool('gog_gmail_drafts_update', { draftId: 'd1', subject: 'S', body: big });
|
|
1554
|
+
expect(args()).toEqual([
|
|
1555
|
+
'gmail', 'drafts', 'update', 'd1',
|
|
1556
|
+
'--subject=S',
|
|
1557
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1558
|
+
]);
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
it('omitRecipients still suppresses to/cc/bcc alongside a large body', async () => {
|
|
1562
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
1563
|
+
subject: 'S', body: big, to: 'a@b.com', cc: 'c@d.com', bcc: 'e@f.com', omitRecipients: true,
|
|
1564
|
+
});
|
|
1565
|
+
expect(args()).toEqual([
|
|
1566
|
+
'gmail', 'drafts', 'create',
|
|
1567
|
+
'--subject=S',
|
|
1568
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1569
|
+
]);
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
it('threading and attachments still apply alongside a large body', async () => {
|
|
1573
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
1574
|
+
subject: 'S', body: big, replyToThreadId: 't1', attach: ['/tmp/a.pdf'],
|
|
1575
|
+
});
|
|
1576
|
+
expect(args()).toEqual([
|
|
1577
|
+
'gmail', 'drafts', 'create',
|
|
1578
|
+
'--subject=S',
|
|
1579
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1580
|
+
'--thread-id=t1',
|
|
1581
|
+
'--attach=/tmp/a.pdf',
|
|
1582
|
+
]);
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
it('replyToMessageId still wins over replyToThreadId alongside a large body', async () => {
|
|
1586
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
1587
|
+
subject: 'S', body: big, replyToMessageId: 'm1', replyToThreadId: 't1',
|
|
1588
|
+
});
|
|
1589
|
+
expect(args()).toContain('--reply-to-message-id=m1');
|
|
1590
|
+
expect(args()).not.toContain('--thread-id=t1');
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
it('gog_gmail_reply routes a large body and bodyHtml to file args', async () => {
|
|
1594
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: big, bodyHtml: bigHtml });
|
|
1595
|
+
expect(args()).toEqual([
|
|
1596
|
+
'gmail', 'reply', 'm1',
|
|
1597
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1598
|
+
{ kind: 'file', flag: 'body-html-file', contents: bigHtml, ext: 'html' },
|
|
1599
|
+
]);
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
it('gog_gmail_reply_all routes a large body to --body-file, leaving the signature boolean a bare flag', async () => {
|
|
1603
|
+
await harness.callTool('gog_gmail_reply_all', { messageId: 'm1', body: big, signature: true });
|
|
1604
|
+
expect(args()).toEqual([
|
|
1605
|
+
'gmail', 'reply-all', 'm1',
|
|
1606
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1607
|
+
'--signature',
|
|
1608
|
+
]);
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
it('gog_gmail_forward routes a large note to --note-file', async () => {
|
|
1612
|
+
await harness.callTool('gog_gmail_forward', { messageId: 'm1', to: 'a@b.com', note: big });
|
|
1613
|
+
expect(args()).toEqual([
|
|
1614
|
+
'gmail', 'forward', 'm1', '--to=a@b.com',
|
|
1615
|
+
{ kind: 'file', flag: 'note-file', contents: big, ext: undefined },
|
|
1616
|
+
]);
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
it('gog_gmail_autoreply routes a large body to --body-file but keeps bodyHtml inline', async () => {
|
|
1620
|
+
// gog 0.34.1 gives `gmail autoreply` a --body-file but no --body-html-file.
|
|
1621
|
+
await harness.callTool('gog_gmail_autoreply', { query: 'is:unread', body: big, bodyHtml: '<p>Hi</p>' });
|
|
1622
|
+
expect(args()).toEqual([
|
|
1623
|
+
'gmail', 'autoreply', 'is:unread',
|
|
1624
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
1625
|
+
'--body-html=<p>Hi</p>',
|
|
1626
|
+
]);
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
it('gog_gmail_vacation_update keeps a large body inline (gog has no --body-file there)', async () => {
|
|
1630
|
+
await harness.callTool('gog_gmail_vacation_update', { enable: true, body: big });
|
|
1631
|
+
expect(args()).toEqual(['gmail', 'settings', 'vacation', 'update', '--enable', `--body=${big}`]);
|
|
1632
|
+
});
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
// gog hard-errors when an inline flag and its --*-file twin are both present
|
|
1636
|
+
// ("use only one of --body-html or --body-html-file"). The tools reject the
|
|
1637
|
+
// combination up front so the caller sees which PARAMS collided.
|
|
1638
|
+
describe('inline/file param conflicts are rejected before gog runs', () => {
|
|
1639
|
+
it('gog_gmail_drafts_create rejects bodyHtml plus bodyHtmlFile', async () => {
|
|
1640
|
+
const res = await harness.callTool('gog_gmail_drafts_create', {
|
|
1641
|
+
subject: 'S', body: 'B', bodyHtml: '<p>Hi</p>', bodyHtmlFile: '/tmp/b.html',
|
|
1642
|
+
});
|
|
1643
|
+
expect(res.isError).toBe(true);
|
|
1644
|
+
expect((res.content[0] as { text: string }).text).toContain('bodyHtml and bodyHtmlFile are mutually exclusive');
|
|
1645
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
it('gog_gmail_drafts_update rejects bodyHtml plus bodyHtmlFile', async () => {
|
|
1649
|
+
const res = await harness.callTool('gog_gmail_drafts_update', {
|
|
1650
|
+
draftId: 'd1', subject: 'S', body: 'B', bodyHtml: '<p>Hi</p>', bodyHtmlFile: '/tmp/b.html',
|
|
1651
|
+
});
|
|
1652
|
+
expect(res.isError).toBe(true);
|
|
1653
|
+
expect((res.content[0] as { text: string }).text).toContain('mutually exclusive');
|
|
1654
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
it('gog_gmail_reply rejects bodyHtml plus bodyHtmlFile', async () => {
|
|
1658
|
+
const res = await harness.callTool('gog_gmail_reply', {
|
|
1659
|
+
messageId: 'm1', bodyHtml: '<p>Hi</p>', bodyHtmlFile: '/tmp/b.html',
|
|
1660
|
+
});
|
|
1661
|
+
expect(res.isError).toBe(true);
|
|
1662
|
+
expect((res.content[0] as { text: string }).text).toContain('bodyHtml and bodyHtmlFile are mutually exclusive');
|
|
1663
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
it('an empty-string bodyHtml still counts as supplied and conflicts', async () => {
|
|
1667
|
+
// Guards the `!== undefined` check against a falsy-but-present value
|
|
1668
|
+
// sliding through to gog, which rejects the pair regardless of content.
|
|
1669
|
+
const res = await harness.callTool('gog_gmail_reply', {
|
|
1670
|
+
messageId: 'm1', body: 'B', bodyHtml: '', bodyHtmlFile: '/tmp/b.html',
|
|
1671
|
+
});
|
|
1672
|
+
expect(res.isError).toBe(true);
|
|
1673
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
it('bodyHtmlFile alone still passes through as --body-html-file', async () => {
|
|
1677
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', bodyHtmlFile: '/tmp/b.html' });
|
|
1678
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1679
|
+
['gmail', 'reply', 'm1', '--body=Hi', '--body-html-file=/tmp/b.html'],
|
|
1680
|
+
{ account: undefined },
|
|
1681
|
+
);
|
|
1682
|
+
});
|
|
1683
|
+
});
|