gogcli-mcp 2.19.1 → 2.19.2
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 +11 -4
- package/dist/lib.js +11 -4
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/remote-runner.ts +21 -11
- package/src/runner.ts +36 -2
- package/src/worker.ts +1 -1
- package/tests/remote-runner.test.ts +83 -12
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
10
|
-
"version": "2.19.
|
|
10
|
+
"version": "2.19.2"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
18
|
-
"version": "2.19.
|
|
18
|
+
"version": "2.19.2",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -31119,6 +31119,13 @@ function isGogFileArg(arg) {
|
|
|
31119
31119
|
return typeof arg !== "string";
|
|
31120
31120
|
}
|
|
31121
31121
|
var runExecutor = new AsyncLocalStorage();
|
|
31122
|
+
var defaultExecutor;
|
|
31123
|
+
function setDefaultGogExecutor(executor) {
|
|
31124
|
+
defaultExecutor = executor ? { executor } : void 0;
|
|
31125
|
+
}
|
|
31126
|
+
function activeExecutor() {
|
|
31127
|
+
return runExecutor.getStore() ?? defaultExecutor;
|
|
31128
|
+
}
|
|
31122
31129
|
var TIMEOUT_MS = 3e4;
|
|
31123
31130
|
function readonlyEnvEnabled() {
|
|
31124
31131
|
return readEnvVar("GOG_READONLY") !== void 0 && parseBoolEnv("GOG_READONLY", { default: true });
|
|
@@ -31279,7 +31286,7 @@ async function run(args, options = {}) {
|
|
|
31279
31286
|
const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
|
|
31280
31287
|
const redact = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
|
|
31281
31288
|
const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
|
|
31282
|
-
const store =
|
|
31289
|
+
const store = activeExecutor();
|
|
31283
31290
|
try {
|
|
31284
31291
|
let output;
|
|
31285
31292
|
if (spawner) {
|
|
@@ -31296,7 +31303,7 @@ async function run(args, options = {}) {
|
|
|
31296
31303
|
}
|
|
31297
31304
|
async function runBinary(args, options = {}) {
|
|
31298
31305
|
const { account, spawner, timeout, readonly: readonly2 = false } = options;
|
|
31299
|
-
if (!spawner &&
|
|
31306
|
+
if (!spawner && activeExecutor()) {
|
|
31300
31307
|
throw new Error(
|
|
31301
31308
|
"Raw byte retrieval is not available over the hosted connector (its transport is text-only). Use the text-extraction path instead, or run the local stdio server to fetch bytes."
|
|
31302
31309
|
);
|
|
@@ -33072,7 +33079,7 @@ function registerTasksTools(server) {
|
|
|
33072
33079
|
}
|
|
33073
33080
|
|
|
33074
33081
|
// src/server.ts
|
|
33075
|
-
var VERSION = true ? "2.19.
|
|
33082
|
+
var VERSION = true ? "2.19.2" : "0.0.0";
|
|
33076
33083
|
var BASE_TOOL_REGISTRARS = [
|
|
33077
33084
|
registerApiTools,
|
|
33078
33085
|
registerAuthTools,
|
|
@@ -33144,7 +33151,7 @@ function useRemoteGogRunner(env = process.env) {
|
|
|
33144
33151
|
const endpoint = readEnvVar("GOG_RUNNER_URL", { env });
|
|
33145
33152
|
const key = readEnvVar("GOG_RUNNER_KEY", { env });
|
|
33146
33153
|
if (!endpoint || !key) return false;
|
|
33147
|
-
|
|
33154
|
+
setDefaultGogExecutor(makeFlyExecutor(endpoint.replace(/\/+$/, ""), key));
|
|
33148
33155
|
return true;
|
|
33149
33156
|
}
|
|
33150
33157
|
|
package/dist/lib.js
CHANGED
|
@@ -23016,6 +23016,13 @@ function isGogFileArg(arg) {
|
|
|
23016
23016
|
return typeof arg !== "string";
|
|
23017
23017
|
}
|
|
23018
23018
|
var runExecutor = new AsyncLocalStorage();
|
|
23019
|
+
var defaultExecutor;
|
|
23020
|
+
function setDefaultGogExecutor(executor) {
|
|
23021
|
+
defaultExecutor = executor ? { executor } : void 0;
|
|
23022
|
+
}
|
|
23023
|
+
function activeExecutor() {
|
|
23024
|
+
return runExecutor.getStore() ?? defaultExecutor;
|
|
23025
|
+
}
|
|
23019
23026
|
var TIMEOUT_MS = 3e4;
|
|
23020
23027
|
var MIN_GOG_VERSION = "0.34.1";
|
|
23021
23028
|
function readonlyEnvEnabled() {
|
|
@@ -23177,7 +23184,7 @@ async function run(args, options = {}) {
|
|
|
23177
23184
|
const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
|
|
23178
23185
|
const redact = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
|
|
23179
23186
|
const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
|
|
23180
|
-
const store =
|
|
23187
|
+
const store = activeExecutor();
|
|
23181
23188
|
try {
|
|
23182
23189
|
let output;
|
|
23183
23190
|
if (spawner) {
|
|
@@ -23194,7 +23201,7 @@ async function run(args, options = {}) {
|
|
|
23194
23201
|
}
|
|
23195
23202
|
async function runBinary(args, options = {}) {
|
|
23196
23203
|
const { account, spawner, timeout, readonly: readonly2 = false } = options;
|
|
23197
|
-
if (!spawner &&
|
|
23204
|
+
if (!spawner && activeExecutor()) {
|
|
23198
23205
|
throw new Error(
|
|
23199
23206
|
"Raw byte retrieval is not available over the hosted connector (its transport is text-only). Use the text-extraction path instead, or run the local stdio server to fetch bytes."
|
|
23200
23207
|
);
|
|
@@ -24978,7 +24985,7 @@ function registerTasksTools(server) {
|
|
|
24978
24985
|
}
|
|
24979
24986
|
|
|
24980
24987
|
// src/server.ts
|
|
24981
|
-
var VERSION = true ? "2.19.
|
|
24988
|
+
var VERSION = true ? "2.19.2" : "0.0.0";
|
|
24982
24989
|
var BASE_TOOL_REGISTRARS = [
|
|
24983
24990
|
registerApiTools,
|
|
24984
24991
|
registerAuthTools,
|
|
@@ -25050,7 +25057,7 @@ function useRemoteGogRunner(env = process.env) {
|
|
|
25050
25057
|
const endpoint = readEnvVar("GOG_RUNNER_URL", { env });
|
|
25051
25058
|
const key = readEnvVar("GOG_RUNNER_KEY", { env });
|
|
25052
25059
|
if (!endpoint || !key) return false;
|
|
25053
|
-
|
|
25060
|
+
setDefaultGogExecutor(makeFlyExecutor(endpoint.replace(/\/+$/, ""), key));
|
|
25054
25061
|
return true;
|
|
25055
25062
|
}
|
|
25056
25063
|
export {
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp",
|
|
5
5
|
"display_name": "gogcli",
|
|
6
|
-
"version": "2.19.
|
|
6
|
+
"version": "2.19.2",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.19.
|
|
10
|
+
"version": "2.19.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "2.19.
|
|
15
|
+
"version": "2.19.2",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/remote-runner.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readEnvVar } from '@chrischall/mcp-utils';
|
|
2
|
-
import {
|
|
2
|
+
import { setDefaultGogExecutor } from './runner.js';
|
|
3
3
|
import { makeFlyExecutor } from './connector-runtime.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -19,16 +19,26 @@ import { makeFlyExecutor } from './connector-runtime.js';
|
|
|
19
19
|
* executes remotely; leave either unset and nothing changes, which is what
|
|
20
20
|
* keeps every existing local install on the binary it already has.
|
|
21
21
|
*
|
|
22
|
-
* ## Why
|
|
22
|
+
* ## Why a process-wide default and not the AsyncLocalStorage
|
|
23
23
|
*
|
|
24
|
-
* `runExecutor` is an AsyncLocalStorage
|
|
25
|
-
* `runExecutor.run(...)` because
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
24
|
+
* `runExecutor` is an AsyncLocalStorage, and the Worker wraps each REQUEST in
|
|
25
|
+
* `runExecutor.run(...)` because one isolate serves many callers whose backend
|
|
26
|
+
* credentials differ. A stdio process is the opposite: one backend for its
|
|
27
|
+
* whole life.
|
|
28
|
+
*
|
|
29
|
+
* This used to reach for `enterWith` on the theory that it "sets the store for
|
|
30
|
+
* the whole process". It does not. `enterWith` sets the store on the async
|
|
31
|
+
* resource that is CURRENT when it runs — here, module evaluation — and the
|
|
32
|
+
* tool calls arrive later as I/O events on the transport's own resources, which
|
|
33
|
+
* do not descend from that. So `getStore()` was undefined at exactly the moment
|
|
34
|
+
* `run()` asked, and the seam reverted to spawning a binary the host does not
|
|
35
|
+
* have. Every hosted gog MCP answered "gog executable not found" while pointed
|
|
36
|
+
* at a healthy backend, and the unit test missed it because a test that calls
|
|
37
|
+
* this function itself awaits inside the resource it just mutated.
|
|
38
|
+
*
|
|
39
|
+
* A process-lifetime value is not a scoped value, so it does not live in a
|
|
40
|
+
* scope: `setDefaultGogExecutor` holds it, and a per-request store still beats
|
|
41
|
+
* it (runner.ts `activeExecutor`) so the Worker path is unchanged.
|
|
32
42
|
*
|
|
33
43
|
* Call before the server starts, so no tool can be serviced ahead of it.
|
|
34
44
|
*/
|
|
@@ -44,6 +54,6 @@ export function useRemoteGogRunner(env: NodeJS.ProcessEnv = process.env): boolea
|
|
|
44
54
|
// nothing — either alone is a misconfiguration, and silently spawning
|
|
45
55
|
// instead would hide it until someone wondered why the binary was needed.
|
|
46
56
|
if (!endpoint || !key) return false;
|
|
47
|
-
|
|
57
|
+
setDefaultGogExecutor(makeFlyExecutor(endpoint.replace(/\/+$/, ''), key));
|
|
48
58
|
return true;
|
|
49
59
|
}
|
package/src/runner.ts
CHANGED
|
@@ -50,6 +50,40 @@ export type GogExecutor = (
|
|
|
50
50
|
// `runExecutor.run({ executor }, ...)`; unset, `run()` falls back to spawning.
|
|
51
51
|
export const runExecutor = new AsyncLocalStorage<{ executor: GogExecutor }>();
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The PROCESS-WIDE executor, for a host that has exactly one backend for the
|
|
55
|
+
* whole process — a stdio bin pointed at a Fly runner (`useRemoteGogRunner`).
|
|
56
|
+
*
|
|
57
|
+
* It exists because AsyncLocalStorage cannot express that. `enterWith` sets the
|
|
58
|
+
* store on the async resource that is current when it runs, and a bin runs it
|
|
59
|
+
* during module evaluation; the tool calls arrive later as I/O events on the
|
|
60
|
+
* transport's own resources, which are not descendants of that evaluation, so
|
|
61
|
+
* `getStore()` is undefined exactly where it is needed. That is not a bug in
|
|
62
|
+
* `enterWith` — a process-lifetime default is simply not a scoped value, and
|
|
63
|
+
* storing it in a scope meant the seam silently reverted to spawning a binary
|
|
64
|
+
* the host does not have.
|
|
65
|
+
*
|
|
66
|
+
* A per-request store still WINS over this (see `activeExecutor`), because the
|
|
67
|
+
* Worker serves many callers from one isolate and each has its own backend
|
|
68
|
+
* credential; this is the fallback for the one-backend case, never a second
|
|
69
|
+
* answer to "whose backend is this".
|
|
70
|
+
*/
|
|
71
|
+
let defaultExecutor: { executor: GogExecutor } | undefined;
|
|
72
|
+
|
|
73
|
+
/** Install the process-wide executor. Passing undefined clears it (tests). */
|
|
74
|
+
export function setDefaultGogExecutor(executor: GogExecutor | undefined): void {
|
|
75
|
+
defaultExecutor = executor ? { executor } : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whose executor applies right now: the request's, else the process's, else
|
|
80
|
+
* none (meaning `run()` spawns the local binary). Both call sites ask through
|
|
81
|
+
* here so they can never disagree about which of the three it is.
|
|
82
|
+
*/
|
|
83
|
+
function activeExecutor(): { executor: GogExecutor } | undefined {
|
|
84
|
+
return runExecutor.getStore() ?? defaultExecutor;
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
export interface RunOptions {
|
|
54
88
|
account?: string;
|
|
55
89
|
spawner?: Spawner;
|
|
@@ -328,7 +362,7 @@ export async function run(args: GogArg[], options: RunOptions = {}): Promise<str
|
|
|
328
362
|
// successful `gog auth tokens` (or any command echoing a credential) would
|
|
329
363
|
// otherwise return raw Google tokens (ya29.…/1//…) into model context, where
|
|
330
364
|
// a sibling tool (gog_gmail_send) could exfiltrate them.
|
|
331
|
-
const store =
|
|
365
|
+
const store = activeExecutor();
|
|
332
366
|
try {
|
|
333
367
|
let output: string;
|
|
334
368
|
if (spawner) {
|
|
@@ -358,7 +392,7 @@ export async function runBinary(args: GogArg[], options: RunOptions = {}): Promi
|
|
|
358
392
|
// An injected spawner is the stdio/test path and always wins. Otherwise, if an
|
|
359
393
|
// ambient forward executor is installed (the Worker/Fly connector), refuse:
|
|
360
394
|
// its text-only transport can't carry bytes intact.
|
|
361
|
-
if (!spawner &&
|
|
395
|
+
if (!spawner && activeExecutor()) {
|
|
362
396
|
throw new Error(
|
|
363
397
|
'Raw byte retrieval is not available over the hosted connector (its transport is text-only). ' +
|
|
364
398
|
'Use the text-extraction path instead, or run the local stdio server to fetch bytes.',
|
package/src/worker.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { gogAuth, type GogProps } from './connector-auth.js';
|
|
|
38
38
|
// connector with all ~360 tools at once. Add whichever paths you want as separate
|
|
39
39
|
// connectors in claude.ai (each authorizes with the same connector key).
|
|
40
40
|
|
|
41
|
-
const VERSION = '2.19.
|
|
41
|
+
const VERSION = '2.19.2'; // x-release-please-version
|
|
42
42
|
|
|
43
43
|
// Build an McpAgent subclass whose init() registers `registrars` onto its server,
|
|
44
44
|
// each handler wrapped in the ALS scope carrying the per-session Fly executor.
|
|
@@ -3,16 +3,30 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
5
5
|
import { useRemoteGogRunner } from '../src/remote-runner.js';
|
|
6
|
-
import { runExecutor } from '../src/runner.js';
|
|
6
|
+
import { run, runBinary, runExecutor, setDefaultGogExecutor } from '../src/runner.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The whole point of this seam is that a host without the `gog` binary can
|
|
10
|
-
* still serve.
|
|
11
|
-
* back to spawning
|
|
12
|
-
*
|
|
10
|
+
* still serve. Every way it has silently failed so far: a half-configured env
|
|
11
|
+
* that falls back to spawning; a bin that never calls `useRemoteGogRunner()`;
|
|
12
|
+
* and — the one that shipped in 2.19.x — parking the executor in an
|
|
13
|
+
* AsyncLocalStorage that the tool calls never inherit, so the seam reported
|
|
14
|
+
* success and then spawned anyway.
|
|
15
|
+
*
|
|
16
|
+
* They share a shape worth naming: every one of them fails by falling back to
|
|
17
|
+
* the local binary, which on a laptop is invisible and on a host is total. So
|
|
18
|
+
* these tests assert on where a call ACTUALLY went, never on whether the wiring
|
|
19
|
+
* step was performed.
|
|
13
20
|
*/
|
|
14
21
|
|
|
15
|
-
afterEach(() =>
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.unstubAllGlobals();
|
|
24
|
+
vi.unstubAllEnvs();
|
|
25
|
+
// The executor is process-wide BY DESIGN now, so unlike an ALS store it does
|
|
26
|
+
// not unwind when a test ends — without this, the first test to install one
|
|
27
|
+
// would silently hand it to every test that ran after it.
|
|
28
|
+
setDefaultGogExecutor(undefined);
|
|
29
|
+
});
|
|
16
30
|
|
|
17
31
|
describe('useRemoteGogRunner', () => {
|
|
18
32
|
it('does nothing unless BOTH variables are set, so local installs are untouched', () => {
|
|
@@ -33,26 +47,83 @@ describe('useRemoteGogRunner', () => {
|
|
|
33
47
|
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'null', GOG_RUNNER_KEY: 'k' })).toBe(false);
|
|
34
48
|
});
|
|
35
49
|
|
|
36
|
-
it('
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
50
|
+
it('addresses and authenticates the backend correctly', async () => {
|
|
51
|
+
// Asserted THROUGH run(), not by reading whichever slot the executor is
|
|
52
|
+
// parked in. The previous version of this test fetched
|
|
53
|
+
// `runExecutor.getStore()` and called what it found, so it was really a
|
|
54
|
+
// test that `enterWith` had been called — which stayed green while the
|
|
55
|
+
// shipped bins routed every call to a local binary (see the next test).
|
|
40
56
|
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ stdout: 'ok' }), { status: 200 }));
|
|
41
57
|
vi.stubGlobal('fetch', fetchMock);
|
|
58
|
+
vi.stubEnv('GOG_PATH', '/nonexistent/gog');
|
|
42
59
|
|
|
43
60
|
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test/', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
44
61
|
|
|
45
62
|
// Cross a macrotask boundary, the way a stdio tool call does.
|
|
46
63
|
await new Promise((r) => setTimeout(r, 0));
|
|
47
|
-
|
|
48
|
-
expect(store?.executor).toBeTypeOf('function');
|
|
64
|
+
expect(await run(['--version'])).toBe('ok');
|
|
49
65
|
|
|
50
|
-
await store!.executor(['--version'], {});
|
|
51
66
|
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
|
52
67
|
// Trailing slash trimmed, so the endpoint is never `…//run`.
|
|
53
68
|
expect(url).toBe('https://r.test/run');
|
|
54
69
|
expect((init.headers as Record<string, string>).Authorization).toBe('Bearer secret');
|
|
55
70
|
});
|
|
71
|
+
|
|
72
|
+
it('still routes remotely from a context the store never reached', async () => {
|
|
73
|
+
// THE ONE THE TEST ABOVE CANNOT CATCH, and it shipped broken.
|
|
74
|
+
//
|
|
75
|
+
// `enterWith` mutates the store of the async resource that is current when
|
|
76
|
+
// it runs. The test above calls useRemoteGogRunner() itself, so everything
|
|
77
|
+
// it awaits afterwards is a descendant of that resource and inherits the
|
|
78
|
+
// store — it passes whether or not the seam works where it matters.
|
|
79
|
+
//
|
|
80
|
+
// A bin does something different: it calls useRemoteGogRunner() while the
|
|
81
|
+
// ES module is evaluating, and then `await runMcp(...)` starts serving. The
|
|
82
|
+
// tool calls arrive later as I/O events on the transport's own async
|
|
83
|
+
// resources, which are NOT descendants of module evaluation, so
|
|
84
|
+
// `getStore()` is undefined by the time `run()` asks. Verified against the
|
|
85
|
+
// published 2.19.1 bin: `useRemoteGogRunner()` returned true, and at call
|
|
86
|
+
// time the store was undefined — every hosted gog MCP answered
|
|
87
|
+
// "gog executable not found" while pointed at a healthy backend.
|
|
88
|
+
//
|
|
89
|
+
// `runExecutor.exit()` is that condition exactly, and deterministically:
|
|
90
|
+
// run the call where the ALS store does not apply.
|
|
91
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ stdout: 'remote' }), { status: 200 }));
|
|
92
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
93
|
+
// So the spawn fallback cannot accidentally succeed on a machine that has
|
|
94
|
+
// gog installed and make this pass for the wrong reason.
|
|
95
|
+
vi.stubEnv('GOG_PATH', '/nonexistent/gog');
|
|
96
|
+
|
|
97
|
+
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
98
|
+
|
|
99
|
+
const output = await runExecutor.exit(() => run(['auth', 'status']));
|
|
100
|
+
expect(output).toBe('remote');
|
|
101
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('lets a per-request store beat the process-wide default', async () => {
|
|
105
|
+
// The Worker path scopes every request in `runExecutor.run({ executor })`
|
|
106
|
+
// because one isolate serves many callers (connector-runtime.ts). A
|
|
107
|
+
// process-wide default must never shadow that: it is the fallback for a
|
|
108
|
+
// stdio bin, not a second answer to "whose backend is this".
|
|
109
|
+
const perRequest = vi.fn(async () => 'per-request');
|
|
110
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ stdout: 'default' }), { status: 200 })));
|
|
111
|
+
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
112
|
+
|
|
113
|
+
const output = await runExecutor.run({ executor: perRequest }, () => run(['auth', 'status']));
|
|
114
|
+
expect(output).toBe('per-request');
|
|
115
|
+
expect(perRequest).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('still refuses binary retrieval when the default is a remote executor', async () => {
|
|
119
|
+
// runBinary's guard asks the same question ("is a text-only forward
|
|
120
|
+
// installed?") and must read the same answer, or a hosted bin would fall
|
|
121
|
+
// through to spawning for bytes while every text call went remote.
|
|
122
|
+
vi.stubEnv('GOG_PATH', '/nonexistent/gog');
|
|
123
|
+
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
124
|
+
|
|
125
|
+
await expect(runExecutor.exit(() => runBinary(['drive', 'read']))).rejects.toThrow(/text-only/);
|
|
126
|
+
});
|
|
56
127
|
});
|
|
57
128
|
|
|
58
129
|
/**
|