gogcli-mcp 2.8.0 → 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.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +20218 -19700
  4. package/dist/lib.js +15845 -23331
  5. package/manifest.json +33 -1
  6. package/package.json +7 -6
  7. package/server.json +2 -2
  8. package/src/connector-auth.ts +46 -0
  9. package/src/connector-runtime.ts +177 -0
  10. package/src/index.ts +7 -5
  11. package/src/lib.ts +8 -7
  12. package/src/runner.ts +225 -30
  13. package/src/server.ts +21 -25
  14. package/src/tools/api.ts +65 -0
  15. package/src/tools/auth.ts +112 -15
  16. package/src/tools/calendar.ts +24 -9
  17. package/src/tools/docs.ts +28 -3
  18. package/src/tools/drive.ts +124 -1
  19. package/src/tools/gmail.ts +7 -3
  20. package/src/tools/sheets.ts +6 -3
  21. package/src/tools/slides.ts +9 -4
  22. package/src/tools/tasks.ts +3 -1
  23. package/src/tools/utils.ts +163 -27
  24. package/src/worker.ts +99 -0
  25. package/tests/connector-auth.test.ts +28 -0
  26. package/tests/connector-runtime.test.ts +474 -0
  27. package/tests/runner-file-args-failure.test.ts +94 -0
  28. package/tests/runner-file-args.test.ts +232 -0
  29. package/tests/runner.test.ts +221 -13
  30. package/tests/server.test.ts +28 -28
  31. package/tests/tools/api.test.ts +107 -0
  32. package/tests/tools/auth.test.ts +187 -31
  33. package/tests/tools/calendar.test.ts +115 -52
  34. package/tests/tools/classroom.test.ts +77 -77
  35. package/tests/tools/contacts.test.ts +24 -24
  36. package/tests/tools/docs.test.ts +84 -46
  37. package/tests/tools/drive.test.ts +226 -55
  38. package/tests/tools/gmail.test.ts +61 -28
  39. package/tests/tools/sheets.test.ts +81 -70
  40. package/tests/tools/slides.test.ts +56 -36
  41. package/tests/tools/tasks.test.ts +33 -33
  42. package/tests/tools/utils.test.ts +116 -2
  43. package/tests/worker.test.ts +142 -0
  44. package/tsconfig.json +4 -1
  45. package/vitest.config.ts +33 -2
  46. package/tests/helpers/test-harness.ts +0 -27
@@ -1,28 +1,28 @@
1
1
  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
- import { setupHandlers as setupHandlersBase, type ToolHandler } from '../helpers/test-harness.js';
4
+ import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
5
5
 
6
6
  vi.mock('../../src/runner.js');
7
7
 
8
- const setupHandlers = () => setupHandlersBase(registerClassroomTools);
8
+ const setupHandlers = () => createTestHarness(registerClassroomTools);
9
9
 
10
- let handlers: Map<string, ToolHandler>;
10
+ let harness: TestHarness;
11
11
 
