gogcli-mcp 1.0.10 → 2.0.2
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 +6 -3
- package/SKILL.md +15 -36
- package/dist/index.js +869 -10
- package/dist/lib.js +880 -10
- package/manifest.json +229 -1
- package/package.json +18 -1
- package/src/lib.ts +15 -1
- package/src/server.ts +27 -3
- package/src/tools/classroom.ts +696 -0
- package/src/tools/drive.ts +17 -6
- package/src/tools/slides.ts +203 -0
- package/src/tools/utils.ts +9 -1
- package/tests/runner.test.ts +16 -0
- package/tests/server.test.ts +43 -0
- package/tests/tools/classroom.test.ts +875 -0
- package/tests/tools/drive.test.ts +49 -3
- package/tests/tools/slides.test.ts +441 -0
- package/tests/tools/utils.test.ts +65 -0
|
@@ -28,11 +28,40 @@ describe('gog_drive_ls', () => {
|
|
|
28
28
|
expect(runner.run).toHaveBeenCalledWith(['drive', 'ls'], { account: undefined });
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it('
|
|
31
|
+
it('passes folderId as --parent flag', async () => {
|
|
32
32
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
33
33
|
const handlers = setupHandlers();
|
|
34
34
|
await handlers.get('gog_drive_ls')!({ folderId: 'folder1' });
|
|
35
|
-
expect(runner.run).toHaveBeenCalledWith(['drive', 'ls', 'folder1'], { account: undefined });
|
|
35
|
+
expect(runner.run).toHaveBeenCalledWith(['drive', 'ls', '--parent=folder1'], { account: undefined });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('supports max, page, query, and allDrives flags', async () => {
|
|
39
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
40
|
+
const handlers = setupHandlers();
|
|
41
|
+
await handlers.get('gog_drive_ls')!({
|
|
42
|
+
folderId: 'folder1',
|
|
43
|
+
max: 50,
|
|
44
|
+
page: 'tok',
|
|
45
|
+
query: "name contains 'x'",
|
|
46
|
+
});
|
|
47
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
48
|
+
['drive', 'ls', '--parent=folder1', '--max=50', '--page=tok', "--query=name contains 'x'"],
|
|
49
|
+
{ account: undefined },
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('passes --no-all-drives when allDrives is false', async () => {
|
|
54
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
55
|
+
const handlers = setupHandlers();
|
|
56
|
+
await handlers.get('gog_drive_ls')!({ allDrives: false });
|
|
57
|
+
expect(runner.run).toHaveBeenCalledWith(['drive', 'ls', '--no-all-drives'], { account: undefined });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('omits all-drives flag when allDrives is true (default)', async () => {
|
|
61
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
62
|
+
const handlers = setupHandlers();
|
|
63
|
+
await handlers.get('gog_drive_ls')!({ allDrives: true });
|
|
64
|
+
expect(runner.run).toHaveBeenCalledWith(['drive', 'ls'], { account: undefined });
|
|
36
65
|
});
|
|
37
66
|
|
|
38
67
|
it('returns error text on failure', async () => {
|
|
@@ -124,13 +153,30 @@ describe('gog_drive_move', () => {
|
|
|
124
153
|
});
|
|
125
154
|
|
|
126
155
|
describe('gog_drive_delete', () => {
|
|
127
|
-
it('calls run with fileId', async () => {
|
|
156
|
+
it('calls run with fileId (trash by default)', async () => {
|
|
128
157
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
129
158
|
const handlers = setupHandlers();
|
|
130
159
|
await handlers.get('gog_drive_delete')!({ fileId: 'file1' });
|
|
131
160
|
expect(runner.run).toHaveBeenCalledWith(['drive', 'delete', 'file1'], { account: undefined });
|
|
132
161
|
});
|
|
133
162
|
|
|
163
|
+
it('appends --permanent when permanent=true', async () => {
|
|
164
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
165
|
+
const handlers = setupHandlers();
|
|
166
|
+
await handlers.get('gog_drive_delete')!({ fileId: 'file1', permanent: true });
|
|
167
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
168
|
+
['drive', 'delete', 'file1', '--permanent'],
|
|
169
|
+
{ account: undefined },
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('omits --permanent when permanent=false', async () => {
|
|
174
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
175
|
+
const handlers = setupHandlers();
|
|
176
|
+
await handlers.get('gog_drive_delete')!({ fileId: 'file1', permanent: false });
|
|
177
|
+
expect(runner.run).toHaveBeenCalledWith(['drive', 'delete', 'file1'], { account: undefined });
|
|
178
|
+
});
|
|
179
|
+
|
|
134
180
|
it('returns error text on failure', async () => {
|
|
135
181
|
vi.mocked(runner.run).mockRejectedValue(new Error('Delete failed'));
|
|
136
182
|
const handlers = setupHandlers();
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { registerSlidesTools } from '../../src/tools/slides.js';
|
|
4
|
+
import * as runner from '../../src/runner.js';
|
|
5
|
+
|
|
6
|
+
vi.mock('../../src/runner.js');
|
|
7
|
+
|
|
8
|
+
type ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;
|
|
9
|
+
|
|
10
|
+
function setupHandlers(): Map<string, ToolHandler> {
|
|
11
|
+
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
12
|
+
const handlers = new Map<string, ToolHandler>();
|
|
13
|
+
vi.spyOn(server, 'registerTool').mockImplementation((name, _config, cb) => {
|
|
14
|
+
handlers.set(name, cb as ToolHandler);
|
|
15
|
+
return undefined as never;
|
|
16
|
+
});
|
|
17
|
+
registerSlidesTools(server);
|
|
18
|
+
return handlers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => vi.clearAllMocks());
|
|
22
|
+
|
|
23
|
+
describe('gog_slides_export', () => {
|
|
24
|
+
it('calls run with presentationId', async () => {
|
|
25
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
26
|
+
const handlers = setupHandlers();
|
|
27
|
+
await handlers.get('gog_slides_export')!({ presentationId: 'p1' });
|
|
28
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'export', 'p1'], { account: undefined });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('passes --out and --format when provided', async () => {
|
|
32
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
33
|
+
const handlers = setupHandlers();
|
|
34
|
+
await handlers.get('gog_slides_export')!({ presentationId: 'p1', out: '/tmp/out.pdf', format: 'pdf' });
|
|
35
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
36
|
+
['slides', 'export', 'p1', '--out=/tmp/out.pdf', '--format=pdf'],
|
|
37
|
+
{ account: undefined },
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns error text on failure', async () => {
|
|
42
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Export failed'));
|
|
43
|
+
const handlers = setupHandlers();
|
|
44
|
+
const result = await handlers.get('gog_slides_export')!({ presentationId: 'bad' });
|
|
45
|
+
expect(result.content[0].text).toBe('Error: Export failed');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('gog_slides_info', () => {
|
|
50
|
+
it('calls run with presentationId', async () => {
|
|
51
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
52
|
+
const handlers = setupHandlers();
|
|
53
|
+
await handlers.get('gog_slides_info')!({ presentationId: 'p1' });
|
|
54
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'info', 'p1'], { account: undefined });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns error text on failure', async () => {
|
|
58
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Info failed'));
|
|
59
|
+
const handlers = setupHandlers();
|
|
60
|
+
const result = await handlers.get('gog_slides_info')!({ presentationId: 'bad' });
|
|
61
|
+
expect(result.content[0].text).toBe('Error: Info failed');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('gog_slides_create', () => {
|
|
66
|
+
it('calls run with title only', async () => {
|
|
67
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
68
|
+
const handlers = setupHandlers();
|
|
69
|
+
await handlers.get('gog_slides_create')!({ title: 'My Deck' });
|
|
70
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'create', 'My Deck'], { account: undefined });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('passes --parent and --template when provided', async () => {
|
|
74
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
75
|
+
const handlers = setupHandlers();
|
|
76
|
+
await handlers.get('gog_slides_create')!({ title: 'My Deck', parent: 'folder1', template: 'tpl1' });
|
|
77
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
78
|
+
['slides', 'create', 'My Deck', '--parent=folder1', '--template=tpl1'],
|
|
79
|
+
{ account: undefined },
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns error text on failure', async () => {
|
|
84
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Create failed'));
|
|
85
|
+
const handlers = setupHandlers();
|
|
86
|
+
const result = await handlers.get('gog_slides_create')!({ title: 'x' });
|
|
87
|
+
expect(result.content[0].text).toBe('Error: Create failed');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('gog_slides_create_from_markdown', () => {
|
|
92
|
+
it('calls run with title only', async () => {
|
|
93
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
94
|
+
const handlers = setupHandlers();
|
|
95
|
+
await handlers.get('gog_slides_create_from_markdown')!({ title: 'Deck' });
|
|
96
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
97
|
+
['slides', 'create-from-markdown', 'Deck'],
|
|
98
|
+
{ account: undefined },
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('passes all optional flags', async () => {
|
|
103
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
104
|
+
const handlers = setupHandlers();
|
|
105
|
+
await handlers.get('gog_slides_create_from_markdown')!({
|
|
106
|
+
title: 'Deck',
|
|
107
|
+
content: '# Slide 1',
|
|
108
|
+
contentFile: '/tmp/deck.md',
|
|
109
|
+
parent: 'folder1',
|
|
110
|
+
debug: true,
|
|
111
|
+
});
|
|
112
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
113
|
+
[
|
|
114
|
+
'slides', 'create-from-markdown', 'Deck',
|
|
115
|
+
'--content=# Slide 1',
|
|
116
|
+
'--content-file=/tmp/deck.md',
|
|
117
|
+
'--parent=folder1',
|
|
118
|
+
'--debug',
|
|
119
|
+
],
|
|
120
|
+
{ account: undefined },
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('omits --debug when false', async () => {
|
|
125
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
126
|
+
const handlers = setupHandlers();
|
|
127
|
+
await handlers.get('gog_slides_create_from_markdown')!({ title: 'Deck', debug: false });
|
|
128
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
129
|
+
['slides', 'create-from-markdown', 'Deck'],
|
|
130
|
+
{ account: undefined },
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns error text on failure', async () => {
|
|
135
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Markdown failed'));
|
|
136
|
+
const handlers = setupHandlers();
|
|
137
|
+
const result = await handlers.get('gog_slides_create_from_markdown')!({ title: 'x' });
|
|
138
|
+
expect(result.content[0].text).toBe('Error: Markdown failed');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('gog_slides_create_from_template', () => {
|
|
143
|
+
it('calls run with templateId and title only', async () => {
|
|
144
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
145
|
+
const handlers = setupHandlers();
|
|
146
|
+
await handlers.get('gog_slides_create_from_template')!({ templateId: 'tpl1', title: 'Deck' });
|
|
147
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
148
|
+
['slides', 'create-from-template', 'tpl1', 'Deck'],
|
|
149
|
+
{ account: undefined },
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('passes --replace for a single replacement entry', async () => {
|
|
154
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
155
|
+
const handlers = setupHandlers();
|
|
156
|
+
await handlers.get('gog_slides_create_from_template')!({
|
|
157
|
+
templateId: 'tpl1',
|
|
158
|
+
title: 'Deck',
|
|
159
|
+
replacements: { name: 'Alice' },
|
|
160
|
+
});
|
|
161
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
162
|
+
['slides', 'create-from-template', 'tpl1', 'Deck', '--replace=name=Alice'],
|
|
163
|
+
{ account: undefined },
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('passes --replace for each entry in replacements', async () => {
|
|
168
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
169
|
+
const handlers = setupHandlers();
|
|
170
|
+
await handlers.get('gog_slides_create_from_template')!({
|
|
171
|
+
templateId: 'tpl1',
|
|
172
|
+
title: 'Deck',
|
|
173
|
+
replacements: { name: 'Alice', company: 'Acme' },
|
|
174
|
+
});
|
|
175
|
+
const call = vi.mocked(runner.run).mock.calls[0]!;
|
|
176
|
+
expect(call[0]).toEqual(expect.arrayContaining(['--replace=name=Alice', '--replace=company=Acme']));
|
|
177
|
+
expect(call[1]).toEqual({ account: undefined });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('passes --replacements, --parent, and --exact', async () => {
|
|
181
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
182
|
+
const handlers = setupHandlers();
|
|
183
|
+
await handlers.get('gog_slides_create_from_template')!({
|
|
184
|
+
templateId: 'tpl1',
|
|
185
|
+
title: 'Deck',
|
|
186
|
+
replacementsFile: '/tmp/r.json',
|
|
187
|
+
parent: 'folder1',
|
|
188
|
+
exact: true,
|
|
189
|
+
});
|
|
190
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
191
|
+
[
|
|
192
|
+
'slides', 'create-from-template', 'tpl1', 'Deck',
|
|
193
|
+
'--replacements=/tmp/r.json',
|
|
194
|
+
'--parent=folder1',
|
|
195
|
+
'--exact',
|
|
196
|
+
],
|
|
197
|
+
{ account: undefined },
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('omits --exact when false', async () => {
|
|
202
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
203
|
+
const handlers = setupHandlers();
|
|
204
|
+
await handlers.get('gog_slides_create_from_template')!({ templateId: 'tpl1', title: 'Deck', exact: false });
|
|
205
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
206
|
+
['slides', 'create-from-template', 'tpl1', 'Deck'],
|
|
207
|
+
{ account: undefined },
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('returns error text on failure', async () => {
|
|
212
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Template failed'));
|
|
213
|
+
const handlers = setupHandlers();
|
|
214
|
+
const result = await handlers.get('gog_slides_create_from_template')!({ templateId: 't', title: 'x' });
|
|
215
|
+
expect(result.content[0].text).toBe('Error: Template failed');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('gog_slides_copy', () => {
|
|
220
|
+
it('calls run with presentationId and title', async () => {
|
|
221
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
222
|
+
const handlers = setupHandlers();
|
|
223
|
+
await handlers.get('gog_slides_copy')!({ presentationId: 'p1', title: 'Copy' });
|
|
224
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
225
|
+
['slides', 'copy', 'p1', 'Copy'],
|
|
226
|
+
{ account: undefined },
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('passes --parent when provided', async () => {
|
|
231
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
232
|
+
const handlers = setupHandlers();
|
|
233
|
+
await handlers.get('gog_slides_copy')!({ presentationId: 'p1', title: 'Copy', parent: 'folder1' });
|
|
234
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
235
|
+
['slides', 'copy', 'p1', 'Copy', '--parent=folder1'],
|
|
236
|
+
{ account: undefined },
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns error text on failure', async () => {
|
|
241
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Copy failed'));
|
|
242
|
+
const handlers = setupHandlers();
|
|
243
|
+
const result = await handlers.get('gog_slides_copy')!({ presentationId: 'p', title: 'x' });
|
|
244
|
+
expect(result.content[0].text).toBe('Error: Copy failed');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('gog_slides_add_slide', () => {
|
|
249
|
+
it('calls run with presentationId and image', async () => {
|
|
250
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
251
|
+
const handlers = setupHandlers();
|
|
252
|
+
await handlers.get('gog_slides_add_slide')!({ presentationId: 'p1', image: '/tmp/img.png' });
|
|
253
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
254
|
+
['slides', 'add-slide', 'p1', '/tmp/img.png'],
|
|
255
|
+
{ account: undefined },
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('passes --notes, --notes-file, and --before', async () => {
|
|
260
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
261
|
+
const handlers = setupHandlers();
|
|
262
|
+
await handlers.get('gog_slides_add_slide')!({
|
|
263
|
+
presentationId: 'p1',
|
|
264
|
+
image: '/tmp/img.png',
|
|
265
|
+
notes: 'Speaker note',
|
|
266
|
+
notesFile: '/tmp/notes.txt',
|
|
267
|
+
before: 'slide5',
|
|
268
|
+
});
|
|
269
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
270
|
+
[
|
|
271
|
+
'slides', 'add-slide', 'p1', '/tmp/img.png',
|
|
272
|
+
'--notes=Speaker note',
|
|
273
|
+
'--notes-file=/tmp/notes.txt',
|
|
274
|
+
'--before=slide5',
|
|
275
|
+
],
|
|
276
|
+
{ account: undefined },
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('returns error text on failure', async () => {
|
|
281
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Add failed'));
|
|
282
|
+
const handlers = setupHandlers();
|
|
283
|
+
const result = await handlers.get('gog_slides_add_slide')!({ presentationId: 'p', image: 'i' });
|
|
284
|
+
expect(result.content[0].text).toBe('Error: Add failed');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('gog_slides_list_slides', () => {
|
|
289
|
+
it('calls run with presentationId', async () => {
|
|
290
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
291
|
+
const handlers = setupHandlers();
|
|
292
|
+
await handlers.get('gog_slides_list_slides')!({ presentationId: 'p1' });
|
|
293
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'list-slides', 'p1'], { account: undefined });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('returns error text on failure', async () => {
|
|
297
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
|
|
298
|
+
const handlers = setupHandlers();
|
|
299
|
+
const result = await handlers.get('gog_slides_list_slides')!({ presentationId: 'bad' });
|
|
300
|
+
expect(result.content[0].text).toBe('Error: List failed');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('gog_slides_delete_slide', () => {
|
|
305
|
+
it('calls run with presentationId and slideId', async () => {
|
|
306
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
307
|
+
const handlers = setupHandlers();
|
|
308
|
+
await handlers.get('gog_slides_delete_slide')!({ presentationId: 'p1', slideId: 's1' });
|
|
309
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
310
|
+
['slides', 'delete-slide', 'p1', 's1'],
|
|
311
|
+
{ account: undefined },
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('returns error text on failure', async () => {
|
|
316
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Delete failed'));
|
|
317
|
+
const handlers = setupHandlers();
|
|
318
|
+
const result = await handlers.get('gog_slides_delete_slide')!({ presentationId: 'p', slideId: 's' });
|
|
319
|
+
expect(result.content[0].text).toBe('Error: Delete failed');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('gog_slides_read_slide', () => {
|
|
324
|
+
it('calls run with presentationId and slideId', async () => {
|
|
325
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
326
|
+
const handlers = setupHandlers();
|
|
327
|
+
await handlers.get('gog_slides_read_slide')!({ presentationId: 'p1', slideId: 's1' });
|
|
328
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
329
|
+
['slides', 'read-slide', 'p1', 's1'],
|
|
330
|
+
{ account: undefined },
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('returns error text on failure', async () => {
|
|
335
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Read failed'));
|
|
336
|
+
const handlers = setupHandlers();
|
|
337
|
+
const result = await handlers.get('gog_slides_read_slide')!({ presentationId: 'p', slideId: 's' });
|
|
338
|
+
expect(result.content[0].text).toBe('Error: Read failed');
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('gog_slides_update_notes', () => {
|
|
343
|
+
it('calls run with presentationId and slideId', async () => {
|
|
344
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
345
|
+
const handlers = setupHandlers();
|
|
346
|
+
await handlers.get('gog_slides_update_notes')!({ presentationId: 'p1', slideId: 's1' });
|
|
347
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
348
|
+
['slides', 'update-notes', 'p1', 's1'],
|
|
349
|
+
{ account: undefined },
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('passes --notes and --notes-file when provided', async () => {
|
|
354
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
355
|
+
const handlers = setupHandlers();
|
|
356
|
+
await handlers.get('gog_slides_update_notes')!({
|
|
357
|
+
presentationId: 'p1',
|
|
358
|
+
slideId: 's1',
|
|
359
|
+
notes: 'speak clearly',
|
|
360
|
+
notesFile: '/tmp/n.txt',
|
|
361
|
+
});
|
|
362
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
363
|
+
[
|
|
364
|
+
'slides', 'update-notes', 'p1', 's1',
|
|
365
|
+
'--notes=speak clearly',
|
|
366
|
+
'--notes-file=/tmp/n.txt',
|
|
367
|
+
],
|
|
368
|
+
{ account: undefined },
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('returns error text on failure', async () => {
|
|
373
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Notes failed'));
|
|
374
|
+
const handlers = setupHandlers();
|
|
375
|
+
const result = await handlers.get('gog_slides_update_notes')!({ presentationId: 'p', slideId: 's' });
|
|
376
|
+
expect(result.content[0].text).toBe('Error: Notes failed');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('gog_slides_replace_slide', () => {
|
|
381
|
+
it('calls run with presentationId, slideId, and image', async () => {
|
|
382
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
383
|
+
const handlers = setupHandlers();
|
|
384
|
+
await handlers.get('gog_slides_replace_slide')!({
|
|
385
|
+
presentationId: 'p1',
|
|
386
|
+
slideId: 's1',
|
|
387
|
+
image: '/tmp/img.png',
|
|
388
|
+
});
|
|
389
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
390
|
+
['slides', 'replace-slide', 'p1', 's1', '/tmp/img.png'],
|
|
391
|
+
{ account: undefined },
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('passes --notes and --notes-file when provided', async () => {
|
|
396
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
397
|
+
const handlers = setupHandlers();
|
|
398
|
+
await handlers.get('gog_slides_replace_slide')!({
|
|
399
|
+
presentationId: 'p1',
|
|
400
|
+
slideId: 's1',
|
|
401
|
+
image: '/tmp/img.png',
|
|
402
|
+
notes: 'updated',
|
|
403
|
+
notesFile: '/tmp/n.txt',
|
|
404
|
+
});
|
|
405
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
406
|
+
[
|
|
407
|
+
'slides', 'replace-slide', 'p1', 's1', '/tmp/img.png',
|
|
408
|
+
'--notes=updated',
|
|
409
|
+
'--notes-file=/tmp/n.txt',
|
|
410
|
+
],
|
|
411
|
+
{ account: undefined },
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('returns error text on failure', async () => {
|
|
416
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Replace failed'));
|
|
417
|
+
const handlers = setupHandlers();
|
|
418
|
+
const result = await handlers.get('gog_slides_replace_slide')!({
|
|
419
|
+
presentationId: 'p',
|
|
420
|
+
slideId: 's',
|
|
421
|
+
image: 'i',
|
|
422
|
+
});
|
|
423
|
+
expect(result.content[0].text).toBe('Error: Replace failed');
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe('gog_slides_run', () => {
|
|
428
|
+
it('passes subcommand and args to runner', async () => {
|
|
429
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
430
|
+
const handlers = setupHandlers();
|
|
431
|
+
await handlers.get('gog_slides_run')!({ subcommand: 'info', args: ['p1'] });
|
|
432
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'info', 'p1'], { account: undefined });
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('returns error text on failure', async () => {
|
|
436
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Run failed'));
|
|
437
|
+
const handlers = setupHandlers();
|
|
438
|
+
const result = await handlers.get('gog_slides_run')!({ subcommand: 'info', args: [] });
|
|
439
|
+
expect(result.content[0].text).toBe('Error: Run failed');
|
|
440
|
+
});
|
|
441
|
+
});
|
|
@@ -76,4 +76,69 @@ describe('runOrDiagnose', () => {
|
|
|
76
76
|
expect(result.content[0].text).toBe('Error: Doc not found');
|
|
77
77
|
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
it('appends transient-retry hint on 429 error', async () => {
|
|
81
|
+
vi.mocked(runner.run)
|
|
82
|
+
.mockRejectedValueOnce(new Error('Request failed with status 429'))
|
|
83
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
84
|
+
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
85
|
+
expect(result.content[0].text).toContain('transient');
|
|
86
|
+
expect(result.content[0].text).toContain('Retry');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('appends transient-retry hint on 500/502/503/504 errors', async () => {
|
|
90
|
+
for (const status of [500, 502, 503, 504]) {
|
|
91
|
+
vi.clearAllMocks();
|
|
92
|
+
vi.mocked(runner.run)
|
|
93
|
+
.mockRejectedValueOnce(new Error(`Request failed with status ${status}`))
|
|
94
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
95
|
+
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
96
|
+
expect(result.content[0].text, `status ${status}`).toContain('transient');
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('appends transient-retry hint on quota/rateLimit errors', async () => {
|
|
101
|
+
for (const msg of ['Quota exceeded', 'rateLimitExceeded', 'userRateLimitExceeded']) {
|
|
102
|
+
vi.clearAllMocks();
|
|
103
|
+
vi.mocked(runner.run)
|
|
104
|
+
.mockRejectedValueOnce(new Error(msg))
|
|
105
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
106
|
+
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
107
|
+
expect(result.content[0].text, msg).toContain('transient');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('appends transient-retry hint on DEADLINE_EXCEEDED error', async () => {
|
|
112
|
+
vi.mocked(runner.run)
|
|
113
|
+
.mockRejectedValueOnce(new Error('DEADLINE_EXCEEDED: context deadline exceeded'))
|
|
114
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
115
|
+
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
116
|
+
expect(result.content[0].text).toContain('transient');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('does not append transient hint on 404 error', async () => {
|
|
120
|
+
vi.mocked(runner.run)
|
|
121
|
+
.mockRejectedValueOnce(new Error('Request failed with status 404'))
|
|
122
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
123
|
+
const result = await runOrDiagnose(['sheets', 'get', 'abc', 'A1'], {});
|
|
124
|
+
expect(result.content[0].text).not.toContain('transient');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does not append transient hint on auth (401) error', async () => {
|
|
128
|
+
vi.mocked(runner.run)
|
|
129
|
+
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
130
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
131
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
132
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
133
|
+
expect(result.content[0].text).not.toContain('transient');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('keeps transient hint when auth list also fails', async () => {
|
|
137
|
+
vi.mocked(runner.run)
|
|
138
|
+
.mockRejectedValueOnce(new Error('Request failed with status 503'))
|
|
139
|
+
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
140
|
+
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
141
|
+
expect(result.content[0].text).toContain('transient');
|
|
142
|
+
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
143
|
+
});
|
|
79
144
|
});
|