gogcli-mcp 4.2.5 → 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 +750 -216
- package/manifest.json +9 -2
- 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 +86 -18
- 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
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import { registerClassroomTools } from '../../src/tools/classroom.js';
|
|
3
3
|
import * as runner from '../../src/runner.js';
|
|
4
4
|
import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
|
|
5
|
+
import { pos } from '../../src/argv.js';
|
|
5
6
|
|
|
6
7
|
vi.mock('../../src/runner.js');
|
|
7
8
|
|
|
@@ -51,7 +52,7 @@ describe('gog_classroom_courses_list', () => {
|
|
|
51
52
|
describe('gog_classroom_courses_get', () => {
|
|
52
53
|
it('calls run with courseId', async () => {
|
|
53
54
|
await harness.callTool('gog_classroom_courses_get', { courseId: 'c1' });
|
|
54
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'courses', 'get', 'c1'], { account: undefined });
|
|
55
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'courses', 'get', pos('c1')], { account: undefined });
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
it('returns error text on failure', async () => {
|
|
@@ -64,20 +65,20 @@ describe('gog_classroom_courses_get', () => {
|
|
|
64
65
|
describe('gog_classroom_students_list', () => {
|
|
65
66
|
it('calls run with courseId only', async () => {
|
|
66
67
|
await harness.callTool('gog_classroom_students_list', { courseId: 'c1' });
|
|
67
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'list', 'c1'], { account: undefined });
|
|
68
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'list', pos('c1')], { account: undefined });
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
it('passes all listing flags', async () => {
|
|
71
72
|
await harness.callTool('gog_classroom_students_list', { courseId: 'c1', max: 20, page: 'tok', all: true });
|
|
72
73
|
expect(runner.run).toHaveBeenCalledWith(
|
|
73
|
-
['classroom', 'students', 'list', 'c1', '--max=20', '--page=tok', '--all'],
|
|
74
|
+
['classroom', 'students', 'list', pos('c1'), '--max=20', '--page=tok', '--all'],
|
|
74
75
|
{ account: undefined },
|
|
75
76
|
);
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
it('omits --all when false', async () => {
|
|
79
80
|
await harness.callTool('gog_classroom_students_list', { courseId: 'c1', all: false });
|
|
80
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'list', 'c1'], { account: undefined });
|
|
81
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'list', pos('c1')], { account: undefined });
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
it('returns error text on failure', async () => {
|
|
@@ -90,7 +91,7 @@ describe('gog_classroom_students_list', () => {
|
|
|
90
91
|
describe('gog_classroom_students_get', () => {
|
|
91
92
|
it('calls run with courseId and userId', async () => {
|
|
92
93
|
await harness.callTool('gog_classroom_students_get', { courseId: 'c1', userId: 'u1' });
|
|
93
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'get', 'c1', 'u1'], { account: undefined });
|
|
94
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'get', pos('c1'), pos('u1')], { account: undefined });
|
|
94
95
|
});
|
|
95
96
|
|
|
96
97
|
it('returns error text on failure', async () => {
|
|
@@ -103,20 +104,20 @@ describe('gog_classroom_students_get', () => {
|
|
|
103
104
|
describe('gog_classroom_teachers_list', () => {
|
|
104
105
|
it('calls run with courseId only', async () => {
|
|
105
106
|
await harness.callTool('gog_classroom_teachers_list', { courseId: 'c1' });
|
|
106
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'list', 'c1'], { account: undefined });
|
|
107
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'list', pos('c1')], { account: undefined });
|
|
107
108
|
});
|
|
108
109
|
|
|
109
110
|
it('passes all listing flags', async () => {
|
|
110
111
|
await harness.callTool('gog_classroom_teachers_list', { courseId: 'c1', max: 20, page: 'tok', all: true });
|
|
111
112
|
expect(runner.run).toHaveBeenCalledWith(
|
|
112
|
-
['classroom', 'teachers', 'list', 'c1', '--max=20', '--page=tok', '--all'],
|
|
113
|
+
['classroom', 'teachers', 'list', pos('c1'), '--max=20', '--page=tok', '--all'],
|
|
113
114
|
{ account: undefined },
|
|
114
115
|
);
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
it('omits --all when false', async () => {
|
|
118
119
|
await harness.callTool('gog_classroom_teachers_list', { courseId: 'c1', all: false });
|
|
119
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'list', 'c1'], { account: undefined });
|
|
120
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'list', pos('c1')], { account: undefined });
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
it('returns error text on failure', async () => {
|
|
@@ -129,7 +130,7 @@ describe('gog_classroom_teachers_list', () => {
|
|
|
129
130
|
describe('gog_classroom_teachers_get', () => {
|
|
130
131
|
it('calls run with courseId and userId', async () => {
|
|
131
132
|
await harness.callTool('gog_classroom_teachers_get', { courseId: 'c1', userId: 'u1' });
|
|
132
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'get', 'c1', 'u1'], { account: undefined });
|
|
133
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'get', pos('c1'), pos('u1')], { account: undefined });
|
|
133
134
|
});
|
|
134
135
|
|
|
135
136
|
it('returns error text on failure', async () => {
|
|
@@ -142,20 +143,20 @@ describe('gog_classroom_teachers_get', () => {
|
|
|
142
143
|
describe('gog_classroom_roster', () => {
|
|
143
144
|
it('calls run with courseId only', async () => {
|
|
144
145
|
await harness.callTool('gog_classroom_roster', { courseId: 'c1' });
|
|
145
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'roster', 'c1'], { account: undefined });
|
|
146
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'roster', pos('c1')], { account: undefined });
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
it('passes all flags', async () => {
|
|
149
150
|
await harness.callTool('gog_classroom_roster', { courseId: 'c1', students: true, teachers: true, max: 50, page: 'tok', all: true });
|
|
150
151
|
expect(runner.run).toHaveBeenCalledWith(
|
|
151
|
-
['classroom', 'roster', 'c1', '--students', '--teachers', '--max=50', '--page=tok', '--all'],
|
|
152
|
+
['classroom', 'roster', pos('c1'), '--students', '--teachers', '--max=50', '--page=tok', '--all'],
|
|
152
153
|
{ account: undefined },
|
|
153
154
|
);
|
|
154
155
|
});
|
|
155
156
|
|
|
156
157
|
it('omits boolean flags when false', async () => {
|
|
157
158
|
await harness.callTool('gog_classroom_roster', { courseId: 'c1', students: false, teachers: false, all: false });
|
|
158
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'roster', 'c1'], { account: undefined });
|
|
159
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'roster', pos('c1')], { account: undefined });
|
|
159
160
|
});
|
|
160
161
|
|
|
161
162
|
it('returns error text on failure', async () => {
|
|
@@ -168,7 +169,7 @@ describe('gog_classroom_roster', () => {
|
|
|
168
169
|
describe('gog_classroom_coursework_list', () => {
|
|
169
170
|
it('calls run with courseId only', async () => {
|
|
170
171
|
await harness.callTool('gog_classroom_coursework_list', { courseId: 'c1' });
|
|
171
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'list', 'c1'], { account: undefined });
|
|
172
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'list', pos('c1')], { account: undefined });
|
|
172
173
|
});
|
|
173
174
|
|
|
174
175
|
it('passes all listing flags', async () => {
|
|
@@ -183,14 +184,14 @@ describe('gog_classroom_coursework_list', () => {
|
|
|
183
184
|
scanPages: 5,
|
|
184
185
|
});
|
|
185
186
|
expect(runner.run).toHaveBeenCalledWith(
|
|
186
|
-
['classroom', 'coursework', 'list', 'c1', '--state=PUBLISHED', '--topic=t1', '--order-by=updateTime desc', '--max=50', '--page=tok', '--all', '--scan-pages=5'],
|
|
187
|
+
['classroom', 'coursework', 'list', pos('c1'), '--state=PUBLISHED', '--topic=t1', '--order-by=updateTime desc', '--max=50', '--page=tok', '--all', '--scan-pages=5'],
|
|
187
188
|
{ account: undefined },
|
|
188
189
|
);
|
|
189
190
|
});
|
|
190
191
|
|
|
191
192
|
it('omits --all when false', async () => {
|
|
192
193
|
await harness.callTool('gog_classroom_coursework_list', { courseId: 'c1', all: false });
|
|
193
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'list', 'c1'], { account: undefined });
|
|
194
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'list', pos('c1')], { account: undefined });
|
|
194
195
|
});
|
|
195
196
|
|
|
196
197
|
it('returns error text on failure', async () => {
|
|
@@ -203,7 +204,7 @@ describe('gog_classroom_coursework_list', () => {
|
|
|
203
204
|
describe('gog_classroom_coursework_get', () => {
|
|
204
205
|
it('calls run with courseId and courseworkId', async () => {
|
|
205
206
|
await harness.callTool('gog_classroom_coursework_get', { courseId: 'c1', courseworkId: 'w1' });
|
|
206
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'get', 'c1', 'w1'], { account: undefined });
|
|
207
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'get', pos('c1'), pos('w1')], { account: undefined });
|
|
207
208
|
});
|
|
208
209
|
|
|
209
210
|
it('returns error text on failure', async () => {
|
|
@@ -216,7 +217,7 @@ describe('gog_classroom_coursework_get', () => {
|
|
|
216
217
|
describe('gog_classroom_submissions_list', () => {
|
|
217
218
|
it('calls run with ids only', async () => {
|
|
218
219
|
await harness.callTool('gog_classroom_submissions_list', { courseId: 'c1', courseworkId: 'w1' });
|
|
219
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'list', 'c1', 'w1'], { account: undefined });
|
|
220
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'list', pos('c1'), pos('w1')], { account: undefined });
|
|
220
221
|
});
|
|
221
222
|
|
|
222
223
|
it('passes all listing flags', async () => {
|
|
@@ -231,14 +232,14 @@ describe('gog_classroom_submissions_list', () => {
|
|
|
231
232
|
all: true,
|
|
232
233
|
});
|
|
233
234
|
expect(runner.run).toHaveBeenCalledWith(
|
|
234
|
-
['classroom', 'submissions', 'list', 'c1', 'w1', '--state=TURNED_IN', '--late=late', '--user=u1', '--max=20', '--page=tok', '--all'],
|
|
235
|
+
['classroom', 'submissions', 'list', pos('c1'), pos('w1'), '--state=TURNED_IN', '--late=late', '--user=u1', '--max=20', '--page=tok', '--all'],
|
|
235
236
|
{ account: undefined },
|
|
236
237
|
);
|
|
237
238
|
});
|
|
238
239
|
|
|
239
240
|
it('omits --all when false', async () => {
|
|
240
241
|
await harness.callTool('gog_classroom_submissions_list', { courseId: 'c1', courseworkId: 'w1', all: false });
|
|
241
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'list', 'c1', 'w1'], { account: undefined });
|
|
242
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'list', pos('c1'), pos('w1')], { account: undefined });
|
|
242
243
|
});
|
|
243
244
|
|
|
244
245
|
it('returns error text on failure', async () => {
|
|
@@ -251,7 +252,7 @@ describe('gog_classroom_submissions_list', () => {
|
|
|
251
252
|
describe('gog_classroom_submissions_get', () => {
|
|
252
253
|
it('calls run with all three ids', async () => {
|
|
253
254
|
await harness.callTool('gog_classroom_submissions_get', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
|
|
254
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'get', 'c1', 'w1', 's1'], { account: undefined });
|
|
255
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'get', pos('c1'), pos('w1'), pos('s1')], { account: undefined });
|
|
255
256
|
});
|
|
256
257
|
|
|
257
258
|
it('returns error text on failure', async () => {
|
|
@@ -264,13 +265,13 @@ describe('gog_classroom_submissions_get', () => {
|
|
|
264
265
|
describe('gog_classroom_submissions_grade', () => {
|
|
265
266
|
it('calls run with ids only', async () => {
|
|
266
267
|
await harness.callTool('gog_classroom_submissions_grade', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
|
|
267
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'grade', 'c1', 'w1', 's1'], { account: undefined });
|
|
268
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'grade', pos('c1'), pos('w1'), pos('s1')], { account: undefined });
|
|
268
269
|
});
|
|
269
270
|
|
|
270
271
|
it('passes --draft and --assigned when provided', async () => {
|
|
271
272
|
await harness.callTool('gog_classroom_submissions_grade', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1', draft: '90', assigned: '95' });
|
|
272
273
|
expect(runner.run).toHaveBeenCalledWith(
|
|
273
|
-
['classroom', 'submissions', 'grade', 'c1', 'w1', 's1', '--draft=90', '--assigned=95'],
|
|
274
|
+
['classroom', 'submissions', 'grade', pos('c1'), pos('w1'), pos('s1'), '--draft=90', '--assigned=95'],
|
|
274
275
|
{ account: undefined },
|
|
275
276
|
);
|
|
276
277
|
});
|
|
@@ -285,7 +286,7 @@ describe('gog_classroom_submissions_grade', () => {
|
|
|
285
286
|
describe('gog_classroom_submissions_return', () => {
|
|
286
287
|
it('calls run with ids', async () => {
|
|
287
288
|
await harness.callTool('gog_classroom_submissions_return', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
|
|
288
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'return', 'c1', 'w1', 's1'], { account: undefined });
|
|
289
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'return', pos('c1'), pos('w1'), pos('s1')], { account: undefined });
|
|
289
290
|
});
|
|
290
291
|
|
|
291
292
|
it('returns error text on failure', async () => {
|
|
@@ -298,7 +299,7 @@ describe('gog_classroom_submissions_return', () => {
|
|
|
298
299
|
describe('gog_classroom_submissions_turn_in', () => {
|
|
299
300
|
it('calls run with ids', async () => {
|
|
300
301
|
await harness.callTool('gog_classroom_submissions_turn_in', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
|
|
301
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'turn-in', 'c1', 'w1', 's1'], { account: undefined });
|
|
302
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'turn-in', pos('c1'), pos('w1'), pos('s1')], { account: undefined });
|
|
302
303
|
});
|
|
303
304
|
|
|
304
305
|
it('returns error text on failure', async () => {
|
|
@@ -311,7 +312,7 @@ describe('gog_classroom_submissions_turn_in', () => {
|
|
|
311
312
|
describe('gog_classroom_submissions_reclaim', () => {
|
|
312
313
|
it('calls run with ids', async () => {
|
|
313
314
|
await harness.callTool('gog_classroom_submissions_reclaim', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
|
|
314
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'reclaim', 'c1', 'w1', 's1'], { account: undefined });
|
|
315
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'reclaim', pos('c1'), pos('w1'), pos('s1')], { account: undefined });
|
|
315
316
|
});
|
|
316
317
|
|
|
317
318
|
it('returns error text on failure', async () => {
|
|
@@ -324,20 +325,20 @@ describe('gog_classroom_submissions_reclaim', () => {
|
|
|
324
325
|
describe('gog_classroom_announcements_list', () => {
|
|
325
326
|
it('calls run with courseId only', async () => {
|
|
326
327
|
await harness.callTool('gog_classroom_announcements_list', { courseId: 'c1' });
|
|
327
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'list', 'c1'], { account: undefined });
|
|
328
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'list', pos('c1')], { account: undefined });
|
|
328
329
|
});
|
|
329
330
|
|
|
330
331
|
it('passes all listing flags', async () => {
|
|
331
332
|
await harness.callTool('gog_classroom_announcements_list', { courseId: 'c1', state: 'PUBLISHED', orderBy: 'updateTime desc', max: 20, page: 'tok', all: true });
|
|
332
333
|
expect(runner.run).toHaveBeenCalledWith(
|
|
333
|
-
['classroom', 'announcements', 'list', 'c1', '--state=PUBLISHED', '--order-by=updateTime desc', '--max=20', '--page=tok', '--all'],
|
|
334
|
+
['classroom', 'announcements', 'list', pos('c1'), '--state=PUBLISHED', '--order-by=updateTime desc', '--max=20', '--page=tok', '--all'],
|
|
334
335
|
{ account: undefined },
|
|
335
336
|
);
|
|
336
337
|
});
|
|
337
338
|
|
|
338
339
|
it('omits --all when false', async () => {
|
|
339
340
|
await harness.callTool('gog_classroom_announcements_list', { courseId: 'c1', all: false });
|
|
340
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'list', 'c1'], { account: undefined });
|
|
341
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'list', pos('c1')], { account: undefined });
|
|
341
342
|
});
|
|
342
343
|
|
|
343
344
|
it('returns error text on failure', async () => {
|
|
@@ -350,7 +351,7 @@ describe('gog_classroom_announcements_list', () => {
|
|
|
350
351
|
describe('gog_classroom_announcements_get', () => {
|
|
351
352
|
it('calls run with ids', async () => {
|
|
352
353
|
await harness.callTool('gog_classroom_announcements_get', { courseId: 'c1', announcementId: 'a1' });
|
|
353
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'get', 'c1', 'a1'], { account: undefined });
|
|
354
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'get', pos('c1'), pos('a1')], { account: undefined });
|
|
354
355
|
});
|
|
355
356
|
|
|
356
357
|
it('returns error text on failure', async () => {
|
|
@@ -364,7 +365,7 @@ describe('gog_classroom_announcements_create', () => {
|
|
|
364
365
|
it('calls run with required text only', async () => {
|
|
365
366
|
await harness.callTool('gog_classroom_announcements_create', { courseId: 'c1', text: 'Hi' });
|
|
366
367
|
expect(runner.run).toHaveBeenCalledWith(
|
|
367
|
-
['classroom', 'announcements', 'create', 'c1', '--text=Hi'],
|
|
368
|
+
['classroom', 'announcements', 'create', pos('c1'), '--text=Hi'],
|
|
368
369
|
{ account: undefined },
|
|
369
370
|
);
|
|
370
371
|
});
|
|
@@ -372,7 +373,7 @@ describe('gog_classroom_announcements_create', () => {
|
|
|
372
373
|
it('passes all optional flags', async () => {
|
|
373
374
|
await harness.callTool('gog_classroom_announcements_create', { courseId: 'c1', text: 'Hi', state: 'DRAFT', scheduled: '2026-05-01T12:00' });
|
|
374
375
|
expect(runner.run).toHaveBeenCalledWith(
|
|
375
|
-
['classroom', 'announcements', 'create', 'c1', '--text=Hi', '--state=DRAFT', '--scheduled=2026-05-01T12:00'],
|
|
376
|
+
['classroom', 'announcements', 'create', pos('c1'), '--text=Hi', '--state=DRAFT', '--scheduled=2026-05-01T12:00'],
|
|
376
377
|
{ account: undefined },
|
|
377
378
|
);
|
|
378
379
|
});
|
|
@@ -387,20 +388,20 @@ describe('gog_classroom_announcements_create', () => {
|
|
|
387
388
|
describe('gog_classroom_topics_list', () => {
|
|
388
389
|
it('calls run with courseId only', async () => {
|
|
389
390
|
await harness.callTool('gog_classroom_topics_list', { courseId: 'c1' });
|
|
390
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'list', 'c1'], { account: undefined });
|
|
391
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'list', pos('c1')], { account: undefined });
|
|
391
392
|
});
|
|
392
393
|
|
|
393
394
|
it('passes all listing flags', async () => {
|
|
394
395
|
await harness.callTool('gog_classroom_topics_list', { courseId: 'c1', max: 20, page: 'tok', all: true });
|
|
395
396
|
expect(runner.run).toHaveBeenCalledWith(
|
|
396
|
-
['classroom', 'topics', 'list', 'c1', '--max=20', '--page=tok', '--all'],
|
|
397
|
+
['classroom', 'topics', 'list', pos('c1'), '--max=20', '--page=tok', '--all'],
|
|
397
398
|
{ account: undefined },
|
|
398
399
|
);
|
|
399
400
|
});
|
|
400
401
|
|
|
401
402
|
it('omits --all when false', async () => {
|
|
402
403
|
await harness.callTool('gog_classroom_topics_list', { courseId: 'c1', all: false });
|
|
403
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'list', 'c1'], { account: undefined });
|
|
404
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'list', pos('c1')], { account: undefined });
|
|
404
405
|
});
|
|
405
406
|
|
|
406
407
|
it('returns error text on failure', async () => {
|
|
@@ -413,7 +414,7 @@ describe('gog_classroom_topics_list', () => {
|
|
|
413
414
|
describe('gog_classroom_topics_get', () => {
|
|
414
415
|
it('calls run with ids', async () => {
|
|
415
416
|
await harness.callTool('gog_classroom_topics_get', { courseId: 'c1', topicId: 't1' });
|
|
416
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'get', 'c1', 't1'], { account: undefined });
|
|
417
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'get', pos('c1'), pos('t1')], { account: undefined });
|
|
417
418
|
});
|
|
418
419
|
|
|
419
420
|
it('returns error text on failure', async () => {
|
|
@@ -452,7 +453,7 @@ describe('gog_classroom_invitations_list', () => {
|
|
|
452
453
|
describe('gog_classroom_invitations_get', () => {
|
|
453
454
|
it('calls run with invitationId', async () => {
|
|
454
455
|
await harness.callTool('gog_classroom_invitations_get', { invitationId: 'i1' });
|
|
455
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'invitations', 'get', 'i1'], { account: undefined });
|
|
456
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'invitations', 'get', pos('i1')], { account: undefined });
|
|
456
457
|
});
|
|
457
458
|
|
|
458
459
|
it('returns error text on failure', async () => {
|
|
@@ -465,7 +466,7 @@ describe('gog_classroom_invitations_get', () => {
|
|
|
465
466
|
describe('gog_classroom_invitations_accept', () => {
|
|
466
467
|
it('calls run with invitationId', async () => {
|
|
467
468
|
await harness.callTool('gog_classroom_invitations_accept', { invitationId: 'i1' });
|
|
468
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'invitations', 'accept', 'i1'], { account: undefined });
|
|
469
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'invitations', 'accept', pos('i1')], { account: undefined });
|
|
469
470
|
});
|
|
470
471
|
|
|
471
472
|
it('returns error text on failure', async () => {
|
|
@@ -483,7 +484,7 @@ describe('gog_classroom_profile_get', () => {
|
|
|
483
484
|
|
|
484
485
|
it('passes userId when provided', async () => {
|
|
485
486
|
await harness.callTool('gog_classroom_profile_get', { userId: 'u1' });
|
|
486
|
-
expect(runner.run).toHaveBeenCalledWith(['classroom', 'profile', 'get', 'u1'], { account: undefined });
|
|
487
|
+
expect(runner.run).toHaveBeenCalledWith(['classroom', 'profile', 'get', pos('u1')], { account: undefined });
|
|
487
488
|
});
|
|
488
489
|
|
|
489
490
|
it('returns error text on failure', async () => {
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import { registerContactsTools } from '../../src/tools/contacts.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_contacts_search', () => {
|
|
|
14
15
|
vi.mocked(runner.run).mockResolvedValue('{"connections":[]}');
|
|
15
16
|
const harness = await setupHandlers();
|
|
16
17
|
await harness.callTool('gog_contacts_search', { query: 'alice' });
|
|
17
|
-
expect(runner.run).toHaveBeenCalledWith(['contacts', 'search', 'alice'], { account: undefined });
|
|
18
|
+
expect(runner.run).toHaveBeenCalledWith(['contacts', 'search', pos('alice')], { account: undefined });
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
it('returns error text on failure', async () => {
|
|
@@ -46,7 +47,7 @@ describe('gog_contacts_get', () => {
|
|
|
46
47
|
vi.mocked(runner.run).mockResolvedValue('{"resourceName":"people/c123"}');
|
|
47
48
|
const harness = await setupHandlers();
|
|
48
49
|
await harness.callTool('gog_contacts_get', { resourceName: 'people/c123' });
|
|
49
|
-
expect(runner.run).toHaveBeenCalledWith(['contacts', 'get', 'people/c123'], { account: undefined });
|
|
50
|
+
expect(runner.run).toHaveBeenCalledWith(['contacts', 'get', pos('people/c123')], { account: undefined });
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
it('returns error text on failure', async () => {
|
package/tests/tools/docs.test.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { registerDocsTools } from '../../src/tools/docs.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_docs_info', () => {
|
|
|
14
15
|
vi.mocked(runner.run).mockResolvedValue('{"title":"My Doc","docId":"abc"}');
|
|
15
16
|
const harness = await setupHandlers();
|
|
16
17
|
const result = await harness.callTool('gog_docs_info', { docId: 'abc' });
|
|
17
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'info', 'abc'], { account: undefined });
|
|
18
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'info', pos('abc')], { account: undefined });
|
|
18
19
|
expect(result.content[0].text).toContain('My Doc');
|
|
19
20
|
});
|
|
20
21
|
|
|
@@ -49,7 +50,7 @@ describe('gog_docs_cat', () => {
|
|
|
49
50
|
vi.mocked(runner.run).mockResolvedValue('Hello world');
|
|
50
51
|
const harness = await setupHandlers();
|
|
51
52
|
const result = await harness.callTool('gog_docs_cat', { docId: 'abc' });
|
|
52
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'cat', 'abc'], { account: undefined });
|
|
53
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'cat', pos('abc')], { account: undefined });
|
|
53
54
|
expect(result.content[0].text).toBe('Hello world');
|
|
54
55
|
});
|
|
55
56
|
|
|
@@ -57,14 +58,14 @@ describe('gog_docs_cat', () => {
|
|
|
57
58
|
vi.mocked(runner.run).mockResolvedValue('text');
|
|
58
59
|
const harness = await setupHandlers();
|
|
59
60
|
await harness.callTool('gog_docs_cat', { docId: 'abc', account: 'other@gmail.com' });
|
|
60
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'cat', 'abc'], { account: 'other@gmail.com' });
|
|
61
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'cat', pos('abc')], { account: 'other@gmail.com' });
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
it('appends --chips when requested', async () => {
|
|
64
65
|
vi.mocked(runner.run).mockResolvedValue('Hello @person');
|
|
65
66
|
const harness = await setupHandlers();
|
|
66
67
|
await harness.callTool('gog_docs_cat', { docId: 'abc', chips: true });
|
|
67
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'cat', 'abc', '--chips'], { account: undefined });
|
|
68
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'cat', pos('abc'), '--chips'], { account: undefined });
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
it('returns error text on failure', async () => {
|
|
@@ -80,7 +81,7 @@ describe('gog_docs_create', () => {
|
|
|
80
81
|
vi.mocked(runner.run).mockResolvedValue('{"docId":"newid","title":"Meeting Notes"}');
|
|
81
82
|
const harness = await setupHandlers();
|
|
82
83
|
await harness.callTool('gog_docs_create', { title: 'Meeting Notes' });
|
|
83
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'create', 'Meeting Notes'], { account: undefined });
|
|
84
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'create', pos('Meeting Notes')], { account: undefined });
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
it('returns error text on failure', async () => {
|
|
@@ -96,7 +97,7 @@ describe('gog_docs_write', () => {
|
|
|
96
97
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
97
98
|
const harness = await setupHandlers();
|
|
98
99
|
await harness.callTool('gog_docs_write', { docId: 'abc', text: 'Hello world' });
|
|
99
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'write', 'abc', '--text=Hello world'], { account: undefined });
|
|
100
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'write', pos('abc'), '--text=Hello world'], { account: undefined });
|
|
100
101
|
});
|
|
101
102
|
|
|
102
103
|
it('adds --append flag when append is true', async () => {
|
|
@@ -104,7 +105,7 @@ describe('gog_docs_write', () => {
|
|
|
104
105
|
const harness = await setupHandlers();
|
|
105
106
|
await harness.callTool('gog_docs_write', { docId: 'abc', text: 'More text', append: true });
|
|
106
107
|
expect(runner.run).toHaveBeenCalledWith(
|
|
107
|
-
['docs', 'write', 'abc', '--text=More text', '--append'],
|
|
108
|
+
['docs', 'write', pos('abc'), '--text=More text', '--append'],
|
|
108
109
|
{ account: undefined },
|
|
109
110
|
);
|
|
110
111
|
});
|
|
@@ -115,7 +116,7 @@ describe('gog_docs_write', () => {
|
|
|
115
116
|
const harness = await setupHandlers();
|
|
116
117
|
await harness.callTool('gog_docs_write', { docId: 'abc', text: 'T', batch: 'b1' });
|
|
117
118
|
expect(runner.run).toHaveBeenCalledWith(
|
|
118
|
-
['docs', 'write', 'abc', '--text=T', '--batch=b1'],
|
|
119
|
+
['docs', 'write', pos('abc'), '--text=T', '--batch=b1'],
|
|
119
120
|
{ account: undefined },
|
|
120
121
|
);
|
|
121
122
|
});
|
|
@@ -126,7 +127,7 @@ describe('gog_docs_write', () => {
|
|
|
126
127
|
const harness = await setupHandlers();
|
|
127
128
|
await harness.callTool('gog_docs_write', { docId: 'abc', text: 'Rewrite', checkOrphans: true });
|
|
128
129
|
expect(runner.run).toHaveBeenCalledWith(
|
|
129
|
-
['docs', 'write', 'abc', '--text=Rewrite', '--check-orphans'],
|
|
130
|
+
['docs', 'write', pos('abc'), '--text=Rewrite', '--check-orphans'],
|
|
130
131
|
{ account: undefined },
|
|
131
132
|
);
|
|
132
133
|
});
|
|
@@ -135,7 +136,7 @@ describe('gog_docs_write', () => {
|
|
|
135
136
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
136
137
|
const harness = await setupHandlers();
|
|
137
138
|
await harness.callTool('gog_docs_write', { docId: 'abc', text: 'text', append: false });
|
|
138
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'write', 'abc', '--text=text'], { account: undefined });
|
|
139
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'write', pos('abc'), '--text=text'], { account: undefined });
|
|
139
140
|
});
|
|
140
141
|
|
|
141
142
|
// gog 0.30.0 paragraph list / indentation / spacing / keep controls
|
|
@@ -148,7 +149,7 @@ describe('gog_docs_write', () => {
|
|
|
148
149
|
keepLinesTogether: true, keepWithNext: true,
|
|
149
150
|
});
|
|
150
151
|
expect(runner.run).toHaveBeenCalledWith(
|
|
151
|
-
['docs', 'write', 'abc', '--text=List', '--bullets', '--bullet-preset=BULLET_DISC_CIRCLE_SQUARE',
|
|
152
|
+
['docs', 'write', pos('abc'), '--text=List', '--bullets', '--bullet-preset=BULLET_DISC_CIRCLE_SQUARE',
|
|
152
153
|
'--indent-start=18', '--indent-end=6', '--indent-first-line=36', '--space-above=4', '--space-below=8',
|
|
153
154
|
'--keep-lines-together', '--keep-with-next'],
|
|
154
155
|
{ account: undefined },
|
|
@@ -163,7 +164,7 @@ describe('gog_docs_write', () => {
|
|
|
163
164
|
keepLinesTogether: false, keepWithNext: false,
|
|
164
165
|
});
|
|
165
166
|
expect(runner.run).toHaveBeenCalledWith(
|
|
166
|
-
['docs', 'write', 'abc', '--text=Steps', '--ordered', '--no-bullets',
|
|
167
|
+
['docs', 'write', pos('abc'), '--text=Steps', '--ordered', '--no-bullets',
|
|
167
168
|
'--no-keep-lines-together', '--no-keep-with-next'],
|
|
168
169
|
{ account: undefined },
|
|
169
170
|
);
|
|
@@ -182,7 +183,7 @@ describe('gog_docs_find_replace', () => {
|
|
|
182
183
|
vi.mocked(runner.run).mockResolvedValue('{"occurrencesChanged":2}');
|
|
183
184
|
const harness = await setupHandlers();
|
|
184
185
|
await harness.callTool('gog_docs_find_replace', { docId: 'abc', find: 'foo', replace: 'bar' });
|
|
185
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'find-replace', 'abc', 'foo', 'bar'], { account: undefined });
|
|
186
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'find-replace', pos('abc'), pos('foo'), pos('bar')], { account: undefined });
|
|
186
187
|
});
|
|
187
188
|
|
|
188
189
|
it('returns error text on failure', async () => {
|
|
@@ -198,7 +199,7 @@ describe('gog_docs_structure', () => {
|
|
|
198
199
|
vi.mocked(runner.run).mockResolvedValue('[1] Heading\n[2] Paragraph');
|
|
199
200
|
const harness = await setupHandlers();
|
|
200
201
|
const result = await harness.callTool('gog_docs_structure', { docId: 'abc' });
|
|
201
|
-
expect(runner.run).toHaveBeenCalledWith(['docs', 'structure', 'abc'], { account: undefined });
|
|
202
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'structure', pos('abc')], { account: undefined });
|
|
202
203
|
expect(result.content[0].text).toContain('Heading');
|
|
203
204
|
});
|
|
204
205
|
|
|
@@ -210,6 +211,34 @@ describe('gog_docs_structure', () => {
|
|
|
210
211
|
});
|
|
211
212
|
});
|
|
212
213
|
|
|
214
|
+
// SEC-3/SEC-4 through the escape hatch: `export --out=~/.zshrc` writes
|
|
215
|
+
// attacker-shaped bytes over a shell profile. GOG_FILE_ROOTS binds it too.
|
|
216
|
+
describe('gog_docs_run path confinement', () => {
|
|
217
|
+
// The suite runs with GOG_FILE_ROOTS='/'; narrow it so confinement bites.
|
|
218
|
+
let prevRoots: string | undefined;
|
|
219
|
+
beforeEach(() => { prevRoots = process.env.GOG_FILE_ROOTS; process.env.GOG_FILE_ROOTS = '/nonexistent-gog-file-root'; });
|
|
220
|
+
afterEach(() => { process.env.GOG_FILE_ROOTS = prevRoots; });
|
|
221
|
+
|
|
222
|
+
it.each([
|
|
223
|
+
['export', ['d1', '--out=~/.zshrc']],
|
|
224
|
+
['export', ['d1', '--output', '~/.zshrc']],
|
|
225
|
+
['insert', ['d1', '--file=/etc/passwd']],
|
|
226
|
+
])('refuses docs %s %j', async (subcommand, args) => {
|
|
227
|
+
const harness = await setupHandlers();
|
|
228
|
+
const result = await harness.callTool('gog_docs_run', { subcommand, args });
|
|
229
|
+
expect(result.isError).toBe(true);
|
|
230
|
+
expect(result.content[0].text).toMatch(/outside the directories/);
|
|
231
|
+
expect(runner.run).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('refuses the -f short form, which would hide the path', async () => {
|
|
235
|
+
const harness = await setupHandlers();
|
|
236
|
+
const result = await harness.callTool('gog_docs_run', { subcommand: 'insert', args: ['d1', '-f', '/etc/passwd'] });
|
|
237
|
+
expect(result.isError).toBe(true);
|
|
238
|
+
expect(runner.run).not.toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
213
242
|
describe('gog_docs_run', () => {
|
|
214
243
|
it('passes raw subcommand and args to runner', async () => {
|
|
215
244
|
vi.mocked(runner.run).mockResolvedValue('{}');
|