gogcli-mcp 4.3.0 → 4.4.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +647 -67
- package/dist/lib.js +654 -67
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/dispatch-confirmation.ts +277 -0
- package/src/gmail-dispatch-guard.ts +36 -36
- package/src/lib.ts +9 -0
- package/src/send-confirm-token.ts +167 -0
- package/src/tools/api.ts +24 -3
- package/src/tools/calendar.ts +168 -7
- package/src/tools/chat.ts +78 -5
- package/src/tools/classroom.ts +83 -3
- package/src/tools/drive.ts +55 -3
- package/src/tools/gmail.ts +111 -20
- package/tests/gmail-dispatch-guard.test.ts +227 -14
- package/tests/send-confirm-token.test.ts +200 -0
- package/tests/tools/calendar.test.ts +4 -1
- package/tests/tools/chat.test.ts +4 -1
- package/tests/tools/classroom.test.ts +4 -1
- package/tests/tools/dispatch-gates.test.ts +396 -0
- package/tests/tools/drive.test.ts +4 -1
- package/tests/tools/gmail-confirm-token.test.ts +274 -0
- package/tests/tools/run-vets.test.ts +131 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
6
|
+
import { metadataThreadId, registerGmailTools } from '../../src/tools/gmail.js';
|
|
7
|
+
import * as runner from '../../src/runner.js';
|
|
8
|
+
import { pos } from '../../src/argv.js';
|
|
9
|
+
import { resetConfirmTokenState } from '../../src/send-confirm-token.js';
|
|
10
|
+
|
|
11
|
+
vi.mock('../../src/runner.js');
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// The two-phase confirmToken fallback on the base package's send tools,
|
|
15
|
+
// driven through the real MCP RPC path. A harness registered WITHOUT an
|
|
16
|
+
// elicitation handler is a client that declares no elicitation — claude.ai's
|
|
17
|
+
// measured shape.
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
21
|
+
let fileRoot: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
process.env = { ...ORIGINAL_ENV };
|
|
26
|
+
delete process.env.GOG_SEND_CONFIRM_FALLBACK;
|
|
27
|
+
delete process.env.GOG_CONFIRM_TTL_SECONDS;
|
|
28
|
+
delete process.env.GOG_CONFIRM_SECRET;
|
|
29
|
+
process.env.GOG_ACCOUNT = 'me@example.com';
|
|
30
|
+
fileRoot = mkdtempSync(join(tmpdir(), 'gct-roots-'));
|
|
31
|
+
process.env.GOG_FILE_ROOTS = fileRoot;
|
|
32
|
+
resetConfirmTokenState();
|
|
33
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
process.env = ORIGINAL_ENV;
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
vi.useRealTimers();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const noElicitation = () => createTestHarness(registerGmailTools);
|
|
43
|
+
const withElicitation = () => createTestHarness(registerGmailTools, {
|
|
44
|
+
elicitation: async () => ({ action: 'accept', content: { confirmed: true } }),
|
|
45
|
+
});
|
|
46
|
+
const json = (r: { content: Array<{ text?: string }> }) => JSON.parse(r.content[0]!.text as string);
|
|
47
|
+
const SENT = '{"id":"sent1"}';
|
|
48
|
+
|
|
49
|
+
const SEND_ARGS = {
|
|
50
|
+
to: 'alice@example.com',
|
|
51
|
+
cc: 'carol@example.com',
|
|
52
|
+
bcc: 'secret-bcc@example.com',
|
|
53
|
+
subject: 'Quarterly numbers',
|
|
54
|
+
body: 'Hi Alice,\n\nNumbers attached.\n\n— me',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const sendCalls = () => vi.mocked(runner.run).mock.calls.filter(([args]) => args[0] === 'gmail' && args[1] === 'send');
|
|
58
|
+
|
|
59
|
+
describe('gog_gmail_send — token fallback', () => {
|
|
60
|
+
it('elicitation supported: unchanged — prompts and sends, confirmToken or not', async () => {
|
|
61
|
+
process.env.GOG_SEND_CONFIRM_FALLBACK = 'token';
|
|
62
|
+
vi.mocked(runner.run).mockResolvedValue(SENT);
|
|
63
|
+
const harness = await withElicitation();
|
|
64
|
+
const result = await harness.callTool('gog_gmail_send', { ...SEND_ARGS, confirmToken: 'gct1.not.real' });
|
|
65
|
+
expect(result.content[0]!.text).toBe(SENT);
|
|
66
|
+
expect(sendCalls()).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('unsupported + env unset: refuses, with the new hint', async () => {
|
|
70
|
+
const harness = await noElicitation();
|
|
71
|
+
const body = json(await harness.callTool('gog_gmail_send', SEND_ARGS));
|
|
72
|
+
expect(body).toMatchObject({ confirmed: false, dispatched: false, reason: 'confirmation-unsupported' });
|
|
73
|
+
expect(body.note).toContain('set GOG_SEND_CONFIRM_FALLBACK=token to enable two-step confirmation');
|
|
74
|
+
expect(runner.run).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('unsupported + GOG_SEND_CONFIRM_FALLBACK=token', () => {
|
|
78
|
+
beforeEach(() => { process.env.GOG_SEND_CONFIRM_FALLBACK = 'token'; });
|
|
79
|
+
|
|
80
|
+
it('phase 1 previews everything (Bcc included, full body) and sends nothing', async () => {
|
|
81
|
+
const long = `${'x'.repeat(5000)}END`;
|
|
82
|
+
const harness = await noElicitation();
|
|
83
|
+
const body = json(await harness.callTool('gog_gmail_send', {
|
|
84
|
+
...SEND_ARGS,
|
|
85
|
+
body: long,
|
|
86
|
+
threadId: 't1',
|
|
87
|
+
replyToMessageId: 'm0',
|
|
88
|
+
attachInline: [{ filename: 'q3.csv', contentBase64: Buffer.from('a,b\n1,2\n').toString('base64') }],
|
|
89
|
+
}));
|
|
90
|
+
expect(body.status).toBe('confirmation-required');
|
|
91
|
+
expect(body.dispatched).toBe(false);
|
|
92
|
+
expect(body.preview).toEqual({
|
|
93
|
+
from: 'me@example.com',
|
|
94
|
+
to: 'alice@example.com',
|
|
95
|
+
cc: 'carol@example.com',
|
|
96
|
+
bcc: 'secret-bcc@example.com',
|
|
97
|
+
subject: 'Quarterly numbers',
|
|
98
|
+
body: long,
|
|
99
|
+
attachments: [{ name: 'q3.csv', size: 8 }],
|
|
100
|
+
threadId: 't1',
|
|
101
|
+
inReplyTo: 'm0',
|
|
102
|
+
quotesOriginal: false,
|
|
103
|
+
});
|
|
104
|
+
expect(body.confirmToken).toMatch(/^gct1\./);
|
|
105
|
+
expect(body.instruction).toMatch(/verbatim/);
|
|
106
|
+
expect(runner.run).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('phase 2 with a valid token sends, exactly once', async () => {
|
|
110
|
+
vi.mocked(runner.run).mockResolvedValue(SENT);
|
|
111
|
+
const harness = await noElicitation();
|
|
112
|
+
const { confirmToken } = json(await harness.callTool('gog_gmail_send', SEND_ARGS));
|
|
113
|
+
const result = await harness.callTool('gog_gmail_send', { ...SEND_ARGS, confirmToken });
|
|
114
|
+
expect(result.content[0]!.text).toBe(SENT);
|
|
115
|
+
expect(sendCalls()).toHaveLength(1);
|
|
116
|
+
expect(sendCalls()[0]![0]).toContain('--bcc=secret-bcc@example.com');
|
|
117
|
+
|
|
118
|
+
const again = await harness.callTool('gog_gmail_send', { ...SEND_ARGS, confirmToken });
|
|
119
|
+
expect(json(again)).toMatchObject({ error: 'TOKEN_REUSED', dispatched: false });
|
|
120
|
+
expect(sendCalls()).toHaveLength(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('phase 2 with ANY changed argument is DRAFT_CHANGED and sends nothing', async () => {
|
|
124
|
+
const harness = await noElicitation();
|
|
125
|
+
const { confirmToken } = json(await harness.callTool('gog_gmail_send', SEND_ARGS));
|
|
126
|
+
const body = json(await harness.callTool('gog_gmail_send', { ...SEND_ARGS, bcc: 'someone-else@example.com', confirmToken }));
|
|
127
|
+
expect(body).toMatchObject({ error: 'DRAFT_CHANGED', reason: 'payload-changed', dispatched: false });
|
|
128
|
+
expect(body.preview.bcc).toBe('someone-else@example.com');
|
|
129
|
+
expect(sendCalls()).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('binds a server-side attachment by content: a same-size swap is DRAFT_CHANGED', async () => {
|
|
133
|
+
const path = join(fileRoot, 'report.txt');
|
|
134
|
+
writeFileSync(path, 'v1');
|
|
135
|
+
const harness = await noElicitation();
|
|
136
|
+
const first = json(await harness.callTool('gog_gmail_send', { ...SEND_ARGS, attach: [path] }));
|
|
137
|
+
expect(first.preview.attachments).toEqual([{ name: path, size: 2 }]);
|
|
138
|
+
writeFileSync(path, 'v2');
|
|
139
|
+
const body = json(await harness.callTool('gog_gmail_send', { ...SEND_ARGS, attach: [path], confirmToken: first.confirmToken }));
|
|
140
|
+
expect(body.error).toBe('DRAFT_CHANGED');
|
|
141
|
+
expect(sendCalls()).toHaveLength(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('rejects an expired token', async () => {
|
|
145
|
+
vi.useFakeTimers({ toFake: ['Date'] });
|
|
146
|
+
vi.setSystemTime(new Date('2026-09-24T10:00:00Z'));
|
|
147
|
+
const harness = await noElicitation();
|
|
148
|
+
const { confirmToken } = json(await harness.callTool('gog_gmail_send', SEND_ARGS));
|
|
149
|
+
vi.setSystemTime(new Date('2026-09-24T10:10:01Z'));
|
|
150
|
+
expect(json(await harness.callTool('gog_gmail_send', { ...SEND_ARGS, confirmToken }))).toMatchObject({ error: 'TOKEN_EXPIRED' });
|
|
151
|
+
expect(sendCalls()).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('rejects a tampered token', async () => {
|
|
155
|
+
const harness = await noElicitation();
|
|
156
|
+
const { confirmToken } = json(await harness.callTool('gog_gmail_send', SEND_ARGS));
|
|
157
|
+
const result = await harness.callTool('gog_gmail_send', { ...SEND_ARGS, confirmToken: `${confirmToken}x` });
|
|
158
|
+
expect(result.isError).toBe(true);
|
|
159
|
+
expect(json(result)).toMatchObject({ error: 'TOKEN_INVALID' });
|
|
160
|
+
expect(sendCalls()).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('rejects a token issued to a different account', async () => {
|
|
164
|
+
const harness = await noElicitation();
|
|
165
|
+
const { confirmToken } = json(await harness.callTool('gog_gmail_send', { ...SEND_ARGS, account: 'other@example.com' }));
|
|
166
|
+
expect(json(await harness.callTool('gog_gmail_send', { ...SEND_ARGS, confirmToken }))).toMatchObject({ error: 'TOKEN_INVALID' });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe.each([
|
|
172
|
+
['gog_gmail_reply', 'reply'],
|
|
173
|
+
['gog_gmail_reply_all', 'reply-all'],
|
|
174
|
+
] as const)('%s — token fallback', (tool, subcommand) => {
|
|
175
|
+
const metadata = (messageIdHeader = '<orig@mail.example.com>') => JSON.stringify({
|
|
176
|
+
message: { id: 'm1', threadId: 't1' },
|
|
177
|
+
headers: {
|
|
178
|
+
from: 'Alice <alice@example.com>',
|
|
179
|
+
to: 'me@example.com',
|
|
180
|
+
cc: 'Carol <carol@example.com>',
|
|
181
|
+
subject: 'Contract terms',
|
|
182
|
+
message_id: messageIdHeader,
|
|
183
|
+
references: '<root@mail.example.com>',
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const replyCalls = () => vi.mocked(runner.run).mock.calls.filter(([args]) => args[1] === subcommand);
|
|
187
|
+
|
|
188
|
+
beforeEach(() => { process.env.GOG_SEND_CONFIRM_FALLBACK = 'token'; });
|
|
189
|
+
|
|
190
|
+
it('phase 1 previews the resolved recipients, Bcc, full body and In-Reply-To; nothing sent', async () => {
|
|
191
|
+
vi.mocked(runner.run).mockResolvedValue(metadata());
|
|
192
|
+
const harness = await noElicitation();
|
|
193
|
+
const body = json(await harness.callTool(tool, { messageId: 'm1', body: 'Agreed.', bcc: ['auditor@example.com'] }));
|
|
194
|
+
expect(body.status).toBe('confirmation-required');
|
|
195
|
+
expect(body.preview).toMatchObject({
|
|
196
|
+
from: 'me@example.com',
|
|
197
|
+
to: subcommand === 'reply' ? ['alice@example.com'] : ['alice@example.com', 'me@example.com'],
|
|
198
|
+
cc: subcommand === 'reply' ? [] : ['carol@example.com'],
|
|
199
|
+
bcc: ['auditor@example.com'],
|
|
200
|
+
subject: 'Re: Contract terms',
|
|
201
|
+
body: 'Agreed.',
|
|
202
|
+
quotesOriginal: true,
|
|
203
|
+
threadId: 't1',
|
|
204
|
+
inReplyTo: '<orig@mail.example.com>',
|
|
205
|
+
replyingToMessageId: 'm1',
|
|
206
|
+
});
|
|
207
|
+
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
208
|
+
expect(replyCalls()).toHaveLength(0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('phase 2 re-reads the original and sends with a valid token', async () => {
|
|
212
|
+
vi.mocked(runner.run).mockResolvedValue(metadata());
|
|
213
|
+
const harness = await noElicitation();
|
|
214
|
+
const { confirmToken } = json(await harness.callTool(tool, { messageId: 'm1', body: 'Agreed.' }));
|
|
215
|
+
vi.mocked(runner.run).mockResolvedValueOnce(metadata()).mockResolvedValueOnce(SENT);
|
|
216
|
+
const result = await harness.callTool(tool, { messageId: 'm1', body: 'Agreed.', confirmToken });
|
|
217
|
+
expect(result.content[0]!.text).toBe(SENT);
|
|
218
|
+
expect(runner.run).toHaveBeenLastCalledWith(
|
|
219
|
+
['gmail', subcommand, pos('m1'), '--body=Agreed.', '--auto-from-addressed-alias=false'],
|
|
220
|
+
{ account: undefined },
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('refuses when the original re-reads differently (DRAFT_CHANGED)', async () => {
|
|
225
|
+
vi.mocked(runner.run).mockResolvedValue(metadata());
|
|
226
|
+
const harness = await noElicitation();
|
|
227
|
+
const { confirmToken } = json(await harness.callTool(tool, { messageId: 'm1', body: 'Agreed.' }));
|
|
228
|
+
vi.mocked(runner.run).mockResolvedValue(metadata('<different@mail.example.com>'));
|
|
229
|
+
const body = json(await harness.callTool(tool, { messageId: 'm1', body: 'Agreed.', confirmToken }));
|
|
230
|
+
expect(body).toMatchObject({ error: 'DRAFT_CHANGED', dispatched: false });
|
|
231
|
+
expect(replyCalls()).toHaveLength(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('rejects a token issued for a different message', async () => {
|
|
235
|
+
vi.mocked(runner.run).mockResolvedValue(metadata());
|
|
236
|
+
const harness = await noElicitation();
|
|
237
|
+
const { confirmToken } = json(await harness.callTool(tool, { messageId: 'm-other', body: 'Agreed.' }));
|
|
238
|
+
expect(json(await harness.callTool(tool, { messageId: 'm1', body: 'Agreed.', confirmToken }))).toMatchObject({ error: 'TOKEN_INVALID' });
|
|
239
|
+
expect(replyCalls()).toHaveLength(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('shows the alias choice, HTML body, file-backed body and signature when set', async () => {
|
|
243
|
+
vi.mocked(runner.run).mockResolvedValue('not json');
|
|
244
|
+
const htmlPath = join(fileRoot, 'body.html');
|
|
245
|
+
writeFileSync(htmlPath, '<p>hi</p>');
|
|
246
|
+
const harness = await noElicitation();
|
|
247
|
+
const a = json(await harness.callTool(tool, { messageId: 'm1', bodyHtmlFile: htmlPath, autoFromAddressedAlias: true, signature: true, noQuote: true }));
|
|
248
|
+
expect(a.preview).toMatchObject({
|
|
249
|
+
from: 'the send-as alias the original was addressed to',
|
|
250
|
+
bodyHtmlFile: htmlPath,
|
|
251
|
+
signature: 'the Gmail signature of the sending address',
|
|
252
|
+
quotesOriginal: false,
|
|
253
|
+
});
|
|
254
|
+
expect(a.preview.threadId).toBeUndefined();
|
|
255
|
+
const b = json(await harness.callTool(tool, { messageId: 'm1', bodyHtml: '<b>x</b>', from: 'alias@example.com', signatureFrom: 'alias@example.com', remove: ['alice@example.com'] }));
|
|
256
|
+
expect(b.preview).toMatchObject({ from: 'alias@example.com', bodyHtml: '<b>x</b>', signature: 'alias@example.com' });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('elicitation supported: unchanged', async () => {
|
|
260
|
+
vi.mocked(runner.run).mockResolvedValueOnce(metadata()).mockResolvedValueOnce(metadata()).mockResolvedValueOnce(SENT);
|
|
261
|
+
const harness = await withElicitation();
|
|
262
|
+
const result = await harness.callTool(tool, { messageId: 'm1', body: 'Agreed.' });
|
|
263
|
+
expect(result.content[0]!.text).toBe(SENT);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('metadataThreadId', () => {
|
|
268
|
+
it('reads message.threadId and ignores anything else', () => {
|
|
269
|
+
expect(metadataThreadId('{"message":{"threadId":"t1"}}')).toBe('t1');
|
|
270
|
+
expect(metadataThreadId('{"message":{"threadId":5}}')).toBeUndefined();
|
|
271
|
+
expect(metadataThreadId('null')).toBeUndefined();
|
|
272
|
+
expect(metadataThreadId('not json')).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
3
|
+
import * as runner from '../../src/runner.js';
|
|
4
|
+
import { registerChatTools, vetChatRun } from '../../src/tools/chat.js';
|
|
5
|
+
import { registerDriveTools, vetDriveRun } from '../../src/tools/drive.js';
|
|
6
|
+
import { registerClassroomTools, vetClassroomRun } from '../../src/tools/classroom.js';
|
|
7
|
+
import { registerCalendarTools, vetCalendarRun } from '../../src/tools/calendar.js';
|
|
8
|
+
import { refusedApiCall } from '../../src/tools/api.js';
|
|
9
|
+
|
|
10
|
+
vi.mock('../../src/runner.js');
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// The escape hatches must not be a way around the dispatch rail (#400): every
|
|
14
|
+
// action a dedicated tool asks the user about is refused by gog_<service>_run
|
|
15
|
+
// and gog_api_call, under every alias gog accepts for it.
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
beforeEach(() => vi.clearAllMocks());
|
|
19
|
+
|
|
20
|
+
describe('vetChatRun', () => {
|
|
21
|
+
it.each([
|
|
22
|
+
['messages', ['send', 'spaces/A', '--text=hi']],
|
|
23
|
+
['messages', ['create', 'spaces/A']],
|
|
24
|
+
['messages', ['post', 'spaces/A']],
|
|
25
|
+
['dm', ['send', 'a@example.com']],
|
|
26
|
+
['DM', ['--color', 'never', 'POST', 'a@example.com']],
|
|
27
|
+
])('refuses %s %j', (sub, args) => {
|
|
28
|
+
expect(vetChatRun(sub, args)).toMatch(/gog_chat_(messages|dm)_send.*asks the user/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each([
|
|
32
|
+
['messages', ['list', 'spaces/A']],
|
|
33
|
+
['messages', ['react', 'spaces/A/messages/B', '--emoji=👍']],
|
|
34
|
+
['dm', ['space', 'a@example.com']],
|
|
35
|
+
['spaces', ['send']],
|
|
36
|
+
['messages', ['search', '--query=send']],
|
|
37
|
+
])('allows %s %j', (sub, args) => {
|
|
38
|
+
expect(vetChatRun(sub, args)).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('vetDriveRun', () => {
|
|
43
|
+
it.each(['share', 'SHARE'])('refuses %s', (sub) => {
|
|
44
|
+
expect(vetDriveRun(sub, ['f1', '--to=anyone'])).toMatch(/gog_drive_share.*asks the user/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each(['permissions', 'unshare', 'copy'])('allows %s', (sub) => {
|
|
48
|
+
expect(vetDriveRun(sub, ['f1'])).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('vetClassroomRun', () => {
|
|
53
|
+
it.each([
|
|
54
|
+
['announcements', ['create', 'c1', '--text=x']],
|
|
55
|
+
['announcement', ['add', 'c1']],
|
|
56
|
+
['ann', ['new', 'c1']],
|
|
57
|
+
['invitations', ['create', 'c1', 'u1']],
|
|
58
|
+
['invitation', ['add', 'c1', 'u1']],
|
|
59
|
+
['invites', ['NEW', 'c1', 'u1']],
|
|
60
|
+
])('refuses %s %j', (sub, args) => {
|
|
61
|
+
expect(vetClassroomRun(sub, args)).toMatch(/gog_classroom_(announcements|invitations)_create.*asks the user/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each([
|
|
65
|
+
['announcements', ['list', 'c1']],
|
|
66
|
+
['invitations', ['accept', 'i1']],
|
|
67
|
+
['guardians', ['create', 'c1']],
|
|
68
|
+
['courses', ['create']],
|
|
69
|
+
])('allows %s %j', (sub, args) => {
|
|
70
|
+
expect(vetClassroomRun(sub, args)).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('vetCalendarRun', () => {
|
|
75
|
+
it.each(['create', 'add', 'new', 'update', 'edit', 'set', 'respond', 'rsvp', 'reply', 'Create'])('refuses %s', (sub) => {
|
|
76
|
+
expect(vetCalendarRun(sub, ['primary'])).toMatch(/gog_calendar_(create|update|respond).*asks the user/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it.each(['events', 'freebusy', 'calendars', 'create-calendar', 'focus-time'])('allows %s', (sub) => {
|
|
80
|
+
expect(vetCalendarRun(sub, [])).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('the vets are wired into the run tools', () => {
|
|
85
|
+
it.each([
|
|
86
|
+
[registerChatTools, 'gog_chat_run', { subcommand: 'messages', args: ['send', 'spaces/A', '--text=hi'] }],
|
|
87
|
+
[registerDriveTools, 'gog_drive_run', { subcommand: 'share', args: ['f1', '--to=anyone'] }],
|
|
88
|
+
[registerClassroomTools, 'gog_classroom_run', { subcommand: 'ann', args: ['create', 'c1', '--text=x'] }],
|
|
89
|
+
[registerCalendarTools, 'gog_calendar_run', { subcommand: 'rsvp', args: ['primary', 'e1', '--status=accepted'] }],
|
|
90
|
+
] as const)('%#: %s refuses without spawning gog', async (register, tool, args) => {
|
|
91
|
+
const harness = await createTestHarness(register);
|
|
92
|
+
const result = await harness.callTool(tool, args);
|
|
93
|
+
expect(result.isError).toBe(true);
|
|
94
|
+
expect(result.content[0]!.text).toMatch(/asks the user to confirm/);
|
|
95
|
+
expect(runner.run).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('refusedApiCall — the dispatch rail\'s other escape hatch', () => {
|
|
100
|
+
it.each([
|
|
101
|
+
['chat', 'spaces.messages.create', 'gog_chat_messages_send'],
|
|
102
|
+
['chat', 'chat.spaces.messages.create', 'gog_chat_messages_send'],
|
|
103
|
+
['drive', 'permissions.create', 'gog_drive_share'],
|
|
104
|
+
['drive', 'permissions.update', 'gog_drive_share'],
|
|
105
|
+
['classroom', 'courses.announcements.create', 'gog_classroom_announcements_create'],
|
|
106
|
+
['classroom', 'invitations.create', 'gog_classroom_invitations_create'],
|
|
107
|
+
['calendar', 'events.insert', 'gog_calendar_create'],
|
|
108
|
+
['Calendar', ' events.patch ', 'gog_calendar_update'],
|
|
109
|
+
['calendar', 'events.update', 'gog_calendar_update'],
|
|
110
|
+
['calendar', 'events.import', 'gog_calendar_create'],
|
|
111
|
+
['calendar', 'events.quickAdd', 'gog_calendar_create'],
|
|
112
|
+
])('refuses %s %s', (api, method, tool) => {
|
|
113
|
+
expect(refusedApiCall(api, method)).toContain(tool);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it.each([
|
|
117
|
+
['chat', 'spaces.messages.list'],
|
|
118
|
+
['drive', 'permissions.list'],
|
|
119
|
+
['drive', 'permissions.delete'],
|
|
120
|
+
['classroom', 'courses.announcements.list'],
|
|
121
|
+
['calendar', 'events.list'],
|
|
122
|
+
['calendar', 'events.delete'],
|
|
123
|
+
['sheets', 'spreadsheets.values.update'],
|
|
124
|
+
])('allows %s %s', (api, method) => {
|
|
125
|
+
expect(refusedApiCall(api, method)).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('still refuses Gmail sends', () => {
|
|
129
|
+
expect(refusedApiCall('gmail', 'users.messages.send')).toMatch(/gog_gmail_send/);
|
|
130
|
+
});
|
|
131
|
+
});
|