gogcli-mcp-drive 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.
@@ -3,6 +3,7 @@ import { registerExtraDriveTools } from '../../src/tools/drive-extra.js';
3
3
  import * as lib from '../../../gogcli-mcp/src/lib.js';
4
4
  import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
5
5
  import { rawTextResult } from '@chrischall/mcp-utils';
6
+ import { pos } from '../../../gogcli-mcp/src/argv.js';
6
7
 
7
8
  vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
8
9
  const actual = await importOriginal<typeof lib>();
@@ -23,13 +24,13 @@ beforeEach(async () => {
23
24
  describe('gog_drive_download', () => {
24
25
  it('calls runOrDiagnose with fileId only', async () => {
25
26
  await harness.callTool('gog_drive_download', { fileId: 'f1' });
26
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'download', 'f1'], { account: undefined });
27
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'download', pos('f1')], { account: undefined });
27
28
  });
28
29
 
29
30
  it('passes --out and --format when provided', async () => {
30
31
  await harness.callTool('gog_drive_download', { fileId: 'f1', out: '/tmp/file.pdf', format: 'pdf', account: 'a@b.com' });
31
32
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
32
- ['drive', 'download', 'f1', '--out=/tmp/file.pdf', '--format=pdf'],
33
+ ['drive', 'download', pos('f1'), '--out=/tmp/file.pdf', '--format=pdf'],
33
34
  { account: 'a@b.com' },
34
35
  );
35
36
  });
