gogcli-mcp-sheets 2.7.1 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { registerExtraSheetsTools } from '../../src/tools/sheets-extra.js';
3
3
  import * as lib from '../../../gogcli-mcp/src/lib.js';
4
- import { setupHandlers as setupHandlersBase, toText } from '../../../gogcli-mcp/tests/helpers/test-harness.js';
4
+ import { createTestHarness } from '@chrischall/mcp-utils/test';
5
+ import { rawTextResult } from '@chrischall/mcp-utils';
5
6
 
6
7
  vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
7
8
  const actual = await importOriginal<typeof lib>();
@@ -13,23 +14,23 @@ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
13
14
  };
14
15
  });
15
16
 
16
- const setupHandlers = () => setupHandlersBase(registerExtraSheetsTools);
17
+ const setupHandlers = () => createTestHarness(registerExtraSheetsTools);
17
18
 
18
19
  beforeEach(() => vi.clearAllMocks());
19
20
 
20
21
  // 1. add-tab
21
22
  describe('gog_sheets_add_tab', () => {
22
23
  it('calls runOrDiagnose with correct args', async () => {
23
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
24
- const handlers = setupHandlers();
25
- await handlers.get('gog_sheets_add_tab')!({ spreadsheetId: 'sid', tabName: 'NewTab' });
24
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
25
+ const harness = await setupHandlers();
26
+ await harness.callTool('gog_sheets_add_tab', { spreadsheetId: 'sid', tabName: 'NewTab' });
26
27
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'add-tab', 'sid', 'NewTab'], { account: undefined });
27
28
  });
28
29
 
29
30
  it('forwards account', async () => {
30
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
31
- const handlers = setupHandlers();
32
- await handlers.get('gog_sheets_add_tab')!({ spreadsheetId: 'sid', tabName: 'T', account: 'a@b.com' });
31
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
32
+ const harness = await setupHandlers();
33
+ await harness.callTool('gog_sheets_add_tab', { spreadsheetId: 'sid', tabName: 'T', account: 'a@b.com' });
33
34
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'add-tab', 'sid', 'T'], { account: 'a@b.com' });
34
35
  });
35
36
  });
@@ -37,19 +38,19 @@ describe('gog_sheets_add_tab', () => {
37
38
  // 2. delete-tab
38
39
  describe('gog_sheets_delete_tab', () => {
39
40
  it('calls runOrDiagnose with correct args', async () => {
40
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
41
- const handlers = setupHandlers();
42
- await handlers.get('gog_sheets_delete_tab')!({ spreadsheetId: 'sid', tabName: 'OldTab' });
43
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'delete-tab', 'sid', 'OldTab'], { account: undefined });
41
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
42
+ const harness = await setupHandlers();
43
+ await harness.callTool('gog_sheets_delete_tab', { spreadsheetId: 'sid', tabName: 'OldTab' });
44
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'delete-tab', 'sid', 'OldTab', '--force'], { account: undefined });
44
45
  });
45
46
  });
46
47
 
47
48
  // 3. rename-tab
48
49
  describe('gog_sheets_rename_tab', () => {
49
50
  it('calls runOrDiagnose with correct args', async () => {
50
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
51
- const handlers = setupHandlers();
52
- await handlers.get('gog_sheets_rename_tab')!({ spreadsheetId: 'sid', oldName: 'Old', newName: 'New' });
51
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
52
+ const harness = await setupHandlers();
53
+ await harness.callTool('gog_sheets_rename_tab', { spreadsheetId: 'sid', oldName: 'Old', newName: 'New' });
53
54
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'rename-tab', 'sid', 'Old', 'New'], { account: undefined });
54
55
  });
55
56
  });
@@ -57,16 +58,16 @@ describe('gog_sheets_rename_tab', () => {
57
58
  // 4. copy
58
59
  describe('gog_sheets_copy', () => {
59
60
  it('calls runOrDiagnose with correct args', async () => {
60
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
61
- const handlers = setupHandlers();
62
- await handlers.get('gog_sheets_copy')!({ spreadsheetId: 'sid', title: 'Copy' });
61
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
62
+ const harness = await setupHandlers();
63
+ await harness.callTool('gog_sheets_copy', { spreadsheetId: 'sid', title: 'Copy' });
63
64
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy', 'sid', 'Copy'], { account: undefined });
64
65
  });
65
66
 
66
67
  it('includes --parent when provided', async () => {
67
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
68
- const handlers = setupHandlers();
69
- await handlers.get('gog_sheets_copy')!({ spreadsheetId: 'sid', title: 'Copy', parent: 'folderId' });
68
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
69
+ const harness = await setupHandlers();
70
+ await harness.callTool('gog_sheets_copy', { spreadsheetId: 'sid', title: 'Copy', parent: 'folderId' });
70
71
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy', 'sid', 'Copy', '--parent=folderId'], { account: undefined });
71
72
  });
72
73
  });
@@ -74,40 +75,47 @@ describe('gog_sheets_copy', () => {
74
75
  // 5. export
75
76
  describe('gog_sheets_export', () => {
76
77
  it('calls runOrDiagnose with correct args', async () => {
77
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
78
- const handlers = setupHandlers();
79
- await handlers.get('gog_sheets_export')!({ spreadsheetId: 'sid' });
78
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
79
+ const harness = await setupHandlers();
80
+ await harness.callTool('gog_sheets_export', { spreadsheetId: 'sid' });
80
81
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'export', 'sid'], { account: undefined });
81
82
  });
82
83
 
83
84
  it('includes --format and --out when provided', async () => {
84
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
85
- const handlers = setupHandlers();
86
- await handlers.get('gog_sheets_export')!({ spreadsheetId: 'sid', format: 'pdf', out: '/tmp/out.pdf' });
85
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
86
+ const harness = await setupHandlers();
87
+ await harness.callTool('gog_sheets_export', { spreadsheetId: 'sid', format: 'pdf', out: '/tmp/out.pdf' });
87
88
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'export', 'sid', '--format=pdf', '--out=/tmp/out.pdf'], { account: undefined });
88
89
  });
90
+
91
+ it('includes --overwrite when requested', async () => {
92
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
93
+ const harness = await setupHandlers();
94
+ await harness.callTool('gog_sheets_export', { spreadsheetId: 'sid', out: '/tmp/out.csv', overwrite: true });
95
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'export', 'sid', '--out=/tmp/out.csv', '--overwrite'], { account: undefined });
96
+ });
89
97
  });
90
98
 
91
99
  // 6. freeze
92
100
  describe('gog_sheets_freeze', () => {
93
101
  it('calls runOrDiagnose with no optional flags', async () => {
94
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
95
- const handlers = setupHandlers();
96
- await handlers.get('gog_sheets_freeze')!({ spreadsheetId: 'sid' });
102
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
103
+ const harness = await setupHandlers();
104
+ await harness.callTool('gog_sheets_freeze', { spreadsheetId: 'sid' });
97
105
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'freeze', 'sid'], { account: undefined });
98
106
  });
99
107
 
100
108
  it('includes --rows, --cols, --sheet when provided', async () => {
101
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
102
- const handlers = setupHandlers();
103
- await handlers.get('gog_sheets_freeze')!({ spreadsheetId: 'sid', rows: 1, cols: 2, sheet: 'Data' });
109
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
110
+ const harness = await setupHandlers();
111
+ await harness.callTool('gog_sheets_freeze', { spreadsheetId: 'sid', rows: 1, cols: 2, sheet: 'Data' });
104
112
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'freeze', 'sid', '--rows=1', '--cols=2', '--sheet=Data'], { account: undefined });
105
113
  });
106
114
 
107
115
  it('includes --rows=0 when rows is 0', async () => {
108
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
109
- const handlers = setupHandlers();
110
- await handlers.get('gog_sheets_freeze')!({ spreadsheetId: 'sid', rows: 0 });
116
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
117
+ const harness = await setupHandlers();
118
+ await harness.callTool('gog_sheets_freeze', { spreadsheetId: 'sid', rows: 0 });
111
119
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'freeze', 'sid', '--rows=0'], { account: undefined });
112
120
  });
113
121
  });
@@ -118,42 +126,42 @@ describe('gog_sheets_insert', () => {
118
126
  // gog's positional <start> is 1-based and, without --after, it inserts *before* it,
119
127
  // so API start_index = positional - 1. To land at our 0-based start we send start+1.
120
128
  // start=5 → positional 6 → API start_index 5.
121
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
122
- const handlers = setupHandlers();
123
- await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'Sheet1', dimension: 'ROWS', start: 5 });
129
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
130
+ const harness = await setupHandlers();
131
+ await harness.callTool('gog_sheets_insert', { spreadsheetId: 'sid', sheet: 'Sheet1', dimension: 'ROWS', start: 5 });
124
132
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'Sheet1', 'ROWS', '6'], { account: undefined });
125
133
  });
126
134
 
127
135
  it('bug: start=1, after:false inserts before 0-based index 1 (new row 2), not at the top', async () => {
128
136
  // Regression for the reported "range.startIndex must be set" / wrong-placement bug.
129
137
  // start=1 → positional 2 → gog API start_index 1 (insert before the 2nd row).
130
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
131
- const handlers = setupHandlers();
132
- await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 1, count: 1 });
138
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
139
+ const harness = await setupHandlers();
140
+ await harness.callTool('gog_sheets_insert', { spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 1, count: 1 });
133
141
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '2', '--count=1'], { account: undefined });
134
142
  });
135
143
 
136
144
  it('shifts start by +1 when after:true so the new dimension lands AFTER start', async () => {
137
145
  // Issue #42: with after:true, start=0 means "insert after index 0" — i.e. land at index 1.
138
146
  // The MCP must compensate for the CLI's 1-based interpretation by passing start+1.
139
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
140
- const handlers = setupHandlers();
141
- await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'COLUMNS', start: 0, count: 3, after: true });
147
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
148
+ const harness = await setupHandlers();
149
+ await harness.callTool('gog_sheets_insert', { spreadsheetId: 'sid', sheet: 'S1', dimension: 'COLUMNS', start: 0, count: 3, after: true });
142
150
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'COLUMNS', '1', '--count=3', '--after'], { account: undefined });
143
151
  });
144
152
 
145
153
  it('issue #42 acceptance: start=28, after:true leaves column 28 untouched (insertion lands at 29)', async () => {
146
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
147
- const handlers = setupHandlers();
148
- await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'COLUMNS', start: 28, after: true, count: 1 });
154
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
155
+ const harness = await setupHandlers();
156
+ await harness.callTool('gog_sheets_insert', { spreadsheetId: 'sid', sheet: 'S1', dimension: 'COLUMNS', start: 28, after: true, count: 1 });
149
157
  // CLI is 1-based; with --after CLI uses startIndex = c.Start, so passing 29 → API startIndex=29.
150
158
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'COLUMNS', '29', '--count=1', '--after'], { account: undefined });
151
159
  });
152
160
 
153
161
  it('includes --count=0 when count is 0', async () => {
154
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
155
- const handlers = setupHandlers();
156
- await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 0, count: 0 });
162
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
163
+ const harness = await setupHandlers();
164
+ await harness.callTool('gog_sheets_insert', { spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 0, count: 0 });
157
165
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '1', '--count=0'], { account: undefined });
158
166
  });
159
167
 
