gogcli-mcp 2.24.0 β†’ 2.26.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.
@@ -0,0 +1,284 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { registerChatTools } from '../../src/tools/chat.js';
3
+ import * as runner from '../../src/runner.js';
4
+ import { createTestHarness } from '@chrischall/mcp-utils/test';
5
+
6
+ vi.mock('../../src/runner.js');
7
+
8
+ const setupHandlers = () => createTestHarness(registerChatTools);
9
+
10
+ beforeEach(() => vi.clearAllMocks());
11
+
12
+ describe('gog_chat_spaces_list', () => {
13
+ it('lists spaces', async () => {
14
+ vi.mocked(runner.run).mockResolvedValue('{"spaces":[]}');
15
+ const harness = await setupHandlers();
16
+ await harness.callTool('gog_chat_spaces_list', {});
17
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'list'], { account: undefined });
18
+ });
19
+
20
+ it('passes pagination flags', async () => {
21
+ vi.mocked(runner.run).mockResolvedValue('{"spaces":[]}');
22
+ const harness = await setupHandlers();
23
+ await harness.callTool('gog_chat_spaces_list', { max: 20, pageToken: 'tok', all: true });
24
+ expect(runner.run).toHaveBeenCalledWith(
25
+ ['chat', 'spaces', 'list', '--max=20', '--page=tok', '--all'],
26
+ { account: undefined },
27
+ );
28
+ });
29
+
30
+ it('returns error text on failure', async () => {
31
+ vi.mocked(runner.run).mockRejectedValue(new Error('Spaces failed'));
32
+ const harness = await setupHandlers();
33
+ const result = await harness.callTool('gog_chat_spaces_list', {});
34
+ expect(result.content[0].text).toBe('Error: Spaces failed');
35
+ });
36
+
37
+ // The constraint is the ACCOUNT, not the token, so it has to be in the
38
+ // description a model reads β€” a source comment would not reach it. Every chat
39
+ // tool carries the note, because any of them can be the first one called.
40
+ it('warns in EVERY description that Chat is Workspace-only', async () => {
41
+ const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
42
+ const server = new McpServer({ name: 'test', version: '0.0.0' });
43
+ const configs = new Map<string, { description?: string }>();
44
+ vi.spyOn(server, 'registerTool').mockImplementation((name, config) => {
45
+ configs.set(name, config as { description?: string });
46
+ return undefined as never;
47
+ });
48
+ registerChatTools(server);
49
+ expect(configs.size).toBe(12);
50
+ for (const [name, config] of configs) {
51
+ expect(config.description, name).toMatch(/consumer accounts|Workspace/i);
52
+ }
53
+ });
54
+ });
55
+
56
+ describe('gog_chat_spaces_find', () => {
57
+ it('searches by display name', async () => {
58
+ vi.mocked(runner.run).mockResolvedValue('{}');
59
+ const harness = await setupHandlers();
60
+ await harness.callTool('gog_chat_spaces_find', { displayName: 'Team' });
61
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'find', 'Team'], { account: undefined });
62
+ });
63
+
64
+ it('passes --exact and --max', async () => {
65
+ vi.mocked(runner.run).mockResolvedValue('{}');
66
+ const harness = await setupHandlers();
67
+ await harness.callTool('gog_chat_spaces_find', { displayName: 'Team', exact: true, max: 5 });
68
+ expect(runner.run).toHaveBeenCalledWith(
69
+ ['chat', 'spaces', 'find', 'Team', '--exact', '--max=5'],
70
+ { account: undefined },
71
+ );
72
+ });
73
+ });
74
+
75
+ describe('gog_chat_spaces_create', () => {
76
+ it('creates a space with members', async () => {
77
+ vi.mocked(runner.run).mockResolvedValue('{}');
78
+ const harness = await setupHandlers();
79
+ await harness.callTool('gog_chat_spaces_create', { displayName: 'Launch', members: ['a@b.com', 'c@d.com'] });
80
+ expect(runner.run).toHaveBeenCalledWith(
81
+ ['chat', 'spaces', 'create', 'Launch', '--member=a@b.com', '--member=c@d.com'],
82
+ { account: undefined },
83
+ );
84
+ });
85
+
86
+ it('creates an empty space', async () => {
87
+ vi.mocked(runner.run).mockResolvedValue('{}');
88
+ const harness = await setupHandlers();
89
+ await harness.callTool('gog_chat_spaces_create', { displayName: 'Solo' });
90
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'create', 'Solo'], { account: undefined });
91
+ });
92
+ });
93
+
94
+ describe('gog_chat_threads_list', () => {
95
+ it('lists threads in a space', async () => {
96
+ vi.mocked(runner.run).mockResolvedValue('{}');
97
+ const harness = await setupHandlers();
98
+ await harness.callTool('gog_chat_threads_list', { space: 'spaces/AAA', max: 10 });
99
+ expect(runner.run).toHaveBeenCalledWith(
100
+ ['chat', 'threads', 'list', 'spaces/AAA', '--max=10'],
101
+ { account: undefined },
102
+ );
103
+ });
104
+ });
105
+
106
+ describe('gog_chat_messages_list', () => {
107
+ it('lists messages in a space', async () => {
108
+ vi.mocked(runner.run).mockResolvedValue('{}');
109
+ const harness = await setupHandlers();
110
+ await harness.callTool('gog_chat_messages_list', { space: 'spaces/AAA' });
111
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'messages', 'list', 'spaces/AAA'], { account: undefined });
112
+ });
113
+
114
+ it('filters by thread, unread and order', async () => {
115
+ vi.mocked(runner.run).mockResolvedValue('{}');
116
+ const harness = await setupHandlers();
117
+ await harness.callTool('gog_chat_messages_list', {
118
+ space: 'spaces/AAA', thread: 'spaces/AAA/threads/T', unread: true, order: 'createTime desc', max: 50,
119
+ });
120
+ expect(runner.run).toHaveBeenCalledWith(
121
+ ['chat', 'messages', 'list', 'spaces/AAA', '--thread=spaces/AAA/threads/T', '--unread',
122
+ '--order=createTime desc', '--max=50'],
123
+ { account: undefined },
124
+ );
125
+ });
126
+
127
+ it('rejects a sort order Chat does not accept', async () => {
128
+ const harness = await setupHandlers();
129
+ const result = await harness.callTool('gog_chat_messages_list', { space: 'spaces/AAA', order: 'newest' });
130
+ expect(result.isError).toBe(true);
131
+ expect(runner.run).not.toHaveBeenCalled();
132
+ });
133
+ });
134
+
135
+ describe('gog_chat_messages_send', () => {
136
+ it('sends text to a space', async () => {
137
+ vi.mocked(runner.run).mockResolvedValue('{}');
138
+ const harness = await setupHandlers();
139
+ await harness.callTool('gog_chat_messages_send', { space: 'spaces/AAA', text: 'hi' });
140
+ expect(runner.run).toHaveBeenCalledWith(
141
+ ['chat', 'messages', 'send', 'spaces/AAA', '--text=hi'],
142
+ { account: undefined },
143
+ );
144
+ });
145
+
146
+ it('replies in a thread and attaches a server-side path', async () => {
147
+ vi.mocked(runner.run).mockResolvedValue('{}');
148
+ const harness = await setupHandlers();
149
+ await harness.callTool('gog_chat_messages_send', {
150
+ space: 'spaces/AAA', text: 'see this', thread: 'spaces/AAA/threads/T', attach: ['/tmp/a.png'],
151
+ });
152
+ expect(runner.run).toHaveBeenCalledWith(
153
+ ['chat', 'messages', 'send', 'spaces/AAA', '--text=see this', '--thread=spaces/AAA/threads/T', '--attach=/tmp/a.png'],
154
+ { account: undefined },
155
+ );
156
+ });
157
+
158
+ it('carries caller-side bytes as a file arg', async () => {
159
+ vi.mocked(runner.run).mockResolvedValue('{}');
160
+ const harness = await setupHandlers();
161
+ await harness.callTool('gog_chat_messages_send', {
162
+ space: 'spaces/AAA',
163
+ text: 'chart',
164
+ attachInline: [{ filename: 'chart.png', contentBase64: Buffer.from('png').toString('base64') }],
165
+ });
166
+ const args = vi.mocked(runner.run).mock.calls[0][0];
167
+ expect(args).toContainEqual(expect.objectContaining({
168
+ kind: 'file', flag: 'attach', filename: 'chart.png', encoding: 'base64',
169
+ }));
170
+ });
171
+
172
+ it('sends an attachment with no text at all', async () => {
173
+ vi.mocked(runner.run).mockResolvedValue('{}');
174
+ const harness = await setupHandlers();
175
+ await harness.callTool('gog_chat_messages_send', { space: 'spaces/AAA', attach: ['/tmp/a.png'] });
176
+ expect(runner.run).toHaveBeenCalledWith(
177
+ ['chat', 'messages', 'send', 'spaces/AAA', '--attach=/tmp/a.png'],
178
+ { account: undefined },
179
+ );
180
+ });
181
+
182
+ it('refuses a message with neither text nor an attachment', async () => {
183
+ const harness = await setupHandlers();
184
+ const result = await harness.callTool('gog_chat_messages_send', { space: 'spaces/AAA' });
185
+ expect(result.isError).toBe(true);
186
+ expect(runner.run).not.toHaveBeenCalled();
187
+ });
188
+ });
189
+
190
+ describe('gog_chat_dm_send', () => {
191
+ it('sends a direct message by email', async () => {
192
+ vi.mocked(runner.run).mockResolvedValue('{}');
193
+ const harness = await setupHandlers();
194
+ await harness.callTool('gog_chat_dm_send', { email: 'a@b.com', text: 'hi' });
195
+ expect(runner.run).toHaveBeenCalledWith(
196
+ ['chat', 'dm', 'send', 'a@b.com', '--text=hi'],
197
+ { account: undefined },
198
+ );
199
+ });
200
+
201
+ it('replies in an existing DM thread', async () => {
202
+ vi.mocked(runner.run).mockResolvedValue('{}');
203
+ const harness = await setupHandlers();
204
+ await harness.callTool('gog_chat_dm_send', { email: 'a@b.com', text: 'hi', thread: 'spaces/D/threads/T' });
205
+ expect(runner.run).toHaveBeenCalledWith(
206
+ ['chat', 'dm', 'send', 'a@b.com', '--text=hi', '--thread=spaces/D/threads/T'],
207
+ { account: undefined },
208
+ );
209
+ });
210
+ });
211
+
212
+ describe('gog_chat_dm_space', () => {
213
+ it('resolves the DM space for an address', async () => {
214
+ vi.mocked(runner.run).mockResolvedValue('{}');
215
+ const harness = await setupHandlers();
216
+ await harness.callTool('gog_chat_dm_space', { email: 'a@b.com' });
217
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'dm', 'space', 'a@b.com'], { account: undefined });
218
+ });
219
+ });
220
+
221
+ describe('gog_chat_reactions_list', () => {
222
+ it('lists reactions, qualifying a bare message ID with its space', async () => {
223
+ vi.mocked(runner.run).mockResolvedValue('{}');
224
+ const harness = await setupHandlers();
225
+ await harness.callTool('gog_chat_reactions_list', { message: 'MSG', space: 'spaces/AAA', max: 10 });
226
+ expect(runner.run).toHaveBeenCalledWith(
227
+ ['chat', 'messages', 'reactions', 'list', 'MSG', '--space=spaces/AAA', '--max=10'],
228
+ { account: undefined },
229
+ );
230
+ });
231
+
232
+ it('omits --space when the message is already fully qualified', async () => {
233
+ vi.mocked(runner.run).mockResolvedValue('{}');
234
+ const harness = await setupHandlers();
235
+ await harness.callTool('gog_chat_reactions_list', { message: 'spaces/AAA/messages/M' });
236
+ expect(runner.run).toHaveBeenCalledWith(
237
+ ['chat', 'messages', 'reactions', 'list', 'spaces/AAA/messages/M'],
238
+ { account: undefined },
239
+ );
240
+ });
241
+ });
242
+
243
+ describe('gog_chat_reactions_create', () => {
244
+ it('adds an emoji reaction', async () => {
245
+ vi.mocked(runner.run).mockResolvedValue('{}');
246
+ const harness = await setupHandlers();
247
+ await harness.callTool('gog_chat_reactions_create', { message: 'spaces/AAA/messages/M', emoji: 'πŸ‘' });
248
+ expect(runner.run).toHaveBeenCalledWith(
249
+ ['chat', 'messages', 'reactions', 'create', 'spaces/AAA/messages/M', 'πŸ‘'],
250
+ { account: undefined },
251
+ );
252
+ });
253
+
254
+ it('passes --space for a bare message ID', async () => {
255
+ vi.mocked(runner.run).mockResolvedValue('{}');
256
+ const harness = await setupHandlers();
257
+ await harness.callTool('gog_chat_reactions_create', { message: 'M', emoji: 'πŸ‘', space: 'spaces/AAA' });
258
+ expect(runner.run).toHaveBeenCalledWith(
259
+ ['chat', 'messages', 'reactions', 'create', 'M', 'πŸ‘', '--space=spaces/AAA'],
260
+ { account: undefined },
261
+ );
262
+ });
263
+ });
264
+
265
+ describe('gog_chat_reactions_delete', () => {
266
+ it('deletes a reaction by resource name', async () => {
267
+ vi.mocked(runner.run).mockResolvedValue('{}');
268
+ const harness = await setupHandlers();
269
+ await harness.callTool('gog_chat_reactions_delete', { reaction: 'spaces/AAA/messages/M/reactions/R' });
270
+ expect(runner.run).toHaveBeenCalledWith(
271
+ ['chat', 'messages', 'reactions', 'delete', 'spaces/AAA/messages/M/reactions/R'],
272
+ { account: undefined },
273
+ );
274
+ });
275
+ });
276
+
277
+ describe('gog_chat_run', () => {
278
+ it('passes the subcommand and args through', async () => {
279
+ vi.mocked(runner.run).mockResolvedValue('{}');
280
+ const harness = await setupHandlers();
281
+ await harness.callTool('gog_chat_run', { subcommand: 'spaces', args: ['list'] });
282
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'list'], { account: undefined });
283
+ });
284
+ });
@@ -279,6 +279,95 @@ describe('gog_gmail_send', () => {
279
279
  );
280
280
  });
