gogcli-mcp-docs 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -0
- package/src/index.ts +10 -0
- package/src/tools/docs-extra.ts +227 -0
- package/tests/tools/docs-extra.test.ts +653 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gogcli-mcp-docs",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Extended Google Docs MCP server via gogcli — all base tools plus full Docs support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gogcli-mcp-docs": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc --noEmit && npm run bundle",
|
|
14
|
+
"bundle": "node ../../scripts/bundle.js src/index.ts dist/index.js",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"test:coverage": "vitest run --coverage"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"gogcli-mcp": "*",
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
23
|
+
"zod": "^4.3.6"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createBaseServer } from 'gogcli-mcp/lib';
|
|
4
|
+
import { registerExtraDocsTools } from './tools/docs-extra.js';
|
|
5
|
+
|
|
6
|
+
const server = createBaseServer({ name: 'gogcli-docs' });
|
|
7
|
+
registerExtraDocsTools(server);
|
|
8
|
+
|
|
9
|
+
const transport = new StdioServerTransport();
|
|
10
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { accountParam, runOrDiagnose } from 'gogcli-mcp/lib';
|
|
4
|
+
|
|
5
|
+
export function registerExtraDocsTools(server: McpServer): void {
|
|
6
|
+
server.registerTool('gog_docs_copy', {
|
|
7
|
+
description: 'Copy a Google Doc to a new document with the given title.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
10
|
+
title: z.string().describe('Title for the new copy'),
|
|
11
|
+
parent: z.string().optional().describe('Parent folder ID to place the copy in'),
|
|
12
|
+
account: accountParam,
|
|
13
|
+
},
|
|
14
|
+
}, async ({ docId, title, parent, account }) => {
|
|
15
|
+
const args = ['docs', 'copy', docId, title];
|
|
16
|
+
if (parent) args.push(`--parent=${parent}`);
|
|
17
|
+
return runOrDiagnose(args, { account });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
server.registerTool('gog_docs_delete', {
|
|
21
|
+
description: 'Delete content within a Google Doc by character index range.',
|
|
22
|
+
annotations: { destructiveHint: true },
|
|
23
|
+
inputSchema: {
|
|
24
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
25
|
+
start: z.number().describe('Start index (character position, 1-based)'),
|
|
26
|
+
end: z.number().describe('End index (character position, exclusive)'),
|
|
27
|
+
tabId: z.string().optional().describe('Tab ID to delete content from (for multi-tab docs)'),
|
|
28
|
+
account: accountParam,
|
|
29
|
+
},
|
|
30
|
+
}, async ({ docId, start, end, tabId, account }) => {
|
|
31
|
+
const args = ['docs', 'delete', `--start=${start}`, `--end=${end}`, docId];
|
|
32
|
+
if (tabId) args.push(`--tab-id=${tabId}`);
|
|
33
|
+
return runOrDiagnose(args, { account });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
server.registerTool('gog_docs_edit', {
|
|
37
|
+
description: 'Edit a Google Doc by finding and replacing text (stream-edit style).',
|
|
38
|
+
annotations: { destructiveHint: true },
|
|
39
|
+
inputSchema: {
|
|
40
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
41
|
+
find: z.string().describe('Text to find'),
|
|
42
|
+
replace: z.string().describe('Replacement text'),
|
|
43
|
+
matchCase: z.boolean().optional().describe('Case-sensitive matching'),
|
|
44
|
+
account: accountParam,
|
|
45
|
+
},
|
|
46
|
+
}, async ({ docId, find, replace, matchCase, account }) => {
|
|
47
|
+
const args = ['docs', 'edit', docId, find, replace];
|
|
48
|
+
if (matchCase) args.push('--match-case');
|
|
49
|
+
return runOrDiagnose(args, { account });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
server.registerTool('gog_docs_export', {
|
|
53
|
+
description: 'Export a Google Doc as PDF, plain text, HTML, DOCX, or other format.',
|
|
54
|
+
annotations: { readOnlyHint: true },
|
|
55
|
+
inputSchema: {
|
|
56
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
57
|
+
format: z.string().optional().describe('Export format: pdf, txt, html, docx, rtf, odt, epub (default: pdf)'),
|
|
58
|
+
out: z.string().optional().describe('Output file path'),
|
|
59
|
+
account: accountParam,
|
|
60
|
+
},
|
|
61
|
+
}, async ({ docId, format, out, account }) => {
|
|
62
|
+
const args = ['docs', 'export', docId];
|
|
63
|
+
if (format) args.push(`--format=${format}`);
|
|
64
|
+
if (out) args.push(`--out=${out}`);
|
|
65
|
+
return runOrDiagnose(args, { account });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
server.registerTool('gog_docs_insert', {
|
|
69
|
+
description: 'Insert text at a specific position in a Google Doc.',
|
|
70
|
+
annotations: { destructiveHint: true },
|
|
71
|
+
inputSchema: {
|
|
72
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
73
|
+
content: z.string().optional().describe('Text content to insert'),
|
|
74
|
+
index: z.number().optional().describe('Character index to insert at (default: end of document)'),
|
|
75
|
+
file: z.string().optional().describe('Path to a file whose content to insert'),
|
|
76
|
+
tabId: z.string().optional().describe('Tab ID to insert into (for multi-tab docs)'),
|
|
77
|
+
account: accountParam,
|
|
78
|
+
},
|
|
79
|
+
}, async ({ docId, content, index, file, tabId, account }) => {
|
|
80
|
+
const args = ['docs', 'insert', docId];
|
|
81
|
+
if (content) args.push(content);
|
|
82
|
+
if (index !== undefined) args.push(`--index=${index}`);
|
|
83
|
+
if (file) args.push(`--file=${file}`);
|
|
84
|
+
if (tabId) args.push(`--tab-id=${tabId}`);
|
|
85
|
+
return runOrDiagnose(args, { account });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
server.registerTool('gog_docs_list_tabs', {
|
|
89
|
+
description: 'List all tabs in a Google Doc.',
|
|
90
|
+
annotations: { readOnlyHint: true },
|
|
91
|
+
inputSchema: {
|
|
92
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
93
|
+
account: accountParam,
|
|
94
|
+
},
|
|
95
|
+
}, async ({ docId, account }) => {
|
|
96
|
+
return runOrDiagnose(['docs', 'list-tabs', docId], { account });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
server.registerTool('gog_docs_sed', {
|
|
100
|
+
description: 'Stream-edit a Google Doc with sed-like regex expressions (s/find/replace/ syntax).',
|
|
101
|
+
annotations: { destructiveHint: true },
|
|
102
|
+
inputSchema: {
|
|
103
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
104
|
+
expression: z.string().optional().describe('Single sed expression (e.g. "s/old/new/g")'),
|
|
105
|
+
expressions: z.array(z.string()).optional().describe('Multiple sed expressions'),
|
|
106
|
+
file: z.string().optional().describe('Path to a sed script file'),
|
|
107
|
+
tab: z.string().optional().describe('Tab name to edit'),
|
|
108
|
+
account: accountParam,
|
|
109
|
+
},
|
|
110
|
+
}, async ({ docId, expression, expressions, file, tab, account }) => {
|
|
111
|
+
const args = ['docs', 'sed', docId];
|
|
112
|
+
if (expression) args.push(expression);
|
|
113
|
+
if (expressions) {
|
|
114
|
+
for (const expr of expressions) {
|
|
115
|
+
args.push(`--expressions=${expr}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (file) args.push(`--file=${file}`);
|
|
119
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
120
|
+
return runOrDiagnose(args, { account });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
server.registerTool('gog_docs_update', {
|
|
124
|
+
description: 'Update a Google Doc — insert or replace text at a specific position.',
|
|
125
|
+
annotations: { destructiveHint: true },
|
|
126
|
+
inputSchema: {
|
|
127
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
128
|
+
text: z.string().optional().describe('Text content to write'),
|
|
129
|
+
file: z.string().optional().describe('Path to a file whose content to use'),
|
|
130
|
+
index: z.number().optional().describe('Character index to insert at'),
|
|
131
|
+
tabId: z.string().optional().describe('Tab ID for multi-tab docs'),
|
|
132
|
+
pageless: z.boolean().optional().describe('Set document to pageless format'),
|
|
133
|
+
account: accountParam,
|
|
134
|
+
},
|
|
135
|
+
}, async ({ docId, text, file, index, tabId, pageless, account }) => {
|
|
136
|
+
const args = ['docs', 'update', docId];
|
|
137
|
+
if (text) args.push(`--text=${text}`);
|
|
138
|
+
if (file) args.push(`--file=${file}`);
|
|
139
|
+
if (index !== undefined) args.push(`--index=${index}`);
|
|
140
|
+
if (tabId) args.push(`--tab-id=${tabId}`);
|
|
141
|
+
if (pageless) args.push('--pageless');
|
|
142
|
+
return runOrDiagnose(args, { account });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// --- Dedicated comment tools (replacing the generic escape hatch in base) ---
|
|
146
|
+
|
|
147
|
+
server.registerTool('gog_docs_comments_list', {
|
|
148
|
+
description:
|
|
149
|
+
'List comments on a Google Doc. Returns open comments by default; set includeResolved=true to include resolved comments.',
|
|
150
|
+
annotations: { readOnlyHint: true },
|
|
151
|
+
inputSchema: {
|
|
152
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
153
|
+
includeResolved: z.boolean().optional().describe('Include resolved comments (default: false, open only)'),
|
|
154
|
+
account: accountParam,
|
|
155
|
+
},
|
|
156
|
+
}, async ({ docId, includeResolved, account }) => {
|
|
157
|
+
const args = ['docs', 'comments', 'list', docId];
|
|
158
|
+
if (includeResolved) args.push('--include-resolved');
|
|
159
|
+
return runOrDiagnose(args, { account });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
server.registerTool('gog_docs_comments_get', {
|
|
163
|
+
description: 'Get a single comment by ID, including its replies.',
|
|
164
|
+
annotations: { readOnlyHint: true },
|
|
165
|
+
inputSchema: {
|
|
166
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
167
|
+
commentId: z.string().describe('Comment ID'),
|
|
168
|
+
account: accountParam,
|
|
169
|
+
},
|
|
170
|
+
}, async ({ docId, commentId, account }) => {
|
|
171
|
+
return runOrDiagnose(['docs', 'comments', 'get', docId, commentId], { account });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
server.registerTool('gog_docs_comments_add', {
|
|
175
|
+
description:
|
|
176
|
+
'Add a comment to a Google Doc. Optionally attach quoted text that appears as the highlighted passage in the Google Docs UI.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
179
|
+
content: z.string().describe('Comment text'),
|
|
180
|
+
quoted: z.string().optional().describe('Quoted text to attach to the comment (shown in UIs when available)'),
|
|
181
|
+
account: accountParam,
|
|
182
|
+
},
|
|
183
|
+
}, async ({ docId, content, quoted, account }) => {
|
|
184
|
+
const args = ['docs', 'comments', 'add', docId, content];
|
|
185
|
+
if (quoted) args.push(`--quoted=${quoted}`);
|
|
186
|
+
return runOrDiagnose(args, { account });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
server.registerTool('gog_docs_comments_reply', {
|
|
190
|
+
description: 'Reply to an existing comment on a Google Doc.',
|
|
191
|
+
inputSchema: {
|
|
192
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
193
|
+
commentId: z.string().describe('Comment ID to reply to'),
|
|
194
|
+
content: z.string().describe('Reply text'),
|
|
195
|
+
account: accountParam,
|
|
196
|
+
},
|
|
197
|
+
}, async ({ docId, commentId, content, account }) => {
|
|
198
|
+
return runOrDiagnose(['docs', 'comments', 'reply', docId, commentId, content], { account });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
server.registerTool('gog_docs_comments_resolve', {
|
|
202
|
+
description: 'Resolve a comment (mark as done). Optionally include a closing message.',
|
|
203
|
+
annotations: { destructiveHint: true },
|
|
204
|
+
inputSchema: {
|
|
205
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
206
|
+
commentId: z.string().describe('Comment ID to resolve'),
|
|
207
|
+
message: z.string().optional().describe('Optional message to include when resolving'),
|
|
208
|
+
account: accountParam,
|
|
209
|
+
},
|
|
210
|
+
}, async ({ docId, commentId, message, account }) => {
|
|
211
|
+
const args = ['docs', 'comments', 'resolve', docId, commentId];
|
|
212
|
+
if (message) args.push(`--message=${message}`);
|
|
213
|
+
return runOrDiagnose(args, { account });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
server.registerTool('gog_docs_comments_delete', {
|
|
217
|
+
description: 'Delete a comment from a Google Doc. This action is permanent.',
|
|
218
|
+
annotations: { destructiveHint: true },
|
|
219
|
+
inputSchema: {
|
|
220
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
221
|
+
commentId: z.string().describe('Comment ID to delete'),
|
|
222
|
+
account: accountParam,
|
|
223
|
+
},
|
|
224
|
+
}, async ({ docId, commentId, account }) => {
|
|
225
|
+
return runOrDiagnose(['docs', 'comments', 'delete', docId, commentId], { account });
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
|
|
4
|
+
vi.mock('gogcli-mcp/lib', async () => {
|
|
5
|
+
const { z } = await import('zod');
|
|
6
|
+
const mockRun = vi.fn<(args: string[], options?: { account?: string }) => Promise<string>>();
|
|
7
|
+
const accountParam = z.string().optional().describe('Google account email');
|
|
8
|
+
const toText = (output: string) => ({ content: [{ type: 'text' as const, text: output }] });
|
|
9
|
+
const toError = (err: unknown) =>
|
|
10
|
+
toText(err instanceof Error ? `Error: ${err.message}` : String(err));
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
createBaseServer: vi.fn(),
|
|
14
|
+
run: mockRun,
|
|
15
|
+
accountParam,
|
|
16
|
+
toText,
|
|
17
|
+
toError,
|
|
18
|
+
runOrDiagnose: async (args: string[], options: { account?: string }) => {
|
|
19
|
+
try {
|
|
20
|
+
return toText(await mockRun(args, options));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return toError(err);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
import { run } from 'gogcli-mcp/lib';
|
|
29
|
+
import { registerExtraDocsTools } from '../../src/tools/docs-extra.js';
|
|
30
|
+
|
|
31
|
+
type ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;
|
|
32
|
+
|
|
33
|
+
function setupHandlers(): Map<string, ToolHandler> {
|
|
34
|
+
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
35
|
+
const handlers = new Map<string, ToolHandler>();
|
|
36
|
+
vi.spyOn(server, 'registerTool').mockImplementation((name, _config, cb) => {
|
|
37
|
+
handlers.set(name, cb as ToolHandler);
|
|
38
|
+
return undefined as never;
|
|
39
|
+
});
|
|
40
|
+
registerExtraDocsTools(server);
|
|
41
|
+
return handlers;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeEach(() => vi.clearAllMocks());
|
|
45
|
+
|
|
46
|
+
// --- gog_docs_copy ---
|
|
47
|
+
|
|
48
|
+
describe('gog_docs_copy', () => {
|
|
49
|
+
it('calls run with correct args', async () => {
|
|
50
|
+
vi.mocked(run).mockResolvedValue('{"docId":"newid"}');
|
|
51
|
+
const handlers = setupHandlers();
|
|
52
|
+
const result = await handlers.get('gog_docs_copy')!({ docId: 'abc', title: 'My Copy' });
|
|
53
|
+
expect(run).toHaveBeenCalledWith(['docs', 'copy', 'abc', 'My Copy'], { account: undefined });
|
|
54
|
+
expect(result.content[0].text).toContain('newid');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('includes --parent when provided', async () => {
|
|
58
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
59
|
+
const handlers = setupHandlers();
|
|
60
|
+
await handlers.get('gog_docs_copy')!({ docId: 'abc', title: 'Copy', parent: 'folder123' });
|
|
61
|
+
expect(run).toHaveBeenCalledWith(
|
|
62
|
+
['docs', 'copy', 'abc', 'Copy', '--parent=folder123'],
|
|
63
|
+
{ account: undefined },
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('omits --parent when not provided', async () => {
|
|
68
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
69
|
+
const handlers = setupHandlers();
|
|
70
|
+
await handlers.get('gog_docs_copy')!({ docId: 'abc', title: 'Copy' });
|
|
71
|
+
expect(run).toHaveBeenCalledWith(['docs', 'copy', 'abc', 'Copy'], { account: undefined });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('forwards account override', async () => {
|
|
75
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
76
|
+
const handlers = setupHandlers();
|
|
77
|
+
await handlers.get('gog_docs_copy')!({ docId: 'abc', title: 'Copy', account: 'other@gmail.com' });
|
|
78
|
+
expect(run).toHaveBeenCalledWith(['docs', 'copy', 'abc', 'Copy'], { account: 'other@gmail.com' });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns error text on failure', async () => {
|
|
82
|
+
vi.mocked(run).mockRejectedValue(new Error('Copy failed'));
|
|
83
|
+
const handlers = setupHandlers();
|
|
84
|
+
const result = await handlers.get('gog_docs_copy')!({ docId: 'bad', title: 'x' });
|
|
85
|
+
expect(result.content[0].text).toBe('Error: Copy failed');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// --- gog_docs_delete ---
|
|
90
|
+
|
|
91
|
+
describe('gog_docs_delete', () => {
|
|
92
|
+
it('calls run with correct args', async () => {
|
|
93
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
94
|
+
const handlers = setupHandlers();
|
|
95
|
+
await handlers.get('gog_docs_delete')!({ docId: 'abc', start: 5, end: 10 });
|
|
96
|
+
expect(run).toHaveBeenCalledWith(
|
|
97
|
+
['docs', 'delete', '--start=5', '--end=10', 'abc'],
|
|
98
|
+
{ account: undefined },
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('includes --tab-id when provided', async () => {
|
|
103
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
104
|
+
const handlers = setupHandlers();
|
|
105
|
+
await handlers.get('gog_docs_delete')!({ docId: 'abc', start: 1, end: 5, tabId: 'tab1' });
|
|
106
|
+
expect(run).toHaveBeenCalledWith(
|
|
107
|
+
['docs', 'delete', '--start=1', '--end=5', 'abc', '--tab-id=tab1'],
|
|
108
|
+
{ account: undefined },
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('omits --tab-id when not provided', async () => {
|
|
113
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
114
|
+
const handlers = setupHandlers();
|
|
115
|
+
await handlers.get('gog_docs_delete')!({ docId: 'abc', start: 1, end: 5 });
|
|
116
|
+
expect(run).toHaveBeenCalledWith(
|
|
117
|
+
['docs', 'delete', '--start=1', '--end=5', 'abc'],
|
|
118
|
+
{ account: undefined },
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns error text on failure', async () => {
|
|
123
|
+
vi.mocked(run).mockRejectedValue(new Error('Delete failed'));
|
|
124
|
+
const handlers = setupHandlers();
|
|
125
|
+
const result = await handlers.get('gog_docs_delete')!({ docId: 'bad', start: 1, end: 5 });
|
|
126
|
+
expect(result.content[0].text).toBe('Error: Delete failed');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// --- gog_docs_edit ---
|
|
131
|
+
|
|
132
|
+
describe('gog_docs_edit', () => {
|
|
133
|
+
it('calls run with correct args', async () => {
|
|
134
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
135
|
+
const handlers = setupHandlers();
|
|
136
|
+
await handlers.get('gog_docs_edit')!({ docId: 'abc', find: 'old', replace: 'new' });
|
|
137
|
+
expect(run).toHaveBeenCalledWith(
|
|
138
|
+
['docs', 'edit', 'abc', 'old', 'new'],
|
|
139
|
+
{ account: undefined },
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('includes --match-case when true', async () => {
|
|
144
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
145
|
+
const handlers = setupHandlers();
|
|
146
|
+
await handlers.get('gog_docs_edit')!({ docId: 'abc', find: 'old', replace: 'new', matchCase: true });
|
|
147
|
+
expect(run).toHaveBeenCalledWith(
|
|
148
|
+
['docs', 'edit', 'abc', 'old', 'new', '--match-case'],
|
|
149
|
+
{ account: undefined },
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('omits --match-case when false', async () => {
|
|
154
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
155
|
+
const handlers = setupHandlers();
|
|
156
|
+
await handlers.get('gog_docs_edit')!({ docId: 'abc', find: 'old', replace: 'new', matchCase: false });
|
|
157
|
+
expect(run).toHaveBeenCalledWith(
|
|
158
|
+
['docs', 'edit', 'abc', 'old', 'new'],
|
|
159
|
+
{ account: undefined },
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns error text on failure', async () => {
|
|
164
|
+
vi.mocked(run).mockRejectedValue(new Error('Edit failed'));
|
|
165
|
+
const handlers = setupHandlers();
|
|
166
|
+
const result = await handlers.get('gog_docs_edit')!({ docId: 'bad', find: 'x', replace: 'y' });
|
|
167
|
+
expect(result.content[0].text).toBe('Error: Edit failed');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// --- gog_docs_export ---
|
|
172
|
+
|
|
173
|
+
describe('gog_docs_export', () => {
|
|
174
|
+
it('calls run with correct args', async () => {
|
|
175
|
+
vi.mocked(run).mockResolvedValue('Exported to file.pdf');
|
|
176
|
+
const handlers = setupHandlers();
|
|
177
|
+
const result = await handlers.get('gog_docs_export')!({ docId: 'abc' });
|
|
178
|
+
expect(run).toHaveBeenCalledWith(['docs', 'export', 'abc'], { account: undefined });
|
|
179
|
+
expect(result.content[0].text).toContain('Exported');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('includes --format when provided', async () => {
|
|
183
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
184
|
+
const handlers = setupHandlers();
|
|
185
|
+
await handlers.get('gog_docs_export')!({ docId: 'abc', format: 'html' });
|
|
186
|
+
expect(run).toHaveBeenCalledWith(
|
|
187
|
+
['docs', 'export', 'abc', '--format=html'],
|
|
188
|
+
{ account: undefined },
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('includes --out when provided', async () => {
|
|
193
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
194
|
+
const handlers = setupHandlers();
|
|
195
|
+
await handlers.get('gog_docs_export')!({ docId: 'abc', out: '/tmp/doc.pdf' });
|
|
196
|
+
expect(run).toHaveBeenCalledWith(
|
|
197
|
+
['docs', 'export', 'abc', '--out=/tmp/doc.pdf'],
|
|
198
|
+
{ account: undefined },
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('includes both --format and --out when provided', async () => {
|
|
203
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
204
|
+
const handlers = setupHandlers();
|
|
205
|
+
await handlers.get('gog_docs_export')!({ docId: 'abc', format: 'txt', out: '/tmp/doc.txt' });
|
|
206
|
+
expect(run).toHaveBeenCalledWith(
|
|
207
|
+
['docs', 'export', 'abc', '--format=txt', '--out=/tmp/doc.txt'],
|
|
208
|
+
{ account: undefined },
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('omits optional flags when not provided', async () => {
|
|
213
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
214
|
+
const handlers = setupHandlers();
|
|
215
|
+
await handlers.get('gog_docs_export')!({ docId: 'abc' });
|
|
216
|
+
expect(run).toHaveBeenCalledWith(['docs', 'export', 'abc'], { account: undefined });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns error text on failure', async () => {
|
|
220
|
+
vi.mocked(run).mockRejectedValue(new Error('Export failed'));
|
|
221
|
+
const handlers = setupHandlers();
|
|
222
|
+
const result = await handlers.get('gog_docs_export')!({ docId: 'bad' });
|
|
223
|
+
expect(result.content[0].text).toBe('Error: Export failed');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// --- gog_docs_insert ---
|
|
228
|
+
|
|
229
|
+
describe('gog_docs_insert', () => {
|
|
230
|
+
it('calls run with content', async () => {
|
|
231
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
232
|
+
const handlers = setupHandlers();
|
|
233
|
+
await handlers.get('gog_docs_insert')!({ docId: 'abc', content: 'Hello world' });
|
|
234
|
+
expect(run).toHaveBeenCalledWith(
|
|
235
|
+
['docs', 'insert', 'abc', 'Hello world'],
|
|
236
|
+
{ account: undefined },
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('includes --index when provided', async () => {
|
|
241
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
242
|
+
const handlers = setupHandlers();
|
|
243
|
+
await handlers.get('gog_docs_insert')!({ docId: 'abc', content: 'text', index: 5 });
|
|
244
|
+
expect(run).toHaveBeenCalledWith(
|
|
245
|
+
['docs', 'insert', 'abc', 'text', '--index=5'],
|
|
246
|
+
{ account: undefined },
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('includes --index=0 when index is zero', async () => {
|
|
251
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
252
|
+
const handlers = setupHandlers();
|
|
253
|
+
await handlers.get('gog_docs_insert')!({ docId: 'abc', content: 'text', index: 0 });
|
|
254
|
+
expect(run).toHaveBeenCalledWith(
|
|
255
|
+
['docs', 'insert', 'abc', 'text', '--index=0'],
|
|
256
|
+
{ account: undefined },
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('includes --file when provided', async () => {
|
|
261
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
262
|
+
const handlers = setupHandlers();
|
|
263
|
+
await handlers.get('gog_docs_insert')!({ docId: 'abc', file: '/tmp/text.txt' });
|
|
264
|
+
expect(run).toHaveBeenCalledWith(
|
|
265
|
+
['docs', 'insert', 'abc', '--file=/tmp/text.txt'],
|
|
266
|
+
{ account: undefined },
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('includes --tab-id when provided', async () => {
|
|
271
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
272
|
+
const handlers = setupHandlers();
|
|
273
|
+
await handlers.get('gog_docs_insert')!({ docId: 'abc', content: 'hi', tabId: 'tab1' });
|
|
274
|
+
expect(run).toHaveBeenCalledWith(
|
|
275
|
+
['docs', 'insert', 'abc', 'hi', '--tab-id=tab1'],
|
|
276
|
+
{ account: undefined },
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('omits optional flags when not provided', async () => {
|
|
281
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
282
|
+
const handlers = setupHandlers();
|
|
283
|
+
await handlers.get('gog_docs_insert')!({ docId: 'abc' });
|
|
284
|
+
expect(run).toHaveBeenCalledWith(['docs', 'insert', 'abc'], { account: undefined });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('returns error text on failure', async () => {
|
|
288
|
+
vi.mocked(run).mockRejectedValue(new Error('Insert failed'));
|
|
289
|
+
const handlers = setupHandlers();
|
|
290
|
+
const result = await handlers.get('gog_docs_insert')!({ docId: 'bad' });
|
|
291
|
+
expect(result.content[0].text).toBe('Error: Insert failed');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// --- gog_docs_list_tabs ---
|
|
296
|
+
|
|
297
|
+
describe('gog_docs_list_tabs', () => {
|
|
298
|
+
it('calls run with correct args', async () => {
|
|
299
|
+
vi.mocked(run).mockResolvedValue('[{"id":"t1","title":"Tab 1"}]');
|
|
300
|
+
const handlers = setupHandlers();
|
|
301
|
+
const result = await handlers.get('gog_docs_list_tabs')!({ docId: 'abc' });
|
|
302
|
+
expect(run).toHaveBeenCalledWith(['docs', 'list-tabs', 'abc'], { account: undefined });
|
|
303
|
+
expect(result.content[0].text).toContain('Tab 1');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('forwards account override', async () => {
|
|
307
|
+
vi.mocked(run).mockResolvedValue('[]');
|
|
308
|
+
const handlers = setupHandlers();
|
|
309
|
+
await handlers.get('gog_docs_list_tabs')!({ docId: 'abc', account: 'other@gmail.com' });
|
|
310
|
+
expect(run).toHaveBeenCalledWith(['docs', 'list-tabs', 'abc'], { account: 'other@gmail.com' });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('returns error text on failure', async () => {
|
|
314
|
+
vi.mocked(run).mockRejectedValue(new Error('List tabs failed'));
|
|
315
|
+
const handlers = setupHandlers();
|
|
316
|
+
const result = await handlers.get('gog_docs_list_tabs')!({ docId: 'bad' });
|
|
317
|
+
expect(result.content[0].text).toBe('Error: List tabs failed');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// --- gog_docs_sed ---
|
|
322
|
+
|
|
323
|
+
describe('gog_docs_sed', () => {
|
|
324
|
+
it('calls run with single expression', async () => {
|
|
325
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
326
|
+
const handlers = setupHandlers();
|
|
327
|
+
await handlers.get('gog_docs_sed')!({ docId: 'abc', expression: 's/old/new/g' });
|
|
328
|
+
expect(run).toHaveBeenCalledWith(
|
|
329
|
+
['docs', 'sed', 'abc', 's/old/new/g'],
|
|
330
|
+
{ account: undefined },
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('calls run with multiple expressions', async () => {
|
|
335
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
336
|
+
const handlers = setupHandlers();
|
|
337
|
+
await handlers.get('gog_docs_sed')!({
|
|
338
|
+
docId: 'abc',
|
|
339
|
+
expressions: ['s/foo/bar/g', 's/baz/qux/g'],
|
|
340
|
+
});
|
|
341
|
+
expect(run).toHaveBeenCalledWith(
|
|
342
|
+
['docs', 'sed', 'abc', '--expressions=s/foo/bar/g', '--expressions=s/baz/qux/g'],
|
|
343
|
+
{ account: undefined },
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('includes --file when provided', async () => {
|
|
348
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
349
|
+
const handlers = setupHandlers();
|
|
350
|
+
await handlers.get('gog_docs_sed')!({ docId: 'abc', file: '/tmp/sed.txt' });
|
|
351
|
+
expect(run).toHaveBeenCalledWith(
|
|
352
|
+
['docs', 'sed', 'abc', '--file=/tmp/sed.txt'],
|
|
353
|
+
{ account: undefined },
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('includes --tab when provided', async () => {
|
|
358
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
359
|
+
const handlers = setupHandlers();
|
|
360
|
+
await handlers.get('gog_docs_sed')!({ docId: 'abc', expression: 's/a/b/', tab: 'Notes' });
|
|
361
|
+
expect(run).toHaveBeenCalledWith(
|
|
362
|
+
['docs', 'sed', 'abc', 's/a/b/', '--tab=Notes'],
|
|
363
|
+
{ account: undefined },
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('omits optional flags when not provided', async () => {
|
|
368
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
369
|
+
const handlers = setupHandlers();
|
|
370
|
+
await handlers.get('gog_docs_sed')!({ docId: 'abc' });
|
|
371
|
+
expect(run).toHaveBeenCalledWith(['docs', 'sed', 'abc'], { account: undefined });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('returns error text on failure', async () => {
|
|
375
|
+
vi.mocked(run).mockRejectedValue(new Error('Sed failed'));
|
|
376
|
+
const handlers = setupHandlers();
|
|
377
|
+
const result = await handlers.get('gog_docs_sed')!({ docId: 'bad' });
|
|
378
|
+
expect(result.content[0].text).toBe('Error: Sed failed');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// --- gog_docs_update ---
|
|
383
|
+
|
|
384
|
+
describe('gog_docs_update', () => {
|
|
385
|
+
it('calls run with --text flag', async () => {
|
|
386
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
387
|
+
const handlers = setupHandlers();
|
|
388
|
+
await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'Hello' });
|
|
389
|
+
expect(run).toHaveBeenCalledWith(
|
|
390
|
+
['docs', 'update', 'abc', '--text=Hello'],
|
|
391
|
+
{ account: undefined },
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('includes --file when provided', async () => {
|
|
396
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
397
|
+
const handlers = setupHandlers();
|
|
398
|
+
await handlers.get('gog_docs_update')!({ docId: 'abc', file: '/tmp/content.txt' });
|
|
399
|
+
expect(run).toHaveBeenCalledWith(
|
|
400
|
+
['docs', 'update', 'abc', '--file=/tmp/content.txt'],
|
|
401
|
+
{ account: undefined },
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('includes --index when provided', async () => {
|
|
406
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
407
|
+
const handlers = setupHandlers();
|
|
408
|
+
await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'hi', index: 10 });
|
|
409
|
+
expect(run).toHaveBeenCalledWith(
|
|
410
|
+
['docs', 'update', 'abc', '--text=hi', '--index=10'],
|
|
411
|
+
{ account: undefined },
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('includes --index=0 when index is zero', async () => {
|
|
416
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
417
|
+
const handlers = setupHandlers();
|
|
418
|
+
await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'hi', index: 0 });
|
|
419
|
+
expect(run).toHaveBeenCalledWith(
|
|
420
|
+
['docs', 'update', 'abc', '--text=hi', '--index=0'],
|
|
421
|
+
{ account: undefined },
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('includes --tab-id when provided', async () => {
|
|
426
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
427
|
+
const handlers = setupHandlers();
|
|
428
|
+
await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'hi', tabId: 'tab1' });
|
|
429
|
+
expect(run).toHaveBeenCalledWith(
|
|
430
|
+
['docs', 'update', 'abc', '--text=hi', '--tab-id=tab1'],
|
|
431
|
+
{ account: undefined },
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('includes --pageless when true', async () => {
|
|
436
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
437
|
+
const handlers = setupHandlers();
|
|
438
|
+
await handlers.get('gog_docs_update')!({ docId: 'abc', pageless: true });
|
|
439
|
+
expect(run).toHaveBeenCalledWith(
|
|
440
|
+
['docs', 'update', 'abc', '--pageless'],
|
|
441
|
+
{ account: undefined },
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('omits --pageless when false', async () => {
|
|
446
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
447
|
+
const handlers = setupHandlers();
|
|
448
|
+
await handlers.get('gog_docs_update')!({ docId: 'abc', pageless: false });
|
|
449
|
+
expect(run).toHaveBeenCalledWith(
|
|
450
|
+
['docs', 'update', 'abc'],
|
|
451
|
+
{ account: undefined },
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('omits optional flags when not provided', async () => {
|
|
456
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
457
|
+
const handlers = setupHandlers();
|
|
458
|
+
await handlers.get('gog_docs_update')!({ docId: 'abc' });
|
|
459
|
+
expect(run).toHaveBeenCalledWith(['docs', 'update', 'abc'], { account: undefined });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('returns error text on failure', async () => {
|
|
463
|
+
vi.mocked(run).mockRejectedValue(new Error('Update failed'));
|
|
464
|
+
const handlers = setupHandlers();
|
|
465
|
+
const result = await handlers.get('gog_docs_update')!({ docId: 'bad' });
|
|
466
|
+
expect(result.content[0].text).toBe('Error: Update failed');
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// --- Dedicated comment tools ---
|
|
471
|
+
|
|
472
|
+
describe('gog_docs_comments_list', () => {
|
|
473
|
+
it('calls run with correct args for open comments', async () => {
|
|
474
|
+
vi.mocked(run).mockResolvedValue('[{"id":"c1","content":"Fix this"}]');
|
|
475
|
+
const handlers = setupHandlers();
|
|
476
|
+
const result = await handlers.get('gog_docs_comments_list')!({ docId: 'abc' });
|
|
477
|
+
expect(run).toHaveBeenCalledWith(['docs', 'comments', 'list', 'abc'], { account: undefined });
|
|
478
|
+
expect(result.content[0].text).toContain('Fix this');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('includes --include-resolved when set', async () => {
|
|
482
|
+
vi.mocked(run).mockResolvedValue('[]');
|
|
483
|
+
const handlers = setupHandlers();
|
|
484
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', includeResolved: true });
|
|
485
|
+
expect(run).toHaveBeenCalledWith(
|
|
486
|
+
['docs', 'comments', 'list', 'abc', '--include-resolved'],
|
|
487
|
+
{ account: undefined },
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('omits --include-resolved when false', async () => {
|
|
492
|
+
vi.mocked(run).mockResolvedValue('[]');
|
|
493
|
+
const handlers = setupHandlers();
|
|
494
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', includeResolved: false });
|
|
495
|
+
expect(run).toHaveBeenCalledWith(['docs', 'comments', 'list', 'abc'], { account: undefined });
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('forwards account override', async () => {
|
|
499
|
+
vi.mocked(run).mockResolvedValue('[]');
|
|
500
|
+
const handlers = setupHandlers();
|
|
501
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', account: 'other@gmail.com' });
|
|
502
|
+
expect(run).toHaveBeenCalledWith(
|
|
503
|
+
['docs', 'comments', 'list', 'abc'],
|
|
504
|
+
{ account: 'other@gmail.com' },
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('returns error text on failure', async () => {
|
|
509
|
+
vi.mocked(run).mockRejectedValue(new Error('List failed'));
|
|
510
|
+
const handlers = setupHandlers();
|
|
511
|
+
const result = await handlers.get('gog_docs_comments_list')!({ docId: 'bad' });
|
|
512
|
+
expect(result.content[0].text).toContain('Error: List failed');
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe('gog_docs_comments_get', () => {
|
|
517
|
+
it('calls run with correct args', async () => {
|
|
518
|
+
vi.mocked(run).mockResolvedValue('{"id":"c1","content":"Fix this","replies":[]}');
|
|
519
|
+
const handlers = setupHandlers();
|
|
520
|
+
const result = await handlers.get('gog_docs_comments_get')!({ docId: 'abc', commentId: 'c1' });
|
|
521
|
+
expect(run).toHaveBeenCalledWith(['docs', 'comments', 'get', 'abc', 'c1'], { account: undefined });
|
|
522
|
+
expect(result.content[0].text).toContain('Fix this');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('returns error text on failure', async () => {
|
|
526
|
+
vi.mocked(run).mockRejectedValue(new Error('Not found'));
|
|
527
|
+
const handlers = setupHandlers();
|
|
528
|
+
const result = await handlers.get('gog_docs_comments_get')!({ docId: 'abc', commentId: 'bad' });
|
|
529
|
+
expect(result.content[0].text).toContain('Error: Not found');
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe('gog_docs_comments_add', () => {
|
|
534
|
+
it('calls run with content', async () => {
|
|
535
|
+
vi.mocked(run).mockResolvedValue('{"id":"c2"}');
|
|
536
|
+
const handlers = setupHandlers();
|
|
537
|
+
await handlers.get('gog_docs_comments_add')!({ docId: 'abc', content: 'Please review' });
|
|
538
|
+
expect(run).toHaveBeenCalledWith(
|
|
539
|
+
['docs', 'comments', 'add', 'abc', 'Please review'],
|
|
540
|
+
{ account: undefined },
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('includes --quoted when provided', async () => {
|
|
545
|
+
vi.mocked(run).mockResolvedValue('{"id":"c2"}');
|
|
546
|
+
const handlers = setupHandlers();
|
|
547
|
+
await handlers.get('gog_docs_comments_add')!({
|
|
548
|
+
docId: 'abc', content: 'Typo here', quoted: 'teh',
|
|
549
|
+
});
|
|
550
|
+
expect(run).toHaveBeenCalledWith(
|
|
551
|
+
['docs', 'comments', 'add', 'abc', 'Typo here', '--quoted=teh'],
|
|
552
|
+
{ account: undefined },
|
|
553
|
+
);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('omits --quoted when not provided', async () => {
|
|
557
|
+
vi.mocked(run).mockResolvedValue('{"id":"c2"}');
|
|
558
|
+
const handlers = setupHandlers();
|
|
559
|
+
await handlers.get('gog_docs_comments_add')!({ docId: 'abc', content: 'Nice' });
|
|
560
|
+
expect(run).toHaveBeenCalledWith(
|
|
561
|
+
['docs', 'comments', 'add', 'abc', 'Nice'],
|
|
562
|
+
{ account: undefined },
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('returns error text on failure', async () => {
|
|
567
|
+
vi.mocked(run).mockRejectedValue(new Error('Add failed'));
|
|
568
|
+
const handlers = setupHandlers();
|
|
569
|
+
const result = await handlers.get('gog_docs_comments_add')!({ docId: 'bad', content: 'x' });
|
|
570
|
+
expect(result.content[0].text).toContain('Error: Add failed');
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe('gog_docs_comments_reply', () => {
|
|
575
|
+
it('calls run with correct args', async () => {
|
|
576
|
+
vi.mocked(run).mockResolvedValue('{"id":"r1"}');
|
|
577
|
+
const handlers = setupHandlers();
|
|
578
|
+
await handlers.get('gog_docs_comments_reply')!({ docId: 'abc', commentId: 'c1', content: 'Done' });
|
|
579
|
+
expect(run).toHaveBeenCalledWith(
|
|
580
|
+
['docs', 'comments', 'reply', 'abc', 'c1', 'Done'],
|
|
581
|
+
{ account: undefined },
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('returns error text on failure', async () => {
|
|
586
|
+
vi.mocked(run).mockRejectedValue(new Error('Reply failed'));
|
|
587
|
+
const handlers = setupHandlers();
|
|
588
|
+
const result = await handlers.get('gog_docs_comments_reply')!({
|
|
589
|
+
docId: 'abc', commentId: 'c1', content: 'x',
|
|
590
|
+
});
|
|
591
|
+
expect(result.content[0].text).toContain('Error: Reply failed');
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe('gog_docs_comments_resolve', () => {
|
|
596
|
+
it('calls run with correct args', async () => {
|
|
597
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
598
|
+
const handlers = setupHandlers();
|
|
599
|
+
await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
600
|
+
expect(run).toHaveBeenCalledWith(
|
|
601
|
+
['docs', 'comments', 'resolve', 'abc', 'c1'],
|
|
602
|
+
{ account: undefined },
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('includes --message when provided', async () => {
|
|
607
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
608
|
+
const handlers = setupHandlers();
|
|
609
|
+
await handlers.get('gog_docs_comments_resolve')!({
|
|
610
|
+
docId: 'abc', commentId: 'c1', message: 'Fixed in v2',
|
|
611
|
+
});
|
|
612
|
+
expect(run).toHaveBeenCalledWith(
|
|
613
|
+
['docs', 'comments', 'resolve', 'abc', 'c1', '--message=Fixed in v2'],
|
|
614
|
+
{ account: undefined },
|
|
615
|
+
);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('omits --message when not provided', async () => {
|
|
619
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
620
|
+
const handlers = setupHandlers();
|
|
621
|
+
await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
622
|
+
expect(run).toHaveBeenCalledWith(
|
|
623
|
+
['docs', 'comments', 'resolve', 'abc', 'c1'],
|
|
624
|
+
{ account: undefined },
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('returns error text on failure', async () => {
|
|
629
|
+
vi.mocked(run).mockRejectedValue(new Error('Resolve failed'));
|
|
630
|
+
const handlers = setupHandlers();
|
|
631
|
+
const result = await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
632
|
+
expect(result.content[0].text).toContain('Error: Resolve failed');
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
describe('gog_docs_comments_delete', () => {
|
|
637
|
+
it('calls run with correct args', async () => {
|
|
638
|
+
vi.mocked(run).mockResolvedValue('{}');
|
|
639
|
+
const handlers = setupHandlers();
|
|
640
|
+
await handlers.get('gog_docs_comments_delete')!({ docId: 'abc', commentId: 'c1' });
|
|
641
|
+
expect(run).toHaveBeenCalledWith(
|
|
642
|
+
['docs', 'comments', 'delete', 'abc', 'c1'],
|
|
643
|
+
{ account: undefined },
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('returns error text on failure', async () => {
|
|
648
|
+
vi.mocked(run).mockRejectedValue(new Error('Delete failed'));
|
|
649
|
+
const handlers = setupHandlers();
|
|
650
|
+
const result = await handlers.get('gog_docs_comments_delete')!({ docId: 'abc', commentId: 'c1' });
|
|
651
|
+
expect(result.content[0].text).toContain('Error: Delete failed');
|
|
652
|
+
});
|
|
653
|
+
});
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
coverage: {
|
|
6
|
+
provider: 'v8',
|
|
7
|
+
include: ['src/**/*.ts'],
|
|
8
|
+
exclude: ['src/index.ts'],
|
|
9
|
+
thresholds: {
|
|
10
|
+
lines: 100,
|
|
11
|
+
functions: 100,
|
|
12
|
+
branches: 100,
|
|
13
|
+
statements: 100,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|