12
- beforeEach(() => {
12
+ beforeEach(async () => {
13
13
  vi.clearAllMocks();
14
14
  vi.mocked(runner.run).mockResolvedValue('{}');
15
- handlers = setupHandlers();
15
+ harness = await setupHandlers();
16
16
  });
17
17
 
18
18
  describe('gog_classroom_courses_list', () => {
19
19
  it('calls run with no flags', async () => {
20
- await handlers.get('gog_classroom_courses_list')!({});
20
+ await harness.callTool('gog_classroom_courses_list', {});
21
21
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'courses', 'list'], { account: undefined });
22
22
  });
23
23
 
24
24
  it('passes all listing flags', async () => {
25
- await handlers.get('gog_classroom_courses_list')!({
25
+ await harness.callTool('gog_classroom_courses_list', {
26
26
  state: 'ACTIVE,ARCHIVED',
27
27
  teacher: 'teacher1',
28
28
  student: 'student1',
@@ -37,38 +37,38 @@ describe('gog_classroom_courses_list', () => {
37
37
  });
38
38
 
39
39
  it('omits --all when false', async () => {
40
- await handlers.get('gog_classroom_courses_list')!({ all: false });
40
+ await harness.callTool('gog_classroom_courses_list', { all: false });
41
41
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'courses', 'list'], { account: undefined });
42
42
  });
43
43
 
44
44
  it('returns error text on failure', async () => {
45
45
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
46
- const result = await handlers.get('gog_classroom_courses_list')!({});
46
+ const result = await harness.callTool('gog_classroom_courses_list', {});
47
47
  expect(result.content[0].text).toBe('Error: List failed');
48
48
  });
49
49
  });
50
50
 
51
51
  describe('gog_classroom_courses_get', () => {
52
52
  it('calls run with courseId', async () => {
53
- await handlers.get('gog_classroom_courses_get')!({ courseId: 'c1' });
53
+ await harness.callTool('gog_classroom_courses_get', { courseId: 'c1' });
54
54
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'courses', 'get', 'c1'], { account: undefined });
55
55
  });
56
56
 
57
57
  it('returns error text on failure', async () => {
58
58
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
59
- const result = await handlers.get('gog_classroom_courses_get')!({ courseId: 'bad' });
59
+ const result = await harness.callTool('gog_classroom_courses_get', { courseId: 'bad' });
60
60
  expect(result.content[0].text).toBe('Error: Not found');
61
61
  });
62
62
  });
63
63
 
64
64
  describe('gog_classroom_students_list', () => {
65
65
  it('calls run with courseId only', async () => {
66
- await handlers.get('gog_classroom_students_list')!({ courseId: 'c1' });
66
+ await harness.callTool('gog_classroom_students_list', { courseId: 'c1' });
67
67
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'list', 'c1'], { account: undefined });
68
68
  });
69
69
 
70
70
  it('passes all listing flags', async () => {
71
- await handlers.get('gog_classroom_students_list')!({ courseId: 'c1', max: 20, page: 'tok', all: true });
71
+ await harness.callTool('gog_classroom_students_list', { courseId: 'c1', max: 20, page: 'tok', all: true });
72
72
  expect(runner.run).toHaveBeenCalledWith(
73
73
  ['classroom', 'students', 'list', 'c1', '--max=20', '--page=tok', '--all'],
74
74
  { account: undefined },
@@ -76,38 +76,38 @@ describe('gog_classroom_students_list', () => {
76
76
  });
77
77
 
78
78
  it('omits --all when false', async () => {
79
- await handlers.get('gog_classroom_students_list')!({ courseId: 'c1', all: false });
79
+ await harness.callTool('gog_classroom_students_list', { courseId: 'c1', all: false });
80
80
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'list', 'c1'], { account: undefined });
81
81
  });
82
82
 
83
83
  it('returns error text on failure', async () => {
84
84
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
85
- const result = await handlers.get('gog_classroom_students_list')!({ courseId: 'c1' });
85
+ const result = await harness.callTool('gog_classroom_students_list', { courseId: 'c1' });
86
86
  expect(result.content[0].text).toBe('Error: List failed');
87
87
  });
88
88
  });
89
89
 
90
90
  describe('gog_classroom_students_get', () => {
91
91
  it('calls run with courseId and userId', async () => {
92
- await handlers.get('gog_classroom_students_get')!({ courseId: 'c1', userId: 'u1' });
92
+ await harness.callTool('gog_classroom_students_get', { courseId: 'c1', userId: 'u1' });
93
93
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'students', 'get', 'c1', 'u1'], { account: undefined });
94
94
  });
95
95
 
96
96
  it('returns error text on failure', async () => {
97
97
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
98
- const result = await handlers.get('gog_classroom_students_get')!({ courseId: 'c1', userId: 'bad' });
98
+ const result = await harness.callTool('gog_classroom_students_get', { courseId: 'c1', userId: 'bad' });
99
99
  expect(result.content[0].text).toBe('Error: Not found');
100
100
  });
101
101
  });
102
102
 
103
103
  describe('gog_classroom_teachers_list', () => {
104
104
  it('calls run with courseId only', async () => {
105
- await handlers.get('gog_classroom_teachers_list')!({ courseId: 'c1' });
105
+ await harness.callTool('gog_classroom_teachers_list', { courseId: 'c1' });
106
106
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'list', 'c1'], { account: undefined });
107
107
  });
108
108
 
