gogcli-mcp-gmail 2.7.1 → 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 +20203 -19642
- package/manifest.json +23 -3
- package/package.json +3 -2
- package/src/index.ts +7 -9
- package/src/tools/gmail-extra.ts +517 -49
- package/tests/tools/gmail-extra.test.ts +735 -135
- 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 },
|
|
@@ -132,11 +380,28 @@ describe('bulk action tools (archive, mark_read, mark_unread, trash)', () => {
|
|
|
132
380
|
});
|
|
133
381
|
});
|
|
134
382
|
}
|
|
383
|
+
|
|
384
|
+
// gog 0.25.0 — --thread is archive-only
|
|
385
|
+
it('gog_gmail_archive passes --thread to archive whole threads by id', async () => {
|
|
386
|
+
await harness.callTool('gog_gmail_archive', { messageIds: ['t1', 't2'], thread: true });
|
|
387
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
388
|
+
['gmail', 'archive', 't1', 't2', '--thread'],
|
|
389
|
+
{ account: undefined },
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('other bulk tools do not expose a thread param', async () => {
|
|
394
|
+
await harness.callTool('gog_gmail_trash', { messageIds: ['m1'], thread: true });
|
|
395
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
396
|
+
['gmail', 'trash', 'm1'],
|
|
397
|
+
{ account: undefined },
|
|
398
|
+
);
|
|
399
|
+
});
|
|
135
400
|
});
|
|
136
401
|
|
|
137
402
|
describe('gog_gmail_message_modify', () => {
|
|
138
403
|
it('calls runOrDiagnose with messageId and label changes', async () => {
|
|
139
|
-
await
|
|
404
|
+
await harness.callTool('gog_gmail_message_modify', {
|
|
140
405
|
messageId: 'm1',
|
|
141
406
|
add: 'STARRED,IMPORTANT',
|
|
142
407
|
remove: 'INBOX',
|
|
@@ -148,7 +413,7 @@ describe('gog_gmail_message_modify', () => {
|
|
|
148
413
|
});
|
|
149
414
|
|
|
150
415
|
it('omits flags when not provided', async () => {
|
|
151
|
-
await
|
|
416
|
+
await harness.callTool('gog_gmail_message_modify', { messageId: 'm1' });
|
|
152
417
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
153
418
|
['gmail', 'messages', 'modify', 'm1'],
|
|
154
419
|
{ account: undefined },
|
|
@@ -158,17 +423,33 @@ describe('gog_gmail_message_modify', () => {
|
|
|
158
423
|
|
|
159
424
|
describe('gog_gmail_batch_delete', () => {
|
|
160
425
|
it('calls runOrDiagnose with messageIds as positional args', async () => {
|
|
161
|
-
await
|
|
426
|
+
await harness.callTool('gog_gmail_batch_delete', { messageIds: ['m1', 'm2', 'm3'] });
|
|
162
427
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
163
428
|
['gmail', 'batch', 'delete', 'm1', 'm2', 'm3'],
|
|
164
429
|
{ account: undefined },
|
|
165
430
|
);
|
|
166
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
|
+
});
|
|
167
448
|
});
|
|
168
449
|
|
|
169
450
|
describe('gog_gmail_batch_modify', () => {
|
|
170
451
|
it('calls runOrDiagnose with messageIds and label flags', async () => {
|
|
171
|
-
await
|
|
452
|
+
await harness.callTool('gog_gmail_batch_modify', {
|
|
172
453
|
messageIds: ['m1', 'm2'],
|
|
173
454
|
add: 'STARRED',
|
|
174
455
|
remove: 'INBOX',
|
|
@@ -180,7 +461,7 @@ describe('gog_gmail_batch_modify', () => {
|
|
|
180
461
|
});
|
|
181
462
|
|
|
182
463
|
it('omits label flags when not provided', async () => {
|
|
183
|
-
await
|
|
464
|
+
await harness.callTool('gog_gmail_batch_modify', { messageIds: ['m1'] });
|
|
184
465
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
185
466
|
['gmail', 'batch', 'modify', 'm1'],
|
|
186
467
|
{ account: undefined },
|
|
@@ -190,7 +471,7 @@ describe('gog_gmail_batch_modify', () => {
|
|
|
190
471
|
|
|
191
472
|
describe('gog_gmail_thread_get', () => {
|
|
192
473
|
it('calls runOrDiagnose with threadId', async () => {
|
|
193
|
-
await
|
|
474
|
+
await harness.callTool('gog_gmail_thread_get', { threadId: 't1' });
|
|
194
475
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
195
476
|
['gmail', 'thread', 'get', 't1'],
|
|
196
477
|
{ account: undefined },
|
|
@@ -198,7 +479,7 @@ describe('gog_gmail_thread_get', () => {
|
|
|
198
479
|
});
|
|
199
480
|
|
|
200
481
|
it('passes all flags', async () => {
|
|
201
|
-
await
|
|
482
|
+
await harness.callTool('gog_gmail_thread_get', {
|
|
202
483
|
threadId: 't1',
|
|
203
484
|
download: true,
|
|
204
485
|
full: true,
|
|
@@ -212,7 +493,7 @@ describe('gog_gmail_thread_get', () => {
|
|
|
212
493
|
});
|
|
213
494
|
|
|
214
495
|
it('omits boolean flags when false', async () => {
|
|
215
|
-
await
|
|
496
|
+
await harness.callTool('gog_gmail_thread_get', {
|
|
216
497
|
threadId: 't1',
|
|
217
498
|
download: false,
|
|
218
499
|
full: false,
|
|
@@ -237,14 +518,14 @@ describe('gog_gmail_thread_get', () => {
|
|
|
237
518
|
});
|
|
238
519
|
|
|
239
520
|
it('does not transform the output when no paging params are given', async () => {
|
|
240
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
241
|
-
const result = await
|
|
521
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult(THREAD));
|
|
522
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1' });
|
|
242
523
|
expect(result.content[0].text).toBe(THREAD);
|
|
243
524
|
});
|
|
244
525
|
|
|
245
526
|
it('latestN returns only the last N messages', async () => {
|
|
246
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
247
|
-
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 });
|
|
248
529
|
// latestN is wrapper-side; no CLI flag is added
|
|
249
530
|
expect(vi.mocked(lib.runOrDiagnose).mock.calls[0]![0]).toEqual(['gmail', 'thread', 'get', 't1']);
|
|
250
531
|
const parsed = JSON.parse(result.content[0].text);
|
|
@@ -252,8 +533,8 @@ describe('gog_gmail_thread_get', () => {
|
|
|
252
533
|
});
|
|
253
534
|
|
|
254
535
|
it('snippetsOnly returns per-message headers and snippet without bodies', async () => {
|
|
255
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
256
|
-
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 });
|
|
257
538
|
const parsed = JSON.parse(result.content[0].text);
|
|
258
539
|
expect(parsed.thread.messages).toHaveLength(3);
|
|
259
540
|
const m1 = parsed.thread.messages[0];
|
|
@@ -265,35 +546,35 @@ describe('gog_gmail_thread_get', () => {
|
|
|
265
546
|
});
|
|
266
547
|
|
|
267
548
|
it('combines latestN and snippetsOnly', async () => {
|
|
268
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
269
|
-
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 });
|
|
270
551
|
const parsed = JSON.parse(result.content[0].text);
|
|
271
552
|
expect(parsed.thread.messages).toHaveLength(1);
|
|
272
553
|
expect(parsed.thread.messages[0].id).toBe('m3');
|
|
273
554
|
});
|
|
274
555
|
|
|
275
556
|
it('returns the raw result when the payload is not JSON', async () => {
|
|
276
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
277
|
-
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 });
|
|
278
559
|
expect(result.content[0].text).toBe('not json');
|
|
279
560
|
});
|
|
280
561
|
|
|
281
562
|
it('returns the raw result when there is no messages array', async () => {
|
|
282
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
283
|
-
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 });
|
|
284
565
|
expect(result.content[0].text).toBe('{"thread":{}}');
|
|
285
566
|
});
|
|
286
567
|
|
|
287
568
|
it('returns the raw result when there is no thread object', async () => {
|
|
288
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
289
|
-
const result = await
|
|
569
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult('{}'));
|
|
570
|
+
const result = await harness.callTool('gog_gmail_thread_get', { threadId: 't1', latestN: 1 });
|
|
290
571
|
expect(result.content[0].text).toBe('{}');
|
|
291
572
|
});
|
|
292
573
|
});
|
|
293
574
|
|
|
294
575
|
describe('gog_gmail_thread_modify', () => {
|
|
295
576
|
it('calls runOrDiagnose with threadId and label flags', async () => {
|
|
296
|
-
await
|
|
577
|
+
await harness.callTool('gog_gmail_thread_modify', {
|
|
297
578
|
threadId: 't1',
|
|
298
579
|
add: 'IMPORTANT',
|
|
299
580
|
remove: 'INBOX',
|
|
@@ -305,7 +586,7 @@ describe('gog_gmail_thread_modify', () => {
|
|
|
305
586
|
});
|
|
306
587
|
|
|
307
588
|
it('omits label flags when not provided', async () => {
|
|
308
|
-
await
|
|
589
|
+
await harness.callTool('gog_gmail_thread_modify', { threadId: 't1' });
|
|
309
590
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
310
591
|
['gmail', 'thread', 'modify', 't1'],
|
|
311
592
|
{ account: undefined },
|
|
@@ -315,7 +596,7 @@ describe('gog_gmail_thread_modify', () => {
|
|
|
315
596
|
|
|
316
597
|
describe('gog_gmail_thread_attachments', () => {
|
|
317
598
|
it('calls runOrDiagnose with threadId', async () => {
|
|
318
|
-
await
|
|
599
|
+
await harness.callTool('gog_gmail_thread_attachments', { threadId: 't1' });
|
|
319
600
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
320
601
|
['gmail', 'thread', 'attachments', 't1'],
|
|
321
602
|
{ account: undefined },
|
|
@@ -323,7 +604,7 @@ describe('gog_gmail_thread_attachments', () => {
|
|
|
323
604
|
});
|
|
324
605
|
|
|
325
606
|
it('passes --download and --out-dir when provided', async () => {
|
|
326
|
-
await
|
|
607
|
+
await harness.callTool('gog_gmail_thread_attachments', {
|
|
327
608
|
threadId: 't1',
|
|
328
609
|
download: true,
|
|
329
610
|
outDir: '/tmp/atts',
|
|
@@ -337,7 +618,7 @@ describe('gog_gmail_thread_attachments', () => {
|
|
|
337
618
|
|
|
338
619
|
describe('gog_gmail_labels_list', () => {
|
|
339
620
|
it('calls runOrDiagnose with no args', async () => {
|
|
340
|
-
await
|
|
621
|
+
await harness.callTool('gog_gmail_labels_list', {});
|
|
341
622
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
342
623
|
['gmail', 'labels', 'list'],
|
|
343
624
|
{ account: undefined },
|
|
@@ -345,7 +626,7 @@ describe('gog_gmail_labels_list', () => {
|
|
|
345
626
|
});
|
|
346
627
|
|
|
347
628
|
it('forwards account', async () => {
|
|
348
|
-
await
|
|
629
|
+
await harness.callTool('gog_gmail_labels_list', { account: 'a@b.com' });
|
|
349
630
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
350
631
|
['gmail', 'labels', 'list'],
|
|
351
632
|
{ account: 'a@b.com' },
|
|
@@ -355,7 +636,7 @@ describe('gog_gmail_labels_list', () => {
|
|
|
355
636
|
|
|
356
637
|
describe('gog_gmail_labels_get', () => {
|
|
357
638
|
it('calls runOrDiagnose with labelIdOrName', async () => {
|
|
358
|
-
await
|
|
639
|
+
await harness.callTool('gog_gmail_labels_get', { labelIdOrName: 'INBOX' });
|
|
359
640
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
360
641
|
['gmail', 'labels', 'get', 'INBOX'],
|
|
361
642
|
{ account: undefined },
|
|
@@ -365,7 +646,7 @@ describe('gog_gmail_labels_get', () => {
|
|
|
365
646
|
|
|
366
647
|
describe('gog_gmail_labels_create', () => {
|
|
367
648
|
it('calls runOrDiagnose with name', async () => {
|
|
368
|
-
await
|
|
649
|
+
await harness.callTool('gog_gmail_labels_create', { name: 'Newsletter' });
|
|
369
650
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
370
651
|
['gmail', 'labels', 'create', 'Newsletter'],
|
|
371
652
|
{ account: undefined },
|
|
@@ -375,7 +656,7 @@ describe('gog_gmail_labels_create', () => {
|
|
|
375
656
|
|
|
376
657
|
describe('gog_gmail_labels_rename', () => {
|
|
377
658
|
it('calls runOrDiagnose with old and new names', async () => {
|
|
378
|
-
await
|
|
659
|
+
await harness.callTool('gog_gmail_labels_rename', { labelIdOrName: 'Old', newName: 'New' });
|
|
379
660
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
380
661
|
['gmail', 'labels', 'rename', 'Old', 'New'],
|
|
381
662
|
{ account: undefined },
|
|
@@ -385,9 +666,9 @@ describe('gog_gmail_labels_rename', () => {
|
|
|
385
666
|
|
|
386
667
|
describe('gog_gmail_labels_delete', () => {
|
|
387
668
|
it('calls runOrDiagnose with labelIdOrName', async () => {
|
|
388
|
-
await
|
|
669
|
+
await harness.callTool('gog_gmail_labels_delete', { labelIdOrName: 'Trash-Me' });
|
|
389
670
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
390
|
-
['gmail', 'labels', 'delete', 'Trash-Me'],
|
|
671
|
+
['gmail', 'labels', 'delete', 'Trash-Me', '--force'],
|
|
391
672
|
{ account: undefined },
|
|
392
673
|
);
|
|
393
674
|
});
|
|
@@ -395,7 +676,7 @@ describe('gog_gmail_labels_delete', () => {
|
|
|
395
676
|
|
|
396
677
|
describe('gog_gmail_labels_modify', () => {
|
|
397
678
|
it('calls runOrDiagnose with threadIds and label flags', async () => {
|
|
398
|
-
await
|
|
679
|
+
await harness.callTool('gog_gmail_labels_modify', {
|
|
399
680
|
threadIds: ['t1', 't2'],
|
|
400
681
|
add: 'Newsletter',
|
|
401
682
|
remove: 'INBOX',
|
|
@@ -407,7 +688,7 @@ describe('gog_gmail_labels_modify', () => {
|
|
|
407
688
|
});
|
|
408
689
|
|
|
409
690
|
it('omits label flags when not provided', async () => {
|
|
410
|
-
await
|
|
691
|
+
await harness.callTool('gog_gmail_labels_modify', { threadIds: ['t1'] });
|
|
411
692
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
412
693
|
['gmail', 'labels', 'modify', 't1'],
|
|
413
694
|
{ account: undefined },
|
|
@@ -417,7 +698,7 @@ describe('gog_gmail_labels_modify', () => {
|
|
|
417
698
|
|
|
418
699
|
describe('gog_gmail_drafts_list', () => {
|
|
419
700
|
it('calls runOrDiagnose with no flags', async () => {
|
|
420
|
-
await
|
|
701
|
+
await harness.callTool('gog_gmail_drafts_list', {});
|
|
421
702
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
422
703
|
['gmail', 'drafts', 'list'],
|
|
423
704
|
{ account: undefined },
|
|
@@ -425,7 +706,7 @@ describe('gog_gmail_drafts_list', () => {
|
|
|
425
706
|
});
|
|
426
707
|
|
|
427
708
|
it('passes pagination flags', async () => {
|
|
428
|
-
await
|
|
709
|
+
await harness.callTool('gog_gmail_drafts_list', { max: 50, page: 'tok', all: true });
|
|
429
710
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
430
711
|
['gmail', 'drafts', 'list', '--max=50', '--page=tok', '--all'],
|
|
431
712
|
{ account: undefined },
|
|
@@ -435,7 +716,7 @@ describe('gog_gmail_drafts_list', () => {
|
|
|
435
716
|
|
|
436
717
|
describe('gog_gmail_drafts_get', () => {
|
|
437
718
|
it('calls runOrDiagnose with draftId', async () => {
|
|
438
|
-
await
|
|
719
|
+
await harness.callTool('gog_gmail_drafts_get', { draftId: 'd1' });
|
|
439
720
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
440
721
|
['gmail', 'drafts', 'get', 'd1'],
|
|
441
722
|
{ account: undefined },
|
|
@@ -443,7 +724,7 @@ describe('gog_gmail_drafts_get', () => {
|
|
|
443
724
|
});
|
|
444
725
|
|
|
445
726
|
it('passes --download when true', async () => {
|
|
446
|
-
await
|
|
727
|
+
await harness.callTool('gog_gmail_drafts_get', { draftId: 'd1', download: true });
|
|
447
728
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
448
729
|
['gmail', 'drafts', 'get', 'd1', '--download'],
|
|
449
730
|
{ account: undefined },
|
|
@@ -453,7 +734,7 @@ describe('gog_gmail_drafts_get', () => {
|
|
|
453
734
|
|
|
454
735
|
describe('gog_gmail_drafts_create', () => {
|
|
455
736
|
it('calls runOrDiagnose with minimal required flags', async () => {
|
|
456
|
-
await
|
|
737
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
457
738
|
subject: 'Hi',
|
|
458
739
|
body: 'Hello',
|
|
459
740
|
});
|
|
@@ -464,7 +745,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
464
745
|
});
|
|
465
746
|
|
|
466
747
|
it('passes all flags including attachments', async () => {
|
|
467
|
-
await
|
|
748
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
468
749
|
to: 'a@b.com,c@d.com',
|
|
469
750
|
cc: 'cc@x.com',
|
|
470
751
|
bcc: 'bcc@x.com',
|
|
@@ -497,8 +778,33 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
497
778
|
);
|
|
498
779
|
});
|
|
499
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
|
+
|
|
500
806
|
it('skips recipient flags when omitRecipients is true, even if to/cc/bcc are supplied', async () => {
|
|
501
|
-
await
|
|
807
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
502
808
|
to: 'a@b.com', cc: 'cc@x.com', bcc: 'bcc@x.com',
|
|
503
809
|
subject: 'Hi', body: 'Hello', omitRecipients: true,
|
|
504
810
|
});
|
|
@@ -510,9 +816,9 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
510
816
|
|
|
511
817
|
it('returnFull re-fetches and returns the full stored draft', async () => {
|
|
512
818
|
vi.mocked(lib.runOrDiagnose)
|
|
513
|
-
.mockResolvedValueOnce(
|
|
514
|
-
.mockResolvedValueOnce(
|
|
515
|
-
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', {
|
|
516
822
|
subject: 'Hi', body: 'Hello', returnFull: true,
|
|
517
823
|
});
|
|
518
824
|
expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(1,
|
|
@@ -524,22 +830,22 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
524
830
|
|
|
525
831
|
it('returnFull does not push --return-full to the CLI', async () => {
|
|
526
832
|
vi.mocked(lib.runOrDiagnose)
|
|
527
|
-
.mockResolvedValueOnce(
|
|
528
|
-
.mockResolvedValueOnce(
|
|
529
|
-
await
|
|
833
|
+
.mockResolvedValueOnce(rawTextResult('{"draftId":"d9"}'))
|
|
834
|
+
.mockResolvedValueOnce(rawTextResult('{}'));
|
|
835
|
+
await harness.callTool('gog_gmail_drafts_create', { subject: 'Hi', body: 'Hello', returnFull: true });
|
|
530
836
|
expect(vi.mocked(lib.runOrDiagnose).mock.calls[0]![0]).not.toContain('--return-full');
|
|
531
837
|
});
|
|
532
838
|
|
|
533
839
|
it('returnFull returns the write result when output is not parseable JSON', async () => {
|
|
534
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
535
|
-
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 });
|
|
536
842
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
537
843
|
expect(result.content[0].text).toBe('not json');
|
|
538
844
|
});
|
|
539
845
|
|
|
540
846
|
it('returnFull returns the write result when no draftId is present', async () => {
|
|
541
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
542
|
-
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 });
|
|
543
849
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
544
850
|
expect(result.content[0].text).toBe('{"message":{"id":"m9"}}');
|
|
545
851
|
});
|
|
@@ -547,7 +853,7 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
547
853
|
|
|
548
854
|
describe('gmail draft reply threading (native --thread-id)', () => {
|
|
549
855
|
it('passes replyToThreadId straight through as --thread-id on create (no thread fetch)', async () => {
|
|
550
|
-
await
|
|
856
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
551
857
|
subject: 'Re: roof', body: 'Sounds good', replyToThreadId: '19dffe06f9668b28', account: 'me@x.com',
|
|
552
858
|
});
|
|
553
859
|
// gog resolves the thread's latest-message headers itself — no extra fetch.
|
|
@@ -559,7 +865,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
559
865
|
});
|
|
560
866
|
|
|
561
867
|
it('passes replyToThreadId as --thread-id on update', async () => {
|
|
562
|
-
await
|
|
868
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
563
869
|
draftId: 'd1', subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
564
870
|
});
|
|
565
871
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
@@ -569,7 +875,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
569
875
|
});
|
|
570
876
|
|
|
571
877
|
it('replyToMessageId wins when both ids are supplied (no --thread-id)', async () => {
|
|
572
|
-
await
|
|
878
|
+
await harness.callTool('gog_gmail_drafts_create', {
|
|
573
879
|
subject: 'S', body: 'B', replyToMessageId: 'mExplicit', replyToThreadId: 't1',
|
|
574
880
|
});
|
|
575
881
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
@@ -582,7 +888,7 @@ describe('gmail draft reply threading (native --thread-id)', () => {
|
|
|
582
888
|
|
|
583
889
|
describe('gog_gmail_drafts_update', () => {
|
|
584
890
|
it('calls runOrDiagnose with draftId and updated fields', async () => {
|
|
585
|
-
await
|
|
891
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
586
892
|
draftId: 'd1',
|
|
587
893
|
subject: 'New subject',
|
|
588
894
|
body: 'New body',
|
|
@@ -594,7 +900,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
594
900
|
});
|
|
595
901
|
|
|
596
902
|
it('passes attachments as repeatable flags', async () => {
|
|
597
|
-
await
|
|
903
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
598
904
|
draftId: 'd1',
|
|
599
905
|
subject: 'S',
|
|
600
906
|
body: 'B',
|
|
@@ -607,7 +913,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
607
913
|
});
|
|
608
914
|
|
|
609
915
|
it('skips recipient flags when omitRecipients is true', async () => {
|
|
610
|
-
await
|
|
916
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
611
917
|
draftId: 'd1', to: 'a@b.com', subject: 'S', body: 'B', omitRecipients: true,
|
|
612
918
|
});
|
|
613
919
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
@@ -617,7 +923,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
617
923
|
});
|
|
618
924
|
|
|
619
925
|
it('passes --clear-attachments when clearAttachments is true', async () => {
|
|
620
|
-
await
|
|
926
|
+
await harness.callTool('gog_gmail_drafts_update', {
|
|
621
927
|
draftId: 'd1', subject: 'S', body: 'B', clearAttachments: true,
|
|
622
928
|
});
|
|
623
929
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
@@ -628,9 +934,9 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
628
934
|
|
|
629
935
|
it('returnFull re-fetches the draft by its known id', async () => {
|
|
630
936
|
vi.mocked(lib.runOrDiagnose)
|
|
631
|
-
.mockResolvedValueOnce(
|
|
632
|
-
.mockResolvedValueOnce(
|
|
633
|
-
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', {
|
|
634
940
|
draftId: 'd1', subject: 'S', body: 'B', returnFull: true,
|
|
635
941
|
});
|
|
636
942
|
expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(2,
|
|
@@ -639,8 +945,8 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
639
945
|
});
|
|
640
946
|
|
|
641
947
|
it('returnFull surfaces a failed update instead of re-fetching a stale draft', async () => {
|
|
642
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(
|
|
643
|
-
const result = await
|
|
948
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(rawTextResult('Error: update failed'));
|
|
949
|
+
const result = await harness.callTool('gog_gmail_drafts_update', {
|
|
644
950
|
draftId: 'd1', subject: 'S', body: 'B', returnFull: true,
|
|
645
951
|
});
|
|
646
952
|
// write failed (non-JSON) → no re-fetch; the error is surfaced
|
|
@@ -651,7 +957,7 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
651
957
|
|
|
652
958
|
describe('gog_gmail_drafts_delete', () => {
|
|
653
959
|
it('calls runOrDiagnose with draftId', async () => {
|
|
654
|
-
await
|
|
960
|
+
await harness.callTool('gog_gmail_drafts_delete', { draftId: 'd1' });
|
|
655
961
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
656
962
|
['gmail', 'drafts', 'delete', 'd1'],
|
|
657
963
|
{ account: undefined },
|
|
@@ -659,7 +965,7 @@ describe('gog_gmail_drafts_delete', () => {
|
|
|
659
965
|
});
|
|
660
966
|
|
|
661
967
|
it('appends --force when force is true', async () => {
|
|
662
|
-
await
|
|
968
|
+
await harness.callTool('gog_gmail_drafts_delete', { draftId: 'd1', force: true });
|
|
663
969
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
664
970
|
['gmail', 'drafts', 'delete', 'd1', '--force'],
|
|
665
971
|
{ account: undefined },
|
|
@@ -667,7 +973,7 @@ describe('gog_gmail_drafts_delete', () => {
|
|
|
667
973
|
});
|
|
668
974
|
|
|
669
975
|
it('omits --force when force is false', async () => {
|
|
670
|
-
await
|
|
976
|
+
await harness.callTool('gog_gmail_drafts_delete', { draftId: 'd1', force: false });
|
|
671
977
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
672
978
|
['gmail', 'drafts', 'delete', 'd1'],
|
|
673
979
|
{ account: undefined },
|
|
@@ -677,7 +983,7 @@ describe('gog_gmail_drafts_delete', () => {
|
|
|
677
983
|
|
|
678
984
|
describe('gog_gmail_drafts_send', () => {
|
|
679
985
|
it('calls runOrDiagnose with draftId', async () => {
|
|
680
|
-
await
|
|
986
|
+
await harness.callTool('gog_gmail_drafts_send', { draftId: 'd1' });
|
|
681
987
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
682
988
|
['gmail', 'drafts', 'send', 'd1'],
|
|
683
989
|
{ account: undefined },
|
|
@@ -687,7 +993,7 @@ describe('gog_gmail_drafts_send', () => {
|
|
|
687
993
|
|
|
688
994
|
describe('gog_gmail_forward', () => {
|
|
689
995
|
it('calls runOrDiagnose with messageId and required --to', async () => {
|
|
690
|
-
await
|
|
996
|
+
await harness.callTool('gog_gmail_forward', { messageId: 'm1', to: 'a@b.com' });
|
|
691
997
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
692
998
|
['gmail', 'forward', 'm1', '--to=a@b.com'],
|
|
693
999
|
{ account: undefined },
|
|
@@ -695,7 +1001,7 @@ describe('gog_gmail_forward', () => {
|
|
|
695
1001
|
});
|
|
696
1002
|
|
|
697
1003
|
it('passes all forward flags', async () => {
|
|
698
|
-
await
|
|
1004
|
+
await harness.callTool('gog_gmail_forward', {
|
|
699
1005
|
messageId: 'm1',
|
|
700
1006
|
to: 'a@b.com',
|
|
701
1007
|
cc: 'cc@x.com',
|
|
@@ -719,7 +1025,7 @@ describe('gog_gmail_forward', () => {
|
|
|
719
1025
|
});
|
|
720
1026
|
|
|
721
1027
|
it('omits --skip-attachments when false', async () => {
|
|
722
|
-
await
|
|
1028
|
+
await harness.callTool('gog_gmail_forward', { messageId: 'm1', to: 'a@b.com', skipAttachments: false });
|
|
723
1029
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
724
1030
|
['gmail', 'forward', 'm1', '--to=a@b.com'],
|
|
725
1031
|
{ account: undefined },
|
|
@@ -727,9 +1033,99 @@ describe('gog_gmail_forward', () => {
|
|
|
727
1033
|
});
|
|
728
1034
|
});
|
|
729
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
|
+
|
|
730
1126
|
describe('gog_gmail_autoreply', () => {
|
|
731
1127
|
it('calls runOrDiagnose with query and --body', async () => {
|
|
732
|
-
await
|
|
1128
|
+
await harness.callTool('gog_gmail_autoreply', { query: 'is:unread', body: 'Thanks' });
|
|
733
1129
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
734
1130
|
['gmail', 'autoreply', 'is:unread', '--body=Thanks'],
|
|
735
1131
|
{ account: undefined },
|
|
@@ -737,7 +1133,7 @@ describe('gog_gmail_autoreply', () => {
|
|
|
737
1133
|
});
|
|
738
1134
|
|
|
739
1135
|
it('passes all autoreply flags', async () => {
|
|
740
|
-
await
|
|
1136
|
+
await harness.callTool('gog_gmail_autoreply', {
|
|
741
1137
|
query: 'is:unread',
|
|
742
1138
|
max: 50,
|
|
743
1139
|
subject: 'Re: out of office',
|
|
@@ -771,7 +1167,7 @@ describe('gog_gmail_autoreply', () => {
|
|
|
771
1167
|
});
|
|
772
1168
|
|
|
773
1169
|
it('omits boolean flags when false', async () => {
|
|
774
|
-
await
|
|
1170
|
+
await harness.callTool('gog_gmail_autoreply', {
|
|
775
1171
|
query: 'is:unread',
|
|
776
1172
|
body: 'Thanks',
|
|
777
1173
|
archive: false,
|
|
@@ -786,7 +1182,7 @@ describe('gog_gmail_autoreply', () => {
|
|
|
786
1182
|
});
|
|
787
1183
|
|
|
788
1184
|
it('supports HTML-only body (no plain --body)', async () => {
|
|
789
|
-
await
|
|
1185
|
+
await harness.callTool('gog_gmail_autoreply', { query: 'is:unread', bodyHtml: '<p>Hi</p>' });
|
|
790
1186
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
791
1187
|
['gmail', 'autoreply', 'is:unread', '--body-html=<p>Hi</p>'],
|
|
792
1188
|
{ account: undefined },
|
|
@@ -796,7 +1192,7 @@ describe('gog_gmail_autoreply', () => {
|
|
|
796
1192
|
|
|
797
1193
|
describe('gog_gmail_messages_search', () => {
|
|
798
1194
|
it('calls runOrDiagnose with just the query', async () => {
|
|
799
|
-
await
|
|
1195
|
+
await harness.callTool('gog_gmail_messages_search', { query: 'from:alice' });
|
|
800
1196
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
801
1197
|
['gmail', 'messages', 'search', 'from:alice'],
|
|
802
1198
|
{ account: undefined },
|
|
@@ -804,7 +1200,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
804
1200
|
});
|
|
805
1201
|
|
|
806
1202
|
it('passes all flags when provided', async () => {
|
|
807
|
-
await
|
|
1203
|
+
await harness.callTool('gog_gmail_messages_search', {
|
|
808
1204
|
query: 'is:unread',
|
|
809
1205
|
max: 10,
|
|
810
1206
|
page: 'tok',
|
|
@@ -821,7 +1217,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
821
1217
|
});
|
|
822
1218
|
|
|
823
1219
|
it('omits flags when false/absent', async () => {
|
|
824
|
-
await
|
|
1220
|
+
await harness.callTool('gog_gmail_messages_search', { query: 'x', all: false, includeBody: false, full: false });
|
|
825
1221
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
826
1222
|
['gmail', 'messages', 'search', 'x'],
|
|
827
1223
|
{ account: undefined },
|
|
@@ -831,7 +1227,7 @@ describe('gog_gmail_messages_search', () => {
|
|
|
831
1227
|
|
|
832
1228
|
describe('gog_gmail_labels_style', () => {
|
|
833
1229
|
it('calls runOrDiagnose with just the label', async () => {
|
|
834
|
-
await
|
|
1230
|
+
await harness.callTool('gog_gmail_labels_style', { labelIdOrName: 'Work' });
|
|
835
1231
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
836
1232
|
['gmail', 'labels', 'style', 'Work'],
|
|
837
1233
|
{ account: undefined },
|
|
@@ -839,7 +1235,7 @@ describe('gog_gmail_labels_style', () => {
|
|
|
839
1235
|
});
|
|
840
1236
|
|
|
841
1237
|
it('passes all style flags when provided', async () => {
|
|
842
|
-
await
|
|
1238
|
+
await harness.callTool('gog_gmail_labels_style', {
|
|
843
1239
|
labelIdOrName: 'Work',
|
|
844
1240
|
backgroundColor: '#000000',
|
|
845
1241
|
textColor: '#ffffff',
|
|
@@ -855,7 +1251,7 @@ describe('gog_gmail_labels_style', () => {
|
|
|
855
1251
|
|
|
856
1252
|
describe('gog_gmail_vacation_get', () => {
|
|
857
1253
|
it('calls runOrDiagnose', async () => {
|
|
858
|
-
await
|
|
1254
|
+
await harness.callTool('gog_gmail_vacation_get', { account: 'me@x.com' });
|
|
859
1255
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
860
1256
|
['gmail', 'settings', 'vacation', 'get'],
|
|
861
1257
|
{ account: 'me@x.com' },
|
|
@@ -865,7 +1261,7 @@ describe('gog_gmail_vacation_get', () => {
|
|
|
865
1261
|
|
|
866
1262
|
describe('gog_gmail_vacation_update', () => {
|
|
867
1263
|
it('calls runOrDiagnose with no flags', async () => {
|
|
868
|
-
await
|
|
1264
|
+
await harness.callTool('gog_gmail_vacation_update', {});
|
|
869
1265
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
870
1266
|
['gmail', 'settings', 'vacation', 'update'],
|
|
871
1267
|
{ account: undefined },
|
|
@@ -873,7 +1269,7 @@ describe('gog_gmail_vacation_update', () => {
|
|
|
873
1269
|
});
|
|
874
1270
|
|
|
875
1271
|
it('enables with subject/body/start/end and scoping', async () => {
|
|
876
|
-
await
|
|
1272
|
+
await harness.callTool('gog_gmail_vacation_update', {
|
|
877
1273
|
enable: true,
|
|
878
1274
|
subject: 'Away',
|
|
879
1275
|
body: '<p>OOO</p>',
|
|
@@ -889,7 +1285,7 @@ describe('gog_gmail_vacation_update', () => {
|
|
|
889
1285
|
});
|
|
890
1286
|
|
|
891
1287
|
it('disables the responder', async () => {
|
|
892
|
-
await
|
|
1288
|
+
await harness.callTool('gog_gmail_vacation_update', { disable: true, enable: false, contactsOnly: false, domainOnly: false });
|
|
893
1289
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
894
1290
|
['gmail', 'settings', 'vacation', 'update', '--disable'],
|
|
895
1291
|
{ account: undefined },
|
|
@@ -899,7 +1295,7 @@ describe('gog_gmail_vacation_update', () => {
|
|
|
899
1295
|
|
|
900
1296
|
describe('gog_gmail_filters_list', () => {
|
|
901
1297
|
it('calls runOrDiagnose', async () => {
|
|
902
|
-
await
|
|
1298
|
+
await harness.callTool('gog_gmail_filters_list', {});
|
|
903
1299
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
904
1300
|
['gmail', 'settings', 'filters', 'list'],
|
|
905
1301
|
{ account: undefined },
|
|
@@ -909,7 +1305,7 @@ describe('gog_gmail_filters_list', () => {
|
|
|
909
1305
|
|
|
910
1306
|
describe('gog_gmail_filters_get', () => {
|
|
911
1307
|
it('calls runOrDiagnose with the filter ID', async () => {
|
|
912
|
-
await
|
|
1308
|
+
await harness.callTool('gog_gmail_filters_get', { filterId: 'f1' });
|
|
913
1309
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
914
1310
|
['gmail', 'settings', 'filters', 'get', 'f1'],
|
|
915
1311
|
{ account: undefined },
|
|
@@ -919,7 +1315,7 @@ describe('gog_gmail_filters_get', () => {
|
|
|
919
1315
|
|
|
920
1316
|
describe('gog_gmail_filters_create', () => {
|
|
921
1317
|
it('calls runOrDiagnose with no flags', async () => {
|
|
922
|
-
await
|
|
1318
|
+
await harness.callTool('gog_gmail_filters_create', {});
|
|
923
1319
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
924
1320
|
['gmail', 'settings', 'filters', 'create'],
|
|
925
1321
|
{ account: undefined },
|
|
@@ -927,7 +1323,7 @@ describe('gog_gmail_filters_create', () => {
|
|
|
927
1323
|
});
|
|
928
1324
|
|
|
929
1325
|
it('passes all criteria and actions when provided', async () => {
|
|
930
|
-
await
|
|
1326
|
+
await harness.callTool('gog_gmail_filters_create', {
|
|
931
1327
|
from: 'alice@x.com',
|
|
932
1328
|
to: 'me@x.com',
|
|
933
1329
|
subject: 'Report',
|
|
@@ -944,13 +1340,13 @@ describe('gog_gmail_filters_create', () => {
|
|
|
944
1340
|
forward: 'fwd@x.com',
|
|
945
1341
|
});
|
|
946
1342
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
947
|
-
['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'],
|
|
948
1344
|
{ account: undefined },
|
|
949
1345
|
);
|
|
950
1346
|
});
|
|
951
1347
|
|
|
952
1348
|
it('omits boolean flags when false', async () => {
|
|
953
|
-
await
|
|
1349
|
+
await harness.callTool('gog_gmail_filters_create', {
|
|
954
1350
|
from: 'a@x.com',
|
|
955
1351
|
hasAttachment: false,
|
|
956
1352
|
archive: false,
|
|
@@ -969,9 +1365,9 @@ describe('gog_gmail_filters_create', () => {
|
|
|
969
1365
|
|
|
970
1366
|
describe('gog_gmail_filters_delete', () => {
|
|
971
1367
|
it('calls runOrDiagnose with the filter ID', async () => {
|
|
972
|
-
await
|
|
1368
|
+
await harness.callTool('gog_gmail_filters_delete', { filterId: 'f1' });
|
|
973
1369
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
974
|
-
['gmail', 'settings', 'filters', 'delete', 'f1'],
|
|
1370
|
+
['gmail', 'settings', 'filters', 'delete', 'f1', '--force'],
|
|
975
1371
|
{ account: undefined },
|
|
976
1372
|
);
|
|
977
1373
|
});
|
|
@@ -979,7 +1375,7 @@ describe('gog_gmail_filters_delete', () => {
|
|
|
979
1375
|
|
|
980
1376
|
describe('gog_gmail_sendas_list', () => {
|
|
981
1377
|
it('calls runOrDiagnose', async () => {
|
|
982
|
-
await
|
|
1378
|
+
await harness.callTool('gog_gmail_sendas_list', {});
|
|
983
1379
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
984
1380
|
['gmail', 'settings', 'sendas', 'list'],
|
|
985
1381
|
{ account: undefined },
|
|
@@ -989,7 +1385,7 @@ describe('gog_gmail_sendas_list', () => {
|
|
|
989
1385
|
|
|
990
1386
|
describe('gog_gmail_sendas_get', () => {
|
|
991
1387
|
it('calls runOrDiagnose with the email', async () => {
|
|
992
|
-
await
|
|
1388
|
+
await harness.callTool('gog_gmail_sendas_get', { email: 'alias@x.com' });
|
|
993
1389
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
994
1390
|
['gmail', 'settings', 'sendas', 'get', 'alias@x.com'],
|
|
995
1391
|
{ account: undefined },
|
|
@@ -999,7 +1395,7 @@ describe('gog_gmail_sendas_get', () => {
|
|
|
999
1395
|
|
|
1000
1396
|
describe('gog_gmail_sendas_create', () => {
|
|
1001
1397
|
it('calls runOrDiagnose with just the email', async () => {
|
|
1002
|
-
await
|
|
1398
|
+
await harness.callTool('gog_gmail_sendas_create', { email: 'alias@x.com' });
|
|
1003
1399
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1004
1400
|
['gmail', 'settings', 'sendas', 'create', 'alias@x.com'],
|
|
1005
1401
|
{ account: undefined },
|
|
@@ -1007,7 +1403,7 @@ describe('gog_gmail_sendas_create', () => {
|
|
|
1007
1403
|
});
|
|
1008
1404
|
|
|
1009
1405
|
it('passes all flags when provided', async () => {
|
|
1010
|
-
await
|
|
1406
|
+
await harness.callTool('gog_gmail_sendas_create', {
|
|
1011
1407
|
email: 'alias@x.com',
|
|
1012
1408
|
displayName: 'Alias',
|
|
1013
1409
|
replyTo: 'reply@x.com',
|
|
@@ -1021,7 +1417,7 @@ describe('gog_gmail_sendas_create', () => {
|
|
|
1021
1417
|
});
|
|
1022
1418
|
|
|
1023
1419
|
it('omits treatAsAlias when false', async () => {
|
|
1024
|
-
await
|
|
1420
|
+
await harness.callTool('gog_gmail_sendas_create', { email: 'alias@x.com', treatAsAlias: false });
|
|
1025
1421
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1026
1422
|
['gmail', 'settings', 'sendas', 'create', 'alias@x.com'],
|
|
1027
1423
|
{ account: undefined },
|
|
@@ -1031,7 +1427,7 @@ describe('gog_gmail_sendas_create', () => {
|
|
|
1031
1427
|
|
|
1032
1428
|
describe('gog_gmail_sendas_update', () => {
|
|
1033
1429
|
it('calls runOrDiagnose with just the email', async () => {
|
|
1034
|
-
await
|
|
1430
|
+
await harness.callTool('gog_gmail_sendas_update', { email: 'alias@x.com' });
|
|
1035
1431
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1036
1432
|
['gmail', 'settings', 'sendas', 'update', 'alias@x.com'],
|
|
1037
1433
|
{ account: undefined },
|
|
@@ -1039,7 +1435,7 @@ describe('gog_gmail_sendas_update', () => {
|
|
|
1039
1435
|
});
|
|
1040
1436
|
|
|
1041
1437
|
it('passes all flags when provided', async () => {
|
|
1042
|
-
await
|
|
1438
|
+
await harness.callTool('gog_gmail_sendas_update', {
|
|
1043
1439
|
email: 'alias@x.com',
|
|
1044
1440
|
displayName: 'Alias',
|
|
1045
1441
|
replyTo: 'reply@x.com',
|
|
@@ -1054,7 +1450,7 @@ describe('gog_gmail_sendas_update', () => {
|
|
|
1054
1450
|
});
|
|
1055
1451
|
|
|
1056
1452
|
it('omits boolean flags when false', async () => {
|
|
1057
|
-
await
|
|
1453
|
+
await harness.callTool('gog_gmail_sendas_update', { email: 'alias@x.com', treatAsAlias: false, makeDefault: false });
|
|
1058
1454
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1059
1455
|
['gmail', 'settings', 'sendas', 'update', 'alias@x.com'],
|
|
1060
1456
|
{ account: undefined },
|
|
@@ -1064,9 +1460,9 @@ describe('gog_gmail_sendas_update', () => {
|
|
|
1064
1460
|
|
|
1065
1461
|
describe('gog_gmail_sendas_delete', () => {
|
|
1066
1462
|
it('calls runOrDiagnose with the email', async () => {
|
|
1067
|
-
await
|
|
1463
|
+
await harness.callTool('gog_gmail_sendas_delete', { email: 'alias@x.com' });
|
|
1068
1464
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1069
|
-
['gmail', 'settings', 'sendas', 'delete', 'alias@x.com'],
|
|
1465
|
+
['gmail', 'settings', 'sendas', 'delete', 'alias@x.com', '--force'],
|
|
1070
1466
|
{ account: undefined },
|
|
1071
1467
|
);
|
|
1072
1468
|
});
|
|
@@ -1074,10 +1470,214 @@ describe('gog_gmail_sendas_delete', () => {
|
|
|
1074
1470
|
|
|
1075
1471
|
describe('gog_gmail_sendas_verify', () => {
|
|
1076
1472
|
it('calls runOrDiagnose with the email', async () => {
|
|
1077
|
-
await
|
|
1473
|
+
await harness.callTool('gog_gmail_sendas_verify', { email: 'alias@x.com' });
|
|
1078
1474
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1079
1475
|
['gmail', 'settings', 'sendas', 'verify', 'alias@x.com'],
|
|
1080
1476
|
{ account: undefined },
|
|
1081
1477
|
);
|
|
1082
1478
|
});
|
|
1083
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
|
+
});
|