281
281
 
282
+ // ==========================================================================
283
+ // INLINE ATTACHMENT BYTES β€” for callers with no filesystem in common with gog
284
+ // (the hosted connector, any GOG_RUNNER_URL backend). The bytes ride with the
285
+ // call and the executor materializes them beside gog.
286
+ // ==========================================================================
287
+ it('turns attachInline bytes into repeatable --attach file args', async () => {
288
+ vi.mocked(runner.run).mockResolvedValue('{}');
289
+ const harness = await setupHandlers();
290
+ const png = Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString('base64');
291
+ await harness.callTool('gog_gmail_send', {
292
+ to: 'bob@example.com',
293
+ subject: 'Layouts',
294
+ body: 'See attached',
295
+ attachInline: [{ filename: 'pendant-layouts.png', contentBase64: png }],
296
+ });
297
+ expect(runner.run).toHaveBeenCalledWith(
298
+ [
299
+ 'gmail', 'send',
300
+ '--to=bob@example.com', '--subject=Layouts', '--body=See attached',
301
+ { kind: 'file', flag: 'attach', contents: png, encoding: 'base64', filename: 'pendant-layouts.png' },
302
+ ],
303
+ { account: undefined },
304
+ );
305
+ });
306
+
307
+ it('combines server-side paths and inline bytes on one message', async () => {
308
+ vi.mocked(runner.run).mockResolvedValue('{}');
309
+ const harness = await setupHandlers();
310
+ const bytes = Buffer.from('hello').toString('base64');
311
+ await harness.callTool('gog_gmail_send', {
312
+ to: 'bob@example.com',
313
+ subject: 'Both',
314
+ body: 'x',
315
+ attach: ['/tmp/on-server.pdf'],
316
+ attachInline: [{ filename: 'from-client.txt', contentBase64: bytes }],
317
+ });
318
+ const args = vi.mocked(runner.run).mock.calls[0][0];
319
+ expect(args).toContain('--attach=/tmp/on-server.pdf');
320
+ expect(args).toContainEqual(
321
+ { kind: 'file', flag: 'attach', contents: bytes, encoding: 'base64', filename: 'from-client.txt' },
322
+ );
323
+ });
324
+
325
+ it('keeps a filename with spaces and non-ASCII intact end to end', async () => {
326
+ vi.mocked(runner.run).mockResolvedValue('{}');
327
+ const harness = await setupHandlers();
328
+ const filename = 'ReΓ§u β€” Γ©tude 2026.png';
329
+ await harness.callTool('gog_gmail_send', {
330
+ to: 'bob@example.com', subject: 's', body: 'b',
331
+ attachInline: [{ filename, contentBase64: Buffer.from('x').toString('base64') }],
332
+ });
333
+ const args = vi.mocked(runner.run).mock.calls[0][0];
334
+ expect(args.at(-1)).toMatchObject({ filename });
335
+ });
336
+
337
+ it('surfaces an invalid-base64 attachment as a readable error, not a corrupt send', async () => {
338
+ vi.mocked(runner.run).mockResolvedValue('{}');
339
+ const harness = await setupHandlers();
340
+ const res = await harness.callTool('gog_gmail_send', {
341
+ to: 'bob@example.com', subject: 's', body: 'b',
342
+ attachInline: [{ filename: 'a.png', contentBase64: 'not!valid!base64!' }],
343
+ });
344
+ expect(res.isError).toBe(true);
345
+ expect(res.content[0].text).toMatch(/not valid base64/);
346
+ expect(runner.run).not.toHaveBeenCalled(); // nothing was sent
347
+ });
348
+
349
+ it('rejects an oversize attachment before the call rather than deep in the stack', async () => {
350
+ vi.mocked(runner.run).mockResolvedValue('{}');
351
+ const harness = await setupHandlers();
352
+ const res = await harness.callTool('gog_gmail_send', {
353
+ to: 'bob@example.com', subject: 's', body: 'b',
354
+ attachInline: [{ filename: 'huge.bin', contentBase64: Buffer.alloc(8 * 1024 * 1024 + 1).toString('base64') }],
355
+ });
356
+ expect(res.isError).toBe(true);
357
+ expect(res.content[0].text).toMatch(/per-file limit/);
358
+ expect(runner.run).not.toHaveBeenCalled();
359
+ });
360
+
361
+ it('leaves the arg list untouched when attachInline is absent', async () => {
362
+ vi.mocked(runner.run).mockResolvedValue('{}');
363
+ const harness = await setupHandlers();
364
+ await harness.callTool('gog_gmail_send', { to: 'b@e.com', subject: 'Hi', body: 'Hello' });
365
+ expect(runner.run).toHaveBeenCalledWith(
366
+ ['gmail', 'send', '--to=b@e.com', '--subject=Hi', '--body=Hello'],
367
+ { account: undefined },
368
+ );
369
+ });
370
+
282
371
  // A body over the shared threshold cannot ride in argv (the hosted runner
283
372
  // caps a single arg; Linux caps MAX_ARG_STRLEN at 128 KiB), so payloadArg
284
373
  // swaps it for a file arg the executor materializes as a temp file.