109
109
  it('passes all listing flags', async () => {
110
- await handlers.get('gog_classroom_teachers_list')!({ courseId: 'c1', max: 20, page: 'tok', all: true });
110
+ await harness.callTool('gog_classroom_teachers_list', { courseId: 'c1', max: 20, page: 'tok', all: true });
111
111
  expect(runner.run).toHaveBeenCalledWith(
112
112
  ['classroom', 'teachers', 'list', 'c1', '--max=20', '--page=tok', '--all'],
113
113
  { account: undefined },
@@ -115,38 +115,38 @@ describe('gog_classroom_teachers_list', () => {
115
115
  });
116
116
 
117
117
  it('omits --all when false', async () => {
118
- await handlers.get('gog_classroom_teachers_list')!({ courseId: 'c1', all: false });
118
+ await harness.callTool('gog_classroom_teachers_list', { courseId: 'c1', all: false });
119
119
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'list', 'c1'], { account: undefined });
120
120
  });
121
121
 
122
122
  it('returns error text on failure', async () => {
123
123
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
124
- const result = await handlers.get('gog_classroom_teachers_list')!({ courseId: 'c1' });
124
+ const result = await harness.callTool('gog_classroom_teachers_list', { courseId: 'c1' });
125
125
  expect(result.content[0].text).toBe('Error: List failed');
126
126
  });
127
127
  });
128
128
 
129
129
  describe('gog_classroom_teachers_get', () => {
130
130
  it('calls run with courseId and userId', async () => {
131
- await handlers.get('gog_classroom_teachers_get')!({ courseId: 'c1', userId: 'u1' });
131
+ await harness.callTool('gog_classroom_teachers_get', { courseId: 'c1', userId: 'u1' });
132
132
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'teachers', 'get', 'c1', 'u1'], { account: undefined });
133
133
  });
134
134
 
135
135
  it('returns error text on failure', async () => {
136
136
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
137
- const result = await handlers.get('gog_classroom_teachers_get')!({ courseId: 'c1', userId: 'bad' });
137
+ const result = await harness.callTool('gog_classroom_teachers_get', { courseId: 'c1', userId: 'bad' });
138
138
  expect(result.content[0].text).toBe('Error: Not found');
139
139
  });
140
140
  });
141
141
 
142
142
  describe('gog_classroom_roster', () => {
143
143
  it('calls run with courseId only', async () => {
144
- await handlers.get('gog_classroom_roster')!({ courseId: 'c1' });
144
+ await harness.callTool('gog_classroom_roster', { courseId: 'c1' });
145
145
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'roster', 'c1'], { account: undefined });
146
146
  });
147
147
 
148
148
  it('passes all flags', async () => {
149
- await handlers.get('gog_classroom_roster')!({ courseId: 'c1', students: true, teachers: true, max: 50, page: 'tok', all: true });
149
+ await harness.callTool('gog_classroom_roster', { courseId: 'c1', students: true, teachers: true, max: 50, page: 'tok', all: true });
150
150
  expect(runner.run).toHaveBeenCalledWith(
151
151
  ['classroom', 'roster', 'c1', '--students', '--teachers', '--max=50', '--page=tok', '--all'],
152
152
  { account: undefined },
@@ -154,25 +154,25 @@ describe('gog_classroom_roster', () => {
154
154
  });
155
155
 
156
156
  it('omits boolean flags when false', async () => {
157
- await handlers.get('gog_classroom_roster')!({ courseId: 'c1', students: false, teachers: false, all: false });
157
+ await harness.callTool('gog_classroom_roster', { courseId: 'c1', students: false, teachers: false, all: false });
158
158
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'roster', 'c1'], { account: undefined });
159
159
  });
160
160
 
161
161
  it('returns error text on failure', async () => {
162
162
  vi.mocked(runner.run).mockRejectedValue(new Error('Roster failed'));
163
- const result = await handlers.get('gog_classroom_roster')!({ courseId: 'c1' });
163
+ const result = await harness.callTool('gog_classroom_roster', { courseId: 'c1' });
164
164
  expect(result.content[0].text).toBe('Error: Roster failed');
165
165
  });
166
166
  });
167
167
 
168
168
  describe('gog_classroom_coursework_list', () => {
169
169
  it('calls run with courseId only', async () => {
170
- await handlers.get('gog_classroom_coursework_list')!({ courseId: 'c1' });
170
+ await harness.callTool('gog_classroom_coursework_list', { courseId: 'c1' });
171
171
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'list', 'c1'], { account: undefined });
172
172
  });
173
173
 
