gogcli-mcp 4.2.4 → 4.3.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 +714 -216
- package/dist/lib.js +751 -217
- package/manifest.json +9 -2
- package/mint.yaml +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/arg-guard.ts +68 -0
- package/src/argv.ts +27 -0
- package/src/attachment-root.ts +111 -0
- package/src/attachments.ts +1 -0
- package/src/blob-upload.ts +4 -2
- package/src/file-roots.ts +66 -0
- package/src/gmail-dispatch-guard.ts +56 -16
- package/src/gmail-results.ts +21 -2
- package/src/lib.ts +6 -0
- package/src/run-path-guard.ts +118 -0
- package/src/runner.ts +87 -19
- package/src/tools/api.ts +39 -7
- package/src/tools/appscript.ts +12 -8
- package/src/tools/auth.ts +13 -3
- package/src/tools/calendar.ts +10 -8
- package/src/tools/chat.ts +16 -13
- package/src/tools/classroom.ts +27 -25
- package/src/tools/contacts.ts +5 -3
- package/src/tools/docs.ts +8 -6
- package/src/tools/drive.ts +42 -18
- package/src/tools/gmail.ts +71 -11
- package/src/tools/sheets.ts +10 -8
- package/src/tools/slides.ts +14 -9
- package/src/tools/tasks.ts +7 -5
- package/src/tools/utils.ts +52 -6
- package/tests/arg-guard.test.ts +80 -0
- package/tests/attachment-root.test.ts +130 -0
- package/tests/attachments.test.ts +8 -0
- package/tests/blob-upload.test.ts +4 -3
- package/tests/file-roots.test.ts +101 -0
- package/tests/gmail-dispatch-guard.test.ts +46 -0
- package/tests/gmail-results.test.ts +35 -1
- package/tests/run-path-guard.test.ts +142 -0
- package/tests/runner.test.ts +136 -0
- package/tests/tools/api.test.ts +74 -8
- package/tests/tools/appscript.test.ts +34 -8
- package/tests/tools/auth.test.ts +44 -17
- package/tests/tools/calendar.test.ts +29 -27
- package/tests/tools/chat.test.ts +46 -20
- package/tests/tools/classroom.test.ts +39 -38
- package/tests/tools/contacts.test.ts +3 -2
- package/tests/tools/docs.test.ts +44 -15
- package/tests/tools/drive.test.ts +106 -19
- package/tests/tools/gmail.test.ts +227 -29
- package/tests/tools/run-tool-examples.test.ts +69 -0
- package/tests/tools/sheets.test.ts +16 -15
- package/tests/tools/slides.test.ts +47 -11
- package/tests/tools/tasks.test.ts +7 -6
- package/tests/tools/utils.test.ts +32 -31
- package/vitest.config.ts +5 -0
|
@@ -3,6 +3,7 @@ import { McpServer } from '@modelcontextprotocol/server';
|
|
|
3
3
|
import { registerSheetsTools } from '../../src/tools/sheets.js';
|
|
4
4
|
import * as runner from '../../src/runner.js';
|
|
5
5
|
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
6
|
+
import { pos } from '../../src/argv.js';
|
|
6
7
|
|
|
7
8
|
vi.mock('../../src/runner.js');
|
|
8
9
|
|
|
@@ -15,7 +16,7 @@ describe('gog_sheets_get', () => {
|
|
|
15
16
|
vi.mocked(runner.run).mockResolvedValue('{"values":[["a","b"]]}');
|
|
16
17
|
const harness = await setupHandlers();
|
|
17
18
|
const result = await harness.callTool('gog_sheets_get', { spreadsheetId: 'sid', range: 'Sheet1!A1:B2' });
|
|
18
|
-
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', 'sid', 'Sheet1!A1:B2'], { account: undefined });
|
|
19
|
+
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', pos('sid'), pos('Sheet1!A1:B2')], { account: undefined });
|
|
19
20
|
expect(result.content[0].text).toBe('{"values":[["a","b"]]}');
|
|
20
21
|
});
|
|
21
22
|
|
|
@@ -23,7 +24,7 @@ describe('gog_sheets_get', () => {
|
|
|
23
24
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
24
25
|
const harness = await setupHandlers();
|
|
25
26
|
await harness.callTool('gog_sheets_get', { spreadsheetId: 'sid', range: 'A1', account: 'other@gmail.com' });
|
|
26
|
-
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', 'sid', 'A1'], { account: 'other@gmail.com' });
|
|
27
|
+
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', pos('sid'), pos('A1')], { account: 'other@gmail.com' });
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
it('appends auth list on failure when auth list succeeds', async () => {
|
|
@@ -59,7 +60,7 @@ describe('gog_sheets_update', () => {
|
|
|
59
60
|
const values = [['hello', 'world']];
|
|
60
61
|
await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1:B1', values });
|
|
61
62
|
expect(runner.run).toHaveBeenCalledWith(
|
|
62
|
-
['sheets', 'update', 'sid', 'A1:B1', `--values-json=${JSON.stringify(values)}`],
|
|
63
|
+
['sheets', 'update', pos('sid'), pos('A1:B1'), `--values-json=${JSON.stringify(values)}`],
|
|
63
64
|
{ account: undefined },
|
|
64
65
|
);
|
|
65
66
|
});
|
|
@@ -92,7 +93,7 @@ describe('gog_sheets_update', () => {
|
|
|
92
93
|
const values = [['x']];
|
|
93
94
|
await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1', values, dry_run: true });
|
|
94
95
|
expect(runner.run).toHaveBeenCalledWith(
|
|
95
|
-
['sheets', 'update', 'sid', 'A1', `--values-json=${JSON.stringify(values)}`, '--dry-run'],
|
|
96
|
+
['sheets', 'update', pos('sid'), pos('A1'), `--values-json=${JSON.stringify(values)}`, '--dry-run'],
|
|
96
97
|
{ account: undefined },
|
|
97
98
|
);
|
|
98
99
|
});
|
|
@@ -110,7 +111,7 @@ describe('gog_sheets_update', () => {
|
|
|
110
111
|
const values = [['=1+1']];
|
|
111
112
|
await harness.callTool('gog_sheets_update', { spreadsheetId: 'sid', range: 'A1', values, fail_on_formula_error: true });
|
|
112
113
|
expect(runner.run).toHaveBeenCalledWith(
|
|
113
|
-
['sheets', 'update', 'sid', 'A1', `--values-json=${JSON.stringify(values)}`, '--fail-on-formula-error'],
|
|
114
|
+
['sheets', 'update', pos('sid'), pos('A1'), `--values-json=${JSON.stringify(values)}`, '--fail-on-formula-error'],
|
|
114
115
|
{ account: undefined },
|
|
115
116
|
);
|
|
116
117
|
});
|
|
@@ -125,7 +126,7 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
125
126
|
});
|
|
126
127
|
// Only the read happened — no update call.
|
|
127
128
|
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
128
|
-
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', 'sid', 'A1:A1'], { account: undefined });
|
|
129
|
+
expect(runner.run).toHaveBeenCalledWith(['sheets', 'get', pos('sid'), pos('A1:A1')], { account: undefined });
|
|
129
130
|
expect(result.content[0].text).toContain('Write aborted');
|
|
130
131
|
expect(result.content[0].text).toContain('A1:A1');
|
|
131
132
|
expect(result.content[0].text).toContain('1 cell');
|
|
@@ -142,7 +143,7 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
142
143
|
});
|
|
143
144
|
expect(runner.run).toHaveBeenCalledTimes(2);
|
|
144
145
|
expect(vi.mocked(runner.run).mock.calls[1]![0]).toEqual(
|
|
145
|
-
['sheets', 'update', 'sid', 'A1', `--values-json=${JSON.stringify(values)}`],
|
|
146
|
+
['sheets', 'update', pos('sid'), pos('A1'), `--values-json=${JSON.stringify(values)}`],
|
|
146
147
|
);
|
|
147
148
|
expect(result.content[0].text).toBe('{"updatedCells":1}');
|
|
148
149
|
});
|
|
@@ -156,7 +157,7 @@ describe('gog_sheets_update fail_if_not_empty guard', () => {
|
|
|
156
157
|
spreadsheetId: 'sid', range: 'Sheet1!A1',
|
|
157
158
|
values: [['a', 'b'], ['c', 'd'], ['e', 'f']], fail_if_not_empty: true,
|
|
158
159
|
});
|
|
159
|
-
expect(vi.mocked(runner.run).mock.calls[0]![0]).toEqual(['sheets', 'get', 'sid', 'Sheet1!A1:B3']);
|
|
160
|
+
expect(vi.mocked(runner.run).mock.calls[0]![0]).toEqual(['sheets', 'get', pos('sid'), pos('Sheet1!A1:B3')]);
|
|
160
161
|
});
|
|
161
162
|
|
|
162
163
|
it('aborts without writing and diagnoses the error when the verification read fails', async () => {
|
|
@@ -234,7 +235,7 @@ describe('gog_sheets_append', () => {
|
|
|
234
235
|
const values = [['r1c1', 'r1c2'], ['r2c1', 'r2c2']];
|
|
235
236
|
await harness.callTool('gog_sheets_append', { spreadsheetId: 'sid', range: 'Sheet1!A:B', values });
|
|
236
237
|
expect(runner.run).toHaveBeenCalledWith(
|
|
237
|
-
['sheets', 'append', 'sid', 'Sheet1!A:B', `--values-json=${JSON.stringify(values)}`],
|
|
238
|
+
['sheets', 'append', pos('sid'), pos('Sheet1!A:B'), `--values-json=${JSON.stringify(values)}`],
|
|
238
239
|
{ account: undefined },
|
|
239
240
|
);
|
|
240
241
|
});
|
|
@@ -252,7 +253,7 @@ describe('gog_sheets_append', () => {
|
|
|
252
253
|
const values = [['x']];
|
|
253
254
|
await harness.callTool('gog_sheets_append', { spreadsheetId: 'sid', range: 'Sheet1!A:A', values, dry_run: true });
|
|
254
255
|
expect(runner.run).toHaveBeenCalledWith(
|
|
255
|
-
['sheets', 'append', 'sid', 'Sheet1!A:A', `--values-json=${JSON.stringify(values)}`, '--dry-run'],
|
|
256
|
+
['sheets', 'append', pos('sid'), pos('Sheet1!A:A'), `--values-json=${JSON.stringify(values)}`, '--dry-run'],
|
|
256
257
|
{ account: undefined },
|
|
257
258
|
);
|
|
258
259
|
});
|
|
@@ -263,7 +264,7 @@ describe('gog_sheets_clear', () => {
|
|
|
263
264
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
264
265
|
const harness = await setupHandlers();
|
|
265
266
|
await harness.callTool('gog_sheets_clear', { spreadsheetId: 'sid', range: 'Sheet1!A1:Z100' });
|
|
266
|
-
expect(runner.run).toHaveBeenCalledWith(['sheets', 'clear', 'sid', 'Sheet1!A1:Z100'], { account: undefined });
|
|
267
|
+
expect(runner.run).toHaveBeenCalledWith(['sheets', 'clear', pos('sid'), pos('Sheet1!A1:Z100')], { account: undefined });
|
|
267
268
|
});
|
|
268
269
|
|
|
269
270
|
it('returns error text on failure', async () => {
|
|
@@ -278,7 +279,7 @@ describe('gog_sheets_clear', () => {
|
|
|
278
279
|
const harness = await setupHandlers();
|
|
279
280
|
await harness.callTool('gog_sheets_clear', { spreadsheetId: 'sid', range: 'Sheet1!A1:Z100', dry_run: true });
|
|
280
281
|
expect(runner.run).toHaveBeenCalledWith(
|
|
281
|
-
['sheets', 'clear', 'sid', 'Sheet1!A1:Z100', '--dry-run'],
|
|
282
|
+
['sheets', 'clear', pos('sid'), pos('Sheet1!A1:Z100'), '--dry-run'],
|
|
282
283
|
{ account: undefined },
|
|
283
284
|
);
|
|
284
285
|
});
|
|
@@ -300,7 +301,7 @@ describe('gog_sheets_metadata', () => {
|
|
|
300
301
|
vi.mocked(runner.run).mockResolvedValue('{"title":"My Sheet","sheets":[{"title":"Sheet1"}]}');
|
|
301
302
|
const harness = await setupHandlers();
|
|
302
303
|
const result = await harness.callTool('gog_sheets_metadata', { spreadsheetId: 'sid' });
|
|
303
|
-
expect(runner.run).toHaveBeenCalledWith(['sheets', 'metadata', 'sid'], { account: undefined });
|
|
304
|
+
expect(runner.run).toHaveBeenCalledWith(['sheets', 'metadata', pos('sid')], { account: undefined });
|
|
304
305
|
expect(result.content[0].text).toContain('My Sheet');
|
|
305
306
|
});
|
|
306
307
|
|
|
@@ -317,7 +318,7 @@ describe('gog_sheets_create', () => {
|
|
|
317
318
|
vi.mocked(runner.run).mockResolvedValue('{"spreadsheetId":"newid","title":"Budget 2026"}');
|
|
318
319
|
const harness = await setupHandlers();
|
|
319
320
|
await harness.callTool('gog_sheets_create', { title: 'Budget 2026' });
|
|
320
|
-
expect(runner.run).toHaveBeenCalledWith(['sheets', 'create', 'Budget 2026'], { account: undefined });
|
|
321
|
+
expect(runner.run).toHaveBeenCalledWith(['sheets', 'create', pos('Budget 2026')], { account: undefined });
|
|
321
322
|
});
|
|
322
323
|
|
|
323
324
|
it('returns error text on failure', async () => {
|
|
@@ -333,7 +334,7 @@ describe('gog_sheets_find_replace', () => {
|
|
|
333
334
|
vi.mocked(runner.run).mockResolvedValue('{"occurrencesChanged":3}');
|
|
334
335
|
const harness = await setupHandlers();
|
|
335
336
|
await harness.callTool('gog_sheets_find_replace', { spreadsheetId: 'sid', find: 'foo', replace: 'bar' });
|
|
336
|
-
expect(runner.run).toHaveBeenCalledWith(['sheets', 'find-replace', 'sid', 'foo', 'bar'], { account: undefined });
|
|
337
|
+
expect(runner.run).toHaveBeenCalledWith(['sheets', 'find-replace', pos('sid'), pos('foo'), pos('bar')], { account: undefined });
|
|
337
338
|
});
|
|
338
339
|
|
|
339
340
|
it('returns error text on failure', async () => {
|
|
@@ -2,6 +2,7 @@ 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
4
|
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
5
|
+
import { pos } from '../../src/argv.js';
|
|
5
6
|
|
|
6
7
|
vi.mock('../../src/runner.js');
|
|
7
8
|
|
|
@@ -14,7 +15,7 @@ describe('gog_slides_export', () => {
|
|
|
14
15
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
15
16
|
const harness = await setupHandlers();
|
|
16
17
|
await harness.callTool('gog_slides_export', { presentationId: 'p1' });
|
|
17
|
-
expect(runner.run).toHaveBeenCalledWith(['slides', 'export', 'p1'], { account: undefined });
|
|
18
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'export', pos('p1')], { account: undefined });
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
it('passes --out and --format when provided', async () => {
|
|
@@ -22,7 +23,7 @@ describe('gog_slides_export', () => {
|
|
|
22
23
|
const harness = await setupHandlers();
|
|
23
24
|
await harness.callTool('gog_slides_export', { presentationId: 'p1', out: '/tmp/deck.pdf', format: 'pdf' });
|
|
24
25
|
expect(runner.run).toHaveBeenCalledWith(
|
|
25
|
-
['slides', 'export', 'p1', '--out=/tmp/deck.pdf', '--format=pdf'],
|
|
26
|
+
['slides', 'export', pos('p1'), '--out=/tmp/deck.pdf', '--format=pdf'],
|
|
26
27
|
{ account: undefined },
|
|
27
28
|
);
|
|
28
29
|
});
|
|
@@ -32,7 +33,7 @@ describe('gog_slides_export', () => {
|
|
|
32
33
|
const harness = await setupHandlers();
|
|
33
34
|
await harness.callTool('gog_slides_export', { presentationId: 'p1', out: '/tmp/deck.pptx', overwrite: true });
|
|
34
35
|
expect(runner.run).toHaveBeenCalledWith(
|
|
35
|
-
['slides', 'export', 'p1', '--out=/tmp/deck.pptx', '--overwrite'],
|
|
36
|
+
['slides', 'export', pos('p1'), '--out=/tmp/deck.pptx', '--overwrite'],
|
|
36
37
|
{ account: undefined },
|
|
37
38
|
);
|
|
38
39
|
});
|
|
@@ -50,7 +51,7 @@ describe('gog_slides_info', () => {
|
|
|
50
51
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
51
52
|
const harness = await setupHandlers();
|
|
52
53
|
await harness.callTool('gog_slides_info', { presentationId: 'p1' });
|
|
53
|
-
expect(runner.run).toHaveBeenCalledWith(['slides', 'info', 'p1'], { account: undefined });
|
|
54
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'info', pos('p1')], { account: undefined });
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
it('returns error text on failure', async () => {
|
|
@@ -66,7 +67,7 @@ describe('gog_slides_create', () => {
|
|
|
66
67
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
67
68
|
const harness = await setupHandlers();
|
|
68
69
|
await harness.callTool('gog_slides_create', { title: 'Deck' });
|
|
69
|
-
expect(runner.run).toHaveBeenCalledWith(['slides', 'create', 'Deck'], { account: undefined });
|
|
70
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'create', pos('Deck')], { account: undefined });
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
it('passes --parent and --template when provided', async () => {
|
|
@@ -74,7 +75,7 @@ describe('gog_slides_create', () => {
|
|
|
74
75
|
const harness = await setupHandlers();
|
|
75
76
|
await harness.callTool('gog_slides_create', { title: 'Deck', parent: 'folder1', template: 'tpl1' });
|
|
76
77
|
expect(runner.run).toHaveBeenCalledWith(
|
|
77
|
-
['slides', 'create', 'Deck', '--parent=folder1', '--template=tpl1'],
|
|
78
|
+
['slides', 'create', pos('Deck'), '--parent=folder1', '--template=tpl1'],
|
|
78
79
|
{ account: undefined },
|
|
79
80
|
);
|
|
80
81
|
});
|
|
@@ -93,7 +94,7 @@ describe('gog_slides_copy', () => {
|
|
|
93
94
|
const harness = await setupHandlers();
|
|
94
95
|
await harness.callTool('gog_slides_copy', { presentationId: 'p1', title: 'Copy' });
|
|
95
96
|
expect(runner.run).toHaveBeenCalledWith(
|
|
96
|
-
['slides', 'copy', 'p1', 'Copy'],
|
|
97
|
+
['slides', 'copy', pos('p1'), pos('Copy')],
|
|
97
98
|
{ account: undefined },
|
|
98
99
|
);
|
|
99
100
|
});
|
|
@@ -103,7 +104,7 @@ describe('gog_slides_copy', () => {
|
|
|
103
104
|
const harness = await setupHandlers();
|
|
104
105
|
await harness.callTool('gog_slides_copy', { presentationId: 'p1', title: 'Copy', parent: 'folder1' });
|
|
105
106
|
expect(runner.run).toHaveBeenCalledWith(
|
|
106
|
-
['slides', 'copy', 'p1', 'Copy', '--parent=folder1'],
|
|
107
|
+
['slides', 'copy', pos('p1'), pos('Copy'), '--parent=folder1'],
|
|
107
108
|
{ account: undefined },
|
|
108
109
|
);
|
|
109
110
|
});
|
|
@@ -121,7 +122,7 @@ describe('gog_slides_list_slides', () => {
|
|
|
121
122
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
122
123
|
const harness = await setupHandlers();
|
|
123
124
|
await harness.callTool('gog_slides_list_slides', { presentationId: 'p1' });
|
|
124
|
-
expect(runner.run).toHaveBeenCalledWith(['slides', 'list-slides', 'p1'], { account: undefined });
|
|
125
|
+
expect(runner.run).toHaveBeenCalledWith(['slides', 'list-slides', pos('p1')], { account: undefined });
|
|
125
126
|
});
|
|
126
127
|
|
|
127
128
|
it('returns error text on failure', async () => {
|
|
@@ -138,7 +139,7 @@ describe('gog_slides_read_slide', () => {
|
|
|
138
139
|
const harness = await setupHandlers();
|
|
139
140
|
await harness.callTool('gog_slides_read_slide', { presentationId: 'p1', slideId: 's1' });
|
|
140
141
|
expect(runner.run).toHaveBeenCalledWith(
|
|
141
|
-
['slides', 'read-slide', 'p1', 's1'],
|
|
142
|
+
['slides', 'read-slide', pos('p1'), pos('s1')],
|
|
142
143
|
{ account: undefined },
|
|
143
144
|
);
|
|
144
145
|
});
|
|
@@ -148,7 +149,7 @@ describe('gog_slides_read_slide', () => {
|
|
|
148
149
|
const harness = await setupHandlers();
|
|
149
150
|
await harness.callTool('gog_slides_read_slide', { presentationId: 'p1', slideId: 's1', detail: true });
|
|
150
151
|
expect(runner.run).toHaveBeenCalledWith(
|
|
151
|
-
['slides', 'read-slide', 'p1', 's1', '--detail'],
|
|
152
|
+
['slides', 'read-slide', pos('p1'), pos('s1'), '--detail'],
|
|
152
153
|
{ account: undefined },
|
|
153
154
|
);
|
|
154
155
|
});
|
|
@@ -176,3 +177,38 @@ describe('gog_slides_run', () => {
|
|
|
176
177
|
expect(result.content[0].text).toBe('Error: Run failed');
|
|
177
178
|
});
|
|
178
179
|
});
|
|
180
|
+
|
|
181
|
+
// SEC-3/SEC-4: every model-supplied server path must resolve inside an
|
|
182
|
+
// operator-configured root (GOG_FILE_ROOTS). The suite runs with '/' so the
|
|
183
|
+
// arg-shape tests can use any path; these narrow it.
|
|
184
|
+
async function withFileRoots<T>(roots: string, fn: () => Promise<T>): Promise<T> {
|
|
185
|
+
const prev = process.env.GOG_FILE_ROOTS;
|
|
186
|
+
process.env.GOG_FILE_ROOTS = roots;
|
|
187
|
+
try {
|
|
188
|
+
return await fn();
|
|
189
|
+
} finally {
|
|
190
|
+
process.env.GOG_FILE_ROOTS = prev;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
describe('gog_slides_export — writes to disk', () => {
|
|
195
|
+
it('refuses an out path outside GOG_FILE_ROOTS', async () => {
|
|
196
|
+
const harness = await setupHandlers();
|
|
197
|
+
const result = await withFileRoots('/srv/gog-files', () => harness.callTool('gog_slides_export', {
|
|
198
|
+
presentationId: 'p1', out: '/Users/me/.zshrc', overwrite: true,
|
|
199
|
+
}));
|
|
200
|
+
expect(result.isError).toBe(true);
|
|
201
|
+
expect(result.content[0].text).toContain('out "/Users/me/.zshrc" is outside');
|
|
202
|
+
expect(runner.run).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// SEC-4: clients auto-approve readOnlyHint tools; this one writes a file and
|
|
206
|
+
// can overwrite one, so the client must be allowed to ask.
|
|
207
|
+
it('is not advertised as read-only, and is marked destructive', async () => {
|
|
208
|
+
const harness = await setupHandlers();
|
|
209
|
+
const { tools } = await harness.client.listTools();
|
|
210
|
+
const tool = tools.find((t) => t.name === 'gog_slides_export')!;
|
|
211
|
+
expect(tool.annotations?.readOnlyHint).not.toBe(true);
|
|
212
|
+
expect(tool.annotations?.destructiveHint).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import { registerTasksTools } from '../../src/tools/tasks.js';
|
|
3
3
|
import * as runner from '../../src/runner.js';
|
|
4
4
|
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
5
|
+
import { pos } from '../../src/argv.js';
|
|
5
6
|
|
|
6
7
|
vi.mock('../../src/runner.js');
|
|
7
8
|
|
|
@@ -30,7 +31,7 @@ describe('gog_tasks_list', () => {
|
|
|
30
31
|
vi.mocked(runner.run).mockResolvedValue('{"items":[]}');
|
|
31
32
|
const harness = await setupHandlers();
|
|
32
33
|
await harness.callTool('gog_tasks_list', { tasklistId: 'list1' });
|
|
33
|
-
expect(runner.run).toHaveBeenCalledWith(['tasks', 'list', 'list1'], { account: undefined });
|
|
34
|
+
expect(runner.run).toHaveBeenCalledWith(['tasks', 'list', pos('list1')], { account: undefined });
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
it('returns error text on failure', async () => {
|
|
@@ -46,7 +47,7 @@ describe('gog_tasks_get', () => {
|
|
|
46
47
|
vi.mocked(runner.run).mockResolvedValue('{"id":"task1"}');
|
|
47
48
|
const harness = await setupHandlers();
|
|
48
49
|
await harness.callTool('gog_tasks_get', { tasklistId: 'list1', taskId: 'task1' });
|
|
49
|
-
expect(runner.run).toHaveBeenCalledWith(['tasks', 'get', 'list1', 'task1'], { account: undefined });
|
|
50
|
+
expect(runner.run).toHaveBeenCalledWith(['tasks', 'get', pos('list1'), pos('task1')], { account: undefined });
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
it('returns error text on failure', async () => {
|
|
@@ -63,7 +64,7 @@ describe('gog_tasks_add', () => {
|
|
|
63
64
|
const harness = await setupHandlers();
|
|
64
65
|
await harness.callTool('gog_tasks_add', { tasklistId: 'list1', title: 'Buy milk' });
|
|
65
66
|
expect(runner.run).toHaveBeenCalledWith(
|
|
66
|
-
['tasks', 'add', 'list1', '--title=Buy milk'],
|
|
67
|
+
['tasks', 'add', pos('list1'), '--title=Buy milk'],
|
|
67
68
|
{ account: undefined },
|
|
68
69
|
);
|
|
69
70
|
});
|
|
@@ -73,7 +74,7 @@ describe('gog_tasks_add', () => {
|
|
|
73
74
|
const harness = await setupHandlers();
|
|
74
75
|
await harness.callTool('gog_tasks_add', { tasklistId: 'list1', title: 'Buy milk', notes: 'Whole milk', due: '2026-04-20' });
|
|
75
76
|
expect(runner.run).toHaveBeenCalledWith(
|
|
76
|
-
['tasks', 'add', 'list1', '--title=Buy milk', '--notes=Whole milk', '--due=2026-04-20'],
|
|
77
|
+
['tasks', 'add', pos('list1'), '--title=Buy milk', '--notes=Whole milk', '--due=2026-04-20'],
|
|
77
78
|
{ account: undefined },
|
|
78
79
|
);
|
|
79
80
|
});
|
|
@@ -91,7 +92,7 @@ describe('gog_tasks_done', () => {
|
|
|
91
92
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
92
93
|
const harness = await setupHandlers();
|
|
93
94
|
await harness.callTool('gog_tasks_done', { tasklistId: 'list1', taskId: 'task1' });
|
|
94
|
-
expect(runner.run).toHaveBeenCalledWith(['tasks', 'done', 'list1', 'task1'], { account: undefined });
|
|
95
|
+
expect(runner.run).toHaveBeenCalledWith(['tasks', 'done', pos('list1'), pos('task1')], { account: undefined });
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
it('returns error text on failure', async () => {
|
|
@@ -107,7 +108,7 @@ describe('gog_tasks_delete', () => {
|
|
|
107
108
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
108
109
|
const harness = await setupHandlers();
|
|
109
110
|
await harness.callTool('gog_tasks_delete', { tasklistId: 'list1', taskId: 'task1' });
|
|
110
|
-
expect(runner.run).toHaveBeenCalledWith(['tasks', 'delete', 'list1', 'task1', '--force'], { account: undefined });
|
|
111
|
+
expect(runner.run).toHaveBeenCalledWith(['tasks', 'delete', pos('list1'), pos('task1'), '--force'], { account: undefined });
|
|
111
112
|
});
|
|
112
113
|
|
|
113
114
|
it('returns error text on failure', async () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import * as runner from '../../src/runner.js';
|
|
3
3
|
import { runOrDiagnose, pushPaginationFlags, formatAccountList, formatAuthHealth } from '../../src/tools/utils.js';
|
|
4
|
+
import { pos } from '../../src/argv.js';
|
|
4
5
|
|
|
5
6
|
// PARTIAL mock: only `run` is stubbed; everything else stays real.
|
|
6
7
|
vi.mock('../../src/runner.js', async (importOriginal) => ({
|
|
@@ -51,7 +52,7 @@ describe('formatAccountList', () => {
|
|
|
51
52
|
email: 'chris.c.hall@gmail.com',
|
|
52
53
|
subject: '109876543210987654321',
|
|
53
54
|
client: 'default',
|
|
54
|
-
services: ['gmail', 'drive'],
|
|
55
|
+
services: ['gmail', pos('drive')],
|
|
55
56
|
scopes: ['https://www.googleapis.com/auth/gmail.modify', 'https://www.googleapis.com/auth/drive'],
|
|
56
57
|
created_at: '2026-01-02T03:04:05Z',
|
|
57
58
|
},
|
|
@@ -94,7 +95,7 @@ describe('formatAccountList', () => {
|
|
|
94
95
|
describe('runOrDiagnose', () => {
|
|
95
96
|
it('returns output text on success (not flagged as an error)', async () => {
|
|
96
97
|
vi.mocked(runner.run).mockResolvedValue('{"ok":true}');
|
|
97
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
98
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
98
99
|
expect(result.content[0].text).toBe('{"ok":true}');
|
|
99
100
|
expect(result.isError).toBeUndefined();
|
|
100
101
|
});
|
|
@@ -106,7 +107,7 @@ describe('runOrDiagnose', () => {
|
|
|
106
107
|
it('leaves a lossless response byte-for-byte untouched', async () => {
|
|
107
108
|
const raw = '{\n "id": "m1",\n "internalDate": "1785209760000"\n}';
|
|
108
109
|
vi.mocked(runner.run).mockResolvedValue(raw);
|
|
109
|
-
const result = await runOrDiagnose(['gmail', 'raw', 'm1'], { lossless: true });
|
|
110
|
+
const result = await runOrDiagnose(['gmail', 'raw', pos('m1')], { lossless: true });
|
|
110
111
|
expect(result.content[0].text).toBe(raw);
|
|
111
112
|
});
|
|
112
113
|
|
|
@@ -182,7 +183,7 @@ describe('runOrDiagnose', () => {
|
|
|
182
183
|
it('does NOT minify a lossless response', async () => {
|
|
183
184
|
const raw = '{\n "id": "m1"\n}';
|
|
184
185
|
vi.mocked(runner.run).mockResolvedValue(raw);
|
|
185
|
-
const result = await runOrDiagnose(['gmail', 'raw', 'm1'], { lossless: true });
|
|
186
|
+
const result = await runOrDiagnose(['gmail', 'raw', pos('m1')], { lossless: true });
|
|
186
187
|
expect(result.content[0].text).toBe(raw);
|
|
187
188
|
});
|
|
188
189
|
|
|
@@ -230,7 +231,7 @@ describe('runOrDiagnose', () => {
|
|
|
230
231
|
vi.mocked(runner.run).mockResolvedValue(
|
|
231
232
|
'{\n "id": "f1",\n "thumbnailLink": "https://lh3.googleusercontent.com/x=s220",\n "webViewLink": "https://docs.google.com/d/f1"\n}',
|
|
232
233
|
);
|
|
233
|
-
const result = await runOrDiagnose(['drive', 'get', 'f1'], { stripMedia: true });
|
|
234
|
+
const result = await runOrDiagnose(['drive', 'get', pos('f1')], { stripMedia: true });
|
|
234
235
|
expect(result.content[0].text).toBe('{"id":"f1","webViewLink":"https://docs.google.com/d/f1"}');
|
|
235
236
|
});
|
|
236
237
|
|
|
@@ -240,14 +241,14 @@ describe('runOrDiagnose', () => {
|
|
|
240
241
|
vi.mocked(runner.run).mockResolvedValue(
|
|
241
242
|
JSON.stringify({ files: [{ id: 'f1', hasThumbnail: false, thumbnailLink: 'https://x/y=s220', webViewLink: 'https://docs/f1' }] }),
|
|
242
243
|
);
|
|
243
|
-
const result = await runOrDiagnose(['drive', 'search', 'q'], { stripMedia: true });
|
|
244
|
+
const result = await runOrDiagnose(['drive', 'search', pos('q')], { stripMedia: true });
|
|
244
245
|
const parsed = JSON.parse(result.content[0].text as string);
|
|
245
246
|
expect(parsed.files[0]).toEqual({ id: 'f1', hasThumbnail: false, webViewLink: 'https://docs/f1' });
|
|
246
247
|
});
|
|
247
248
|
|
|
248
249
|
it('does not strip media unless asked', async () => {
|
|
249
250
|
vi.mocked(runner.run).mockResolvedValue('{"thumbnailLink":"https://x/y=s220"}');
|
|
250
|
-
const result = await runOrDiagnose(['drive', 'get', 'f1'], {});
|
|
251
|
+
const result = await runOrDiagnose(['drive', 'get', pos('f1')], {});
|
|
251
252
|
expect(result.content[0].text).toBe('{"thumbnailLink":"https://x/y=s220"}');
|
|
252
253
|
});
|
|
253
254
|
|
|
@@ -263,7 +264,7 @@ describe('runOrDiagnose', () => {
|
|
|
263
264
|
vi.mocked(runner.run)
|
|
264
265
|
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
265
266
|
.mockResolvedValueOnce('user@gmail.com');
|
|
266
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
267
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
267
268
|
expect(result.content[0].text).toBe(
|
|
268
269
|
'Error: Doc not found\n\nConfigured accounts:\nuser@gmail.com',
|
|
269
270
|
);
|
|
@@ -284,7 +285,7 @@ describe('runOrDiagnose', () => {
|
|
|
284
285
|
vi.mocked(runner.run)
|
|
285
286
|
.mockRejectedValueOnce(new Error('refusing to delete gmail draft r123 without --force (non-interactive)'))
|
|
286
287
|
.mockResolvedValueOnce(authListJson);
|
|
287
|
-
const result = await runOrDiagnose(['gmail', 'drafts', 'delete', 'r123'], {});
|
|
288
|
+
const result = await runOrDiagnose(['gmail', 'drafts', 'delete', pos('r123')], {});
|
|
288
289
|
const text = result.content[0].text;
|
|
289
290
|
expect(text).toContain('Configured accounts:\nchris.c.hall@gmail.com');
|
|
290
291
|
expect(text).not.toContain('scopes');
|
|
@@ -297,7 +298,7 @@ describe('runOrDiagnose', () => {
|
|
|
297
298
|
vi.mocked(runner.run)
|
|
298
299
|
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
299
300
|
.mockResolvedValueOnce('{"accounts":[]}');
|
|
300
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
301
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
301
302
|
expect(result.content[0].text).toBe('Error: Doc not found\n\nConfigured accounts:\n(none)');
|
|
302
303
|
});
|
|
303
304
|
|
|
@@ -305,7 +306,7 @@ describe('runOrDiagnose', () => {
|
|
|
305
306
|
vi.mocked(runner.run)
|
|
306
307
|
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
307
308
|
.mockResolvedValueOnce('user@gmail.com');
|
|
308
|
-
const result = await runOrDiagnose(['docs', 'comments', 'list', 'abc'], {});
|
|
309
|
+
const result = await runOrDiagnose(['docs', 'comments', 'list', pos('abc')], {});
|
|
309
310
|
expect(result.content[0].text).toContain('Error: Request failed with status 401');
|
|
310
311
|
expect(result.content[0].text).toContain('Configured accounts:\nuser@gmail.com');
|
|
311
312
|
expect(result.content[0].text).toContain('gog_auth_add');
|
|
@@ -315,7 +316,7 @@ describe('runOrDiagnose', () => {
|
|
|
315
316
|
vi.mocked(runner.run)
|
|
316
317
|
.mockRejectedValueOnce(new Error('unauthorized access'))
|
|
317
318
|
.mockResolvedValueOnce('user@gmail.com');
|
|
318
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
319
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
319
320
|
expect(result.content[0].text).toContain('gog_auth_add');
|
|
320
321
|
});
|
|
321
322
|
|
|
@@ -323,7 +324,7 @@ describe('runOrDiagnose', () => {
|
|
|
323
324
|
vi.mocked(runner.run)
|
|
324
325
|
.mockRejectedValueOnce(new Error('token has been expired or revoked'))
|
|
325
326
|
.mockResolvedValueOnce('user@gmail.com');
|
|
326
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
327
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
327
328
|
expect(result.content[0].text).toContain('gog_auth_add');
|
|
328
329
|
});
|
|
329
330
|
|
|
@@ -331,7 +332,7 @@ describe('runOrDiagnose', () => {
|
|
|
331
332
|
vi.mocked(runner.run)
|
|
332
333
|
.mockRejectedValueOnce(new Error('invalid_grant'))
|
|
333
334
|
.mockResolvedValueOnce('user@gmail.com');
|
|
334
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
335
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
335
336
|
expect(result.content[0].text).toContain('gog_auth_add');
|
|
336
337
|
});
|
|
337
338
|
|
|
@@ -346,7 +347,7 @@ describe('runOrDiagnose', () => {
|
|
|
346
347
|
vi.mocked(runner.run)
|
|
347
348
|
.mockRejectedValueOnce(new Error('429 rateLimitExceeded: the access token expired mid-request, retry'))
|
|
348
349
|
.mockResolvedValueOnce('user@gmail.com');
|
|
349
|
-
const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
|
|
350
|
+
const result = await runOrDiagnose(['sheets', 'get', pos('A1')], {});
|
|
350
351
|
const text = result.content[0].text as string;
|
|
351
352
|
expect(text).toContain('often transient');
|
|
352
353
|
expect(text).not.toContain('gog_auth_add');
|
|
@@ -359,7 +360,7 @@ describe('runOrDiagnose', () => {
|
|
|
359
360
|
vi.mocked(runner.run)
|
|
360
361
|
.mockRejectedValueOnce(new Error('401 unauthorized (quota project unset)'))
|
|
361
362
|
.mockResolvedValueOnce('user@gmail.com');
|
|
362
|
-
const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
|
|
363
|
+
const result = await runOrDiagnose(['sheets', 'get', pos('A1')], {});
|
|
363
364
|
expect(result.content[0].text).toContain('gog_auth_add');
|
|
364
365
|
});
|
|
365
366
|
|
|
@@ -375,7 +376,7 @@ describe('runOrDiagnose', () => {
|
|
|
375
376
|
new Error('page token accepted; the requested export link has expired and must be regenerated'),
|
|
376
377
|
)
|
|
377
378
|
.mockResolvedValueOnce('user@gmail.com');
|
|
378
|
-
const result = await runOrDiagnose(['drive', 'export', 'abc'], {});
|
|
379
|
+
const result = await runOrDiagnose(['drive', pos('export'), pos('abc')], {});
|
|
379
380
|
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
380
381
|
});
|
|
381
382
|
|
|
@@ -383,7 +384,7 @@ describe('runOrDiagnose', () => {
|
|
|
383
384
|
vi.mocked(runner.run)
|
|
384
385
|
.mockRejectedValueOnce(new Error('oauth2: "invalid_grant" "Token has been expired or revoked."'))
|
|
385
386
|
.mockResolvedValueOnce('user@gmail.com');
|
|
386
|
-
const result = await runOrDiagnose(['gmail', 'search', 'is:unread'], {});
|
|
387
|
+
const result = await runOrDiagnose(['gmail', 'search', pos('is:unread')], {});
|
|
387
388
|
const text = result.content[0].text as string;
|
|
388
389
|
// plain-English cause
|
|
389
390
|
expect(text).toContain('7-day');
|
|
@@ -398,7 +399,7 @@ describe('runOrDiagnose', () => {
|
|
|
398
399
|
vi.mocked(runner.run)
|
|
399
400
|
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
400
401
|
.mockResolvedValueOnce('user@gmail.com');
|
|
401
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
402
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
402
403
|
const text = result.content[0].text as string;
|
|
403
404
|
expect(text).toContain('gog_auth_add');
|
|
404
405
|
expect(text).not.toContain('In production');
|
|
@@ -408,7 +409,7 @@ describe('runOrDiagnose', () => {
|
|
|
408
409
|
vi.mocked(runner.run)
|
|
409
410
|
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
410
411
|
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
411
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
412
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
412
413
|
expect(result.content[0].text).toContain('Error: Request failed with status 401');
|
|
413
414
|
expect(result.content[0].text).toContain('gog_auth_add');
|
|
414
415
|
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
@@ -418,7 +419,7 @@ describe('runOrDiagnose', () => {
|
|
|
418
419
|
vi.mocked(runner.run)
|
|
419
420
|
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
420
421
|
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
421
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
422
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
422
423
|
expect(result.content[0].text).toBe('Error: Doc not found');
|
|
423
424
|
expect(result.isError).toBe(true);
|
|
424
425
|
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
@@ -428,7 +429,7 @@ describe('runOrDiagnose', () => {
|
|
|
428
429
|
vi.mocked(runner.run)
|
|
429
430
|
.mockRejectedValueOnce(new Error('Request failed with status 429'))
|
|
430
431
|
.mockResolvedValueOnce('user@gmail.com');
|
|
431
|
-
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
432
|
+
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
432
433
|
expect(result.content[0].text).toContain('transient');
|
|
433
434
|
expect(result.content[0].text).toContain('Retry');
|
|
434
435
|
});
|
|
@@ -439,7 +440,7 @@ describe('runOrDiagnose', () => {
|
|
|
439
440
|
vi.mocked(runner.run)
|
|
440
441
|
.mockRejectedValueOnce(new Error(`Request failed with status ${status}`))
|
|
441
442
|
.mockResolvedValueOnce('user@gmail.com');
|
|
442
|
-
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
443
|
+
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
443
444
|
expect(result.content[0].text, `status ${status}`).toContain('transient');
|
|
444
445
|
}
|
|
445
446
|
});
|
|
@@ -450,7 +451,7 @@ describe('runOrDiagnose', () => {
|
|
|
450
451
|
vi.mocked(runner.run)
|
|
451
452
|
.mockRejectedValueOnce(new Error(msg))
|
|
452
453
|
.mockResolvedValueOnce('user@gmail.com');
|
|
453
|
-
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
454
|
+
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
454
455
|
expect(result.content[0].text, msg).toContain('transient');
|
|
455
456
|
}
|
|
456
457
|
});
|
|
@@ -459,7 +460,7 @@ describe('runOrDiagnose', () => {
|
|
|
459
460
|
vi.mocked(runner.run)
|
|
460
461
|
.mockRejectedValueOnce(new Error('DEADLINE_EXCEEDED: context deadline exceeded'))
|
|
461
462
|
.mockResolvedValueOnce('user@gmail.com');
|
|
462
|
-
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
463
|
+
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
463
464
|
expect(result.content[0].text).toContain('transient');
|
|
464
465
|
});
|
|
465
466
|
|
|
@@ -467,7 +468,7 @@ describe('runOrDiagnose', () => {
|
|
|
467
468
|
vi.mocked(runner.run)
|
|
468
469
|
.mockRejectedValueOnce(new Error('Request failed with status 404'))
|
|
469
470
|
.mockResolvedValueOnce('user@gmail.com');
|
|
470
|
-
const result = await runOrDiagnose(['sheets', 'get', 'abc', 'A1'], {});
|
|
471
|
+
const result = await runOrDiagnose(['sheets', 'get', pos('abc'), pos('A1')], {});
|
|
471
472
|
expect(result.content[0].text).not.toContain('transient');
|
|
472
473
|
});
|
|
473
474
|
|
|
@@ -475,7 +476,7 @@ describe('runOrDiagnose', () => {
|
|
|
475
476
|
vi.mocked(runner.run)
|
|
476
477
|
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
477
478
|
.mockResolvedValueOnce('user@gmail.com');
|
|
478
|
-
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
479
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
479
480
|
expect(result.content[0].text).toContain('gog_auth_add');
|
|
480
481
|
expect(result.content[0].text).not.toContain('transient');
|
|
481
482
|
});
|
|
@@ -484,7 +485,7 @@ describe('runOrDiagnose', () => {
|
|
|
484
485
|
vi.mocked(runner.run)
|
|
485
486
|
.mockRejectedValueOnce(new Error('Request failed with status 503'))
|
|
486
487
|
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
487
|
-
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
488
|
+
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
488
489
|
expect(result.content[0].text).toContain('transient');
|
|
489
490
|
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
490
491
|
});
|
|
@@ -493,7 +494,7 @@ describe('runOrDiagnose', () => {
|
|
|
493
494
|
vi.mocked(runner.run)
|
|
494
495
|
.mockRejectedValueOnce(new Error('Range (Sheet1!AP1:AW1) exceeds grid limits. Max rows: 1000, max columns: 41'))
|
|
495
496
|
.mockResolvedValueOnce('user@gmail.com');
|
|
496
|
-
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'AP1'], {});
|
|
497
|
+
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('AP1')], {});
|
|
497
498
|
expect(result.content[0].text).toContain('exceeds grid limits');
|
|
498
499
|
expect(result.content[0].text).toContain('gog_sheets_insert');
|
|
499
500
|
});
|
|
@@ -502,7 +503,7 @@ describe('runOrDiagnose', () => {
|
|
|
502
503
|
vi.mocked(runner.run)
|
|
503
504
|
.mockRejectedValueOnce(new Error('Spreadsheet not found'))
|
|
504
505
|
.mockResolvedValueOnce('user@gmail.com');
|
|
505
|
-
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
506
|
+
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
506
507
|
expect(result.content[0].text).not.toContain('gog_sheets_insert');
|
|
507
508
|
});
|
|
508
509
|
|
|
@@ -510,7 +511,7 @@ describe('runOrDiagnose', () => {
|
|
|
510
511
|
vi.mocked(runner.run)
|
|
511
512
|
.mockRejectedValueOnce(new Error('exceeds grid limits. Max rows: 1000, max columns: 41'))
|
|
512
513
|
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
513
|
-
const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
|
|
514
|
+
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
514
515
|
expect(result.content[0].text).toContain('gog_sheets_insert');
|
|
515
516
|
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
516
517
|
});
|
package/vitest.config.ts
CHANGED
|
@@ -20,6 +20,11 @@ export default defineConfig({
|
|
|
20
20
|
GOG_ACCOUNT: '',
|
|
21
21
|
GOG_PATH: '',
|
|
22
22
|
GOG_READONLY: '',
|
|
23
|
+
// The arg-shape tests pass arbitrary server paths (/tmp/shot.png,
|
|
24
|
+
// /path/to/file); confinement is exercised explicitly by the tests that
|
|
25
|
+
// narrow GOG_FILE_ROOTS themselves (tests/file-roots.test.ts and the
|
|
26
|
+
// per-tool "refuses a path outside" cases).
|
|
27
|
+
GOG_FILE_ROOTS: '/',
|
|
23
28
|
},
|
|
24
29
|
coverage: {
|
|
25
30
|
provider: 'v8',
|