gogcli-mcp 2.20.0 → 2.21.1

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,8 +1,17 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import * as runner from '../../src/runner.js';
3
3
  import { runOrDiagnose, pushPaginationFlags, formatAccountList, formatAuthHealth } from '../../src/tools/utils.js';
4
-
5
- vi.mock('../../src/runner.js');
4
+ import { RunnerTransportError } from '../../src/connector-runtime.js';
5
+
6
+ // PARTIAL mock: only `run` is stubbed. A full automock would also replace
7
+ // `RunnerTransportError` and `isRunnerTransportError`, and diagnose()'s whole
8
+ // point is that it classifies the REAL class structurally — an automocked
9
+ // brand check answers undefined for everything and the tests below would pass
10
+ // while proving nothing.
11
+ vi.mock('../../src/runner.js', async (importOriginal) => ({
12
+ ...(await importOriginal<typeof runner>()),
13
+ run: vi.fn(),
14
+ }));
6
15
 
7
16
  beforeEach(() => vi.clearAllMocks());
8
17
 
@@ -190,6 +199,50 @@ describe('runOrDiagnose', () => {
190
199
  expect(result.content[0].text).toContain('gog_auth_add');
191
200
  });
192
201
 
202
+ it('calls a rate-limited failure transient even though it says "token expired"', async () => {
203
+ // The reported symptom was servers flapping into a needs-auth state and
204
+ // then working seconds later. Telling someone to re-authorize an account
205
+ // whose credential is fine is the expensive kind of wrong: re-auth is
206
+ // manual, and it does not fix a 429.
207
+ //
208
+ // `invalid_grant` is exempt from this and stays auth (below) — it is the
209
+ // one signal that definitively means the refresh token is dead.
210
+ vi.mocked(runner.run)
211
+ .mockRejectedValueOnce(new Error('429 rateLimitExceeded: the access token expired mid-request, retry'))
212
+ .mockResolvedValueOnce('user@gmail.com');
213
+ const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
214
+ const text = result.content[0].text as string;
215
+ expect(text).toContain('often transient');
216
+ expect(text).not.toContain('gog_auth_add');
217
+ });
218
+
219
+ it('still calls an explicit 401 an auth error even alongside a transient signal', async () => {
220
+ // The exemption above is for the LOOSE match only. A literal 401 is Google
221
+ // saying "not authenticated", and downgrading that to "retry" would loop a
222
+ // caller forever against a request that can never succeed.
223
+ vi.mocked(runner.run)
224
+ .mockRejectedValueOnce(new Error('401 unauthorized (quota project unset)'))
225
+ .mockResolvedValueOnce('user@gmail.com');
226
+ const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
227
+ expect(result.content[0].text).toContain('gog_auth_add');
228
+ });
229
+
230
+ it('does not read "token" and "expired" as auth when they are unrelated sentences', async () => {
231
+ // The pattern was /token.*(expired|revoked)/ with a greedy `.*`, so any
232
+ // message mentioning a token anywhere and an expiry anywhere later — across
233
+ // whole paragraphs — was reported as an auth failure.
234
+ vi.mocked(runner.run)
235
+ // Deliberately ONE line: `.` does not cross newlines, so a multi-line
236
+ // message would pass this test without the greedy match ever being
237
+ // exercised — and gog's real errors are frequently one long line.
238
+ .mockRejectedValueOnce(
239
+ new Error('page token accepted; the requested export link has expired and must be regenerated'),
240
+ )
241
+ .mockResolvedValueOnce('user@gmail.com');
242
+ const result = await runOrDiagnose(['drive', 'export', 'abc'], {});
243
+ expect(result.content[0].text).not.toContain('gog_auth_add');
244
+ });
245
+
193
246
  it('gives invalid_grant a richer hint than a plain 401: cause + durable fix + both re-auth paths', async () => {
194
247
  vi.mocked(runner.run)
195
248
  .mockRejectedValueOnce(new Error('oauth2: "invalid_grant" "Token has been expired or revoked."'))
@@ -411,3 +464,47 @@ describe('formatAuthHealth', () => {
411
464
  expect(formatAuthHealth('{"foo":1}', NOW)).toBe('{"foo":1}');
412
465
  });
413
466
  });
467
+
468
+ // The connector's own transport failing is NOT the Google credential failing.
469
+ // diagnose() must recognise that structurally, from the error's type, because
470
+ // the runner's prose ("unauthorized") is indistinguishable from Google's.
471
+ describe('diagnose: runner-authored transport failures', () => {
472
+ it('blames the runner key, not the Google account, for a transport-auth failure', async () => {
473
+ vi.mocked(runner.run)
474
+ .mockRejectedValueOnce(
475
+ new RunnerTransportError('gog-runner rejected the bearer token; GOG_RUNNER_KEY vs RUNNER_KEY', 'transport-auth', 401),
476
+ )
477
+ .mockResolvedValueOnce('user@gmail.com');
478
+ const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
479
+ const text = result.content[0].text as string;
480
+ expect(text).not.toContain('gog_auth_add');
481
+ expect(text).not.toMatch(/re-authorize the account/i);
482
+ expect(text).toContain('GOG_RUNNER_KEY');
483
+ expect(text).toContain('RUNNER_KEY');
484
+ });
485
+
486
+ it('lets the TYPE beat the prose: runner words that read like Google words get no re-auth hint', async () => {
487
+ // The regression this whole change exists to prevent. A runner-authored
488
+ // failure whose text happens to contain every Google auth signal must still
489
+ // not produce Google auth advice — the request never reached Google.
490
+ vi.mocked(runner.run)
491
+ .mockRejectedValueOnce(
492
+ new RunnerTransportError('unauthorized: 401 invalid_grant, token has been expired or revoked', 'transport-request', 400),
493
+ )
494
+ .mockResolvedValueOnce('user@gmail.com');
495
+ const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
496
+ const text = result.content[0].text as string;
497
+ expect(text).not.toContain('gog_auth_add');
498
+ expect(text).not.toContain('often transient');
499
+ });
500
+
501
+ it('advises a retry for a retryable transport failure', async () => {
502
+ vi.mocked(runner.run)
503
+ .mockRejectedValueOnce(new RunnerTransportError('gog-runner is restarting; retry this call.', 'transport-retryable', 503))
504
+ .mockResolvedValueOnce('user@gmail.com');
505
+ const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
506
+ const text = result.content[0].text as string;
507
+ expect(text).toContain('often transient');
508
+ expect(text).not.toContain('gog_auth_add');
509
+ });
510
+ });