174
174
  it('passes all listing flags', async () => {
175
- await handlers.get('gog_classroom_coursework_list')!({
175
+ await harness.callTool('gog_classroom_coursework_list', {
176
176
  courseId: 'c1',
177
177
  state: 'PUBLISHED',
178
178
  topic: 't1',
@@ -189,38 +189,38 @@ describe('gog_classroom_coursework_list', () => {
189
189
  });
190
190
 
191
191
  it('omits --all when false', async () => {
192
- await handlers.get('gog_classroom_coursework_list')!({ courseId: 'c1', all: false });
192
+ await harness.callTool('gog_classroom_coursework_list', { courseId: 'c1', all: false });
193
193
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'list', 'c1'], { account: undefined });
194
194
  });
195
195
 
196
196
  it('returns error text on failure', async () => {
197
197
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
198
- const result = await handlers.get('gog_classroom_coursework_list')!({ courseId: 'c1' });
198
+ const result = await harness.callTool('gog_classroom_coursework_list', { courseId: 'c1' });
199
199
  expect(result.content[0].text).toBe('Error: List failed');
200
200
  });
201
201
  });
202
202
 
203
203
  describe('gog_classroom_coursework_get', () => {
204
204
  it('calls run with courseId and courseworkId', async () => {
205
- await handlers.get('gog_classroom_coursework_get')!({ courseId: 'c1', courseworkId: 'w1' });
205
+ await harness.callTool('gog_classroom_coursework_get', { courseId: 'c1', courseworkId: 'w1' });
206
206
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'coursework', 'get', 'c1', 'w1'], { account: undefined });
207
207
  });
208
208
 
209
209
  it('returns error text on failure', async () => {
210
210
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
211
- const result = await handlers.get('gog_classroom_coursework_get')!({ courseId: 'c1', courseworkId: 'bad' });
211
+ const result = await harness.callTool('gog_classroom_coursework_get', { courseId: 'c1', courseworkId: 'bad' });
212
212
  expect(result.content[0].text).toBe('Error: Not found');
213
213
  });
214
214
  });
215
215
 
216
216
  describe('gog_classroom_submissions_list', () => {
217
217
  it('calls run with ids only', async () => {
218
- await handlers.get('gog_classroom_submissions_list')!({ courseId: 'c1', courseworkId: 'w1' });
218
+ await harness.callTool('gog_classroom_submissions_list', { courseId: 'c1', courseworkId: 'w1' });
219
219
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'list', 'c1', 'w1'], { account: undefined });
220
220
  });
221
221
 
222
222
  it('passes all listing flags', async () => {
223
- await handlers.get('gog_classroom_submissions_list')!({
223
+ await harness.callTool('gog_classroom_submissions_list', {
224
224
  courseId: 'c1',
225
225
  courseworkId: 'w1',
226
226
  state: 'TURNED_IN',
@@ -237,38 +237,38 @@ describe('gog_classroom_submissions_list', () => {
237
237
  });
238
238
 
239
239
  it('omits --all when false', async () => {
240
- await handlers.get('gog_classroom_submissions_list')!({ courseId: 'c1', courseworkId: 'w1', all: false });
240
+ await harness.callTool('gog_classroom_submissions_list', { courseId: 'c1', courseworkId: 'w1', all: false });
241
241
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'list', 'c1', 'w1'], { account: undefined });
242
242
  });
243
243
 
244
244
  it('returns error text on failure', async () => {
245
245
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
246
- const result = await handlers.get('gog_classroom_submissions_list')!({ courseId: 'c1', courseworkId: 'w1' });
246
+ const result = await harness.callTool('gog_classroom_submissions_list', { courseId: 'c1', courseworkId: 'w1' });
247
247
  expect(result.content[0].text).toBe('Error: List failed');
248
248
  });
249
249
  });
250
250
 
251
251
  describe('gog_classroom_submissions_get', () => {
252
252
  it('calls run with all three ids', async () => {
253
- await handlers.get('gog_classroom_submissions_get')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
253
+ await harness.callTool('gog_classroom_submissions_get', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
254
254
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'get', 'c1', 'w1', 's1'], { account: undefined });
255
255
  });
256
256
 
257
257
  it('returns error text on failure', async () => {
258
258
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
259
- const result = await handlers.get('gog_classroom_submissions_get')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 'bad' });
259
+ const result = await harness.callTool('gog_classroom_submissions_get', { courseId: 'c1', courseworkId: 'w1', submissionId: 'bad' });
260
260
  expect(result.content[0].text).toBe('Error: Not found');
261
261
  });
262
262
  });