@@ -37,7 +38,7 @@ describe('gog_drive_download', () => {
37
38
  it('passes --overwrite when requested', async () => {
38
39
  await harness.callTool('gog_drive_download', { fileId: 'f1', out: '/tmp/file.pdf', overwrite: true });
39
40
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
40
- ['drive', 'download', 'f1', '--out=/tmp/file.pdf', '--overwrite'],
41
+ ['drive', 'download', pos('f1'), '--out=/tmp/file.pdf', '--overwrite'],
41
42
  { account: undefined },
42
43
  );
43
44
  });
@@ -46,7 +47,7 @@ describe('gog_drive_download', () => {
46
47
  describe('gog_drive_upload', () => {
47
48
  it('calls runOrDiagnose with localPath only', async () => {
48
49
  await harness.callTool('gog_drive_upload', { localPath: '/tmp/x.txt' });
49
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', '/tmp/x.txt'], { account: undefined });
50
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', pos('/tmp/x.txt')], { account: undefined });
50
51
  });
51
52
 
52
53
  it('passes all upload flags', async () => {
@@ -62,7 +63,7 @@ describe('gog_drive_upload', () => {
62
63
  });
63
64
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
64
65
  [
65
- 'drive', 'upload', '/tmp/x.txt',
66
+ 'drive', 'upload', pos('/tmp/x.txt'),
66
67
  '--name=renamed.txt',
67
68
  '--parent=folder1',
68
69
  '--replace=file2',
@@ -77,7 +78,7 @@ describe('gog_drive_upload', () => {
77
78
 
78
79
  it('omits boolean flags when false', async () => {
79
80
  await harness.callTool('gog_drive_upload', { localPath: '/tmp/x.txt', keepRevisionForever: false, convert: false });
80
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', '/tmp/x.txt'], { account: undefined });
81
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', pos('/tmp/x.txt')], { account: undefined });
81
82
  });
82
83
 
83
84
  // gog 0.35.0 (openclaw/gogcli 1b26124) adds --if-version: an atomic
@@ -86,7 +87,7 @@ describe('gog_drive_upload', () => {
86
87
  it('passes --if-version alongside --replace for conditional replacement', async () => {
87
88
  await harness.callTool('gog_drive_upload', { localPath: '/tmp/x.txt', replace: 'file2', ifVersion: 7 });
88
89
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
89
- ['drive', 'upload', '/tmp/x.txt', '--replace=file2', '--if-version=7'],
90
+ ['drive', 'upload', pos('/tmp/x.txt'), '--replace=file2', '--if-version=7'],
90
91
  { account: undefined },
91
92
  );
92
93
  });
@@ -179,7 +180,7 @@ describe('gog_drive_upload', () => {
179
180
  // positional, no file arg, for every local stdio caller that has one.
180
181
  it('leaves the localPath path unchanged', async () => {
181
182
  await harness.callTool('gog_drive_upload', { localPath: '/tmp/x.txt' });
182
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', '/tmp/x.txt'], { account: undefined });
183
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', pos('/tmp/x.txt')], { account: undefined });
183
184
  });
184
185
  });
185
186
 
@@ -208,7 +209,7 @@ describe('gog_drive_sync_push', () => {
208
209
  it('calls runOrDiagnose with localPath and required parent', async () => {
209
210
  await harness.callTool('gog_drive_sync_push', { localPath: '/tmp/dir', parent: 'folder1' });
210
211
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
211
- ['drive', 'sync', 'push', '/tmp/dir', '--parent=folder1'],
212
+ ['drive', 'sync', 'push', pos('/tmp/dir'), '--parent=folder1'],
212
213
  { account: undefined },
213
214
  );
214
215
  });
@@ -218,7 +219,7 @@ describe('gog_drive_sync_push', () => {
218
219
  localPath: '/tmp/dir', parent: 'folder1', dryRun: true, allDrives: false,
219
220
  });
220
221
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
221
- ['drive', 'sync', 'push', '/tmp/dir', '--parent=folder1', '--dry-run', '--no-all-drives'],
222
+ ['drive', 'sync', 'push', pos('/tmp/dir'), '--parent=folder1', '--dry-run', '--no-all-drives'],
222
223
  { account: undefined },
223
224
  );
224
225
  });
@@ -228,7 +229,7 @@ describe('gog_drive_sync_push', () => {
228
229
  localPath: '/tmp/dir', parent: 'folder1', dryRun: false, allDrives: true,
229
230
  });
230
231
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
231
- ['drive', 'sync', 'push', '/tmp/dir', '--parent=folder1'],
232
+ ['drive', 'sync', 'push', pos('/tmp/dir'), '--parent=folder1'],
232
233
  { account: undefined },
233
234
  );
234
235
  });
@@ -237,13 +238,13 @@ describe('gog_drive_sync_push', () => {
237
238
  describe('gog_drive_copy', () => {
238
239
  it('calls runOrDiagnose with fileId and name', async () => {
239
240
  await harness.callTool('gog_drive_copy', { fileId: 'f1', name: 'Copy' });
240
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'copy', 'f1', 'Copy'], { account: undefined });
241
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'copy', pos('f1'), pos('Copy')], { account: undefined });
241
242
  });
242
243
 
243
244
  it('passes --parent when provided', async () => {
244
245
  await harness.callTool('gog_drive_copy', { fileId: 'f1', name: 'Copy', parent: 'folder1' });
245
246
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
246
- ['drive', 'copy', 'f1', 'Copy', '--parent=folder1'],
247
+ ['drive', 'copy', pos('f1'), pos('Copy'), '--parent=folder1'],
247
248
  { account: undefined },
248
249
  );
249
250
  });
@@ -252,25 +253,25 @@ describe('gog_drive_copy', () => {
252
253
  describe('gog_drive_url', () => {
253
254
  it('calls runOrDiagnose with single fileId', async () => {
254
255
  await harness.callTool('gog_drive_url', { fileIds: ['f1'] });
255
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'url', 'f1'], { account: undefined });
256
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'url', pos('f1')], { account: undefined });
256
257
  });
257
258
 
258
259
  it('calls runOrDiagnose with multiple fileIds', async () => {
259
260
  await harness.callTool('gog_drive_url', { fileIds: ['f1', 'f2', 'f3'] });
260
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'url', 'f1', 'f2', 'f3'], { account: undefined });
261
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'url', pos('f1'), pos('f2'), pos('f3')], { account: undefined });
261
262
  });
262
263
  });
