gogcli-mcp 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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +20220 -19700
- package/dist/lib.js +15847 -23331
- package/manifest.json +33 -1
- package/package.json +7 -6
- package/server.json +2 -2
- package/src/connector-auth.ts +46 -0
- package/src/connector-runtime.ts +177 -0
- package/src/index.ts +7 -5
- package/src/lib.ts +8 -7
- package/src/runner.ts +225 -30
- package/src/server.ts +21 -25
- package/src/tools/api.ts +65 -0
- package/src/tools/auth.ts +112 -15
- package/src/tools/calendar.ts +24 -9
- package/src/tools/docs.ts +30 -3
- package/src/tools/drive.ts +124 -1
- package/src/tools/gmail.ts +7 -3
- package/src/tools/sheets.ts +6 -3
- package/src/tools/slides.ts +9 -4
- package/src/tools/tasks.ts +3 -1
- package/src/tools/utils.ts +163 -27
- package/src/worker.ts +99 -0
- package/tests/connector-auth.test.ts +28 -0
- package/tests/connector-runtime.test.ts +474 -0
- package/tests/runner-file-args-failure.test.ts +94 -0
- package/tests/runner-file-args.test.ts +232 -0
- package/tests/runner.test.ts +221 -13
- package/tests/server.test.ts +28 -28
- package/tests/tools/api.test.ts +107 -0
- package/tests/tools/auth.test.ts +187 -31
- package/tests/tools/calendar.test.ts +115 -52
- package/tests/tools/classroom.test.ts +77 -77
- package/tests/tools/contacts.test.ts +24 -24
- package/tests/tools/docs.test.ts +93 -44
- package/tests/tools/drive.test.ts +226 -55
- package/tests/tools/gmail.test.ts +61 -28
- package/tests/tools/sheets.test.ts +81 -70
- package/tests/tools/slides.test.ts +56 -36
- package/tests/tools/tasks.test.ts +33 -33
- package/tests/tools/utils.test.ts +116 -2
- package/tests/worker.test.ts +142 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +33 -2
- package/tests/helpers/test-harness.ts +0 -27
|
@@ -2,27 +2,27 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { registerSheetsTools } from '../../src/tools/sheets.js';
|
|
4
4
|
import * as runner from '../../src/runner.js';
|
|
5
|
-
import {
|
|
5
|
+
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
6
6
|
|
|
7
7
|
vi.mock('../../src/runner.js');
|
|
8
8
|
|
|
9
|
-
const setupHandlers = () =>
|
|
9
|
+
const setupHandlers = () => createTestHarness(registerSheetsTools);
|
|
10
10
|
|
|
11
11
|
beforeEach(() => vi.clearAllMocks());
|
|
12
12
|
|
|
13
13
|
describe('gog_sheets_get', () => {
|
|
14
14
|
it('calls run with correct args', async () => {
|
|
15
15
|
vi.mocked(runner.run).mockResolvedValue('{"values":[["a","b"]]}');
|
|
16
|
-
const
|
|
17
|
-
const result = await
|
|
16
|
+
const harness = await setupHandlers();
|
|
17
|
+
const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'sid', range: 'Sheet1!A1:B2' });
|
|
18
18
|
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', 'sid', 'Sheet1!A1:B2'], { account: undefined });
|
|
19
19
|
expect(result.content[0].text).toBe('{"values":[["a","b"]]}');
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it('forwards account override', async () => {
|
|
23
23
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
24
|
-
const
|
|
25
|
-
await
|
|
24
|
+
const harness = await setupHandlers();
|
|
25
|
+
await harness.callTool('gog_sheets_get', { spreadsheetId: 'sid', range: 'A1', account: 'other@gmail.com' });
|
|
26
26
|
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', 'sid', 'A1'], { account: 'other@gmail.com' });
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -30,15 +30,15 @@ describe('gog_sheets_get', () => {
|
|
|
30
30
|
vi.mocked(runner.run)
|
|
31
31
|
.mockRejectedValueOnce(new Error('Spreadsheet not found'))
|
|
32
32
|
.mockResolvedValueOnce('user@gmail.com');
|
|
33
|
-
const
|
|
34
|
-
const result = await
|
|
33
|
+
const harness = await setupHandlers();
|
|
34
|
+
const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'bad', range: 'A1' });
|
|
35
35
|
expect(result.content[0].text).toBe('Error: Spreadsheet not found\n\nConfigured accounts:\nuser@gmail.com');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it('returns plain error text when auth list also fails', async () => {
|
|
39
39
|
vi.mocked(runner.run).mockRejectedValue(new Error('Spreadsheet not found'));
|
|
40
|
-
const
|
|
41
|
-
const result = await
|
|
40
|
+
const harness = await setupHandlers();
|
|
41
|
+
const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'bad', range: 'A1' });
|
|
42
42
|
expect(result.content[0].text).toBe('Error: Spreadsheet not found');
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -46,8 +46,8 @@ describe('gog_sheets_get', () => {
|
|
|
46
46
|
vi.mocked(runner.run)
|
|
47
47
|
.mockRejectedValueOnce('raw error string')
|
|
48
48
|
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
49
|
-
const
|
|
50
|
-
const result = await
|
|
49
|
+
const harness = await setupHandlers();
|
|
50
|
+
const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'bad', range: 'A1' });
|
|
51
51
|
expect(result.content[0].text).toBe('raw error string');
|
|
52
52
|
});
|
|
53
53
|
});
|
|
@@ -55,9 +55,9 @@ describe('gog_sheets_get', () => {
|
|
|
55
55
|
describe('gog_sheets_update', () => {
|
|
56
56
|
it('passes values via --values-json flag', async () => {
|
|
57
57
|
vi.mocked(runner.run).mockResolvedValue('{"updatedCells":2}');
|
|
58
|
-
const
|
|
58
|
+
const harness = await setupHandlers();
|
|
59
59
|
const values = [['hello', 'world']];
|
|
60
|
-
await
|
|
60
|
+
await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1:B1', values });
|
|
61
61
|
expect(runner.run).toHaveBeenCalledWith(
|
|
62
62
|
['sheets', 'update', 'sid', 'A1:B1', `--values-json=${JSON.stringify(values)}`],
|
|
63
63
|
{ account: undefined },
|
|
@@ -66,9 +66,9 @@ describe('gog_sheets_update', () => {
|
|
|
66
66
|
|
|
67
67
|
it('preserves non-string cell types (numbers, booleans, nulls, formulas) in --values-json', async () => {
|
|
68
68
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
69
|
-
const
|
|
69
|
+
const harness = await setupHandlers();
|
|
70
70
|
const values = [['Sheet', 'A', 'B', 'C'], ['Row', 1, 2.5, '=A2+B2'], ['Bool', true, false, null]];
|
|
71
|
-
await
|
|
71
|
+
await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1:D3', values });
|
|
72
72
|
const call = vi.mocked(runner.run).mock.calls[0]!;
|
|
73
73
|
expect(call[0][4]).toBe(`--values-json=${JSON.stringify(values)}`);
|
|
74
74
|
// Spot-check the serialized JSON keeps non-string primitives intact (no stringification)
|
|
@@ -81,16 +81,16 @@ describe('gog_sheets_update', () => {
|
|
|
81
81
|
|
|
82
82
|
it('returns error text on failure', async () => {
|
|
83
83
|
vi.mocked(runner.run).mockRejectedValue(new Error('Update failed'));
|
|
84
|
-
const
|
|
85
|
-
const result = await
|
|
84
|
+
const harness = await setupHandlers();
|
|
85
|
+
const result = await harness.callTool('gog_sheets_update', { spreadsheetId: 'bad', range: 'A1', values: [['x']] });
|
|
86
86
|
expect(result.content[0].text).toBe('Error: Update failed');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
it('appends --dry-run when dry_run is true', async () => {
|
|
90
90
|
vi.mocked(runner.run).mockResolvedValue('{"dryRun":true}');
|
|
91
|
-
const
|
|
91
|
+
const harness = await setupHandlers();
|
|
92
92
|
const values = [['x']];
|
|
93
|
-
await
|
|
93
|
+
await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1', values, dry_run: true });
|
|
94
94
|
expect(runner.run).toHaveBeenCalledWith(
|
|
95
95
|
['sheets', 'update', 'sid', 'A1', `--values-json=${JSON.stringify(values)}`, '--dry-run'],
|
|
96
96
|
{ account: undefined },
|
|
@@ -99,17 +99,28 @@ describe('gog_sheets_update', () => {
|
|
|
99
99
|
|
|
100
100
|
it('omits --dry-run when dry_run is false or unset', async () => {
|
|
101
101
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
102
|
-
const
|
|
103
|
-
await
|
|
102
|
+
const harness = await setupHandlers();
|
|
103
|
+
await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1', values: [['x']], dry_run: false });
|
|
104
104
|
expect(vi.mocked(runner.run).mock.calls[0]![0]).not.toContain('--dry-run');
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
it('appends --fail-on-formula-error when fail_on_formula_error is true', async () => {
|
|
108
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
109
|
+
const harness = await setupHandlers();
|
|
110
|
+
const values = [['=1+1']];
|
|
111
|
+
await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1', values, fail_on_formula_error: true });
|
|
112
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
113
|
+
['sheets', 'update', 'sid', 'A1', `--values-json=${JSON.stringify(values)}`, '--fail-on-formula-error'],
|
|
114
|
+
{ account: undefined },
|
|
115
|
+
);
|
|
116
|
+
});
|
|
106
117
|
});
|
|
107
118
|
|
|
108
119
|
describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
109
120
|
it('aborts the write when the target range already holds data', async () => {
|
|
110
121
|
vi.mocked(runner.run).mockResolvedValueOnce('{"values":[["existing"]]}');
|
|
111
|
-
const
|
|
112
|
-
const result = await
|
|
122
|
+
const harness = await setupHandlers();
|
|
123
|
+
const result = await harness.callTool('gog_sheets_update', {
|
|
113
124
|
spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true,
|
|
114
125
|
});
|
|
115
126
|
// Only the read happened — no update call.
|
|
@@ -124,9 +135,9 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
124
135
|
vi.mocked(runner.run)
|
|
125
136
|
.mockResolvedValueOnce('{}')
|
|
126
137
|
.mockResolvedValueOnce('{"updatedCells":1}');
|
|
127
|
-
const
|
|
138
|
+
const harness = await setupHandlers();
|
|
128
139
|
const values = [['new']];
|
|
129
|
-
const result = await
|
|
140
|
+
const result = await harness.callTool('gog_sheets_update', {
|
|
130
141
|
spreadsheetId: 'sid', range: 'A1', values, fail_if_not_empty: true,
|
|
131
142
|
});
|
|
132
143
|
expect(runner.run).toHaveBeenCalledTimes(2);
|
|
@@ -140,8 +151,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
140
151
|
vi.mocked(runner.run)
|
|
141
152
|
.mockResolvedValueOnce('{}')
|
|
142
153
|
.mockResolvedValueOnce('{}');
|
|
143
|
-
const
|
|
144
|
-
await
|
|
154
|
+
const harness = await setupHandlers();
|
|
155
|
+
await harness.callTool('gog_sheets_update', {
|
|
145
156
|
spreadsheetId: 'sid', range: 'Sheet1!A1',
|
|
146
157
|
values: [['a', 'b'], ['c', 'd'], ['e', 'f']], fail_if_not_empty: true,
|
|
147
158
|
});
|
|
@@ -152,8 +163,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
152
163
|
vi.mocked(runner.run)
|
|
153
164
|
.mockRejectedValueOnce(new Error('read boom')) // the verification get
|
|
154
165
|
.mockResolvedValueOnce('{"accounts":[{"email":"u@x.com"}]}'); // diagnose -> auth list
|
|
155
|
-
const
|
|
156
|
-
const result = await
|
|
166
|
+
const harness = await setupHandlers();
|
|
167
|
+
const result = await harness.callTool('gog_sheets_update', {
|
|
157
168
|
spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true,
|
|
158
169
|
});
|
|
159
170
|
// get + auth list (for diagnosis), but the update is never attempted
|
|
@@ -167,8 +178,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
167
178
|
vi.mocked(runner.run)
|
|
168
179
|
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
169
180
|
.mockResolvedValueOnce('{"accounts":[{"email":"u@x.com"}]}');
|
|
170
|
-
const
|
|
171
|
-
const result = await
|
|
181
|
+
const harness = await setupHandlers();
|
|
182
|
+
const result = await harness.callTool('gog_sheets_update', {
|
|
172
183
|
spreadsheetId: 'sid', range: 'A1', values: [['x']], fail_if_not_empty: true,
|
|
173
184
|
});
|
|
174
185
|
expect(result.content[0].text).toContain('gog_auth_add');
|
|
@@ -176,8 +187,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
176
187
|
|
|
177
188
|
it('aborts when emptiness cannot be verified from unparseable output', async () => {
|
|
178
189
|
vi.mocked(runner.run).mockResolvedValueOnce('not json');
|
|
179
|
-
const
|
|
180
|
-
const result = await
|
|
190
|
+
const harness = await setupHandlers();
|
|
191
|
+
const result = await harness.callTool('gog_sheets_update', {
|
|
181
192
|
spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true,
|
|
182
193
|
});
|
|
183
194
|
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
@@ -188,8 +199,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
188
199
|
vi.mocked(runner.run)
|
|
189
200
|
.mockResolvedValueOnce('{}')
|
|
190
201
|
.mockResolvedValueOnce('{"dryRun":true}');
|
|
191
|
-
const
|
|
192
|
-
await
|
|
202
|
+
const harness = await setupHandlers();
|
|
203
|
+
await harness.callTool('gog_sheets_update', {
|
|
193
204
|
spreadsheetId: 'sid', range: 'A1', values: [['new']], fail_if_not_empty: true, dry_run: true,
|
|
194
205
|
});
|
|
195
206
|
expect(vi.mocked(runner.run).mock.calls[1]![0]).toContain('--dry-run');
|
|
@@ -197,8 +208,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
197
208
|
|
|
198
209
|
it('skips the guard read when values has no rows', async () => {
|
|
199
210
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
200
|
-
const
|
|
201
|
-
await
|
|
211
|
+
const harness = await setupHandlers();
|
|
212
|
+
await harness.callTool('gog_sheets_update', {
|
|
202
213
|
spreadsheetId: 'sid', range: 'A1', values: [], fail_if_not_empty: true,
|
|
203
214
|
});
|
|
204
215
|
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
@@ -207,8 +218,8 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
207
218
|
|
|
208
219
|
it('skips the guard read when rows have no columns', async () => {
|
|
209
220
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
210
|
-
const
|
|
211
|
-
await
|
|
221
|
+
const harness = await setupHandlers();
|
|
222
|
+
await harness.callTool('gog_sheets_update', {
|
|
212
223
|
spreadsheetId: 'sid', range: 'A1', values: [[]], fail_if_not_empty: true,
|
|
213
224
|
});
|
|
214
225
|
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
@@ -219,9 +230,9 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
219
230
|
describe('gog_sheets_append', () => {
|
|
220
231
|
it('passes values via --values-json flag', async () => {
|
|
221
232
|
vi.mocked(runner.run).mockResolvedValue('{"updates":{}}');
|
|
222
|
-
const
|
|
233
|
+
const harness = await setupHandlers();
|
|
223
234
|
const values = [['r1c1', 'r1c2'], ['r2c1', 'r2c2']];
|
|
224
|
-
await
|
|
235
|
+
await harness.callTool('gog_sheets_append', { spreadsheetId: 'sid', range: 'Sheet1!A:B', values });
|
|
225
236
|
expect(runner.run).toHaveBeenCalledWith(
|
|
226
237
|
['sheets', 'append', 'sid', 'Sheet1!A:B', `--values-json=${JSON.stringify(values)}`],
|
|
227
238
|
{ account: undefined },
|
|
@@ -230,16 +241,16 @@ describe('gog_sheets_append', () => {
|
|
|
230
241
|
|
|
231
242
|
it('returns error text on failure', async () => {
|
|
232
243
|
vi.mocked(runner.run).mockRejectedValue(new Error('Append failed'));
|
|
233
|
-
const
|
|
234
|
-
const result = await
|
|
244
|
+
const harness = await setupHandlers();
|
|
245
|
+
const result = await harness.callTool('gog_sheets_append', { spreadsheetId: 'bad', range: 'A1', values: [['x']] });
|
|
235
246
|
expect(result.content[0].text).toBe('Error: Append failed');
|
|
236
247
|
});
|
|
237
248
|
|
|
238
249
|
it('appends --dry-run when dry_run is true', async () => {
|
|
239
250
|
vi.mocked(runner.run).mockResolvedValue('{"dryRun":true}');
|
|
240
|
-
const
|
|
251
|
+
const harness = await setupHandlers();
|
|
241
252
|
const values = [['x']];
|
|
242
|
-
await
|
|
253
|
+
await harness.callTool('gog_sheets_append', { spreadsheetId: 'sid', range: 'Sheet1!A:A', values, dry_run: true });
|
|
243
254
|
expect(runner.run).toHaveBeenCalledWith(
|
|
244
255
|
['sheets', 'append', 'sid', 'Sheet1!A:A', `--values-json=${JSON.stringify(values)}`, '--dry-run'],
|
|
245
256
|
{ account: undefined },
|
|
@@ -250,22 +261,22 @@ describe('gog_sheets_append', () => {
|
|
|
250
261
|
describe('gog_sheets_clear', () => {
|
|
251
262
|
it('calls run with correct args', async () => {
|
|
252
263
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
253
|
-
const
|
|
254
|
-
await
|
|
264
|
+
const harness = await setupHandlers();
|
|
265
|
+
await harness.callTool('gog_sheets_clear', { spreadsheetId: 'sid', range: 'Sheet1!A1:Z100' });
|
|
255
266
|
expect(runner.run).toHaveBeenCalledWith(['sheets', 'clear', 'sid', 'Sheet1!A1:Z100'], { account: undefined });
|
|
256
267
|
});
|
|
257
268
|
|
|
258
269
|
it('returns error text on failure', async () => {
|
|
259
270
|
vi.mocked(runner.run).mockRejectedValue(new Error('Clear failed'));
|
|
260
|
-
const
|
|
261
|
-
const result = await
|
|
271
|
+
const harness = await setupHandlers();
|
|
272
|
+
const result = await harness.callTool('gog_sheets_clear', { spreadsheetId: 'bad', range: 'A1' });
|
|
262
273
|
expect(result.content[0].text).toBe('Error: Clear failed');
|
|
263
274
|
});
|
|
264
275
|
|
|
265
276
|
it('appends --dry-run when dry_run is true', async () => {
|
|
266
277
|
vi.mocked(runner.run).mockResolvedValue('{"dryRun":true}');
|
|
267
|
-
const
|
|
268
|
-
await
|
|
278
|
+
const harness = await setupHandlers();
|
|
279
|
+
await harness.callTool('gog_sheets_clear', { spreadsheetId: 'sid', range: 'Sheet1!A1:Z100', dry_run: true });
|
|
269
280
|
expect(runner.run).toHaveBeenCalledWith(
|
|
270
281
|
['sheets', 'clear', 'sid', 'Sheet1!A1:Z100', '--dry-run'],
|
|
271
282
|
{ account: undefined },
|
|
@@ -287,16 +298,16 @@ describe('gog_sheets_metadata', () => {
|
|
|
287
298
|
|
|
288
299
|
it('calls run with spreadsheetId only', async () => {
|
|
289
300
|
vi.mocked(runner.run).mockResolvedValue('{"title":"My Sheet","sheets":[{"title":"Sheet1"}]}');
|
|
290
|
-
const
|
|
291
|
-
const result = await
|
|
301
|
+
const harness = await setupHandlers();
|
|
302
|
+
const result = await harness.callTool('gog_sheets_metadata', { spreadsheetId: 'sid' });
|
|
292
303
|
expect(runner.run).toHaveBeenCalledWith(['sheets', 'metadata', 'sid'], { account: undefined });
|
|
293
304
|
expect(result.content[0].text).toContain('My Sheet');
|
|
294
305
|
});
|
|
295
306
|
|
|
296
307
|
it('returns error text on failure', async () => {
|
|
297
308
|
vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
|
|
298
|
-
const
|
|
299
|
-
const result = await
|
|
309
|
+
const harness = await setupHandlers();
|
|
310
|
+
const result = await harness.callTool('gog_sheets_metadata', { spreadsheetId: 'bad' });
|
|
300
311
|
expect(result.content[0].text).toBe('Error: Not found');
|
|
301
312
|
});
|
|
302
313
|
});
|
|
@@ -304,15 +315,15 @@ describe('gog_sheets_metadata', () => {
|
|
|
304
315
|
describe('gog_sheets_create', () => {
|
|
305
316
|
it('calls run with title', async () => {
|
|
306
317
|
vi.mocked(runner.run).mockResolvedValue('{"spreadsheetId":"newid","title":"Budget 2026"}');
|
|
307
|
-
const
|
|
308
|
-
await
|
|
318
|
+
const harness = await setupHandlers();
|
|
319
|
+
await harness.callTool('gog_sheets_create', { title: 'Budget 2026' });
|
|
309
320
|
expect(runner.run).toHaveBeenCalledWith(['sheets', 'create', 'Budget 2026'], { account: undefined });
|
|
310
321
|
});
|
|
311
322
|
|
|
312
323
|
it('returns error text on failure', async () => {
|
|
313
324
|
vi.mocked(runner.run).mockRejectedValue(new Error('Create failed'));
|
|
314
|
-
const
|
|
315
|
-
const result = await
|
|
325
|
+
const harness = await setupHandlers();
|
|
326
|
+
const result = await harness.callTool('gog_sheets_create', { title: 'Bad' });
|
|
316
327
|
expect(result.content[0].text).toBe('Error: Create failed');
|
|
317
328
|
});
|
|
318
329
|
});
|
|
@@ -320,15 +331,15 @@ describe('gog_sheets_create', () => {
|
|
|
320
331
|
describe('gog_sheets_find_replace', () => {
|
|
321
332
|
it('calls run with find and replace args', async () => {
|
|
322
333
|
vi.mocked(runner.run).mockResolvedValue('{"occurrencesChanged":3}');
|
|
323
|
-
const
|
|
324
|
-
await
|
|
334
|
+
const harness = await setupHandlers();
|
|
335
|
+
await harness.callTool('gog_sheets_find_replace', { spreadsheetId: 'sid', find: 'foo', replace: 'bar' });
|
|
325
336
|
expect(runner.run).toHaveBeenCalledWith(['sheets', 'find-replace', 'sid', 'foo', 'bar'], { account: undefined });
|
|
326
337
|
});
|
|
327
338
|
|
|
328
339
|
it('returns error text on failure', async () => {
|
|
329
340
|
vi.mocked(runner.run).mockRejectedValue(new Error('Replace failed'));
|
|
330
|
-
const
|
|
331
|
-
const result = await
|
|
341
|
+
const harness = await setupHandlers();
|
|
342
|
+
const result = await harness.callTool('gog_sheets_find_replace', { spreadsheetId: 'bad', find: 'x', replace: 'y' });
|
|
332
343
|
expect(result.content[0].text).toBe('Error: Replace failed');
|
|
333
344
|
});
|
|
334
345
|
});
|
|
@@ -336,8 +347,8 @@ describe('gog_sheets_find_replace', () => {
|
|
|
336
347
|
describe('gog_sheets_run', () => {
|
|
337
348
|
it('passes raw subcommand and args to runner', async () => {
|
|
338
349
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
339
|
-
const
|
|
340
|
-
await
|
|
350
|
+
const harness = await setupHandlers();
|
|
351
|
+
await harness.callTool('gog_sheets_run', {
|
|
341
352
|
subcommand: 'freeze',
|
|
342
353
|
args: ['sid', '--rows=1'],
|
|
343
354
|
});
|
|
@@ -346,15 +357,15 @@ describe('gog_sheets_run', () => {
|
|
|
346
357
|
|
|
347
358
|
it('works with empty args array', async () => {
|
|
348
359
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
349
|
-
const
|
|
350
|
-
await
|
|
360
|
+
const harness = await setupHandlers();
|
|
361
|
+
await harness.callTool('gog_sheets_run', { subcommand: 'metadata', args: [] });
|
|
351
362
|
expect(runner.run).toHaveBeenCalledWith(['sheets', 'metadata'], { account: undefined });
|
|
352
363
|
});
|
|
353
364
|
|
|
354
365
|
it('returns error text on failure', async () => {
|
|
355
366
|
vi.mocked(runner.run).mockRejectedValue(new Error('Run failed'));
|
|
356
|
-
const
|
|
357
|
-
const result = await
|
|
367
|
+
const harness = await setupHandlers();
|
|
368
|
+
const result = await harness.callTool('gog_sheets_run', { subcommand: 'freeze', args: [] });
|
|
358
369
|
expect(result.content[0].text).toBe('Error: Run failed');
|
|
359
370
|
});
|
|
360
371
|
});
|
|
@@ -1,36 +1,46 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { registerSlidesTools } from '../../src/tools/slides.js';
|
|
3
3
|
import * as runner from '../../src/runner.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
5
5
|
|
|
6
6
|
vi.mock('../../src/runner.js');
|
|
7
7
|
|
|
8
|
-
const setupHandlers = () =>
|
|
8
|
+
const setupHandlers = () => createTestHarness(registerSlidesTools);
|
|
9
9
|
|
|
10
10
|
beforeEach(() => vi.clearAllMocks());
|
|
11
11
|
|
|
12
12
|
describe('gog_slides_export', () => {
|
|
13
13
|
it('calls run with presentationId', async () => {
|
|
14
14
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
15
|
-
const
|
|
16
|
-
await
|
|
15
|
+
const harness = await setupHandlers();
|
|
16
|
+
await harness.callTool('gog_slides_export', { presentationId: 'p1' });
|
|
17
17
|
expect(runner.run).toHaveBeenCalledWith(['slides', 'export', 'p1'], { account: undefined });
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it('passes --out and --format when provided', async () => {
|
|
21
21
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
22
|
-
const
|
|
23
|
-
await
|
|
22
|
+
const harness = await setupHandlers();
|
|
23
|
+
await harness.callTool('gog_slides_export', { presentationId: 'p1', out: '/tmp/deck.pdf', format: 'pdf' });
|
|
24
24
|
expect(runner.run).toHaveBeenCalledWith(
|
|
25
25
|
['slides', 'export', 'p1', '--out=/tmp/deck.pdf', '--format=pdf'],
|
|
26
26
|
{ account: undefined },
|
|
27
27
|
);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it('passes --overwrite when requested', async () => {
|
|
31
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
32
|
+
const harness = await setupHandlers();
|
|
33
|
+
await harness.callTool('gog_slides_export', { presentationId: 'p1', out: '/tmp/deck.pptx', overwrite: true });
|
|
34
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
35
|
+
['slides', 'export', 'p1', '--out=/tmp/deck.pptx', '--overwrite'],
|
|
36
|
+
{ account: undefined },
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
30
40
|
it('returns error text on failure', async () => {
|
|
31
41
|
vi.mocked(runner.run).mockRejectedValue(new Error('Export failed'));
|
|
32
|
-
const
|
|
33
|
-
const result = await
|
|
42
|
+
const harness = await setupHandlers();
|
|
43
|
+
const result = await harness.callTool('gog_slides_export', { presentationId: 'bad' });
|
|
34
44
|
expect(result.content[0].text).toBe('Error: Export failed');
|
|
35
45
|
});
|
|
36
46
|
});
|
|
@@ -38,15 +48,15 @@ describe('gog_slides_export', () => {
|
|
|
38
48
|
describe('gog_slides_info', () => {
|
|
39
49
|
it('calls run with presentationId', async () => {
|
|
40
50
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
41
|
-
const
|
|
42
|
-
await
|
|
51
|
+
const harness = await setupHandlers();
|
|
52
|
+
await harness.callTool('gog_slides_info', { presentationId: 'p1' });
|
|
43
53
|
expect(runner.run).toHaveBeenCalledWith(['slides', 'info', 'p1'], { account: undefined });
|
|
44
54
|
});
|
|
45
55
|
|
|
46
56
|
it('returns error text on failure', async () => {
|
|
47
57
|
vi.mocked(runner.run).mockRejectedValue(new Error('Info failed'));
|
|
48
|
-
const
|
|
49
|
-
const result = await
|
|
58
|
+
const harness = await setupHandlers();
|
|
59
|
+
const result = await harness.callTool('gog_slides_info', { presentationId: 'bad' });
|
|
50
60
|
expect(result.content[0].text).toBe('Error: Info failed');
|
|
51
61
|
});
|
|
52
62
|
});
|
|
@@ -54,15 +64,15 @@ describe('gog_slides_info', () => {
|
|
|
54
64
|
describe('gog_slides_create', () => {
|
|
55
65
|
it('calls run with title only', async () => {
|
|
56
66
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
57
|
-
const
|
|
58
|
-
await
|
|
67
|
+
const harness = await setupHandlers();
|
|
68
|
+
await harness.callTool('gog_slides_create', { title: 'Deck' });
|
|
59
69
|
expect(runner.run).toHaveBeenCalledWith(['slides', 'create', 'Deck'], { account: undefined });
|
|
60
70
|
});
|
|
61
71
|
|
|
62
72
|
it('passes --parent and --template when provided', async () => {
|
|
63
73
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
64
|
-
const
|
|
65
|
-
await
|
|
74
|
+
const harness = await setupHandlers();
|
|
75
|
+
await harness.callTool('gog_slides_create', { title: 'Deck', parent: 'folder1', template: 'tpl1' });
|
|
66
76
|
expect(runner.run).toHaveBeenCalledWith(
|
|
67
77
|
['slides', 'create', 'Deck', '--parent=folder1', '--template=tpl1'],
|
|
68
78
|
{ account: undefined },
|
|
@@ -71,8 +81,8 @@ describe('gog_slides_create', () => {
|
|
|
71
81
|
|
|
72
82
|
it('returns error text on failure', async () => {
|
|
73
83
|
vi.mocked(runner.run).mockRejectedValue(new Error('Create failed'));
|
|
74
|
-
const
|
|
75
|
-
const result = await
|
|
84
|
+
const harness = await setupHandlers();
|
|
85
|
+
const result = await harness.callTool('gog_slides_create', { title: 'x' });
|
|
76
86
|
expect(result.content[0].text).toBe('Error: Create failed');
|
|
77
87
|
});
|
|
78
88
|
});
|
|
@@ -80,8 +90,8 @@ describe('gog_slides_create', () => {
|
|
|
80
90
|
describe('gog_slides_copy', () => {
|
|
81
91
|
it('calls run with presentationId and title', async () => {
|
|
82
92
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
83
|
-
const
|
|
84
|
-
await
|
|
93
|
+
const harness = await setupHandlers();
|
|
94
|
+
await harness.callTool('gog_slides_copy', { presentationId: 'p1', title: 'Copy' });
|
|
85
95
|
expect(runner.run).toHaveBeenCalledWith(
|
|
86
96
|
['slides', 'copy', 'p1', 'Copy'],
|
|
87
97
|
{ account: undefined },
|
|
@@ -90,8 +100,8 @@ describe('gog_slides_copy', () => {
|
|
|
90
100
|
|
|
91
101
|
it('passes --parent when provided', async () => {
|
|
92
102
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
93
|
-
const
|
|
94
|
-
await
|
|
103
|
+
const harness = await setupHandlers();
|
|
104
|
+
await harness.callTool('gog_slides_copy', { presentationId: 'p1', title: 'Copy', parent: 'folder1' });
|
|
95
105
|
expect(runner.run).toHaveBeenCalledWith(
|
|
96
106
|
['slides', 'copy', 'p1', 'Copy', '--parent=folder1'],
|
|
97
107
|
{ account: undefined },
|
|
@@ -100,8 +110,8 @@ describe('gog_slides_copy', () => {
|
|
|
100
110
|
|
|
101
111
|
it('returns error text on failure', async () => {
|
|
102
112
|
vi.mocked(runner.run).mockRejectedValue(new Error('Copy failed'));
|
|
103
|
-
const
|
|
104
|
-
const result = await
|
|
113
|
+
const harness = await setupHandlers();
|
|
114
|
+
const result = await harness.callTool('gog_slides_copy', { presentationId: 'p', title: 'x' });
|
|
105
115
|
expect(result.content[0].text).toBe('Error: Copy failed');
|
|
106
116
|
});
|
|
107
117
|
});
|
|
@@ -109,15 +119,15 @@ describe('gog_slides_copy', () => {
|
|
|
109
119
|
describe('gog_slides_list_slides', () => {
|
|
110
120
|
it('calls run with presentationId', async () => {
|
|
111
121
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
112
|
-
const
|
|
113
|
-
await
|
|
122
|
+
const harness = await setupHandlers();
|
|
123
|
+
await harness.callTool('gog_slides_list_slides', { presentationId: 'p1' });
|
|
114
124
|
expect(runner.run).toHaveBeenCalledWith(['slides', 'list-slides', 'p1'], { account: undefined });
|
|
115
125
|
});
|
|
116
126
|
|
|
117
127
|
it('returns error text on failure', async () => {
|
|
118
128
|
vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
|
|
119
|
-
const
|
|
120
|
-
const result = await
|
|
129
|
+
const harness = await setupHandlers();
|
|
130
|
+
const result = await harness.callTool('gog_slides_list_slides', { presentationId: 'bad' });
|
|
121
131
|
expect(result.content[0].text).toBe('Error: List failed');
|
|
122
132
|
});
|
|
123
133
|
});
|
|
@@ -125,18 +135,28 @@ describe('gog_slides_list_slides', () => {
|
|
|
125
135
|
describe('gog_slides_read_slide', () => {
|
|
126
136
|
it('calls run with presentationId and slideId', async () => {
|
|
127
137
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
128
|
-
const
|
|
129
|
-
await
|
|
138
|
+
const harness = await setupHandlers();
|
|
139
|
+
await harness.callTool('gog_slides_read_slide', { presentationId: 'p1', slideId: 's1' });
|
|
130
140
|
expect(runner.run).toHaveBeenCalledWith(
|
|
131
141
|
['slides', 'read-slide', 'p1', 's1'],
|
|
132
142
|
{ account: undefined },
|
|
133
143
|
);
|
|
134
144
|
});
|
|
135
145
|
|
|
146
|
+
it('adds --detail when detail is true', async () => {
|
|
147
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
148
|
+
const harness = await setupHandlers();
|
|
149
|
+
await harness.callTool('gog_slides_read_slide', { presentationId: 'p1', slideId: 's1', detail: true });
|
|
150
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
151
|
+
['slides', 'read-slide', 'p1', 's1', '--detail'],
|
|
152
|
+
{ account: undefined },
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
136
156
|
it('returns error text on failure', async () => {
|
|
137
157
|
vi.mocked(runner.run).mockRejectedValue(new Error('Read failed'));
|
|
138
|
-
const
|
|
139
|
-
const result = await
|
|
158
|
+
const harness = await setupHandlers();
|
|
159
|
+
const result = await harness.callTool('gog_slides_read_slide', { presentationId: 'p', slideId: 's' });
|
|
140
160
|
expect(result.content[0].text).toBe('Error: Read failed');
|
|
141
161
|
});
|
|
142
162
|
});
|
|
@@ -144,15 +164,15 @@ describe('gog_slides_read_slide', () => {
|
|
|
144
164
|
describe('gog_slides_run', () => {
|
|
145
165
|
it('passes subcommand and args to runner', async () => {
|
|
146
166
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
147
|
-
const
|
|
148
|
-
await
|
|
167
|
+
const harness = await setupHandlers();
|
|
168
|
+
await harness.callTool('gog_slides_run', { subcommand: 'info', args: ['p1'] });
|
|
149
169
|
expect(runner.run).toHaveBeenCalledWith(['slides', 'info', 'p1'], { account: undefined });
|
|
150
170
|
});
|
|
151
171
|
|
|
152
172
|
it('returns error text on failure', async () => {
|
|
153
173
|
vi.mocked(runner.run).mockRejectedValue(new Error('Run failed'));
|
|
154
|
-
const
|
|
155
|
-
const result = await
|
|
174
|
+
const harness = await setupHandlers();
|
|
175
|
+
const result = await harness.callTool('gog_slides_run', { subcommand: 'info', args: [] });
|
|
156
176
|
expect(result.content[0].text).toBe('Error: Run failed');
|
|
157
177
|
});
|
|
158
178
|
});
|