263
263
 
264
264
  describe('gog_classroom_submissions_grade', () => {
265
265
  it('calls run with ids only', async () => {
266
- await handlers.get('gog_classroom_submissions_grade')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
266
+ await harness.callTool('gog_classroom_submissions_grade', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
267
267
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'grade', 'c1', 'w1', 's1'], { account: undefined });
268
268
  });
269
269
 
270
270
  it('passes --draft and --assigned when provided', async () => {
271
- await handlers.get('gog_classroom_submissions_grade')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1', draft: '90', assigned: '95' });
271
+ await harness.callTool('gog_classroom_submissions_grade', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1', draft: '90', assigned: '95' });
272
272
  expect(runner.run).toHaveBeenCalledWith(
273
273
  ['classroom', 'submissions', 'grade', 'c1', 'w1', 's1', '--draft=90', '--assigned=95'],
274
274
  { account: undefined },
@@ -277,58 +277,58 @@ describe('gog_classroom_submissions_grade', () => {
277
277
 
278
278
  it('returns error text on failure', async () => {
279
279
  vi.mocked(runner.run).mockRejectedValue(new Error('Grade failed'));
280
- const result = await handlers.get('gog_classroom_submissions_grade')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
280
+ const result = await harness.callTool('gog_classroom_submissions_grade', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
281
281
  expect(result.content[0].text).toBe('Error: Grade failed');
282
282
  });
283
283
  });
284
284
 
285
285
  describe('gog_classroom_submissions_return', () => {
286
286
  it('calls run with ids', async () => {
287
- await handlers.get('gog_classroom_submissions_return')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
287
+ await harness.callTool('gog_classroom_submissions_return', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
288
288
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'return', 'c1', 'w1', 's1'], { account: undefined });
289
289
  });
290
290
 
291
291
  it('returns error text on failure', async () => {
292
292
  vi.mocked(runner.run).mockRejectedValue(new Error('Return failed'));
293
- const result = await handlers.get('gog_classroom_submissions_return')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
293
+ const result = await harness.callTool('gog_classroom_submissions_return', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
294
294
  expect(result.content[0].text).toBe('Error: Return failed');
295
295
  });
296
296
  });
297
297
 
298
298
  describe('gog_classroom_submissions_turn_in', () => {
299
299
  it('calls run with ids', async () => {
300
- await handlers.get('gog_classroom_submissions_turn_in')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
300
+ await harness.callTool('gog_classroom_submissions_turn_in', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
301
301
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'turn-in', 'c1', 'w1', 's1'], { account: undefined });
302
302
  });
303
303
 
304
304
  it('returns error text on failure', async () => {
305
305
  vi.mocked(runner.run).mockRejectedValue(new Error('Turn-in failed'));
306
- const result = await handlers.get('gog_classroom_submissions_turn_in')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
306
+ const result = await harness.callTool('gog_classroom_submissions_turn_in', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
307
307
  expect(result.content[0].text).toBe('Error: Turn-in failed');
308
308
  });
309
309
  });
310
310
 
311
311
  describe('gog_classroom_submissions_reclaim', () => {
312
312
  it('calls run with ids', async () => {
313
- await handlers.get('gog_classroom_submissions_reclaim')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
313
+ await harness.callTool('gog_classroom_submissions_reclaim', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
314
314
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'submissions', 'reclaim', 'c1', 'w1', 's1'], { account: undefined });
315
315
  });
316
316
 
317
317
  it('returns error text on failure', async () => {
318
318
  vi.mocked(runner.run).mockRejectedValue(new Error('Reclaim failed'));
319
- const result = await handlers.get('gog_classroom_submissions_reclaim')!({ courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
319
+ const result = await harness.callTool('gog_classroom_submissions_reclaim', { courseId: 'c1', courseworkId: 'w1', submissionId: 's1' });
320
320
  expect(result.content[0].text).toBe('Error: Reclaim failed');
321
321
  });
322
322
  });
323
323
 
324
324
  describe('gog_classroom_announcements_list', () => {
325
325
  it('calls run with courseId only', async () => {
326
- await handlers.get('gog_classroom_announcements_list')!({ courseId: 'c1' });
326
+ await harness.callTool('gog_classroom_announcements_list', { courseId: 'c1' });
327
327
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'list', 'c1'], { account: undefined });
328
328
  });
329
329
 
330
330
  it('passes all listing flags', async () => {
331
- await handlers.get('gog_classroom_announcements_list')!({ courseId: 'c1', state: 'PUBLISHED', orderBy: 'updateTime desc', max: 20, page: 'tok', all: true });
331
+ await harness.callTool('gog_classroom_announcements_list', { courseId: 'c1', state: 'PUBLISHED', orderBy: 'updateTime desc', max: 20, page: 'tok', all: true });
332
332
  expect(runner.run).toHaveBeenCalledWith(
333
333
  ['classroom', 'announcements', 'list', 'c1', '--state=PUBLISHED', '--order-by=updateTime desc', '--max=20', '--page=tok', '--all'],
334
334
  { account: undefined },
@@ -336,33 +336,33 @@ describe('gog_classroom_announcements_list', () => {
336
336
  });
337
337
 
338
338
  it('omits --all when false', async () => {
339
- await handlers.get('gog_classroom_announcements_list')!({ courseId: 'c1', all: false });
339
+ await harness.callTool('gog_classroom_announcements_list', { courseId: 'c1', all: false });
340
340
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'list', 'c1'], { account: undefined });
341
341
  });
342
342
 
343
343
  it('returns error text on failure', async () => {
344
344
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
345
- const result = await handlers.get('gog_classroom_announcements_list')!({ courseId: 'c1' });
345
+ const result = await harness.callTool('gog_classroom_announcements_list', { courseId: 'c1' });
346
346
  expect(result.content[0].text).toBe('Error: List failed');
347
347
  });
348
348
  });
349
349
 
350
350
  describe('gog_classroom_announcements_get', () => {
351
351
  it('calls run with ids', async () => {
352
- await handlers.get('gog_classroom_announcements_get')!({ courseId: 'c1', announcementId: 'a1' });
352
+ await harness.callTool('gog_classroom_announcements_get', { courseId: 'c1', announcementId: 'a1' });
353
353
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'announcements', 'get', 'c1', 'a1'], { account: undefined });
354
354
  });
355
355
 
356
356
  it('returns error text on failure', async () => {
357
357
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
358
- const result = await handlers.get('gog_classroom_announcements_get')!({ courseId: 'c1', announcementId: 'bad' });
358
+ const result = await harness.callTool('gog_classroom_announcements_get', { courseId: 'c1', announcementId: 'bad' });
359
359
  expect(result.content[0].text).toBe('Error: Not found');
360
360
  });
361
361
  });
362
362
 
363
363
  describe('gog_classroom_announcements_create', () => {
364
364
  it('calls run with required text only', async () => {
365
- await handlers.get('gog_classroom_announcements_create')!({ courseId: 'c1', text: 'Hi' });
365
+ await harness.callTool('gog_classroom_announcements_create', { courseId: 'c1', text: 'Hi' });
366
366
  expect(runner.run).toHaveBeenCalledWith(
367
367
  ['classroom', 'announcements', 'create', 'c1', '--text=Hi'],
368
368
  { account: undefined },
@@ -370,7 +370,7 @@ describe('gog_classroom_announcements_create', () => {
370
370
  });
371
371
 
372
372
  it('passes all optional flags', async () => {
373
- await handlers.get('gog_classroom_announcements_create')!({ courseId: 'c1', text: 'Hi', state: 'DRAFT', scheduled: '2026-05-01T12:00' });
373
+ await harness.callTool('gog_classroom_announcements_create', { courseId: 'c1', text: 'Hi', state: 'DRAFT', scheduled: '2026-05-01T12:00' });
374
374
  expect(runner.run).toHaveBeenCalledWith(
375
375
  ['classroom', 'announcements', 'create', 'c1', '--text=Hi', '--state=DRAFT', '--scheduled=2026-05-01T12:00'],
376
376
  { account: undefined },
@@ -379,19 +379,19 @@ describe('gog_classroom_announcements_create', () => {
379
379
 
380
380
  it('returns error text on failure', async () => {
381
381
  vi.mocked(runner.run).mockRejectedValue(new Error('Create failed'));
382
- const result = await handlers.get('gog_classroom_announcements_create')!({ courseId: 'c1', text: 'x' });
382
+ const result = await harness.callTool('gog_classroom_announcements_create', { courseId: 'c1', text: 'x' });
383
383
  expect(result.content[0].text).toBe('Error: Create failed');
384
384
  });
385
385
  });
386
386
 
387
387
  describe('gog_classroom_topics_list', () => {
388
388
  it('calls run with courseId only', async () => {
389
- await handlers.get('gog_classroom_topics_list')!({ courseId: 'c1' });
389
+ await harness.callTool('gog_classroom_topics_list', { courseId: 'c1' });
390
390
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'list', 'c1'], { account: undefined });
391
391
  });
392
392
 
393
393
  it('passes all listing flags', async () => {
394
- await handlers.get('gog_classroom_topics_list')!({ courseId: 'c1', max: 20, page: 'tok', all: true });
394
+ await harness.callTool('gog_classroom_topics_list', { courseId: 'c1', max: 20, page: 'tok', all: true });
395
395
  expect(runner.run).toHaveBeenCalledWith(
396
396
  ['classroom', 'topics', 'list', 'c1', '--max=20', '--page=tok', '--all'],
397
397
  { account: undefined },
@@ -399,38 +399,38 @@ describe('gog_classroom_topics_list', () => {
399
399
  });
400
400
 
401
401
  it('omits --all when false', async () => {
402
- await handlers.get('gog_classroom_topics_list')!({ courseId: 'c1', all: false });
402
+ await harness.callTool('gog_classroom_topics_list', { courseId: 'c1', all: false });
403
403
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'list', 'c1'], { account: undefined });
404
404
  });
405
405
 
406
406
  it('returns error text on failure', async () => {
407
407
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
408
- const result = await handlers.get('gog_classroom_topics_list')!({ courseId: 'c1' });
408
+ const result = await harness.callTool('gog_classroom_topics_list', { courseId: 'c1' });
409
409
  expect(result.content[0].text).toBe('Error: List failed');
410
410
  });
411
411
  });
412
412
 
413
413
  describe('gog_classroom_topics_get', () => {
414
414
  it('calls run with ids', async () => {
415
- await handlers.get('gog_classroom_topics_get')!({ courseId: 'c1', topicId: 't1' });
415
+ await harness.callTool('gog_classroom_topics_get', { courseId: 'c1', topicId: 't1' });
416
416
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'topics', 'get', 'c1', 't1'], { account: undefined });
417
417
  });
418
418
 
419
419
  it('returns error text on failure', async () => {
420
420
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
421
- const result = await handlers.get('gog_classroom_topics_get')!({ courseId: 'c1', topicId: 'bad' });
421
+ const result = await harness.callTool('gog_classroom_topics_get', { courseId: 'c1', topicId: 'bad' });
422
422
  expect(result.content[0].text).toBe('Error: Not found');
423
423
  });
424
424
  });
425
425
 
426
426
  describe('gog_classroom_invitations_list', () => {
427
427
  it('calls run with no flags', async () => {
428
- await handlers.get('gog_classroom_invitations_list')!({});
428
+ await harness.callTool('gog_classroom_invitations_list', {});
429
429
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'invitations', 'list'], { account: undefined });
430
430
  });
431
431
 
432
432
  it('passes all listing flags', async () => {
433
- await handlers.get('gog_classroom_invitations_list')!({ course: 'c1', user: 'u1', max: 20, page: 'tok', all: true });
433
+ await harness.callTool('gog_classroom_invitations_list', { course: 'c1', user: 'u1', max: 20, page: 'tok', all: true });
434
434
  expect(runner.run).toHaveBeenCalledWith(
435
435
  ['classroom', 'invitations', 'list', '--course=c1', '--user=u1', '--max=20', '--page=tok', '--all'],
436
436
  { account: undefined },
@@ -438,75 +438,75 @@ describe('gog_classroom_invitations_list', () => {
438
438
  });
439
439
 
440
440
  it('omits --all when false', async () => {
441
- await handlers.get('gog_classroom_invitations_list')!({ all: false });
441
+ await harness.callTool('gog_classroom_invitations_list', { all: false });
442
442
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'invitations', 'list'], { account: undefined });
443
443
  });
444
444
 
445
445
  it('returns error text on failure', async () => {
446
446
  vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
447
- const result = await handlers.get('gog_classroom_invitations_list')!({});
447
+ const result = await harness.callTool('gog_classroom_invitations_list', {});
448
448
  expect(result.content[0].text).toBe('Error: List failed');
449
449
  });
450
450
  });
451
451
 
452
452
  describe('gog_classroom_invitations_get', () => {
453
453
  it('calls run with invitationId', async () => {
454
- await handlers.get('gog_classroom_invitations_get')!({ invitationId: 'i1' });
454
+ await harness.callTool('gog_classroom_invitations_get', { invitationId: 'i1' });
455
455
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'invitations', 'get', 'i1'], { account: undefined });
456
456
  });
457
457
 
458
458
  it('returns error text on failure', async () => {
459
459
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
460
- const result = await handlers.get('gog_classroom_invitations_get')!({ invitationId: 'bad' });
460
+ const result = await harness.callTool('gog_classroom_invitations_get', { invitationId: 'bad' });
461
461
  expect(result.content[0].text).toBe('Error: Not found');
462
462
  });
463
463
  });
464
464
 
465
465
  describe('gog_classroom_invitations_accept', () => {
466
466
  it('calls run with invitationId', async () => {
467
- await handlers.get('gog_classroom_invitations_accept')!({ invitationId: 'i1' });
467
+ await harness.callTool('gog_classroom_invitations_accept', { invitationId: 'i1' });
468
468
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'invitations', 'accept', 'i1'], { account: undefined });
469
469
  });
470
470
 
471
471
  it('returns error text on failure', async () => {
472
472
  vi.mocked(runner.run).mockRejectedValue(new Error('Accept failed'));
473
- const result = await handlers.get('gog_classroom_invitations_accept')!({ invitationId: 'bad' });
473
+ const result = await harness.callTool('gog_classroom_invitations_accept', { invitationId: 'bad' });
474
474
  expect(result.content[0].text).toBe('Error: Accept failed');
475
475
  });
476
476
  });
477
477
 
478
478
  describe('gog_classroom_profile_get', () => {
479
479
  it('calls run with no userId (self)', async () => {
480
- await handlers.get('gog_classroom_profile_get')!({});
480
+ await harness.callTool('gog_classroom_profile_get', {});
481
481
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'profile', 'get'], { account: undefined });
482
482
  });
483
483
 
484
484
  it('passes userId when provided', async () => {
485
- await handlers.get('gog_classroom_profile_get')!({ userId: 'u1' });
485
+ await harness.callTool('gog_classroom_profile_get', { userId: 'u1' });
486
486
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'profile', 'get', 'u1'], { account: undefined });
487
487
  });
488
488
 
489
489
  it('returns error text on failure', async () => {
490
490
  vi.mocked(runner.run).mockRejectedValue(new Error('Profile failed'));
491
- const result = await handlers.get('gog_classroom_profile_get')!({});
491
+ const result = await harness.callTool('gog_classroom_profile_get', {});
492
492
  expect(result.content[0].text).toBe('Error: Profile failed');
493
493
  });
494
494
  });