263
264
 
264
265
  describe('gog_drive_permissions', () => {
265
266
  it('calls runOrDiagnose with fileId', async () => {
266
267
  await harness.callTool('gog_drive_permissions', { fileId: 'f1' });
267
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'permissions', 'f1'], { account: undefined });
268
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'permissions', pos('f1')], { account: undefined });
268
269
  });
269
270
 
270
271
  it('passes --max and --page when provided', async () => {
271
272
  await harness.callTool('gog_drive_permissions', { fileId: 'f1', max: 50, page: 'tok' });
272
273
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
273
- ['drive', 'permissions', 'f1', '--max=50', '--page=tok'],
274
+ ['drive', 'permissions', pos('f1'), '--max=50', '--page=tok'],
274
275
  { account: undefined },
275
276
  );
276
277
  });
@@ -280,7 +281,7 @@ describe('gog_drive_unshare', () => {
280
281
  it('calls runOrDiagnose with fileId and permissionId', async () => {
281
282
  await harness.callTool('gog_drive_unshare', { fileId: 'f1', permissionId: 'p1' });
282
283
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
283
- ['drive', 'unshare', 'f1', 'p1', '--force'],
284
+ ['drive', 'unshare', pos('f1'), pos('p1'), '--force'],
284
285
  { account: undefined },
285
286
  );
286
287
  });
@@ -310,7 +311,7 @@ describe('gog_drive_comments_list', () => {
310
311
  it('calls runOrDiagnose with fileId', async () => {
311
312
  await harness.callTool('gog_drive_comments_list', { fileId: 'f1' });
312
313
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
313
- ['drive', 'comments', 'list', 'f1'],
314
+ ['drive', 'comments', 'list', pos('f1')],
314
315
  { account: undefined },
315
316
  );
316
317
  });
@@ -321,7 +322,7 @@ describe('gog_drive_comments_list', () => {
321
322
  fileId: 'f1', since: '2026-06-01T00:00:00Z', includeQuoted: true, max: 5, page: 'tok', all: true,
322
323
  });
323
324
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
324
- ['drive', 'comments', 'list', 'f1', '--since=2026-06-01T00:00:00Z', '--include-quoted', '--max=5', '--page=tok', '--all'],
325
+ ['drive', 'comments', 'list', pos('f1'), '--since=2026-06-01T00:00:00Z', '--include-quoted', '--max=5', '--page=tok', '--all'],
325
326
  { account: undefined },
326
327
  );
327
328
  });
