gogcli-mcp 4.1.0 → 4.2.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 +73 -12
- package/dist/lib.js +37 -3
- package/manifest.json +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/src/runner.ts +29 -3
- package/tests/runner.test.ts +72 -0
|
@@ -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": "4.
|
|
10
|
+
"version": "4.2.0"
|
|
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": "4.
|
|
18
|
+
"version": "4.2.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,32 @@ var __export = (target, all) => {
|
|
|
5
5
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/cancel/index.js
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
var storage = new AsyncLocalStorage();
|
|
11
|
+
function withCallSignal(signal, fn) {
|
|
12
|
+
return signal ? storage.run({ signal }, fn) : fn();
|
|
13
|
+
}
|
|
14
|
+
function currentCallSignal() {
|
|
15
|
+
return storage.getStore()?.signal;
|
|
16
|
+
}
|
|
17
|
+
function killOnCancel(child, signal = "SIGTERM") {
|
|
18
|
+
const ambient = currentCallSignal();
|
|
19
|
+
if (!ambient)
|
|
20
|
+
return () => {
|
|
21
|
+
};
|
|
22
|
+
if (ambient.aborted) {
|
|
23
|
+
child.kill(signal);
|
|
24
|
+
return () => {
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const onAbort = () => {
|
|
28
|
+
child.kill(signal);
|
|
29
|
+
};
|
|
30
|
+
ambient.addEventListener("abort", onAbort, { once: true });
|
|
31
|
+
return () => ambient.removeEventListener("abort", onAbort);
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
// ../../node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
9
35
|
var __create = Object.create;
|
|
10
36
|
var __defProp2 = Object.defineProperty;
|
|
@@ -34526,17 +34552,41 @@ Hint: ${err.hint}`);
|
|
|
34526
34552
|
}
|
|
34527
34553
|
throw err;
|
|
34528
34554
|
}
|
|
34555
|
+
function callSignalFrom(args) {
|
|
34556
|
+
const ctx = args.at(-1);
|
|
34557
|
+
if (typeof ctx !== "object" || ctx === null)
|
|
34558
|
+
return void 0;
|
|
34559
|
+
const req = ctx.mcpReq;
|
|
34560
|
+
if (typeof req !== "object" || req === null)
|
|
34561
|
+
return void 0;
|
|
34562
|
+
const signal = req.signal;
|
|
34563
|
+
return signal instanceof AbortSignal ? signal : void 0;
|
|
34564
|
+
}
|
|
34529
34565
|
function surfaceToolHints(server) {
|
|
34530
34566
|
const register2 = server.registerTool.bind(server);
|
|
34531
|
-
server.registerTool = (name, config2, cb) => register2(name, config2, (...args) =>
|
|
34532
|
-
|
|
34533
|
-
|
|
34534
|
-
|
|
34535
|
-
|
|
34536
|
-
|
|
34537
|
-
|
|
34538
|
-
|
|
34539
|
-
|
|
34567
|
+
server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
|
|
34568
|
+
// THE CALLER'S CANCELLATION, made ambient for the whole handler
|
|
34569
|
+
// (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
|
|
34570
|
+
// measured against @modelcontextprotocol/server 2.0.0, a cancelled
|
|
34571
|
+
// call aborts `ctx.mcpReq.signal` with the caller's reason and the
|
|
34572
|
+
// handler runs to completion regardless — so the HTTP request stays
|
|
34573
|
+
// in flight, the child keeps burning metered CPU, and the upstream
|
|
34574
|
+
// keeps being hit for somebody who has gone. claude.ai sent 101
|
|
34575
|
+
// cancellations in the week to 2026-09-20.
|
|
34576
|
+
//
|
|
34577
|
+
// Here because this is the one wrapper every tool already passes
|
|
34578
|
+
// through: threading the signal by hand would mean editing several
|
|
34579
|
+
// hundred handlers and missing exactly the ones nobody edits.
|
|
34580
|
+
withCallSignal(callSignalFrom(args), () => {
|
|
34581
|
+
let result;
|
|
34582
|
+
try {
|
|
34583
|
+
result = cb(...args);
|
|
34584
|
+
} catch (err) {
|
|
34585
|
+
return hintResultOrRethrow(err);
|
|
34586
|
+
}
|
|
34587
|
+
return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
|
|
34588
|
+
})
|
|
34589
|
+
));
|
|
34540
34590
|
}
|
|
34541
34591
|
async function createMcpServer(opts) {
|
|
34542
34592
|
const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
|
|
@@ -34971,8 +35021,15 @@ async function spawnGog(fullArgs, opts) {
|
|
|
34971
35021
|
const stdoutChunks = [];
|
|
34972
35022
|
const stderrChunks = [];
|
|
34973
35023
|
let settled = false;
|
|
35024
|
+
const cancelled = currentCallSignal();
|
|
35025
|
+
const stopWatchingCancel = killOnCancel(child);
|
|
35026
|
+
const stopWatching = () => {
|
|
35027
|
+
clearTimeout(timer);
|
|
35028
|
+
stopWatchingCancel();
|
|
35029
|
+
};
|
|
34974
35030
|
const timer = setTimeout(() => {
|
|
34975
35031
|
settled = true;
|
|
35032
|
+
stopWatchingCancel();
|
|
34976
35033
|
child.kill();
|
|
34977
35034
|
reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
|
|
34978
35035
|
}, effectiveTimeout);
|
|
@@ -34983,9 +35040,13 @@ async function spawnGog(fullArgs, opts) {
|
|
|
34983
35040
|
stderrChunks.push(chunk);
|
|
34984
35041
|
});
|
|
34985
35042
|
child.on("close", (code) => {
|
|
34986
|
-
|
|
35043
|
+
stopWatching();
|
|
34987
35044
|
if (settled) return;
|
|
34988
35045
|
settled = true;
|
|
35046
|
+
if (cancelled?.aborted) {
|
|
35047
|
+
reject(cancelled.reason instanceof Error ? cancelled.reason : new Error("the caller cancelled this call"));
|
|
35048
|
+
return;
|
|
35049
|
+
}
|
|
34989
35050
|
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
34990
35051
|
if (code === 0) {
|
|
34991
35052
|
if (binary) {
|
|
@@ -35003,7 +35064,7 @@ async function spawnGog(fullArgs, opts) {
|
|
|
35003
35064
|
}
|
|
35004
35065
|
});
|
|
35005
35066
|
child.on("error", (err) => {
|
|
35006
|
-
|
|
35067
|
+
stopWatching();
|
|
35007
35068
|
if (settled) return;
|
|
35008
35069
|
settled = true;
|
|
35009
35070
|
if (err.code === "ENOENT") {
|
|
@@ -37521,7 +37582,7 @@ function registerTasksTools(server) {
|
|
|
37521
37582
|
}
|
|
37522
37583
|
|
|
37523
37584
|
// src/server.ts
|
|
37524
|
-
var VERSION = true ? "4.
|
|
37585
|
+
var VERSION = true ? "4.2.0" : "0.0.0";
|
|
37525
37586
|
var BASE_TOOL_REGISTRARS = [
|
|
37526
37587
|
registerApiTools,
|
|
37527
37588
|
registerAppScriptTools,
|
package/dist/lib.js
CHANGED
|
@@ -19672,6 +19672,29 @@ function date4(params) {
|
|
|
19672
19672
|
return _coercedDate(ZodDate, params);
|
|
19673
19673
|
}
|
|
19674
19674
|
|
|
19675
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/cancel/index.js
|
|
19676
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
19677
|
+
var storage = new AsyncLocalStorage();
|
|
19678
|
+
function currentCallSignal() {
|
|
19679
|
+
return storage.getStore()?.signal;
|
|
19680
|
+
}
|
|
19681
|
+
function killOnCancel(child, signal = "SIGTERM") {
|
|
19682
|
+
const ambient = currentCallSignal();
|
|
19683
|
+
if (!ambient)
|
|
19684
|
+
return () => {
|
|
19685
|
+
};
|
|
19686
|
+
if (ambient.aborted) {
|
|
19687
|
+
child.kill(signal);
|
|
19688
|
+
return () => {
|
|
19689
|
+
};
|
|
19690
|
+
}
|
|
19691
|
+
const onAbort = () => {
|
|
19692
|
+
child.kill(signal);
|
|
19693
|
+
};
|
|
19694
|
+
ambient.addEventListener("abort", onAbort, { once: true });
|
|
19695
|
+
return () => ambient.removeEventListener("abort", onAbort);
|
|
19696
|
+
}
|
|
19697
|
+
|
|
19675
19698
|
// ../../node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
19676
19699
|
var __create = Object.create;
|
|
19677
19700
|
var __defProp2 = Object.defineProperty;
|
|
@@ -33364,8 +33387,15 @@ async function spawnGog(fullArgs, opts) {
|
|
|
33364
33387
|
const stdoutChunks = [];
|
|
33365
33388
|
const stderrChunks = [];
|
|
33366
33389
|
let settled = false;
|
|
33390
|
+
const cancelled = currentCallSignal();
|
|
33391
|
+
const stopWatchingCancel = killOnCancel(child);
|
|
33392
|
+
const stopWatching = () => {
|
|
33393
|
+
clearTimeout(timer);
|
|
33394
|
+
stopWatchingCancel();
|
|
33395
|
+
};
|
|
33367
33396
|
const timer = setTimeout(() => {
|
|
33368
33397
|
settled = true;
|
|
33398
|
+
stopWatchingCancel();
|
|
33369
33399
|
child.kill();
|
|
33370
33400
|
reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
|
|
33371
33401
|
}, effectiveTimeout);
|
|
@@ -33376,9 +33406,13 @@ async function spawnGog(fullArgs, opts) {
|
|
|
33376
33406
|
stderrChunks.push(chunk);
|
|
33377
33407
|
});
|
|
33378
33408
|
child.on("close", (code) => {
|
|
33379
|
-
|
|
33409
|
+
stopWatching();
|
|
33380
33410
|
if (settled) return;
|
|
33381
33411
|
settled = true;
|
|
33412
|
+
if (cancelled?.aborted) {
|
|
33413
|
+
reject(cancelled.reason instanceof Error ? cancelled.reason : new Error("the caller cancelled this call"));
|
|
33414
|
+
return;
|
|
33415
|
+
}
|
|
33382
33416
|
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
33383
33417
|
if (code === 0) {
|
|
33384
33418
|
if (binary) {
|
|
@@ -33396,7 +33430,7 @@ async function spawnGog(fullArgs, opts) {
|
|
|
33396
33430
|
}
|
|
33397
33431
|
});
|
|
33398
33432
|
child.on("error", (err) => {
|
|
33399
|
-
|
|
33433
|
+
stopWatching();
|
|
33400
33434
|
if (settled) return;
|
|
33401
33435
|
settled = true;
|
|
33402
33436
|
if (err.code === "ENOENT") {
|
|
@@ -35917,7 +35951,7 @@ function registerTasksTools(server) {
|
|
|
35917
35951
|
}
|
|
35918
35952
|
|
|
35919
35953
|
// src/server.ts
|
|
35920
|
-
var VERSION = true ? "4.
|
|
35954
|
+
var VERSION = true ? "4.2.0" : "0.0.0";
|
|
35921
35955
|
var BASE_TOOL_REGISTRARS = [
|
|
35922
35956
|
registerApiTools,
|
|
35923
35957
|
registerAppScriptTools,
|
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": "4.
|
|
6
|
+
"version": "4.2.0",
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp",
|
|
5
5
|
"description": "MCP server wrapping gogcli for Google service access",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"test:coverage": "vitest run --coverage"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@chrischall/mcp-utils": "^
|
|
44
|
+
"@chrischall/mcp-utils": "^2.0.0",
|
|
45
45
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
46
46
|
"zod": "^4.6.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^26.
|
|
49
|
+
"@types/node": "^26.6.1",
|
|
50
50
|
"@vitest/coverage-v8": "^5.0.1",
|
|
51
51
|
"esbuild": "^0.28.2",
|
|
52
52
|
"typescript": "^7.0.2",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp"
|
|
9
9
|
},
|
|
10
|
-
"version": "4.
|
|
10
|
+
"version": "4.2.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "4.
|
|
15
|
+
"version": "4.2.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChildProcess } from 'node:child_process';
|
|
2
2
|
import { delimiter, join } from 'node:path';
|
|
3
|
-
import { parseBoolEnv, readEnvVar, redactSecrets as redactSharedSecrets } from '@chrischall/mcp-utils';
|
|
3
|
+
import { currentCallSignal, killOnCancel, parseBoolEnv, readEnvVar, redactSecrets as redactSharedSecrets } from '@chrischall/mcp-utils';
|
|
4
4
|
import { naiveSourceTimeZone } from './timestamps.js';
|
|
5
5
|
|
|
6
6
|
export type Spawner = (
|
|
@@ -380,8 +380,26 @@ async function spawnGog(
|
|
|
380
380
|
const stderrChunks: Buffer[] = [];
|
|
381
381
|
let settled = false;
|
|
382
382
|
|
|
383
|
+
// THE CALLER GIVING UP KILLS THE CHILD (mcp-utils `cancel`). Until this,
|
|
384
|
+
// a cancelled tool call left a whole `gog` process running to the
|
|
385
|
+
// timeout below — still talking to Google, still charged to the CPU a
|
|
386
|
+
// hosted child is metered on, for somebody who has gone. Measured on the
|
|
387
|
+
// mcp-host fleet: claude.ai sent 101 cancellations in the week to
|
|
388
|
+
// 2026-09-20, and every one of them was ignored here.
|
|
389
|
+
//
|
|
390
|
+
// The signal is ambient rather than passed: `surfaceToolHints` puts it
|
|
391
|
+
// in scope for the whole handler, so this works for every tool in every
|
|
392
|
+
// package without one of them threading it through.
|
|
393
|
+
const cancelled = currentCallSignal();
|
|
394
|
+
const stopWatchingCancel = killOnCancel(child);
|
|
395
|
+
const stopWatching = (): void => {
|
|
396
|
+
clearTimeout(timer);
|
|
397
|
+
stopWatchingCancel();
|
|
398
|
+
};
|
|
399
|
+
|
|
383
400
|
const timer = setTimeout(() => {
|
|
384
401
|
settled = true;
|
|
402
|
+
stopWatchingCancel();
|
|
385
403
|
child.kill();
|
|
386
404
|
reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
|
|
387
405
|
}, effectiveTimeout);
|
|
@@ -390,9 +408,17 @@ async function spawnGog(
|
|
|
390
408
|
child.stderr!.on('data', (chunk: Buffer) => { stderrChunks.push(chunk); });
|
|
391
409
|
|
|
392
410
|
child.on('close', (code: number | null) => {
|
|
393
|
-
|
|
411
|
+
stopWatching();
|
|
394
412
|
if (settled) return;
|
|
395
413
|
settled = true;
|
|
414
|
+
// A child WE killed because the caller left exits with no code and no
|
|
415
|
+
// stderr, which the branch below would report as `gog exited with code
|
|
416
|
+
// null` — a sentence about gogcli for something gogcli did not do. The
|
|
417
|
+
// caller's own reason is the honest error.
|
|
418
|
+
if (cancelled?.aborted) {
|
|
419
|
+
reject(cancelled.reason instanceof Error ? cancelled.reason : new Error('the caller cancelled this call'));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
396
422
|
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
397
423
|
if (code === 0) {
|
|
398
424
|
// Binary mode: return the raw stdout bytes base64-encoded, never a utf8
|
|
@@ -413,7 +439,7 @@ async function spawnGog(
|
|
|
413
439
|
});
|
|
414
440
|
|
|
415
441
|
child.on('error', (err: Error) => {
|
|
416
|
-
|
|
442
|
+
stopWatching();
|
|
417
443
|
if (settled) return;
|
|
418
444
|
settled = true;
|
|
419
445
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
package/tests/runner.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { spawn as mockedSpawn } from 'node:child_process';
|
|
4
|
+
import { withCallSignal } from '@chrischall/mcp-utils';
|
|
4
5
|
import { run, runBinary } from '../src/runner.js';
|
|
5
6
|
import type { Spawner } from '../src/runner.js';
|
|
6
7
|
|
|
@@ -949,3 +950,74 @@ describe('run executor', () => {
|
|
|
949
950
|
);
|
|
950
951
|
});
|
|
951
952
|
});
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* The caller giving up kills the `gog` child (mcp-utils `cancel`).
|
|
956
|
+
*
|
|
957
|
+
* Until this, a cancelled tool call left a whole process running to the
|
|
958
|
+
* runner's own timeout — still talking to Google, still charged to the CPU a
|
|
959
|
+
* hosted child is metered on, for somebody who has gone. Measured on the
|
|
960
|
+
* mcp-host fleet: claude.ai sent 101 cancellations in the week to
|
|
961
|
+
* 2026-09-20, and every one of them was ignored here.
|
|
962
|
+
*/
|
|
963
|
+
describe('cancellation', () => {
|
|
964
|
+
/** A child that never finishes on its own — only a kill ends it. */
|
|
965
|
+
function hangingProc(): ReturnType<Spawner> & { killed: unknown[] } {
|
|
966
|
+
const proc = new EventEmitter() as ReturnType<Spawner> & { killed: unknown[] };
|
|
967
|
+
const io = proc as unknown as { stdout: EventEmitter; stderr: EventEmitter };
|
|
968
|
+
io.stdout = new EventEmitter();
|
|
969
|
+
io.stderr = new EventEmitter();
|
|
970
|
+
proc.killed = [];
|
|
971
|
+
proc.kill = vi.fn((sig?: unknown) => {
|
|
972
|
+
proc.killed.push(sig);
|
|
973
|
+
// A killed child closes with no code and no stderr, which is exactly
|
|
974
|
+
// the shape the error path has to tell apart from a gog failure.
|
|
975
|
+
setTimeout(() => proc.emit('close', null), 0);
|
|
976
|
+
return true;
|
|
977
|
+
}) as unknown as ReturnType<Spawner>['kill'];
|
|
978
|
+
return proc;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
it('kills the child and rejects with the caller’s own reason', async () => {
|
|
982
|
+
const proc = hangingProc();
|
|
983
|
+
const controller = new AbortController();
|
|
984
|
+
const reason = new Error('caller went away');
|
|
985
|
+
|
|
986
|
+
const call = withCallSignal(controller.signal, () =>
|
|
987
|
+
run(['sheets', 'get', 'id1', 'A1'], { spawner: (() => proc) as unknown as Spawner }),
|
|
988
|
+
);
|
|
989
|
+
controller.abort(reason);
|
|
990
|
+
|
|
991
|
+
// Not `gog exited with code null`, which is what the ordinary close path
|
|
992
|
+
// would have said — a sentence about gogcli for something gogcli did not
|
|
993
|
+
// do.
|
|
994
|
+
await expect(call).rejects.toThrow(reason);
|
|
995
|
+
expect(proc.killed, 'the gog child was left running').toEqual(['SIGTERM']);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('names the cancellation when the reason is not an Error', async () => {
|
|
999
|
+
// `AbortController.abort()` takes ANY value — a string, a DOMException,
|
|
1000
|
+
// undefined — so the reason is not guaranteed to be throwable. Rejecting
|
|
1001
|
+
// with a raw string would surface as an error with no message at the
|
|
1002
|
+
// tool boundary, which is the shape this whole change exists to avoid.
|
|
1003
|
+
const proc = hangingProc();
|
|
1004
|
+
const controller = new AbortController();
|
|
1005
|
+
|
|
1006
|
+
const call = withCallSignal(controller.signal, () =>
|
|
1007
|
+
run(['sheets', 'get', 'id1', 'A1'], { spawner: (() => proc) as unknown as Spawner }),
|
|
1008
|
+
);
|
|
1009
|
+
controller.abort('the caller went away');
|
|
1010
|
+
|
|
1011
|
+
await expect(call).rejects.toThrow(/cancelled/i);
|
|
1012
|
+
expect(proc.killed).toEqual(['SIGTERM']);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it('leaves an ordinary call alone, and stops listening when it finishes', async () => {
|
|
1016
|
+
const controller = new AbortController();
|
|
1017
|
+
const spawner = makeSpawner(0, '{"ok":true}');
|
|
1018
|
+
await withCallSignal(controller.signal, () => run(['sheets', 'get', 'id1', 'A1'], { spawner }));
|
|
1019
|
+
// Aborting AFTER the call settled must reach nothing: a listener that
|
|
1020
|
+
// outlives its call is a leak per invocation.
|
|
1021
|
+
expect(() => controller.abort(new Error('too late'))).not.toThrow();
|
|
1022
|
+
});
|
|
1023
|
+
});
|