495
495
 
496
496
  describe('gog_classroom_run', () => {
497
497
  it('passes subcommand and args to runner', async () => {
498
- await handlers.get('gog_classroom_run')!({ subcommand: 'guardians', args: ['list', 'u1'] });
498
+ await harness.callTool('gog_classroom_run', { subcommand: 'guardians', args: ['list', 'u1'] });
499
499
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'guardians', 'list', 'u1'], { account: undefined });
500
500
  });
501
501
 
502
502
  it('passes account through', async () => {
503
- await handlers.get('gog_classroom_run')!({ subcommand: 'materials', args: [], account: 'a@b.com' });
503
+ await harness.callTool('gog_classroom_run', { subcommand: 'materials', args: [], account: 'a@b.com' });
504
504
  expect(runner.run).toHaveBeenCalledWith(['classroom', 'materials'], { account: 'a@b.com' });
505
505
  });
506
506
 
507
507
  it('returns error text on failure', async () => {
508
508
  vi.mocked(runner.run).mockRejectedValue(new Error('Run failed'));
509
- const result = await handlers.get('gog_classroom_run')!({ subcommand: 'guardians', args: [] });
509
+ const result = await harness.callTool('gog_classroom_run', { subcommand: 'guardians', args: [] });
510
510
  expect(result.content[0].text).toBe('Error: Run failed');
511
511
  });
512
512
  });