@@ -331,7 +332,7 @@ describe('gog_drive_comments_get', () => {
331
332
  it('calls runOrDiagnose with fileId and commentId', async () => {
332
333
  await harness.callTool('gog_drive_comments_get', { fileId: 'f1', commentId: 'c1' });
333
334
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
334
- ['drive', 'comments', 'get', 'f1', 'c1'],
335
+ ['drive', 'comments', 'get', pos('f1'), pos('c1')],
335
336
  { account: undefined },
336
337
  );
337
338
  });
@@ -341,7 +342,7 @@ describe('gog_drive_comments_add', () => {
341
342
  it('calls runOrDiagnose with fileId and content', async () => {
342
343
  await harness.callTool('gog_drive_comments_add', { fileId: 'f1', content: 'LGTM' });
343
344
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
344
- ['drive', 'comments', 'create', 'f1', 'LGTM'],
345
+ ['drive', 'comments', 'create', pos('f1'), pos('LGTM')],
345
346
  { account: undefined },
346
347
  );
347
348
  });
@@ -351,7 +352,7 @@ describe('gog_drive_comments_update', () => {
351
352
  it('calls runOrDiagnose with fileId, commentId, and content', async () => {
352
353
  await harness.callTool('gog_drive_comments_update', { fileId: 'f1', commentId: 'c1', content: 'edited' });
353
354
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
354
- ['drive', 'comments', 'update', 'f1', 'c1', 'edited'],
355
+ ['drive', 'comments', 'update', pos('f1'), pos('c1'), pos('edited')],
355
356
  { account: undefined },
356
357
  );
357
358
  });
@@ -361,7 +362,7 @@ describe('gog_drive_comments_delete', () => {
361
362
  it('calls runOrDiagnose with fileId and commentId', async () => {
362
363
  await harness.callTool('gog_drive_comments_delete', { fileId: 'f1', commentId: 'c1' });
363
364
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
364
- ['drive', 'comments', 'delete', 'f1', 'c1', '--force'],
365
+ ['drive', 'comments', 'delete', pos('f1'), pos('c1'), '--force'],
365
366
  { account: undefined },
366
367
  );
367
368
  });
@@ -371,7 +372,7 @@ describe('gog_drive_comments_reply', () => {
371
372
  it('calls runOrDiagnose with fileId, commentId, and content', async () => {
372
373
  await harness.callTool('gog_drive_comments_reply', { fileId: 'f1', commentId: 'c1', content: 'thanks' });
373
374
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
374
- ['drive', 'comments', 'reply', 'f1', 'c1', 'thanks'],
375
+ ['drive', 'comments', 'reply', pos('f1'), pos('c1'), pos('thanks')],
375
376
  { account: undefined },
376
377
  );
377
378
  });
@@ -382,7 +383,7 @@ describe('gog_drive_comments_reply', () => {
382
383
  fileId: 'f1', commentId: 'c1', content: 'lgtm, resolving', action: 'resolve',
383
384
  });
384
385
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
385
- ['drive', 'comments', 'reply', 'f1', 'c1', 'lgtm, resolving', '--action=resolve'],
386
+ ['drive', 'comments', 'reply', pos('f1'), pos('c1'), pos('lgtm, resolving'), '--action=resolve'],
386
387
  { account: undefined },
387
388
  );
388
389
  });
@@ -392,7 +393,7 @@ describe('gog_drive_comments_reply', () => {
392
393
  fileId: 'f1', commentId: 'c1', content: 'actually wait', action: 'reopen',
393
394
  });
394
395
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
395
- ['drive', 'comments', 'reply', 'f1', 'c1', 'actually wait', '--action=reopen'],
396
+ ['drive', 'comments', 'reply', pos('f1'), pos('c1'), pos('actually wait'), '--action=reopen'],
396
397
  { account: undefined },
397
398
  );
398
399
  });