@@ -161,46 +169,46 @@ describe('gog_sheets_insert', () => {
161
169
  // start=0 → positional 1 → gog API start_index 0. Sending positional 0 would make gog
162
170
  // reject the call outright ("start must be >= 1"), so the +1 shift is what makes a
163
171
  // top-of-sheet insert reachable at all.
164
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
165
- const handlers = setupHandlers();
166
- await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 0, after: false });
172
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
173
+ const harness = await setupHandlers();
174
+ await harness.callTool('gog_sheets_insert', { spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 0, after: false });
167
175
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '1'], { account: undefined });
168
176
  });
169
177
 
170
178
  it('passes --inherit-from-before=true when set', async () => {
171
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
172
- const handlers = setupHandlers();
173
- await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 5, after: true, inheritFromBefore: true });
179
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
180
+ const harness = await setupHandlers();
181
+ await harness.callTool('gog_sheets_insert', { spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 5, after: true, inheritFromBefore: true });
174
182
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '6', '--after', '--inherit-from-before=true'], { account: undefined });
175
183
  });
176
184
 
177
185
  it('passes --inherit-from-before=false to inherit from the following neighbor', async () => {
178
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
179
- const handlers = setupHandlers();
180
- await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 5, after: true, inheritFromBefore: false });
186
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
187
+ const harness = await setupHandlers();
188
+ await harness.callTool('gog_sheets_insert', { spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 5, after: true, inheritFromBefore: false });
181
189
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '6', '--after', '--inherit-from-before=false'], { account: undefined });
182
190
  });
183
191
  });
184
192
 
185
193
  describe('gog_sheets_copy_paste', () => {
186
194
  it('calls runOrDiagnose with required source/dest', async () => {
187
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
188
- const handlers = setupHandlers();
189
- await handlers.get('gog_sheets_copy_paste')!({ spreadsheetId: 'sid', source: 'Sheet1!A2:H71', dest: 'Sheet1!A2:H120' });
195
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
196
+ const harness = await setupHandlers();
197
+ await harness.callTool('gog_sheets_copy_paste', { spreadsheetId: 'sid', source: 'Sheet1!A2:H71', dest: 'Sheet1!A2:H120' });
190
198
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy-paste', 'sid', 'Sheet1!A2:H71', 'Sheet1!A2:H120'], { account: undefined });
191
199
  });
192
200
 
193
201
  it('appends --type and --transpose when provided', async () => {
194
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
195
- const handlers = setupHandlers();
196
- await handlers.get('gog_sheets_copy_paste')!({ spreadsheetId: 'sid', source: 'A1:B2', dest: 'D1:E2', type: 'FORMULA', transpose: true });
202
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
203
+ const harness = await setupHandlers();
204
+ await harness.callTool('gog_sheets_copy_paste', { spreadsheetId: 'sid', source: 'A1:B2', dest: 'D1:E2', type: 'FORMULA', transpose: true });
197
205
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy-paste', 'sid', 'A1:B2', 'D1:E2', '--type=FORMULA', '--transpose'], { account: undefined });
198
206
  });
199
207
 
200
208
  it('forwards account', async () => {
201
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
202
- const handlers = setupHandlers();
203
- await handlers.get('gog_sheets_copy_paste')!({ spreadsheetId: 'sid', source: 'A1', dest: 'B1', account: 'a@b.com' });
209
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
210
+ const harness = await setupHandlers();
211
+ await harness.callTool('gog_sheets_copy_paste', { spreadsheetId: 'sid', source: 'A1', dest: 'B1', account: 'a@b.com' });
204
212
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy-paste', 'sid', 'A1', 'B1'], { account: 'a@b.com' });
205
213
  });
206
214
  });
@@ -208,16 +216,16 @@ describe('gog_sheets_copy_paste', () => {
208
216
  // 8. merge
209
217
  describe('gog_sheets_merge', () => {
210
218
  it('calls runOrDiagnose with correct args', async () => {
211
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
212
- const handlers = setupHandlers();
213
- await handlers.get('gog_sheets_merge')!({ spreadsheetId: 'sid', range: 'A1:C3' });
219
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
220
+ const harness = await setupHandlers();
221
+ await harness.callTool('gog_sheets_merge', { spreadsheetId: 'sid', range: 'A1:C3' });
214
222
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'merge', 'sid', 'A1:C3'], { account: undefined });
215
223
  });
216
224
 
217
225
  it('includes --type when provided', async () => {
218
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
219
- const handlers = setupHandlers();
220
- await handlers.get('gog_sheets_merge')!({ spreadsheetId: 'sid', range: 'A1:C3', type: 'MERGE_COLUMNS' });
226
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
227
+ const harness = await setupHandlers();
228
+ await harness.callTool('gog_sheets_merge', { spreadsheetId: 'sid', range: 'A1:C3', type: 'MERGE_COLUMNS' });
221
229
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'merge', 'sid', 'A1:C3', '--type=MERGE_COLUMNS'], { account: undefined });
222
230
  });
223
231
  });
@@ -225,9 +233,9 @@ describe('gog_sheets_merge', () => {
225
233
  // 9. unmerge
226
234
  describe('gog_sheets_unmerge', () => {
227
235
  it('calls runOrDiagnose with correct args', async () => {
228
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
229
- const handlers = setupHandlers();
230
- await handlers.get('gog_sheets_unmerge')!({ spreadsheetId: 'sid', range: 'A1:C3' });
236
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
237
+ const harness = await setupHandlers();
238
+ await harness.callTool('gog_sheets_unmerge', { spreadsheetId: 'sid', range: 'A1:C3' });
231
239
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'unmerge', 'sid', 'A1:C3'], { account: undefined });
232
240
  });
233
241
  });
@@ -235,10 +243,10 @@ describe('gog_sheets_unmerge', () => {
235
243
  // 10. format
236
244
  describe('gog_sheets_format', () => {
237
245
  it('passes raw formatJson + formatFields through unchanged (escape hatch)', async () => {
238
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
239
- const handlers = setupHandlers();
246
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
247
+ const harness = await setupHandlers();
240
248
  const fj = '{"textFormat":{"bold":true}}';
241
- await handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1:B2', formatJson: fj, formatFields: 'textFormat.bold' });
249
+ await harness.callTool('gog_sheets_format', { spreadsheetId: 'sid', range: 'A1:B2', formatJson: fj, formatFields: 'textFormat.bold' });
242
250
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
243
251
  ['sheets', 'format', 'sid', 'A1:B2', `--format-json=${fj}`, '--format-fields=textFormat.bold'],
244
252
  { account: undefined },
@@ -246,9 +254,9 @@ describe('gog_sheets_format', () => {
246
254
  });
247
255
 
248
256
  it('passes raw formatJson without formatFields', async () => {
249
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
250
- const handlers = setupHandlers();
251
- await handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1', formatJson: '{}' });
257
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
258
+ const harness = await setupHandlers();
259
+ await harness.callTool('gog_sheets_format', { spreadsheetId: 'sid', range: 'A1', formatJson: '{}' });
252
260
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
253
261
  ['sheets', 'format', 'sid', 'A1', '--format-json={}'],
254
262
  { account: undefined },
@@ -256,9 +264,9 @@ describe('gog_sheets_format', () => {
256
264
  });
257
265
 
258
266
  it('composes named flags into CellFormat JSON + auto-computed fields', async () => {
259
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
260
- const handlers = setupHandlers();
261
- await handlers.get('gog_sheets_format')!({
267
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
268
+ const harness = await setupHandlers();
269
+ await harness.callTool('gog_sheets_format', {
262
270
  spreadsheetId: 'sid', range: 'A1:C3',
263
271
  bold: true,
264
272
  backgroundColor: '#FFF5D9',
@@ -288,9 +296,9 @@ describe('gog_sheets_format', () => {
288
296
  });
289
297
 
290
298
  it('composes all named flags including textColor + alignment', async () => {
291
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
292
- const handlers = setupHandlers();
293
- await handlers.get('gog_sheets_format')!({
299
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
300
+ const harness = await setupHandlers();
301
+ await harness.callTool('gog_sheets_format', {
294
302
  spreadsheetId: 'sid', range: 'A1',
295
303
  italic: true,
296
304
  underline: true,
@@ -315,33 +323,34 @@ describe('gog_sheets_format', () => {
315
323
  });
316
324
 
317
325
  it('rejects bad hex in textColor', async () => {
318
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
319
- const handlers = setupHandlers();
320
- await expect(
321
- handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1', textColor: 'not-a-hex' }),
322
- ).rejects.toThrow(/Invalid textColor/);
326
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
327
+ const harness = await setupHandlers();
328
+ // Thrown handler errors surface through the MCP RPC path as error results.
329
+ const result = await harness.callTool('gog_sheets_format', { spreadsheetId: 'sid', range: 'A1', textColor: 'not-a-hex' });
330
+ expect(result.isError).toBe(true);
331
+ expect(result.content[0].text).toMatch(/Invalid textColor/);
323
332
  });
324
333
 
325
334
  it('rejects bad hex in backgroundColor', async () => {
326
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
327
- const handlers = setupHandlers();
328
- await expect(
329
- handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1', backgroundColor: 'orange' }),
330
- ).rejects.toThrow(/Invalid backgroundColor/);
335
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
336
+ const harness = await setupHandlers();
337
+ const result = await harness.callTool('gog_sheets_format', { spreadsheetId: 'sid', range: 'A1', backgroundColor: 'orange' });
338
+ expect(result.isError).toBe(true);
339
+ expect(result.content[0].text).toMatch(/Invalid backgroundColor/);
331
340
  });
332
341
 
333
342
  it('rejects no-op calls with neither flags nor formatJson', async () => {
334
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
335
- const handlers = setupHandlers();
336
- await expect(
337
- handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1' }),
338
- ).rejects.toThrow(/requires at least one named flag/);
343
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
344
+ const harness = await setupHandlers();
345
+ const result = await harness.callTool('gog_sheets_format', { spreadsheetId: 'sid', range: 'A1' });
346
+ expect(result.isError).toBe(true);
347
+ expect(result.content[0].text).toMatch(/requires at least one named flag/);
339
348
  });
340
349
 
341
350
  it('honors caller-provided formatFields when also using named flags', async () => {
342
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
343
- const handlers = setupHandlers();
344
- await handlers.get('gog_sheets_format')!({
351
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
352
+ const harness = await setupHandlers();
353
+ await harness.callTool('gog_sheets_format', {
345
354
  spreadsheetId: 'sid', range: 'A1',
346
355
  bold: true,
347
356
  formatFields: 'textFormat.bold,textFormat.italic',
@@ -353,9 +362,9 @@ describe('gog_sheets_format', () => {
353
362
 
354
363
  describe('gog_sheets_list_tabs', () => {
355
364
  it('uses metadata with a --select projection', async () => {
356
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
357
- const handlers = setupHandlers();
358
- await handlers.get('gog_sheets_list_tabs')!({ spreadsheetId: 'sid' });
365
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
366
+ const harness = await setupHandlers();
367
+ await harness.callTool('gog_sheets_list_tabs', { spreadsheetId: 'sid' });
359
368
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
360
369
  ['sheets', 'metadata', 'sid', '--select=sheets.properties.sheetId,sheets.properties.title,sheets.properties.index,sheets.properties.gridProperties'],
361
370
  { account: undefined },
@@ -366,30 +375,30 @@ describe('gog_sheets_list_tabs', () => {
366
375
  // 11. number-format
367
376
  describe('gog_sheets_number_format', () => {
368
377
  it('calls runOrDiagnose with correct args', async () => {
369
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
370
- const handlers = setupHandlers();
371
- await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A10' });
378
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
379
+ const harness = await setupHandlers();
380
+ await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A10' });
372
381
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'number-format', 'sid', 'A1:A10'], { account: undefined });
373
382
  });
374
383
 
375
384
  it('includes --type and --pattern when provided', async () => {
376
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
377
- const handlers = setupHandlers();
378
- await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1', type: 'CURRENCY', pattern: '#,##0.00' });
385
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
386
+ const harness = await setupHandlers();
387
+ await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1', type: 'CURRENCY', pattern: '#,##0.00' });
379
388
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'number-format', 'sid', 'A1', '--type=CURRENCY', '--pattern=#,##0.00'], { account: undefined });
380
389
  });
381
390
 
382
391
  // Issue #43: warn when DATE/DATE_TIME format applied to small integers (silent 1899/1900 render).
383
392
  describe('DATE/DATE_TIME small-integer warning', () => {
384
393
  it('emits a warning when DATE applied to a range of small integers', async () => {
385
- const handlers = setupHandlers();
394
+ const harness = await setupHandlers();
386
395
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
387
396
  if (args[1] === 'get') {
388
- return toText(JSON.stringify({ range: 'Sheet1!A1:A3', values: [[1], [2], [3]] }));
397
+ return rawTextResult(JSON.stringify({ range: 'Sheet1!A1:A3', values: [[1], [2], [3]] }));
389
398
  }
390
- return toText('{"ok":true}');
399
+ return rawTextResult('{"ok":true}');
391
400
  });
392
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:A3', type: 'DATE' });
401
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'Sheet1!A1:A3', type: 'DATE' });
393
402
  // Peek call goes out first.
394
403
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
395
404
  ['sheets', 'get', 'sid', 'Sheet1!A1:A3', '--render=UNFORMATTED_VALUE'],
@@ -407,21 +416,21 @@ describe('gog_sheets_number_format', () => {
407
416
  });
408
417
 
409
418
  it('also warns for DATE_TIME', async () => {
410
- const handlers = setupHandlers();
419
+ const harness = await setupHandlers();
411
420
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
412
421
  if (args[1] === 'get') {
413
- return toText(JSON.stringify({ range: 'Sheet1!A1:A2', values: [[5], [7]] }));
422
+ return rawTextResult(JSON.stringify({ range: 'Sheet1!A1:A2', values: [[5], [7]] }));
414
423
  }
415
- return toText('{"ok":true}');
424
+ return rawTextResult('{"ok":true}');
416
425
  });
417
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:A2', type: 'DATE_TIME' });
426
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'Sheet1!A1:A2', type: 'DATE_TIME' });
418
427
  expect(result.content[0].text).toMatch(/warning/i);
419
428
  });
420
429
 
421
430
  it('does not peek or warn when force:true', async () => {
422
- const handlers = setupHandlers();
423
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{"ok":true}'));
424
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:A3', type: 'DATE', force: true });
431
+ const harness = await setupHandlers();
432
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"ok":true}'));
433
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'Sheet1!A1:A3', type: 'DATE', force: true });
425
434
  // Only the format call should fire — no peek.
426
435
  expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
427
436
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
@@ -432,22 +441,22 @@ describe('gog_sheets_number_format', () => {
432
441
  });
433
442
 
434
443
  it('peeks and warns when force:false is passed explicitly (round-trips the no-force path)', async () => {
435
- const handlers = setupHandlers();
444
+ const harness = await setupHandlers();
436
445
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
437
446
  if (args[1] === 'get') {
438
- return toText(JSON.stringify({ range: 'A1:A3', values: [[1], [2], [3]] }));
447
+ return rawTextResult(JSON.stringify({ range: 'A1:A3', values: [[1], [2], [3]] }));
439
448
  }
440
- return toText('{"ok":true}');
449
+ return rawTextResult('{"ok":true}');
441
450
  });
442
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE', force: false });
451
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE', force: false });
443
452
  expect(lib.runOrDiagnose).toHaveBeenCalledTimes(2);
444
453
  expect(result.content[0].text).toMatch(/warning/i);
445
454
  });
446
455
 
447
456
  it('does not peek when type is not DATE/DATE_TIME', async () => {
448
- const handlers = setupHandlers();
449
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{"ok":true}'));
450
- await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A3', type: 'CURRENCY' });
457
+ const harness = await setupHandlers();
458
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"ok":true}'));
459
+ await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'CURRENCY' });
451
460
  expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
452
461
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
453
462
  ['sheets', 'number-format', 'sid', 'A1:A3', '--type=CURRENCY'],
@@ -456,112 +465,112 @@ describe('gog_sheets_number_format', () => {
456
465
  });
457
466
 
458
467
  it('does not warn when values include a large integer (>=10000)', async () => {
459
- const handlers = setupHandlers();
468
+ const harness = await setupHandlers();
460
469
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
461
470
  if (args[1] === 'get') {
462
471
  // 45000 is a real day-serial (year ~2023) — legitimate date value.
463
- return toText(JSON.stringify({ range: 'A1:A2', values: [[1], [45000]] }));
472
+ return rawTextResult(JSON.stringify({ range: 'A1:A2', values: [[1], [45000]] }));
464
473
  }
465
- return toText('{"ok":true}');
474
+ return rawTextResult('{"ok":true}');
466
475
  });
467
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A2', type: 'DATE' });
476
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A2', type: 'DATE' });
468
477
  expect(result.content[0].text).not.toMatch(/warning/i);
469
478
  });
470
479
 
471
480
  it('does not warn when values include a negative integer (deltas/error codes, not day-serials)', async () => {
472
- const handlers = setupHandlers();
481
+ const harness = await setupHandlers();
473
482
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
474
483
  if (args[1] === 'get') {
475
- return toText(JSON.stringify({ range: 'A1:A3', values: [[1], [-5], [3]] }));
484
+ return rawTextResult(JSON.stringify({ range: 'A1:A3', values: [[1], [-5], [3]] }));
476
485
  }
477
- return toText('{"ok":true}');
486
+ return rawTextResult('{"ok":true}');
478
487
  });
479
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
488
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
480
489
  expect(result.content[0].text).not.toMatch(/warning/i);
481
490
  });
482
491
 
483
492
  it('does not warn when values include non-integer numbers', async () => {
484
- const handlers = setupHandlers();
493
+ const harness = await setupHandlers();
485
494
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
486
495
  if (args[1] === 'get') {
487
- return toText(JSON.stringify({ range: 'A1:A2', values: [[1], [2.5]] }));
496
+ return rawTextResult(JSON.stringify({ range: 'A1:A2', values: [[1], [2.5]] }));
488
497
  }
489
- return toText('{"ok":true}');
498
+ return rawTextResult('{"ok":true}');
490
499
  });
491
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A2', type: 'DATE' });
500
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A2', type: 'DATE' });
492
501
  expect(result.content[0].text).not.toMatch(/warning/i);
493
502
  });
494
503
 
495
504
  it('does not warn when range has no values (missing values key)', async () => {
496
- const handlers = setupHandlers();
505
+ const harness = await setupHandlers();
497
506
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
498
507
  if (args[1] === 'get') {
499
- return toText(JSON.stringify({ range: 'A1:A3' }));
508
+ return rawTextResult(JSON.stringify({ range: 'A1:A3' }));
500
509
  }
501
- return toText('{"ok":true}');
510
+ return rawTextResult('{"ok":true}');
502
511
  });
503
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
512
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
504
513
  expect(result.content[0].text).not.toMatch(/warning/i);
505
514
  });
506
515
 
507
516
  it('does not warn when range has empty values array', async () => {
508
- const handlers = setupHandlers();
517
+ const harness = await setupHandlers();
509
518
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
510
519
  if (args[1] === 'get') {
511
- return toText(JSON.stringify({ range: 'A1:A3', values: [] }));
520
+ return rawTextResult(JSON.stringify({ range: 'A1:A3', values: [] }));
512
521
  }
513
- return toText('{"ok":true}');
522
+ return rawTextResult('{"ok":true}');
514
523
  });
515
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
524
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
516
525
  expect(result.content[0].text).not.toMatch(/warning/i);
517
526
  });
518
527
 
519
528
  it('does not warn when values are strings', async () => {
520
- const handlers = setupHandlers();
529
+ const harness = await setupHandlers();
521
530
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
522
531
  if (args[1] === 'get') {
523
- return toText(JSON.stringify({ range: 'A1:A2', values: [['hello'], ['world']] }));
532
+ return rawTextResult(JSON.stringify({ range: 'A1:A2', values: [['hello'], ['world']] }));
524
533
  }
525
- return toText('{"ok":true}');
534
+ return rawTextResult('{"ok":true}');
526
535
  });
527
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A2', type: 'DATE' });
536
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A2', type: 'DATE' });
528
537
  expect(result.content[0].text).not.toMatch(/warning/i);
529
538
  });
530
539
 
531
540
  it('ignores null and empty cells when deciding to warn', async () => {
532
- const handlers = setupHandlers();
541
+ const harness = await setupHandlers();
533
542
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
534
543
  if (args[1] === 'get') {
535
544
  // Real-world: nulls/empty strings interspersed with the ordinal ints.
536
- return toText(JSON.stringify({ range: 'A1:A4', values: [[1], [null], [''], [3]] }));
545
+ return rawTextResult(JSON.stringify({ range: 'A1:A4', values: [[1], [null], [''], [3]] }));
537
546
  }
538
- return toText('{"ok":true}');
547
+ return rawTextResult('{"ok":true}');
539
548
  });
540
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A4', type: 'DATE' });
549
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A4', type: 'DATE' });
541
550
  expect(result.content[0].text).toMatch(/warning/i);
542
551
  });
543
552
 
544
553
  it('does not warn when every cell is null or empty (no small integers seen)', async () => {
545
- const handlers = setupHandlers();
554
+ const harness = await setupHandlers();
546
555
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
547
556
  if (args[1] === 'get') {
548
- return toText(JSON.stringify({ range: 'A1:A3', values: [[null], [''], [null]] }));
557
+ return rawTextResult(JSON.stringify({ range: 'A1:A3', values: [[null], [''], [null]] }));
549
558
  }
550
- return toText('{"ok":true}');
559
+ return rawTextResult('{"ok":true}');
551
560
  });
552
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
561
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
553
562
  expect(result.content[0].text).not.toMatch(/warning/i);
554
563
  });
555
564
 
556
565
  it('proceeds with format when peek fails (does not block)', async () => {
557
- const handlers = setupHandlers();
566
+ const harness = await setupHandlers();
558
567
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
559
568
  if (args[1] === 'get') {
560
- return toText('Error: something went wrong');
569
+ return rawTextResult('Error: something went wrong');
561
570
  }
562
- return toText('{"ok":true}');
571
+ return rawTextResult('{"ok":true}');
563
572
  });
564
- const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
573
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
565
574
  // Format call should still happen.
566
575
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
567
576
  ['sheets', 'number-format', 'sid', 'A1:A3', '--type=DATE'],
@@ -572,14 +581,14 @@ describe('gog_sheets_number_format', () => {
572
581
  });
573
582
 
574
583
  it('forwards account on both peek and format calls', async () => {
575
- const handlers = setupHandlers();
584
+ const harness = await setupHandlers();
576
585
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
577
586
  if (args[1] === 'get') {
578
- return toText(JSON.stringify({ range: 'A1', values: [[1]] }));
587
+ return rawTextResult(JSON.stringify({ range: 'A1', values: [[1]] }));
579
588
  }
580
- return toText('{}');
589
+ return rawTextResult('{}');
581
590
  });
582
- await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1', type: 'DATE', account: 'a@b.com' });
591
+ await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1', type: 'DATE', account: 'a@b.com' });
583
592
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
584
593
  ['sheets', 'get', 'sid', 'A1', '--render=UNFORMATTED_VALUE'],
585
594
  { account: 'a@b.com' },
@@ -595,23 +604,23 @@ describe('gog_sheets_number_format', () => {
595
604
  // 12. read-format
596
605
  describe('gog_sheets_read_format', () => {
597
606
  it('calls runOrDiagnose with correct args', async () => {
598
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
599
- const handlers = setupHandlers();
600
- await handlers.get('gog_sheets_read_format')!({ spreadsheetId: 'sid', range: 'A1:B2' });
607
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
608
+ const harness = await setupHandlers();
609
+ await harness.callTool('gog_sheets_read_format', { spreadsheetId: 'sid', range: 'A1:B2' });
601
610
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'read-format', 'sid', 'A1:B2'], { account: undefined });
602
611
  });
603
612
 
604
613
  it('includes --effective when true', async () => {
605
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
606
- const handlers = setupHandlers();
607
- await handlers.get('gog_sheets_read_format')!({ spreadsheetId: 'sid', range: 'A1', effective: true });
614
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
615
+ const harness = await setupHandlers();
616
+ await harness.callTool('gog_sheets_read_format', { spreadsheetId: 'sid', range: 'A1', effective: true });
608
617
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'read-format', 'sid', 'A1', '--effective'], { account: undefined });
609
618
  });
610
619
 
611
620
  it('omits --effective when false', async () => {
612
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
613
- const handlers = setupHandlers();
614
- await handlers.get('gog_sheets_read_format')!({ spreadsheetId: 'sid', range: 'A1', effective: false });
621
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
622
+ const harness = await setupHandlers();
623
+ await harness.callTool('gog_sheets_read_format', { spreadsheetId: 'sid', range: 'A1', effective: false });
615
624
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'read-format', 'sid', 'A1'], { account: undefined });
616
625
  });
617
626
  });
@@ -619,23 +628,23 @@ describe('gog_sheets_read_format', () => {
619
628
  // 13. resize-columns
620
629
  describe('gog_sheets_resize_columns', () => {
621
630
  it('calls runOrDiagnose with correct args', async () => {
622
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
623
- const handlers = setupHandlers();
624
- await handlers.get('gog_sheets_resize_columns')!({ spreadsheetId: 'sid', columns: 'A:C' });
631
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
632
+ const harness = await setupHandlers();
633
+ await harness.callTool('gog_sheets_resize_columns', { spreadsheetId: 'sid', columns: 'A:C' });
625
634
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-columns', 'sid', 'A:C'], { account: undefined });
626
635
  });
627
636
 
628
637
  it('includes --width and --auto when provided', async () => {
629
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
630
- const handlers = setupHandlers();
631
- await handlers.get('gog_sheets_resize_columns')!({ spreadsheetId: 'sid', columns: 'A:C', width: 200, auto: true });
638
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
639
+ const harness = await setupHandlers();
640
+ await harness.callTool('gog_sheets_resize_columns', { spreadsheetId: 'sid', columns: 'A:C', width: 200, auto: true });
632
641
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-columns', 'sid', 'A:C', '--width=200', '--auto'], { account: undefined });
633
642
  });
634
643
 
635
644
  it('includes --width=0 when width is 0', async () => {
636
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
637
- const handlers = setupHandlers();
638
- await handlers.get('gog_sheets_resize_columns')!({ spreadsheetId: 'sid', columns: 'A:A', width: 0 });
645
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
646
+ const harness = await setupHandlers();
647
+ await harness.callTool('gog_sheets_resize_columns', { spreadsheetId: 'sid', columns: 'A:A', width: 0 });
639
648
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-columns', 'sid', 'A:A', '--width=0'], { account: undefined });
640
649
  });
641
650
  });
@@ -643,23 +652,23 @@ describe('gog_sheets_resize_columns', () => {
643
652
  // 14. resize-rows
644
653
  describe('gog_sheets_resize_rows', () => {
645
654
  it('calls runOrDiagnose with correct args', async () => {
646
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
647
- const handlers = setupHandlers();
648
- await handlers.get('gog_sheets_resize_rows')!({ spreadsheetId: 'sid', rows: '1:10' });
655
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
656
+ const harness = await setupHandlers();
657
+ await harness.callTool('gog_sheets_resize_rows', { spreadsheetId: 'sid', rows: '1:10' });
649
658
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-rows', 'sid', '1:10'], { account: undefined });
650
659
  });
651
660
 
652
661
  it('includes --height and --auto when provided', async () => {
653
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
654
- const handlers = setupHandlers();
655
- await handlers.get('gog_sheets_resize_rows')!({ spreadsheetId: 'sid', rows: '1:5', height: 40, auto: true });
662
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
663
+ const harness = await setupHandlers();
664
+ await harness.callTool('gog_sheets_resize_rows', { spreadsheetId: 'sid', rows: '1:5', height: 40, auto: true });
656
665
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-rows', 'sid', '1:5', '--height=40', '--auto'], { account: undefined });
657
666
  });
658
667
 
659
668
  it('includes --height=0 when height is 0', async () => {
660
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
661
- const handlers = setupHandlers();
662
- await handlers.get('gog_sheets_resize_rows')!({ spreadsheetId: 'sid', rows: '1:1', height: 0 });
669
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
670
+ const harness = await setupHandlers();
671
+ await harness.callTool('gog_sheets_resize_rows', { spreadsheetId: 'sid', rows: '1:1', height: 0 });
663
672
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'resize-rows', 'sid', '1:1', '--height=0'], { account: undefined });
664
673
  });
665
674
  });
@@ -667,9 +676,9 @@ describe('gog_sheets_resize_rows', () => {
667
676
  // 15. notes
668
677
  describe('gog_sheets_notes', () => {
669
678
  it('calls runOrDiagnose with correct args', async () => {
670
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
671
- const handlers = setupHandlers();
672
- await handlers.get('gog_sheets_notes')!({ spreadsheetId: 'sid', range: 'A1:B5' });
679
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
680
+ const harness = await setupHandlers();
681
+ await harness.callTool('gog_sheets_notes', { spreadsheetId: 'sid', range: 'A1:B5' });
673
682
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'notes', 'sid', 'A1:B5'], { account: undefined });
674
683
  });
675
684
  });
@@ -677,26 +686,38 @@ describe('gog_sheets_notes', () => {
677
686
  // 16. update-note
678
687
  describe('gog_sheets_update_note', () => {
679
688
  it('calls runOrDiagnose with correct args', async () => {
680
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
681
- const handlers = setupHandlers();
682
- await handlers.get('gog_sheets_update_note')!({ spreadsheetId: 'sid', range: 'A1', note: 'Hello' });
689
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
690
+ const harness = await setupHandlers();
691
+ await harness.callTool('gog_sheets_update_note', { spreadsheetId: 'sid', range: 'A1', note: 'Hello' });
683
692
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'update-note', 'sid', 'A1', '--note=Hello'], { account: undefined });
684
693
  });
685
694
 
686
695
  it('passes empty string to clear note', async () => {
687
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
688
- const handlers = setupHandlers();
689
- await handlers.get('gog_sheets_update_note')!({ spreadsheetId: 'sid', range: 'A1', note: '' });
696
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
697
+ const harness = await setupHandlers();
698
+ await harness.callTool('gog_sheets_update_note', { spreadsheetId: 'sid', range: 'A1', note: '' });
690
699
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'update-note', 'sid', 'A1', '--note='], { account: undefined });
691
700
  });
701
+
702
+ it('spills a large note to a --note-file payload arg', async () => {
703
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
704
+ const harness = await setupHandlers();
705
+ const big = 'n'.repeat(lib.PAYLOAD_INLINE_MAX + 1);
706
+ await harness.callTool('gog_sheets_update_note', { spreadsheetId: 'sid', range: 'A1', note: big });
707
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
708
+ ['sheets', 'update-note', 'sid', 'A1',
709
+ { kind: 'file', flag: 'note-file', contents: big, ext: undefined }],
710
+ { account: undefined },
711
+ );
712
+ });
692
713
  });
693
714
 
694
715
  // 17. links
695
716
  describe('gog_sheets_links', () => {
696
717
  it('calls runOrDiagnose with correct args', async () => {
697
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
698
- const handlers = setupHandlers();
699
- await handlers.get('gog_sheets_links')!({ spreadsheetId: 'sid', range: 'A1:Z100' });
718
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
719
+ const harness = await setupHandlers();
720
+ await harness.callTool('gog_sheets_links', { spreadsheetId: 'sid', range: 'A1:Z100' });
700
721
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'links', 'sid', 'A1:Z100'], { account: undefined });
701
722
  });
702
723
  });
@@ -704,9 +725,9 @@ describe('gog_sheets_links', () => {
704
725
  // gog 0.23.0
705
726
  describe('gog_sheets_links_set', () => {
706
727
  it('sets a single-cell hyperlink with cell/url/text positionals', async () => {
707
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
708
- const handlers = setupHandlers();
709
- await handlers.get('gog_sheets_links_set')!({ spreadsheetId: 'sid', cell: 'A1', url: 'https://x.com', text: 'Link' });
728
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
729
+ const harness = await setupHandlers();
730
+ await harness.callTool('gog_sheets_links_set', { spreadsheetId: 'sid', cell: 'A1', url: 'https://x.com', text: 'Link' });
710
731
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
711
732
  ['sheets', 'links', 'set', 'sid', 'A1', 'https://x.com', 'Link'],
712
733
  { account: undefined },
@@ -714,9 +735,9 @@ describe('gog_sheets_links_set', () => {
714
735
  });
715
736
 
716
737
  it('sets a multi-link cell with --runs-json', async () => {
717
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
718
- const handlers = setupHandlers();
719
- await handlers.get('gog_sheets_links_set')!({ spreadsheetId: 'sid', cell: 'B2', runsJson: '[{"text":"A","uri":"https://a"}]' });
738
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
739
+ const harness = await setupHandlers();
740
+ await harness.callTool('gog_sheets_links_set', { spreadsheetId: 'sid', cell: 'B2', runsJson: '[{"text":"A","uri":"https://a"}]' });
720
741
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
721
742
  ['sheets', 'links', 'set', 'sid', 'B2', '--runs-json=[{"text":"A","uri":"https://a"}]'],
722
743
  { account: undefined },
@@ -724,9 +745,9 @@ describe('gog_sheets_links_set', () => {
724
745
  });
725
746
 
726
747
  it('sets batch hyperlinks with --cells-json', async () => {
727
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
728
- const handlers = setupHandlers();
729
- await handlers.get('gog_sheets_links_set')!({ spreadsheetId: 'sid', cellsJson: '[{"cell":"A1","url":"https://a"}]' });
748
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
749
+ const harness = await setupHandlers();
750
+ await harness.callTool('gog_sheets_links_set', { spreadsheetId: 'sid', cellsJson: '[{"cell":"A1","url":"https://a"}]' });
730
751
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
731
752
  ['sheets', 'links', 'set', 'sid', '--cells-json=[{"cell":"A1","url":"https://a"}]'],
732
753
  { account: undefined },
@@ -737,9 +758,9 @@ describe('gog_sheets_links_set', () => {
737
758
  // 18. named-ranges list
738
759
  describe('gog_sheets_named_ranges_list', () => {
739
760
  it('calls runOrDiagnose with correct args', async () => {
740
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
741
- const handlers = setupHandlers();
742
- await handlers.get('gog_sheets_named_ranges_list')!({ spreadsheetId: 'sid' });
761
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
762
+ const harness = await setupHandlers();
763
+ await harness.callTool('gog_sheets_named_ranges_list', { spreadsheetId: 'sid' });
743
764
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'list', 'sid'], { account: undefined });
744
765
  });
745
766
  });
@@ -747,9 +768,9 @@ describe('gog_sheets_named_ranges_list', () => {
747
768
  // 19. named-ranges get
748
769
  describe('gog_sheets_named_ranges_get', () => {
749
770
  it('calls runOrDiagnose with correct args', async () => {
750
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
751
- const handlers = setupHandlers();
752
- await handlers.get('gog_sheets_named_ranges_get')!({ spreadsheetId: 'sid', nameOrId: 'MyRange' });
771
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
772
+ const harness = await setupHandlers();
773
+ await harness.callTool('gog_sheets_named_ranges_get', { spreadsheetId: 'sid', nameOrId: 'MyRange' });
753
774
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'get', 'sid', 'MyRange'], { account: undefined });
754
775
  });
755
776
  });
@@ -757,9 +778,9 @@ describe('gog_sheets_named_ranges_get', () => {
757
778
  // 20. named-ranges add
758
779
  describe('gog_sheets_named_ranges_add', () => {
759
780
  it('calls runOrDiagnose with correct args', async () => {
760
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
761
- const handlers = setupHandlers();
762
- await handlers.get('gog_sheets_named_ranges_add')!({ spreadsheetId: 'sid', name: 'Totals', range: 'Sheet1!A1:B10' });
781
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
782
+ const harness = await setupHandlers();
783
+ await harness.callTool('gog_sheets_named_ranges_add', { spreadsheetId: 'sid', name: 'Totals', range: 'Sheet1!A1:B10' });
763
784
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'add', 'sid', 'Totals', 'Sheet1!A1:B10'], { account: undefined });
764
785
  });
765
786
  });
@@ -767,16 +788,16 @@ describe('gog_sheets_named_ranges_add', () => {
767
788
  // 21. named-ranges update
768
789
  describe('gog_sheets_named_ranges_update', () => {
769
790
  it('calls runOrDiagnose with no optional flags', async () => {
770
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
771
- const handlers = setupHandlers();
772
- await handlers.get('gog_sheets_named_ranges_update')!({ spreadsheetId: 'sid', nameOrId: 'MyRange' });
791
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
792
+ const harness = await setupHandlers();
793
+ await harness.callTool('gog_sheets_named_ranges_update', { spreadsheetId: 'sid', nameOrId: 'MyRange' });
773
794
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'update', 'sid', 'MyRange'], { account: undefined });
774
795
  });
775
796
 
776
797
  it('includes --name and --range when provided', async () => {
777
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
778
- const handlers = setupHandlers();
779
- await handlers.get('gog_sheets_named_ranges_update')!({ spreadsheetId: 'sid', nameOrId: 'MyRange', name: 'NewName', range: 'Sheet1!C1:D5' });
798
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
799
+ const harness = await setupHandlers();
800
+ await harness.callTool('gog_sheets_named_ranges_update', { spreadsheetId: 'sid', nameOrId: 'MyRange', name: 'NewName', range: 'Sheet1!C1:D5' });
780
801
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'update', 'sid', 'MyRange', '--name=NewName', '--range=Sheet1!C1:D5'], { account: undefined });
781
802
  });
782
803
  });
@@ -784,9 +805,9 @@ describe('gog_sheets_named_ranges_update', () => {
784
805
  // 22. named-ranges delete
785
806
  describe('gog_sheets_named_ranges_delete', () => {
786
807
  it('calls runOrDiagnose with correct args', async () => {
787
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
788
- const handlers = setupHandlers();
789
- await handlers.get('gog_sheets_named_ranges_delete')!({ spreadsheetId: 'sid', nameOrId: 'OldRange' });
808
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
809
+ const harness = await setupHandlers();
810
+ await harness.callTool('gog_sheets_named_ranges_delete', { spreadsheetId: 'sid', nameOrId: 'OldRange' });
790
811
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'named-ranges', 'delete', 'sid', 'OldRange'], { account: undefined });
791
812
  });
792
813
  });
@@ -794,10 +815,10 @@ describe('gog_sheets_named_ranges_delete', () => {
794
815
  // 23. batch-update (gog 0.18.0)
795
816
  describe('gog_sheets_batch_update', () => {
796
817
  it('passes --data-json and required spreadsheetId', async () => {
797
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
798
- const handlers = setupHandlers();
818
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
819
+ const harness = await setupHandlers();
799
820
  const data = '[{"range":"Sheet1!A1:B1","values":[["a","b"]]}]';
800
- await handlers.get('gog_sheets_batch_update')!({ spreadsheetId: 'sid', dataJson: data });
821
+ await harness.callTool('gog_sheets_batch_update', { spreadsheetId: 'sid', dataJson: data });
801
822
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
802
823
  ['sheets', 'batch-update', 'sid', `--data-json=${data}`],
803
824
  { account: undefined },
@@ -805,9 +826,9 @@ describe('gog_sheets_batch_update', () => {
805
826
  });
806
827
 
807
828
  it('passes optional --input, --include-values-in-response, --response-render, --response-date-time-render', async () => {
808
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
809
- const handlers = setupHandlers();
810
- await handlers.get('gog_sheets_batch_update')!({
829
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
830
+ const harness = await setupHandlers();
831
+ await harness.callTool('gog_sheets_batch_update', {
811
832
  spreadsheetId: 'sid',
812
833
  dataJson: '[]',
813
834
  input: 'RAW',
@@ -830,9 +851,9 @@ describe('gog_sheets_batch_update', () => {
830
851
  });
831
852
 
832
853
  it('omits --include-values-in-response when false', async () => {
833
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
834
- const handlers = setupHandlers();
835
- await handlers.get('gog_sheets_batch_update')!({
854
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
855
+ const harness = await setupHandlers();
856
+ await harness.callTool('gog_sheets_batch_update', {
836
857
  spreadsheetId: 'sid', dataJson: '[]', includeValuesInResponse: false,
837
858
  });
838
859
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
@@ -845,9 +866,9 @@ describe('gog_sheets_batch_update', () => {
845
866
  // 24. reorder-tab (gog 0.18.0)
846
867
  describe('gog_sheets_reorder_tab', () => {
847
868
  it('passes --tab and --to', async () => {
848
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
849
- const handlers = setupHandlers();
850
- await handlers.get('gog_sheets_reorder_tab')!({ spreadsheetId: 'sid', tab: 'Data', to: 2 });
869
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
870
+ const harness = await setupHandlers();
871
+ await harness.callTool('gog_sheets_reorder_tab', { spreadsheetId: 'sid', tab: 'Data', to: 2 });
851
872
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
852
873
  ['sheets', 'reorder-tab', 'sid', '--tab=Data', '--to=2'],
853
874
  { account: undefined },
@@ -855,9 +876,9 @@ describe('gog_sheets_reorder_tab', () => {
855
876
  });
856
877
 
857
878
  it('sends --to=0 explicitly to reach the leftmost position', async () => {
858
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
859
- const handlers = setupHandlers();
860
- await handlers.get('gog_sheets_reorder_tab')!({ spreadsheetId: 'sid', tab: '123', to: 0 });
879
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
880
+ const harness = await setupHandlers();
881
+ await harness.callTool('gog_sheets_reorder_tab', { spreadsheetId: 'sid', tab: '123', to: 0 });
861
882
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
862
883
  ['sheets', 'reorder-tab', 'sid', '--tab=123', '--to=0'],
863
884
  { account: undefined },
@@ -865,9 +886,9 @@ describe('gog_sheets_reorder_tab', () => {
865
886
  });
866
887
 
867
888
  it('forwards account', async () => {
868
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
869
- const handlers = setupHandlers();
870
- await handlers.get('gog_sheets_reorder_tab')!({ spreadsheetId: 'sid', tab: 'Data', to: 1, account: 'a@b.com' });
889
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
890
+ const harness = await setupHandlers();
891
+ await harness.callTool('gog_sheets_reorder_tab', { spreadsheetId: 'sid', tab: 'Data', to: 1, account: 'a@b.com' });
871
892
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
872
893
  ['sheets', 'reorder-tab', 'sid', '--tab=Data', '--to=1'],
873
894
  { account: 'a@b.com' },
@@ -878,16 +899,16 @@ describe('gog_sheets_reorder_tab', () => {
878
899
  // 25. chart list (gog 0.19.0)
879
900
  describe('gog_sheets_chart_list', () => {
880
901
  it('calls runOrDiagnose with correct args', async () => {
881
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
882
- const handlers = setupHandlers();
883
- await handlers.get('gog_sheets_chart_list')!({ spreadsheetId: 'sid' });
902
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
903
+ const harness = await setupHandlers();
904
+ await harness.callTool('gog_sheets_chart_list', { spreadsheetId: 'sid' });
884
905
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'list', 'sid'], { account: undefined });
885
906
  });
886
907
 
887
908
  it('forwards account', async () => {
888
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
889
- const handlers = setupHandlers();
890
- await handlers.get('gog_sheets_chart_list')!({ spreadsheetId: 'sid', account: 'a@b.com' });
909
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
910
+ const harness = await setupHandlers();
911
+ await harness.callTool('gog_sheets_chart_list', { spreadsheetId: 'sid', account: 'a@b.com' });
891
912
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'list', 'sid'], { account: 'a@b.com' });
892
913
  });
893
914
  });
@@ -895,9 +916,9 @@ describe('gog_sheets_chart_list', () => {
895
916
  // 26. chart get
896
917
  describe('gog_sheets_chart_get', () => {
897
918
  it('calls runOrDiagnose with correct args', async () => {
898
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
899
- const handlers = setupHandlers();
900
- await handlers.get('gog_sheets_chart_get')!({ spreadsheetId: 'sid', chartId: '12345' });
919
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
920
+ const harness = await setupHandlers();
921
+ await harness.callTool('gog_sheets_chart_get', { spreadsheetId: 'sid', chartId: '12345' });
901
922
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'get', 'sid', '12345'], { account: undefined });
902
923
  });
903
924
  });
@@ -905,10 +926,10 @@ describe('gog_sheets_chart_get', () => {
905
926
  // 27. chart create
906
927
  describe('gog_sheets_chart_create', () => {
907
928
  it('calls runOrDiagnose with only --spec-json', async () => {
908
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
909
- const handlers = setupHandlers();
929
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
930
+ const harness = await setupHandlers();
910
931
  const spec = '{"basicChart":{"chartType":"COLUMN"}}';
911
- await handlers.get('gog_sheets_chart_create')!({ spreadsheetId: 'sid', specJson: spec });
932
+ await harness.callTool('gog_sheets_chart_create', { spreadsheetId: 'sid', specJson: spec });
912
933
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
913
934
  ['sheets', 'chart', 'create', 'sid', `--spec-json=${spec}`],
914
935
  { account: undefined },
@@ -916,9 +937,9 @@ describe('gog_sheets_chart_create', () => {
916
937
  });
917
938
 
918
939
  it('includes --sheet, --anchor, --width, --height when provided', async () => {
919
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
920
- const handlers = setupHandlers();
921
- await handlers.get('gog_sheets_chart_create')!({
940
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
941
+ const harness = await setupHandlers();
942
+ await harness.callTool('gog_sheets_chart_create', {
922
943
  spreadsheetId: 'sid', specJson: '{}', sheet: 'Data', anchor: 'E10', width: 800, height: 400,
923
944
  });
924
945
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
@@ -928,9 +949,9 @@ describe('gog_sheets_chart_create', () => {
928
949
  });
929
950
 
930
951
  it('includes --width=0 and --height=0 when zero', async () => {
931
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
932
- const handlers = setupHandlers();
933
- await handlers.get('gog_sheets_chart_create')!({ spreadsheetId: 'sid', specJson: '{}', width: 0, height: 0 });
952
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
953
+ const harness = await setupHandlers();
954
+ await harness.callTool('gog_sheets_chart_create', { spreadsheetId: 'sid', specJson: '{}', width: 0, height: 0 });
934
955
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
935
956
  ['sheets', 'chart', 'create', 'sid', '--spec-json={}', '--width=0', '--height=0'],
936
957
  { account: undefined },
@@ -941,9 +962,9 @@ describe('gog_sheets_chart_create', () => {
941
962
  // 28. chart update
942
963
  describe('gog_sheets_chart_update', () => {
943
964
  it('calls runOrDiagnose with chartId and --spec-json', async () => {
944
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
945
- const handlers = setupHandlers();
946
- await handlers.get('gog_sheets_chart_update')!({ spreadsheetId: 'sid', chartId: '99', specJson: '{}' });
965
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
966
+ const harness = await setupHandlers();
967
+ await harness.callTool('gog_sheets_chart_update', { spreadsheetId: 'sid', chartId: '99', specJson: '{}' });
947
968
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
948
969
  ['sheets', 'chart', 'update', 'sid', '99', '--spec-json={}'],
949
970
  { account: undefined },
@@ -954,19 +975,19 @@ describe('gog_sheets_chart_update', () => {
954
975
  // 29. chart delete
955
976
  describe('gog_sheets_chart_delete', () => {
956
977
  it('calls runOrDiagnose with correct args', async () => {
957
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
958
- const handlers = setupHandlers();
959
- await handlers.get('gog_sheets_chart_delete')!({ spreadsheetId: 'sid', chartId: '7' });
960
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'delete', 'sid', '7'], { account: undefined });
978
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
979
+ const harness = await setupHandlers();
980
+ await harness.callTool('gog_sheets_chart_delete', { spreadsheetId: 'sid', chartId: '7' });
981
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'delete', 'sid', '7', '--force'], { account: undefined });
961
982
  });
962
983
  });
963
984
 
964
985
  // 30. table list
965
986
  describe('gog_sheets_table_list', () => {
966
987
  it('calls runOrDiagnose with correct args', async () => {
967
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
968
- const handlers = setupHandlers();
969
- await handlers.get('gog_sheets_table_list')!({ spreadsheetId: 'sid' });
988
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
989
+ const harness = await setupHandlers();
990
+ await harness.callTool('gog_sheets_table_list', { spreadsheetId: 'sid' });
970
991
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'list', 'sid'], { account: undefined });
971
992
  });
972
993
  });
@@ -974,9 +995,9 @@ describe('gog_sheets_table_list', () => {
974
995
  // 31. table get
975
996
  describe('gog_sheets_table_get', () => {
976
997
  it('calls runOrDiagnose with correct args', async () => {
977
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
978
- const handlers = setupHandlers();
979
- await handlers.get('gog_sheets_table_get')!({ spreadsheetId: 'sid', tableId: 't1' });
998
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
999
+ const harness = await setupHandlers();
1000
+ await harness.callTool('gog_sheets_table_get', { spreadsheetId: 'sid', tableId: 't1' });
980
1001
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'get', 'sid', 't1'], { account: undefined });
981
1002
  });
982
1003
  });
@@ -984,10 +1005,10 @@ describe('gog_sheets_table_get', () => {
984
1005
  // 32. table create
985
1006
  describe('gog_sheets_table_create', () => {
986
1007
  it('calls runOrDiagnose with range, --name, --columns-json', async () => {
987
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
988
- const handlers = setupHandlers();
1008
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1009
+ const harness = await setupHandlers();
989
1010
  const cols = '[{"columnName":"Name","columnType":"TEXT"}]';
990
- await handlers.get('gog_sheets_table_create')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:B10', name: 'People', columnsJson: cols });
1011
+ await harness.callTool('gog_sheets_table_create', { spreadsheetId: 'sid', range: 'Sheet1!A1:B10', name: 'People', columnsJson: cols });
991
1012
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
992
1013
  ['sheets', 'table', 'create', 'sid', 'Sheet1!A1:B10', '--name=People', `--columns-json=${cols}`],
993
1014
  { account: undefined },
@@ -998,9 +1019,9 @@ describe('gog_sheets_table_create', () => {
998
1019
  // 33. table append
999
1020
  describe('gog_sheets_table_append', () => {
1000
1021
  it('calls runOrDiagnose with --values-json (no --input)', async () => {
1001
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1002
- const handlers = setupHandlers();
1003
- await handlers.get('gog_sheets_table_append')!({ spreadsheetId: 'sid', tableId: 't1', valuesJson: '[["a",1]]' });
1022
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1023
+ const harness = await setupHandlers();
1024
+ await harness.callTool('gog_sheets_table_append', { spreadsheetId: 'sid', tableId: 't1', valuesJson: '[["a",1]]' });
1004
1025
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1005
1026
  ['sheets', 'table', 'append', 'sid', 't1', '--values-json=[["a",1]]'],
1006
1027
  { account: undefined },
@@ -1008,9 +1029,9 @@ describe('gog_sheets_table_append', () => {
1008
1029
  });
1009
1030
 
1010
1031
  it('includes --input when provided', async () => {
1011
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1012
- const handlers = setupHandlers();
1013
- await handlers.get('gog_sheets_table_append')!({ spreadsheetId: 'sid', tableId: 't1', valuesJson: '[]', input: 'RAW' });
1032
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1033
+ const harness = await setupHandlers();
1034
+ await harness.callTool('gog_sheets_table_append', { spreadsheetId: 'sid', tableId: 't1', valuesJson: '[]', input: 'RAW' });
1014
1035
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1015
1036
  ['sheets', 'table', 'append', 'sid', 't1', '--values-json=[]', '--input=RAW'],
1016
1037
  { account: undefined },
@@ -1021,19 +1042,19 @@ describe('gog_sheets_table_append', () => {
1021
1042
  // 34. table clear
1022
1043
  describe('gog_sheets_table_clear', () => {
1023
1044
  it('calls runOrDiagnose with correct args', async () => {
1024
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1025
- const handlers = setupHandlers();
1026
- await handlers.get('gog_sheets_table_clear')!({ spreadsheetId: 'sid', tableId: 't1' });
1027
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'clear', 'sid', 't1'], { account: undefined });
1045
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1046
+ const harness = await setupHandlers();
1047
+ await harness.callTool('gog_sheets_table_clear', { spreadsheetId: 'sid', tableId: 't1' });
1048
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'clear', 'sid', 't1', '--force'], { account: undefined });
1028
1049
  });
1029
1050
  });
1030
1051
 
1031
1052
  // 35. table delete
1032
1053
  describe('gog_sheets_table_delete', () => {
1033
1054
  it('keep_data=false deletes table (and data) directly with --discard-data --force', async () => {
1034
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1035
- const handlers = setupHandlers();
1036
- await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1', keep_data: false });
1055
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1056
+ const harness = await setupHandlers();
1057
+ await harness.callTool('gog_sheets_table_delete', { spreadsheetId: 'sid', tableId: 't1', keep_data: false });
1037
1058
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'delete', 'sid', 't1', '--discard-data', '--force'], { account: undefined });
1038
1059
  expect(lib.run).not.toHaveBeenCalled();
1039
1060
  });
@@ -1044,8 +1065,8 @@ describe('gog_sheets_table_delete', () => {
1044
1065
  .mockResolvedValueOnce('{"values":[["Name","City","N"],["A","NYC","=1+1"]]}') // get --render=FORMULA
1045
1066
  .mockResolvedValueOnce('{"deleted":{"tableId":"t1"}}') // table delete
1046
1067
  .mockResolvedValueOnce('{"updatedRows":2}'); // update restore
1047
- const handlers = setupHandlers();
1048
- const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1', account: 'a@b.com' });
1068
+ const harness = await setupHandlers();
1069
+ const res = await harness.callTool('gog_sheets_table_delete', { spreadsheetId: 'sid', tableId: 't1', account: 'a@b.com' });
1049
1070
  expect(lib.run).toHaveBeenNthCalledWith(1, ['sheets', 'table', 'get', 'sid', 't1'], { account: 'a@b.com' });
1050
1071
  expect(lib.run).toHaveBeenNthCalledWith(2, ['sheets', 'get', 'sid', 'Sheet1!A1:C2', '--render=FORMULA'], { account: 'a@b.com' });
1051
1072
  expect(lib.run).toHaveBeenNthCalledWith(3, ['sheets', 'table', 'delete', 'sid', 't1', '--discard-data', '--force'], { account: 'a@b.com' });
@@ -1059,26 +1080,26 @@ describe('gog_sheets_table_delete', () => {
1059
1080
  .mockResolvedValueOnce('{"table":{"a1":"Sheet1!A1:C1"}}') // table get
1060
1081
  .mockResolvedValueOnce('{}') // get --render=FORMULA, no values key
1061
1082
  .mockResolvedValueOnce('{"deleted":{}}'); // table delete
1062
- const handlers = setupHandlers();
1063
- const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
1083
+ const harness = await setupHandlers();
1084
+ const res = await harness.callTool('gog_sheets_table_delete', { spreadsheetId: 'sid', tableId: 't1' });
1064
1085
  expect(lib.run).toHaveBeenCalledTimes(3); // no update call
1065
1086
  expect(res.content[0].text).toContain('0 row');
1066
1087
  });
1067
1088
 
1068
1089
  it('aborts without deleting when the table has no a1 range', async () => {
1069
1090
  vi.mocked(lib.run).mockResolvedValueOnce('{"name":"Repro"}'); // table get, no a1
1070
- vi.mocked(lib.diagnose).mockResolvedValue(toText('diagnosed'));
1071
- const handlers = setupHandlers();
1072
- const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
1091
+ vi.mocked(lib.diagnose).mockResolvedValue(rawTextResult('diagnosed'));
1092
+ const harness = await setupHandlers();
1093
+ const res = await harness.callTool('gog_sheets_table_delete', { spreadsheetId: 'sid', tableId: 't1' });
1073
1094
  expect(lib.run).toHaveBeenCalledTimes(1); // only the read, no delete
1074
1095
  expect(res.content[0].text).toBe('diagnosed');
1075
1096
  });
1076
1097
 
1077
1098
  it('aborts without deleting when the pre-delete read fails', async () => {
1078
1099
  vi.mocked(lib.run).mockRejectedValueOnce(new Error('read boom')); // table get throws
1079
- vi.mocked(lib.diagnose).mockResolvedValue(toText('diagnosed read'));
1080
- const handlers = setupHandlers();
1081
- const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
1100
+ vi.mocked(lib.diagnose).mockResolvedValue(rawTextResult('diagnosed read'));
1101
+ const harness = await setupHandlers();
1102
+ const res = await harness.callTool('gog_sheets_table_delete', { spreadsheetId: 'sid', tableId: 't1' });
1082
1103
  expect(lib.run).toHaveBeenCalledTimes(1);
1083
1104
  expect(res.content[0].text).toBe('diagnosed read');
1084
1105
  });
@@ -1088,9 +1109,9 @@ describe('gog_sheets_table_delete', () => {
1088
1109
  .mockResolvedValueOnce('{"table":{"a1":"Sheet1!A1:C2"}}')
1089
1110
  .mockResolvedValueOnce('{"values":[["A"]]}')
1090
1111
  .mockRejectedValueOnce(new Error('delete boom')); // table delete throws
1091
- vi.mocked(lib.diagnose).mockResolvedValue(toText('diagnosed delete'));
1092
- const handlers = setupHandlers();
1093
- const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
1112
+ vi.mocked(lib.diagnose).mockResolvedValue(rawTextResult('diagnosed delete'));
1113
+ const harness = await setupHandlers();
1114
+ const res = await harness.callTool('gog_sheets_table_delete', { spreadsheetId: 'sid', tableId: 't1' });
1094
1115
  expect(res.content[0].text).toBe('diagnosed delete');
1095
1116
  });
1096
1117
 
@@ -1100,8 +1121,8 @@ describe('gog_sheets_table_delete', () => {
1100
1121
  .mockResolvedValueOnce('{"values":[["keepme"]]}')
1101
1122
  .mockResolvedValueOnce('{"deleted":{}}')
1102
1123
  .mockRejectedValueOnce(new Error('restore boom')); // update throws
1103
- const handlers = setupHandlers();
1104
- const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
1124
+ const harness = await setupHandlers();
1125
+ const res = await harness.callTool('gog_sheets_table_delete', { spreadsheetId: 'sid', tableId: 't1' });
1105
1126
  const text = res.content[0].text;
1106
1127
  expect(text).toContain('restoring its data FAILED');
1107
1128
  expect(text).toContain('keepme'); // the data is handed back for manual recovery
@@ -1112,16 +1133,16 @@ describe('gog_sheets_table_delete', () => {
1112
1133
  // 36. banding list
1113
1134
  describe('gog_sheets_banding_list', () => {
1114
1135
  it('calls runOrDiagnose with no --sheet', async () => {
1115
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1116
- const handlers = setupHandlers();
1117
- await handlers.get('gog_sheets_banding_list')!({ spreadsheetId: 'sid' });
1136
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1137
+ const harness = await setupHandlers();
1138
+ await harness.callTool('gog_sheets_banding_list', { spreadsheetId: 'sid' });
1118
1139
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'list', 'sid'], { account: undefined });
1119
1140
  });
1120
1141
 
1121
1142
  it('includes --sheet when provided', async () => {
1122
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1123
- const handlers = setupHandlers();
1124
- await handlers.get('gog_sheets_banding_list')!({ spreadsheetId: 'sid', sheet: 'Data' });
1143
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1144
+ const harness = await setupHandlers();
1145
+ await harness.callTool('gog_sheets_banding_list', { spreadsheetId: 'sid', sheet: 'Data' });
1125
1146
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'list', 'sid', '--sheet=Data'], { account: undefined });
1126
1147
  });
1127
1148
  });
@@ -1129,16 +1150,16 @@ describe('gog_sheets_banding_list', () => {
1129
1150
  // 37. banding set
1130
1151
  describe('gog_sheets_banding_set', () => {
1131
1152
  it('calls runOrDiagnose with range only', async () => {
1132
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1133
- const handlers = setupHandlers();
1134
- await handlers.get('gog_sheets_banding_set')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:D20' });
1153
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1154
+ const harness = await setupHandlers();
1155
+ await harness.callTool('gog_sheets_banding_set', { spreadsheetId: 'sid', range: 'Sheet1!A1:D20' });
1135
1156
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'set', 'sid', 'Sheet1!A1:D20'], { account: undefined });
1136
1157
  });
1137
1158
 
1138
1159
  it('includes --row-properties-json and --column-properties-json when provided', async () => {
1139
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1140
- const handlers = setupHandlers();
1141
- await handlers.get('gog_sheets_banding_set')!({
1160
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1161
+ const harness = await setupHandlers();
1162
+ await harness.callTool('gog_sheets_banding_set', {
1142
1163
  spreadsheetId: 'sid', range: 'A1:D20',
1143
1164
  rowPropertiesJson: '{"firstBandColor":{}}',
1144
1165
  columnPropertiesJson: '{"secondBandColor":{}}',
@@ -1153,47 +1174,47 @@ describe('gog_sheets_banding_set', () => {
1153
1174
  // 38. banding clear
1154
1175
  describe('gog_sheets_banding_clear', () => {
1155
1176
  it('calls runOrDiagnose with no optional flags', async () => {
1156
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1157
- const handlers = setupHandlers();
1158
- await handlers.get('gog_sheets_banding_clear')!({ spreadsheetId: 'sid' });
1159
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid'], { account: undefined });
1177
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1178
+ const harness = await setupHandlers();
1179
+ await harness.callTool('gog_sheets_banding_clear', { spreadsheetId: 'sid' });
1180
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--force'], { account: undefined });
1160
1181
  });
1161
1182
 
1162
1183
  it('includes --id when provided (including 0)', async () => {
1163
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1164
- const handlers = setupHandlers();
1165
- await handlers.get('gog_sheets_banding_clear')!({ spreadsheetId: 'sid', id: 0 });
1166
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--id=0'], { account: undefined });
1184
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1185
+ const harness = await setupHandlers();
1186
+ await harness.callTool('gog_sheets_banding_clear', { spreadsheetId: 'sid', id: 0 });
1187
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--id=0', '--force'], { account: undefined });
1167
1188
  });
1168
1189
 
1169
1190
  it('includes --all and --sheet when clearing a whole sheet', async () => {
1170
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1171
- const handlers = setupHandlers();
1172
- await handlers.get('gog_sheets_banding_clear')!({ spreadsheetId: 'sid', all: true, sheet: 'Data' });
1173
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--all', '--sheet=Data'], { account: undefined });
1191
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1192
+ const harness = await setupHandlers();
1193
+ await harness.callTool('gog_sheets_banding_clear', { spreadsheetId: 'sid', all: true, sheet: 'Data' });
1194
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--all', '--sheet=Data', '--force'], { account: undefined });
1174
1195
  });
1175
1196
 
1176
1197
  it('omits --all when false', async () => {
1177
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1178
- const handlers = setupHandlers();
1179
- await handlers.get('gog_sheets_banding_clear')!({ spreadsheetId: 'sid', all: false, id: 5 });
1180
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--id=5'], { account: undefined });
1198
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1199
+ const harness = await setupHandlers();
1200
+ await harness.callTool('gog_sheets_banding_clear', { spreadsheetId: 'sid', all: false, id: 5 });
1201
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--id=5', '--force'], { account: undefined });
1181
1202
  });
1182
1203
  });
1183
1204
 
1184
1205
  // 39. conditional-format list
1185
1206
  describe('gog_sheets_conditional_format_list', () => {
1186
1207
  it('calls runOrDiagnose with no --sheet', async () => {
1187
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1188
- const handlers = setupHandlers();
1189
- await handlers.get('gog_sheets_conditional_format_list')!({ spreadsheetId: 'sid' });
1208
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1209
+ const harness = await setupHandlers();
1210
+ await harness.callTool('gog_sheets_conditional_format_list', { spreadsheetId: 'sid' });
1190
1211
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'list', 'sid'], { account: undefined });
1191
1212
  });
1192
1213
 
1193
1214
  it('includes --sheet when provided', async () => {
1194
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1195
- const handlers = setupHandlers();
1196
- await handlers.get('gog_sheets_conditional_format_list')!({ spreadsheetId: 'sid', sheet: 'Data' });
1215
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1216
+ const harness = await setupHandlers();
1217
+ await harness.callTool('gog_sheets_conditional_format_list', { spreadsheetId: 'sid', sheet: 'Data' });
1197
1218
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'list', 'sid', '--sheet=Data'], { account: undefined });
1198
1219
  });
1199
1220
  });
@@ -1201,9 +1222,9 @@ describe('gog_sheets_conditional_format_list', () => {
1201
1222
  // 40. conditional-format add
1202
1223
  describe('gog_sheets_conditional_format_add', () => {
1203
1224
  it('calls runOrDiagnose with type and --format-json (no expr/fields)', async () => {
1204
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1205
- const handlers = setupHandlers();
1206
- await handlers.get('gog_sheets_conditional_format_add')!({
1225
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1226
+ const harness = await setupHandlers();
1227
+ await harness.callTool('gog_sheets_conditional_format_add', {
1207
1228
  spreadsheetId: 'sid', range: 'A1:A100', type: 'not-blank', formatJson: '{"backgroundColor":{}}',
1208
1229
  });
1209
1230
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
@@ -1213,9 +1234,9 @@ describe('gog_sheets_conditional_format_add', () => {
1213
1234
  });
1214
1235
 
1215
1236
  it('includes --expr and --format-fields when provided', async () => {
1216
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1217
- const handlers = setupHandlers();
1218
- await handlers.get('gog_sheets_conditional_format_add')!({
1237
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1238
+ const harness = await setupHandlers();
1239
+ await harness.callTool('gog_sheets_conditional_format_add', {
1219
1240
  spreadsheetId: 'sid', range: 'B1:B50', type: 'number-gt', expr: '100', formatJson: '{}', formatFields: 'backgroundColor,textFormat.bold',
1220
1241
  });
1221
1242
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
@@ -1225,9 +1246,9 @@ describe('gog_sheets_conditional_format_add', () => {
1225
1246
  });
1226
1247
 
1227
1248
  it('includes --expr when it is an empty string', async () => {
1228
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1229
- const handlers = setupHandlers();
1230
- await handlers.get('gog_sheets_conditional_format_add')!({
1249
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1250
+ const harness = await setupHandlers();
1251
+ await harness.callTool('gog_sheets_conditional_format_add', {
1231
1252
  spreadsheetId: 'sid', range: 'A1', type: 'text-eq', expr: '', formatJson: '{}',
1232
1253
  });
1233
1254
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
@@ -1235,52 +1256,97 @@ describe('gog_sheets_conditional_format_add', () => {
1235
1256
  { account: undefined },
1236
1257
  );
1237
1258
  });
1259
+
1260
+ it('passes --gradient-rule-json for gradient rules (no type/format-json)', async () => {
1261
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1262
+ const harness = await setupHandlers();
1263
+ await harness.callTool('gog_sheets_conditional_format_add', {
1264
+ spreadsheetId: 'sid', range: 'Sheet1!A1:A100', gradientRuleJson: '{"minpoint":{},"maxpoint":{}}',
1265
+ });
1266
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1267
+ ['sheets', 'conditional-format', 'add', 'sid', 'Sheet1!A1:A100', '--gradient-rule-json={"minpoint":{},"maxpoint":{}}'],
1268
+ { account: undefined },
1269
+ );
1270
+ });
1271
+ });
1272
+
1273
+ // 40b. filter set
1274
+ describe('gog_sheets_filter_set', () => {
1275
+ it('calls runOrDiagnose without --force by default', async () => {
1276
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1277
+ const harness = await setupHandlers();
1278
+ await harness.callTool('gog_sheets_filter_set', { spreadsheetId: 'sid', range: 'Sheet1!A1:C10' });
1279
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1280
+ ['sheets', 'filter', 'set', 'sid', 'Sheet1!A1:C10'],
1281
+ { account: undefined },
1282
+ );
1283
+ });
1284
+
1285
+ it('appends --force when replace is true', async () => {
1286
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1287
+ const harness = await setupHandlers();
1288
+ await harness.callTool('gog_sheets_filter_set', { spreadsheetId: 'sid', range: 'Sheet1!A1:C10', replace: true });
1289
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1290
+ ['sheets', 'filter', 'set', 'sid', 'Sheet1!A1:C10', '--force'],
1291
+ { account: undefined },
1292
+ );
1293
+ });
1294
+
1295
+ it('forwards account', async () => {
1296
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1297
+ const harness = await setupHandlers();
1298
+ await harness.callTool('gog_sheets_filter_set', { spreadsheetId: 'sid', range: 'Named', account: 'x@y.com' });
1299
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1300
+ ['sheets', 'filter', 'set', 'sid', 'Named'],
1301
+ { account: 'x@y.com' },
1302
+ );
1303
+ });
1238
1304
  });
1239
1305
 
1240
1306
  // 41. conditional-format clear
1241
1307
  describe('gog_sheets_conditional_format_clear', () => {
1242
1308
  it('calls runOrDiagnose with --sheet only', async () => {
1243
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1244
- const handlers = setupHandlers();
1245
- await handlers.get('gog_sheets_conditional_format_clear')!({ spreadsheetId: 'sid', sheet: 'Data' });
1246
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data'], { account: undefined });
1309
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1310
+ const harness = await setupHandlers();
1311
+ await harness.callTool('gog_sheets_conditional_format_clear', { spreadsheetId: 'sid', sheet: 'Data' });
1312
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--force'], { account: undefined });
1247
1313
  });
1248
1314
 
1249
1315
  it('includes --index when provided (including 0)', async () => {
1250
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1251
- const handlers = setupHandlers();
1252
- await handlers.get('gog_sheets_conditional_format_clear')!({ spreadsheetId: 'sid', sheet: 'Data', index: 0 });
1253
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--index=0'], { account: undefined });
1316
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1317
+ const harness = await setupHandlers();
1318
+ await harness.callTool('gog_sheets_conditional_format_clear', { spreadsheetId: 'sid', sheet: 'Data', index: 0 });
1319
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--index=0', '--force'], { account: undefined });
1254
1320
  });
1255
1321
 
1256
1322
  it('includes --all when true', async () => {
1257
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1258
- const handlers = setupHandlers();
1259
- await handlers.get('gog_sheets_conditional_format_clear')!({ spreadsheetId: 'sid', sheet: 'Data', all: true });
1260
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--all'], { account: undefined });
1323
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1324
+ const harness = await setupHandlers();
1325
+ await harness.callTool('gog_sheets_conditional_format_clear', { spreadsheetId: 'sid', sheet: 'Data', all: true });
1326
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--all', '--force'], { account: undefined });
1261
1327
  });
1262
1328
 
1263
1329
  it('omits --all when false', async () => {
1264
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1265
- const handlers = setupHandlers();
1266
- await handlers.get('gog_sheets_conditional_format_clear')!({ spreadsheetId: 'sid', sheet: 'Data', all: false, index: 2 });
1267
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--index=2'], { account: undefined });
1330
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1331
+ const harness = await setupHandlers();
1332
+ await harness.callTool('gog_sheets_conditional_format_clear', { spreadsheetId: 'sid', sheet: 'Data', all: false, index: 2 });
1333
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--index=2', '--force'], { account: undefined });
1268
1334
  });
1269
1335
  });
1270
1336
 
1271
1337
  // 49. snapshot (backup before destructive edits)
1272
1338
  describe('gog_sheets_snapshot', () => {
1273
1339
  it('copies the spreadsheet to a named backup', async () => {
1274
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1275
- const handlers = setupHandlers();
1276
- await handlers.get('gog_sheets_snapshot')!({ spreadsheetId: 'sid', name: 'Backup A' });
1340
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1341
+ const harness = await setupHandlers();
1342
+ await harness.callTool('gog_sheets_snapshot', { spreadsheetId: 'sid', name: 'Backup A' });
1277
1343
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'copy', 'sid', 'Backup A'], { account: undefined });
1278
1344
  });
1279
1345
 
1280
1346
  it('includes --parent and forwards account', async () => {
1281
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1282
- const handlers = setupHandlers();
1283
- await handlers.get('gog_sheets_snapshot')!({ spreadsheetId: 'sid', name: 'Backup A', parent: 'folder1', account: 'a@b.com' });
1347
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1348
+ const harness = await setupHandlers();
1349
+ await harness.callTool('gog_sheets_snapshot', { spreadsheetId: 'sid', name: 'Backup A', parent: 'folder1', account: 'a@b.com' });
1284
1350
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'copy', 'sid', 'Backup A', '--parent=folder1'], { account: 'a@b.com' });
1285
1351
  });
1286
1352
  });
@@ -1288,9 +1354,9 @@ describe('gog_sheets_snapshot', () => {
1288
1354
  // gog 0.24.0
1289
1355
  describe('gog_sheets_validation_get', () => {
1290
1356
  it('reads validation rules for a range', async () => {
1291
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1292
- const handlers = setupHandlers();
1293
- await handlers.get('gog_sheets_validation_get')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:A10' });
1357
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1358
+ const harness = await setupHandlers();
1359
+ await harness.callTool('gog_sheets_validation_get', { spreadsheetId: 'sid', range: 'Sheet1!A1:A10' });
1294
1360
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1295
1361
  ['sheets', 'validation', 'get', 'sid', 'Sheet1!A1:A10'],
1296
1362
  { account: undefined },
@@ -1300,9 +1366,9 @@ describe('gog_sheets_validation_get', () => {
1300
1366
 
1301
1367
  describe('gog_sheets_validation_set', () => {
1302
1368
  it('sets a dropdown rule with repeated values', async () => {
1303
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1304
- const handlers = setupHandlers();
1305
- await handlers.get('gog_sheets_validation_set')!({
1369
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1370
+ const harness = await setupHandlers();
1371
+ await harness.callTool('gog_sheets_validation_set', {
1306
1372
  spreadsheetId: 'sid', range: 'A1:A10', type: 'ONE_OF_LIST', values: ['Red', 'Green'],
1307
1373
  });
1308
1374
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
@@ -1312,9 +1378,9 @@ describe('gog_sheets_validation_set', () => {
1312
1378
  });
1313
1379
 
1314
1380
  it('passes strict, input message, custom UI and filtered-rows flags', async () => {
1315
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1316
- const handlers = setupHandlers();
1317
- await handlers.get('gog_sheets_validation_set')!({
1381
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1382
+ const harness = await setupHandlers();
1383
+ await harness.callTool('gog_sheets_validation_set', {
1318
1384
  spreadsheetId: 'sid', range: 'A1', type: 'NUMBER_BETWEEN', values: ['1', '10'],
1319
1385
  strict: true, inputMessage: 'Pick 1-10', showCustomUi: true, filteredRowsIncluded: true,
1320
1386
  });
@@ -1328,9 +1394,9 @@ describe('gog_sheets_validation_set', () => {
1328
1394
 
1329
1395
  describe('gog_sheets_validation_clear', () => {
1330
1396
  it('clears rules from a range', async () => {
1331
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1332
- const handlers = setupHandlers();
1333
- await handlers.get('gog_sheets_validation_clear')!({ spreadsheetId: 'sid', range: 'A1:A10', filteredRowsIncluded: true });
1397
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1398
+ const harness = await setupHandlers();
1399
+ await harness.callTool('gog_sheets_validation_clear', { spreadsheetId: 'sid', range: 'A1:A10', filteredRowsIncluded: true });
1334
1400
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1335
1401
  ['sheets', 'validation', 'clear', 'sid', 'A1:A10', '--filtered-rows-included'],
1336
1402
  { account: undefined },
@@ -1340,26 +1406,77 @@ describe('gog_sheets_validation_clear', () => {
1340
1406
 
1341
1407
  describe('gog_sheets_delete_dimension', () => {
1342
1408
  it('deletes a row span from a sheet target', async () => {
1343
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1344
- const handlers = setupHandlers();
1345
- await handlers.get('gog_sheets_delete_dimension')!({
1409
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1410
+ const harness = await setupHandlers();
1411
+ await harness.callTool('gog_sheets_delete_dimension', {
1346
1412
  spreadsheetId: 'sid', rangeOrSheet: 'Sheet1', dimension: 'ROWS', start: 5, end: 7,
1347
1413
  });
1348
1414
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1349
- ['sheets', 'delete-dimension', 'sid', 'Sheet1', '--dimension=ROWS', '--start=5', '--end=7'],
1415
+ ['sheets', 'delete-dimension', 'sid', 'Sheet1', '--dimension=ROWS', '--start=5', '--end=7', '--force'],
1350
1416
  { account: undefined },
1351
1417
  );
1352
1418
  });
1353
1419
 
1354
1420
  it('deletes columns by A1 range without start/end', async () => {
1355
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1356
- const handlers = setupHandlers();
1357
- await handlers.get('gog_sheets_delete_dimension')!({
1421
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1422
+ const harness = await setupHandlers();
1423
+ await harness.callTool('gog_sheets_delete_dimension', {
1358
1424
  spreadsheetId: 'sid', rangeOrSheet: 'Sheet1!C:D', dimension: 'COLUMNS', account: 'a@b.com',
1359
1425
  });
1360
1426
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1361
- ['sheets', 'delete-dimension', 'sid', 'Sheet1!C:D', '--dimension=COLUMNS'],
1427
+ ['sheets', 'delete-dimension', 'sid', 'Sheet1!C:D', '--dimension=COLUMNS', '--force'],
1362
1428
  { account: 'a@b.com' },
1363
1429
  );
1364
1430
  });
1365
1431
  });
1432
+
1433
+ // Falsy-branch coverage for the validation tools (no optional flags)
1434
+ describe('validation tools bare calls', () => {
1435
+ it('gog_sheets_validation_set without values (e.g. BOOLEAN checkbox)', async () => {
1436
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1437
+ const harness = await setupHandlers();
1438
+ await harness.callTool('gog_sheets_validation_set', { spreadsheetId: 'sid', range: 'A1', type: 'BOOLEAN' });
1439
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1440
+ ['sheets', 'validation', 'set', 'sid', 'A1', '--type=BOOLEAN'],
1441
+ { account: undefined },
1442
+ );
1443
+ });
1444
+
1445
+ it('gog_sheets_validation_clear without filteredRowsIncluded', async () => {
1446
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
1447
+ const harness = await setupHandlers();
1448
+ await harness.callTool('gog_sheets_validation_clear', { spreadsheetId: 'sid', range: 'A1' });
1449
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1450
+ ['sheets', 'validation', 'clear', 'sid', 'A1'],
1451
+ { account: undefined },
1452
+ );
1453
+ });
1454
+ });
1455
+
1456
+ // resultText degradations: a non-text tool result (never produced by
1457
+ // runOrDiagnose today, but allowed by the MCP result shape) degrades
1458
+ // gracefully instead of crashing the post-processing.
1459
+ describe('non-text result passthrough', () => {
1460
+ it('gog_sheets_number_format skips the warning when the peek result is non-text', async () => {
1461
+ const harness = await setupHandlers();
1462
+ vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
1463
+ if (args[1] === 'get') return { content: [] };
1464
+ return rawTextResult('{"ok":true}');
1465
+ });
1466
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
1467
+ expect(result.content[0].text).toBe('{"ok":true}');
1468
+ });
1469
+
1470
+ it('gog_sheets_number_format merges the warning onto a non-text format result', async () => {
1471
+ const harness = await setupHandlers();
1472
+ vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
1473
+ if (args[1] === 'get') {
1474
+ return rawTextResult(JSON.stringify({ range: 'A1:A3', values: [[1], [2], [3]] }));
1475
+ }
1476
+ return { content: [] };
1477
+ });
1478
+ const result = await harness.callTool('gog_sheets_number_format', { spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
1479
+ expect(result.content[0].type).toBe('text');
1480
+ expect(result.content[0].text).toMatch(/warning/i);
1481
+ });
1482
+ });