@@ -404,7 +405,7 @@ describe('gog_drive_comments_resolve', () => {
404
405
  it('calls runOrDiagnose with fileId and commentId', async () => {
405
406
  await harness.callTool('gog_drive_comments_resolve', { fileId: 'f1', commentId: 'c1' });
406
407
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
407
- ['drive', 'comments', 'resolve', 'f1', 'c1'],
408
+ ['drive', 'comments', 'resolve', pos('f1'), pos('c1')],
408
409
  { account: undefined },
409
410
  );
410
411
  });
@@ -412,7 +413,7 @@ describe('gog_drive_comments_resolve', () => {
412
413
  it('forwards account', async () => {
413
414
  await harness.callTool('gog_drive_comments_resolve', { fileId: 'f1', commentId: 'c1', account: 'a@b.com' });
414
415
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
415
- ['drive', 'comments', 'resolve', 'f1', 'c1'],
416
+ ['drive', 'comments', 'resolve', pos('f1'), pos('c1')],
416
417
  { account: 'a@b.com' },
417
418
  );
418
419
  });
@@ -422,7 +423,7 @@ describe('gog_drive_comments_reopen', () => {
422
423
  it('calls runOrDiagnose with fileId and commentId', async () => {
423
424
  await harness.callTool('gog_drive_comments_reopen', { fileId: 'f1', commentId: 'c1' });
424
425
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
425
- ['drive', 'comments', 'reopen', 'f1', 'c1'],
426
+ ['drive', 'comments', 'reopen', pos('f1'), pos('c1')],
426
427
  { account: undefined },
427
428
  );
428
429
  });
@@ -559,7 +560,7 @@ describe('gog_drive_labels_list', () => {
559
560
  describe('gog_drive_labels_get', () => {
560
561
  it('calls runOrDiagnose with name only', async () => {
561
562
  await harness.callTool('gog_drive_labels_get', { name: 'labels/abc' });
562
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'labels', 'get', 'labels/abc'], { account: undefined });
563
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'labels', 'get', pos('labels/abc')], { account: undefined });
563
564
  });
564
565
 
565
566
  it('passes all flags', async () => {
@@ -567,14 +568,14 @@ describe('gog_drive_labels_get', () => {
567
568
  name: 'labels/abc', language: 'en', view: 'LABEL_VIEW_BASIC', adminAccess: true,
568
569
  });
569
570
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
570
- ['drive', 'labels', 'get', 'labels/abc', '--language=en', '--view=LABEL_VIEW_BASIC', '--admin-access'],
571
+ ['drive', 'labels', 'get', pos('labels/abc'), '--language=en', '--view=LABEL_VIEW_BASIC', '--admin-access'],
571
572
  { account: undefined },
572
573
  );
573
574
  });
574
575
 
575
576
  it('omits --admin-access when false', async () => {
576
577
  await harness.callTool('gog_drive_labels_get', { name: 'labels/abc', adminAccess: false });
577
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'labels', 'get', 'labels/abc'], { account: undefined });
578
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'labels', 'get', pos('labels/abc')], { account: undefined });
578
579
  });
579
580
  });
580
581
 
@@ -582,7 +583,7 @@ describe('gog_drive_labels_file_list', () => {
582
583
  it('calls runOrDiagnose with fileId only', async () => {
583
584
  await harness.callTool('gog_drive_labels_file_list', { fileId: 'f1' });
584
585
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
585
- ['drive', 'labels', 'file', 'list', 'f1'],
586
+ ['drive', 'labels', 'file', 'list', pos('f1')],
586
587
  { account: undefined },
587
588
  );
588
589
  });
@@ -590,7 +591,7 @@ describe('gog_drive_labels_file_list', () => {
590
591
  it('passes --max and --page when provided', async () => {
591
592
  await harness.callTool('gog_drive_labels_file_list', { fileId: 'f1', max: 25, page: 'tok' });
592
593
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
593
- ['drive', 'labels', 'file', 'list', 'f1', '--max=25', '--page=tok'],
594
+ ['drive', 'labels', 'file', 'list', pos('f1'), '--max=25', '--page=tok'],
594
595
  { account: undefined },
595
596
  );
596
597
  });
@@ -600,7 +601,7 @@ describe('gog_drive_labels_file_apply', () => {
600
601
  it('calls runOrDiagnose with fileId and labelId only', async () => {
601
602
  await harness.callTool('gog_drive_labels_file_apply', { fileId: 'f1', labelId: 'l1' });
602
603
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
603
- ['drive', 'labels', 'file', 'apply', 'f1', 'l1'],
604
+ ['drive', 'labels', 'file', 'apply', pos('f1'), pos('l1')],
604
605
  { account: undefined },
605
606
  );
606
607
  });
@@ -619,7 +620,7 @@ describe('gog_drive_labels_file_apply', () => {
619
620
  });
620
621
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
621
622
  [
622
- 'drive', 'labels', 'file', 'apply', 'f1', 'l1',
623
+ 'drive', 'labels', 'file', 'apply', pos('f1'), pos('l1'),
623
624
  '--text=t1=hello', '--text=t2=world',
624
625
  '--selection=s1=c1,c2',
625
626
  '--integer=i1=42',
@@ -637,7 +638,7 @@ describe('gog_drive_labels_file_apply', () => {
637
638
  fileId: 'f1', labelId: 'l1', text: [], selection: [], integer: [], date: [], user: [], unset: [],
638
639
  });
639
640
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
640
- ['drive', 'labels', 'file', 'apply', 'f1', 'l1'],
641
+ ['drive', 'labels', 'file', 'apply', pos('f1'), pos('l1')],
641
642
  { account: undefined },
642
643
  );
643
644
  });
@@ -647,7 +648,7 @@ describe('gog_drive_labels_file_remove', () => {
647
648
  it('calls runOrDiagnose with fileId and labelId', async () => {
648
649
  await harness.callTool('gog_drive_labels_file_remove', { fileId: 'f1', labelId: 'l1' });
649
650
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
650
- ['drive', 'labels', 'file', 'remove', 'f1', 'l1', '--force'],
651
+ ['drive', 'labels', 'file', 'remove', pos('f1'), pos('l1'), '--force'],
651
652
  { account: undefined },
652
653
  );
653
654
  });
@@ -732,7 +733,7 @@ describe('gog_drive_audit_user', () => {
732
733
  it('calls runOrDiagnose with user only', async () => {
733
734
  await harness.callTool('gog_drive_audit_user', { user: 'a@b.com' });
734
735
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
735
- ['drive', 'audit', 'user', 'a@b.com'],
736
+ ['drive', 'audit', 'user', pos('a@b.com')],
736
737
  { account: undefined },
737
738
  );
738
739
  });
@@ -742,7 +743,7 @@ describe('gog_drive_audit_user', () => {
742
743
  user: 'a@b.com', file: 'f1', parent: 'fo1', depth: 3, max: 100, noAllDrives: true,
743
744
  });
744
745
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
745
- ['drive', 'audit', 'user', 'a@b.com', '--file=f1', '--parent=fo1', '--depth=3', '--max=100', '--no-all-drives'],
746
+ ['drive', 'audit', 'user', pos('a@b.com'), '--file=f1', '--parent=fo1', '--depth=3', '--max=100', '--no-all-drives'],
746
747
  { account: undefined },
747
748
  );
748
749
  });
@@ -750,7 +751,7 @@ describe('gog_drive_audit_user', () => {
750
751
  it('handles depth/max=0 and omits noAllDrives when false', async () => {
751
752
  await harness.callTool('gog_drive_audit_user', { user: 'a@b.com', depth: 0, max: 0, noAllDrives: false });
752
753
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
753
- ['drive', 'audit', 'user', 'a@b.com', '--depth=0', '--max=0'],
754
+ ['drive', 'audit', 'user', pos('a@b.com'), '--depth=0', '--max=0'],
754
755
  { account: undefined },
755
756
  );
756
757
  });
@@ -761,7 +762,7 @@ describe('gog_drive_revisions_list', () => {
761
762
  it('lists revisions with pagination', async () => {
762
763
  await harness.callTool('gog_drive_revisions_list', { fileId: 'f1', max: 10, page: 'tok', all: true });
763
764
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
764
- ['drive', 'revisions', 'list', 'f1', '--max=10', '--page=tok', '--all'],
765
+ ['drive', 'revisions', 'list', pos('f1'), '--max=10', '--page=tok', '--all'],
765
766
  { account: undefined },
766
767
  );
767
768
  });
@@ -771,7 +772,7 @@ describe('gog_drive_revisions_get', () => {
771
772
  it('gets one revision', async () => {
772
773
  await harness.callTool('gog_drive_revisions_get', { fileId: 'f1', revisionId: 'r3', account: 'a@b.com' });
773
774
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
774
- ['drive', 'revisions', 'get', 'f1', 'r3'],
775
+ ['drive', 'revisions', 'get', pos('f1'), pos('r3')],
775
776
  { account: 'a@b.com' },
776
777
  );
777
778
  });
@@ -782,7 +783,7 @@ describe('gog_drive_shortcut_create', () => {
782
783
  it('creates a shortcut to a target in a folder', async () => {
783
784
  await harness.callTool('gog_drive_shortcut_create', { targetId: 'f1', parent: 'folder9' });
784
785
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
785
- ['drive', 'shortcut', 'create', 'f1', '--parent=folder9'],
786
+ ['drive', 'shortcut', 'create', pos('f1'), '--parent=folder9'],
786
787
  { account: undefined },
787
788
  );
788
789
  });
@@ -790,7 +791,7 @@ describe('gog_drive_shortcut_create', () => {
790
791
  it('passes an explicit shortcut name', async () => {
791
792
  await harness.callTool('gog_drive_shortcut_create', { targetId: 'f1', parent: 'folder9', name: 'My Link', account: 'a@b.com' });
792
793
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
793
- ['drive', 'shortcut', 'create', 'f1', '--parent=folder9', '--name=My Link'],
794
+ ['drive', 'shortcut', 'create', pos('f1'), '--parent=folder9', '--name=My Link'],
794
795
  { account: 'a@b.com' },
795
796
  );
796
797
  });
@@ -810,3 +811,40 @@ describe('gog_drive_upload ifVersion description', () => {
810
811
  expect(desc).toMatch(/number/i);
811
812
  });
812
813
  });
814
+
815
+ // SEC-3/SEC-4: every model-supplied server path must resolve inside an
816
+ // operator-configured root (GOG_FILE_ROOTS). The suite runs with '/' so the
817
+ // arg-shape tests can use any path; these narrow it.
818
+ async function withFileRoots<T>(roots: string, fn: () => Promise<T>): Promise<T> {
819
+ const prev = process.env.GOG_FILE_ROOTS;
820
+ process.env.GOG_FILE_ROOTS = roots;
821
+ try {
822
+ return await fn();
823
+ } finally {
824
+ process.env.GOG_FILE_ROOTS = prev;
825
+ }
826
+ }
827
+
828
+ describe('server paths are confined to GOG_FILE_ROOTS', () => {
829
+ it.each([
830
+ ['gog_drive_download', {"fileId": "f1", "out": "/Users/me/.zshrc", "overwrite": true}, 'out'],
831
+ ['gog_drive_upload', {"localPath": "/Users/me/.config/gogcli/credentials.json"}, 'localPath'],
832
+ ['gog_drive_sync_push', {"localPath": "/Users/me/.ssh", "parent": "p"}, 'localPath'],
833
+ ] as Array<[string, Record<string, unknown>, string]>)('%s refuses %j outside the roots', async (tool, args, param) => {
834
+ const h = harness;
835
+ const result = await withFileRoots('/srv/gog-files', () => h.callTool(tool, args));
836
+ expect(result.isError).toBe(true);
837
+ expect(result.content[0].text).toMatch(new RegExp(`${param} ".*" is outside the directories`));
838
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
839
+ });
840
+
841
+ // Clients auto-approve readOnlyHint tools; these write (and can overwrite)
842
+ // files on the gog host, so they must not claim to be read-only.
843
+ it.each(["gog_drive_download"])('%s is not advertised as read-only and is marked destructive', async (name) => {
844
+ const h = harness;
845
+ const { tools } = await h.client.listTools();
846
+ const tool = tools.find((t) => t.name === name)!;
847
+ expect(tool.annotations?.readOnlyHint).not.toBe(true);
848
+ expect(tool.annotations?.destructiveHint).toBe(true);
849
+ });
850
+ });
package/vitest.config.ts CHANGED
@@ -2,6 +2,11 @@ import { defineConfig } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
+ // The arg-shape tests pass arbitrary server paths; confinement is exercised
6
+ // explicitly by the tests that narrow GOG_FILE_ROOTS themselves.
7
+ env: {
8
+ GOG_FILE_ROOTS: '/',
9
+ },
5
10
  coverage: {
6
11
  provider: 'v8',
7
12
  include: ['src/**/*.ts'],