drawio-mcp-server 2.1.0 → 2.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.
@@ -224,94 +224,6 @@ var DrawMcp = (() => {
224
224
  );
225
225
  }
226
226
  }
227
- function import_mermaid(ui2, options) {
228
- const opts = options;
229
- const mermaidSource = opts.mermaid_source;
230
- const mode = opts.mode ?? "native";
231
- const insertMode = opts.insert_mode ?? "add";
232
- if (!mermaidSource || typeof mermaidSource !== "string") {
233
- return Promise.resolve({
234
- success: false,
235
- message: "mermaid_source must be a non-empty string"
236
- });
237
- }
238
- if (typeof ui2?.parseMermaidDiagram !== "function") {
239
- return Promise.resolve({
240
- success: false,
241
- message: "ui.parseMermaidDiagram is not available; this Draw.io build does not expose Mermaid support."
242
- });
243
- }
244
- const enableParser = mode === "native";
245
- return new Promise((resolve) => {
246
- let settled = false;
247
- const settle = (result) => {
248
- if (settled) {
249
- return;
250
- }
251
- settled = true;
252
- resolve(result);
253
- };
254
- try {
255
- ui2.parseMermaidDiagram(
256
- mermaidSource,
257
- void 0,
258
- (xml) => {
259
- if (!xml || typeof xml !== "string") {
260
- settle({
261
- success: false,
262
- message: "Mermaid parser returned empty XML"
263
- });
264
- return;
265
- }
266
- try {
267
- const importResult = import_diagram(ui2, {
268
- data: xml,
269
- format: "xml",
270
- mode: insertMode
271
- });
272
- if (!importResult.success) {
273
- settle({
274
- success: false,
275
- message: `Mermaid converted, but inserting into the diagram failed: ${importResult.message}`
276
- });
277
- return;
278
- }
279
- settle({
280
- success: true,
281
- mode,
282
- message: importResult.message,
283
- cells: importResult.cells,
284
- xml
285
- });
286
- } catch (err) {
287
- settle({
288
- success: false,
289
- message: `Insert after Mermaid conversion failed: ${err instanceof Error ? err.message : String(err)}`
290
- });
291
- }
292
- },
293
- (err) => {
294
- settle({
295
- success: false,
296
- message: `Mermaid render failed: ${err?.message ?? String(err)}`
297
- });
298
- },
299
- (err) => {
300
- settle({
301
- success: false,
302
- message: `Mermaid parse error: ${err?.message ?? String(err)}`
303
- });
304
- },
305
- enableParser
306
- );
307
- } catch (err) {
308
- settle({
309
- success: false,
310
- message: `parseMermaidDiagram threw: ${err instanceof Error ? err.message : String(err)}`
311
- });
312
- }
313
- });
314
- }
315
227
  function transform_NamedNodeMap_to_record(attributes) {
316
228
  const tstr = Object.prototype.toString.call(attributes);
317
229
  if (tstr !== "[object NamedNodeMap]") {
@@ -2304,6 +2216,271 @@ var DrawMcp = (() => {
2304
2216
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "default";
2305
2217
  }
2306
2218
 
2219
+ // ../drawio-mcp-compat/dist/index.js
2220
+ var SEMVER_HEAD = /^(\d+)\.(\d+)\.(\d+)/;
2221
+ function parseVersion(raw) {
2222
+ const m = SEMVER_HEAD.exec(raw);
2223
+ if (!m)
2224
+ return null;
2225
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
2226
+ }
2227
+ function compareVersion(a, b) {
2228
+ for (let i = 0; i < 3; i++) {
2229
+ if (a[i] < b[i])
2230
+ return -1;
2231
+ if (a[i] > b[i])
2232
+ return 1;
2233
+ }
2234
+ return 0;
2235
+ }
2236
+ function isBelowFloor(v, floor) {
2237
+ const parsed = parseVersion(floor);
2238
+ if (!parsed)
2239
+ return false;
2240
+ return compareVersion(v, parsed) < 0;
2241
+ }
2242
+ function isInRange(v, r) {
2243
+ const min = parseVersion(r.min);
2244
+ if (!min)
2245
+ return false;
2246
+ if (compareVersion(v, min) < 0)
2247
+ return false;
2248
+ if (r.maxExclusive === null)
2249
+ return true;
2250
+ const max = parseVersion(r.maxExclusive);
2251
+ if (!max)
2252
+ return true;
2253
+ return compareVersion(v, max) < 0;
2254
+ }
2255
+
2256
+ // src/drawio-compat/detect.ts
2257
+ function detectDrawioVersion(ui2) {
2258
+ const raw = globalThis.EditorUi?.VERSION ?? ui2?.constructor?.VERSION;
2259
+ if (typeof raw !== "string") {
2260
+ return { ok: false, reason: "missing", raw: null };
2261
+ }
2262
+ const semver = parseVersion(raw);
2263
+ if (!semver) return { ok: false, reason: "unparseable", raw };
2264
+ return { ok: true, raw, semver };
2265
+ }
2266
+ var cached = null;
2267
+ function getDetectedDrawioVersion(ui2) {
2268
+ if (cached !== null) return cached;
2269
+ const result = detectDrawioVersion(ui2);
2270
+ if (result.ok) cached = result;
2271
+ return result;
2272
+ }
2273
+
2274
+ // src/drawio-compat/dispatch.ts
2275
+ function dispatchTool(toolName, detected, matrix) {
2276
+ const entries = matrix.versionedTools[toolName];
2277
+ if (!entries || entries.length === 0) return null;
2278
+ if (!detected.ok) {
2279
+ return { kind: "no-version", reason: detected.reason };
2280
+ }
2281
+ if (isBelowFloor(detected.semver, matrix.supportedFloor)) {
2282
+ return {
2283
+ kind: "below-floor",
2284
+ floor: matrix.supportedFloor,
2285
+ detected: detected.raw
2286
+ };
2287
+ }
2288
+ for (const entry of entries) {
2289
+ if (isInRange(detected.semver, entry.range)) {
2290
+ return { kind: "matched", impl: entry.impl };
2291
+ }
2292
+ }
2293
+ const sorted = [...entries].sort((a, b) => {
2294
+ const am = parseVersion(a.range.min);
2295
+ const bm = parseVersion(b.range.min);
2296
+ return compareVersion(am, bm);
2297
+ });
2298
+ return {
2299
+ kind: "above-window",
2300
+ lastRangeMin: sorted.at(-1).range.min,
2301
+ detected: detected.raw
2302
+ };
2303
+ }
2304
+
2305
+ // src/tools/import-mermaid/shared.ts
2306
+ function validateOptions(options) {
2307
+ const opts = options;
2308
+ const source = opts.mermaid_source;
2309
+ if (!source || typeof source !== "string") {
2310
+ return {
2311
+ success: false,
2312
+ message: "mermaid_source must be a non-empty string"
2313
+ };
2314
+ }
2315
+ return {
2316
+ source,
2317
+ mode: opts.mode ?? "native",
2318
+ insertMode: opts.insert_mode ?? "add"
2319
+ };
2320
+ }
2321
+ function runInsertFlow(ui2, mode, insertMode, resolve) {
2322
+ let settled = false;
2323
+ const settle = (result) => {
2324
+ if (settled) return;
2325
+ settled = true;
2326
+ resolve(result);
2327
+ };
2328
+ const onXml = (xml) => {
2329
+ if (!xml || typeof xml !== "string") {
2330
+ settle({ success: false, message: "Mermaid parser returned empty XML" });
2331
+ return;
2332
+ }
2333
+ try {
2334
+ const importResult = import_diagram(ui2, {
2335
+ data: xml,
2336
+ format: "xml",
2337
+ mode: insertMode
2338
+ });
2339
+ if (!importResult.success) {
2340
+ settle({
2341
+ success: false,
2342
+ message: `Mermaid converted, but inserting into the diagram failed: ${importResult.message}`
2343
+ });
2344
+ return;
2345
+ }
2346
+ settle({
2347
+ success: true,
2348
+ mode,
2349
+ message: importResult.message,
2350
+ cells: importResult.cells,
2351
+ xml
2352
+ });
2353
+ } catch (err) {
2354
+ settle({
2355
+ success: false,
2356
+ message: `Insert after Mermaid conversion failed: ${err instanceof Error ? err.message : String(err)}`
2357
+ });
2358
+ }
2359
+ };
2360
+ const onError = (err) => {
2361
+ settle({
2362
+ success: false,
2363
+ message: `Mermaid render failed: ${err?.message ?? String(err)}`
2364
+ });
2365
+ };
2366
+ return { onXml, onError };
2367
+ }
2368
+
2369
+ // src/tools/import-mermaid/v29.ts
2370
+ function import_mermaid(ui2, options) {
2371
+ const validated = validateOptions(options);
2372
+ if ("success" in validated) return Promise.resolve(validated);
2373
+ const { source, mode, insertMode } = validated;
2374
+ if (typeof ui2?.parseMermaidDiagram !== "function") {
2375
+ return Promise.resolve({
2376
+ success: false,
2377
+ message: "ui.parseMermaidDiagram is not available; this Draw.io build does not expose Mermaid support."
2378
+ });
2379
+ }
2380
+ const enableParser = mode === "native";
2381
+ return new Promise((resolve) => {
2382
+ const { onXml, onError } = runInsertFlow(ui2, mode, insertMode, resolve);
2383
+ try {
2384
+ ui2.parseMermaidDiagram(
2385
+ source,
2386
+ void 0,
2387
+ onXml,
2388
+ onError,
2389
+ onError,
2390
+ enableParser
2391
+ );
2392
+ } catch (err) {
2393
+ resolve({
2394
+ success: false,
2395
+ message: `parseMermaidDiagram threw: ${err instanceof Error ? err.message : String(err)}`
2396
+ });
2397
+ }
2398
+ });
2399
+ }
2400
+
2401
+ // src/tools/import-mermaid/v30.ts
2402
+ function import_mermaid2(ui2, options) {
2403
+ const validated = validateOptions(options);
2404
+ if ("success" in validated) return Promise.resolve(validated);
2405
+ const { source, mode, insertMode } = validated;
2406
+ if (typeof ui2?.parseMermaidDiagram !== "function") {
2407
+ return Promise.resolve({
2408
+ success: false,
2409
+ message: "ui.parseMermaidDiagram is not available; this Draw.io build does not expose Mermaid support."
2410
+ });
2411
+ }
2412
+ return new Promise((resolve) => {
2413
+ const { onXml, onError } = runInsertFlow(ui2, mode, insertMode, resolve);
2414
+ try {
2415
+ if (mode === "embed" && typeof ui2.parseMermaidImage === "function") {
2416
+ ui2.parseMermaidImage(source, onXml, onError);
2417
+ } else {
2418
+ ui2.parseMermaidDiagram(source, void 0, onXml, onError, onError);
2419
+ }
2420
+ } catch (err) {
2421
+ resolve({
2422
+ success: false,
2423
+ message: `Mermaid API threw: ${err instanceof Error ? err.message : String(err)}`
2424
+ });
2425
+ }
2426
+ });
2427
+ }
2428
+
2429
+ // src/drawio-compat/matrix.ts
2430
+ var COMPAT_MATRIX = {
2431
+ supportedFloor: "29.0.0",
2432
+ versionedTools: {
2433
+ "import-mermaid": [
2434
+ {
2435
+ range: { min: "29.0.0", maxExclusive: "30.0.0" },
2436
+ impl: import_mermaid
2437
+ },
2438
+ {
2439
+ range: { min: "30.0.0", maxExclusive: null },
2440
+ impl: import_mermaid2
2441
+ }
2442
+ ]
2443
+ }
2444
+ };
2445
+
2446
+ // src/tools/import-mermaid/index.ts
2447
+ var TOOL_NAME = "import-mermaid";
2448
+ function import_mermaid3(ui2, options) {
2449
+ const detected = getDetectedDrawioVersion(ui2);
2450
+ const outcome = dispatchTool(TOOL_NAME, detected, COMPAT_MATRIX);
2451
+ if (outcome === null) {
2452
+ return Promise.resolve({
2453
+ success: false,
2454
+ message: `import-mermaid has no matrix entry; refusing to guess.`
2455
+ });
2456
+ }
2457
+ switch (outcome.kind) {
2458
+ case "matched":
2459
+ return outcome.impl(ui2, options);
2460
+ case "above-window": {
2461
+ const entries = COMPAT_MATRIX.versionedTools[TOOL_NAME] ?? [];
2462
+ const fallback = entries.at(-1);
2463
+ if (!fallback) {
2464
+ return Promise.resolve({
2465
+ success: false,
2466
+ message: `no impl available for drawio v${outcome.detected}`
2467
+ });
2468
+ }
2469
+ return fallback.impl(ui2, options);
2470
+ }
2471
+ case "below-floor":
2472
+ return Promise.resolve({
2473
+ success: false,
2474
+ message: `drawio v${outcome.detected} predates supported floor v${outcome.floor}. Upgrade drawio.`
2475
+ });
2476
+ case "no-version":
2477
+ return Promise.resolve({
2478
+ success: false,
2479
+ message: `cannot detect drawio version (${outcome.reason}); pin a supported drawio build.`
2480
+ });
2481
+ }
2482
+ }
2483
+
2307
2484
  // src/tool-registry.ts
2308
2485
  var VISIBLE_PAGE_EXECUTION = {
2309
2486
  mode: "visible-page"
@@ -2583,7 +2760,7 @@ var DrawMcp = (() => {
2583
2760
  page_tool({
2584
2761
  name: "import-mermaid",
2585
2762
  params: /* @__PURE__ */ new Set(["mermaid_source", "mode", "insert_mode", "target_page"]),
2586
- handler: import_mermaid,
2763
+ handler: import_mermaid3,
2587
2764
  pageExecution: VISIBLE_PAGE_MUTATION_EXECUTION,
2588
2765
  skip: skip_page_execution_for_new_page_mermaid
2589
2766
  }),
@@ -2621,6 +2798,61 @@ var DrawMcp = (() => {
2621
2798
  })
2622
2799
  );
2623
2800
 
2801
+ // src/drawio-compat/report.ts
2802
+ function computeCompatReport(matrix = COMPAT_MATRIX) {
2803
+ const detected = getDetectedDrawioVersion();
2804
+ if (!detected.ok) {
2805
+ return {
2806
+ drawioVersion: detected.raw,
2807
+ state: "no-version",
2808
+ floor: matrix.supportedFloor,
2809
+ detail: detected.reason
2810
+ };
2811
+ }
2812
+ if (isBelowFloor(detected.semver, matrix.supportedFloor)) {
2813
+ return {
2814
+ drawioVersion: detected.raw,
2815
+ state: "below-floor",
2816
+ floor: matrix.supportedFloor
2817
+ };
2818
+ }
2819
+ const anyBounded = Object.values(matrix.versionedTools).some(
2820
+ (entries) => entries.at(-1)?.range.maxExclusive !== null
2821
+ );
2822
+ if (anyBounded) {
2823
+ const boundedFor = Object.entries(matrix.versionedTools).filter(
2824
+ ([, entries]) => entries.at(-1)?.range.maxExclusive !== null
2825
+ );
2826
+ for (const [, entries] of boundedFor) {
2827
+ const newest = entries.at(-1);
2828
+ const max = parseVersion(newest.range.maxExclusive);
2829
+ if (max) {
2830
+ if (detected.semver[0] > max[0] || detected.semver[0] === max[0] && detected.semver[1] > max[1] || detected.semver[0] === max[0] && detected.semver[1] === max[1] && detected.semver[2] >= max[2]) {
2831
+ return {
2832
+ drawioVersion: detected.raw,
2833
+ state: "above-window",
2834
+ floor: matrix.supportedFloor,
2835
+ detail: newest.range.min
2836
+ };
2837
+ }
2838
+ }
2839
+ }
2840
+ }
2841
+ return {
2842
+ drawioVersion: detected.raw,
2843
+ state: "ok",
2844
+ floor: matrix.supportedFloor
2845
+ };
2846
+ }
2847
+ function sendCompatReport(send, log = console.log.bind(console)) {
2848
+ const report = computeCompatReport();
2849
+ send({ __control: "compat-report", ...report });
2850
+ log(
2851
+ `[drawio-mcp] drawio version: ${report.drawioVersion ?? "unknown"} \u2014 compat state: ${report.state} (floor: ${report.floor})`
2852
+ );
2853
+ return report;
2854
+ }
2855
+
2624
2856
  // src/bootstrap.ts
2625
2857
  var SHAPE_EXTRACTION_MAX_ATTEMPTS = 10;
2626
2858
  var SHAPE_EXTRACTION_INTERVAL_MS = 1e3;
@@ -2822,6 +3054,7 @@ var DrawMcp = (() => {
2822
3054
  cleanups.push(() => clearInterval(intervalId));
2823
3055
  }
2824
3056
  }
3057
+ sendCompatReport(transport2.send.bind(transport2));
2825
3058
  return {
2826
3059
  syncDocumentState,
2827
3060
  dispose: () => {
@@ -1,31 +1,83 @@
1
1
  import { stripSchemaRecursively } from "./strip-schema.js";
2
2
  /**
3
- * Creates a wrapped version of McpServer.tool that automatically strips
4
- * `$schema` from the inputSchema after tool registration.
3
+ * Wraps an `McpServer` so that the `tools/list` response strips the
4
+ * `$schema` key from every tool's `inputSchema` and `outputSchema`.
5
5
  *
6
- * The `$schema` key injected by zodToJsonSchema causes Claude Code to silently
7
- * drop tools because the `$` character fails Anthropic's validation regex
8
- * `^[a-zA-Z0-9_.-]{1,64}$`.
6
+ * Why this is necessary:
9
7
  *
10
- * @param server - The McpServer instance to wrap
11
- * @returns The same server instance, but with a modified tool() method
8
+ * - The MCP SDK lazily serializes tool schemas: it stores the Zod object
9
+ * verbatim at registration time and only converts to JSON Schema
10
+ * (via `zodToJsonSchema` / `z4mini.toJSONSchema`) when a client issues
11
+ * `tools/list`. The conversion emits a top-level
12
+ * `"$schema": "http://json-schema.org/draft-07/schema#"` field.
13
+ *
14
+ * - Claude Code's MCP ingest layer can't tolerate the `$` character in
15
+ * schema keys (Anthropic's parameter-name regex is
16
+ * `^[a-zA-Z0-9_.-]{1,64}$`). When `$schema` is present, the entire
17
+ * `properties` object is dropped, leaving tools effectively schemaless.
18
+ * The user can see the tool name but the LLM has no way to know which
19
+ * arguments to send, so calls fail at runtime.
20
+ *
21
+ * - A previous attempt at this fix tried to strip `$schema` from the
22
+ * stored `_registeredTools[name].inputSchema` via `setTimeout(0)`.
23
+ * That was a no-op against modern SDKs because the stored value at
24
+ * that point is the raw Zod object (which doesn't have `$schema`).
25
+ * The `$schema` key is added by `toJsonSchemaCompat` at serialization
26
+ * time, not at registration time.
27
+ *
28
+ * The fix here intercepts the `tools/list` request handler that the SDK
29
+ * registers internally on the first `server.tool(...)` call, and wraps
30
+ * it so the response runs through `stripSchemaRecursively` before being
31
+ * returned to the client.
12
32
  */
13
33
  export function createServerWithSchemaStripping(server) {
14
- // Store original tool method
15
34
  const originalTool = server.tool.bind(server);
16
- // Override tool method
17
- server.tool = function tool(name, description, inputSchema, handler) {
18
- // Call original tool method
19
- const result = originalTool(name, description, inputSchema, handler);
20
- // Strip $schema from the registered tool's inputSchema
21
- // This must be done after registration
22
- setTimeout(() => {
23
- const registeredTool = server._registeredTools?.[name];
24
- if (registeredTool?.inputSchema) {
25
- registeredTool.inputSchema = stripSchemaRecursively(registeredTool.inputSchema);
26
- }
27
- }, 0);
35
+ let patched = false;
36
+ server.tool = function tool(...args) {
37
+ const result = originalTool(...args);
38
+ // The SDK lazily registers the tools/list handler on the first
39
+ // server.tool() call (via setToolRequestHandlers). After that call
40
+ // returns, the handler is in place and we can wrap it.
41
+ if (!patched) {
42
+ patched = true;
43
+ patchToolsListHandler(server);
44
+ }
28
45
  return result;
29
46
  };
30
47
  return server;
31
48
  }
49
+ function patchToolsListHandler(server) {
50
+ // The McpServer wraps an inner Server (the low-level protocol layer).
51
+ // Its `_requestHandlers` Map stores per-method handlers keyed by the
52
+ // method name literal (e.g. "tools/list").
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ const innerServer = server.server;
55
+ const handlers = innerServer?._requestHandlers;
56
+ if (!handlers) {
57
+ // SDK internals changed; fail closed (no-op) so we don't crash the
58
+ // server. The tools will still work but schemas will leak `$schema`.
59
+ return;
60
+ }
61
+ const original = handlers.get("tools/list");
62
+ if (!original) {
63
+ return;
64
+ }
65
+ handlers.set("tools/list", async (...callArgs) => {
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ const response = await original(...callArgs);
68
+ if (response && Array.isArray(response.tools)) {
69
+ response.tools = response.tools.map(stripToolSchemas);
70
+ }
71
+ return response;
72
+ });
73
+ }
74
+ function stripToolSchemas(tool) {
75
+ const next = { ...tool };
76
+ if (next.inputSchema) {
77
+ next.inputSchema = stripSchemaRecursively(next.inputSchema);
78
+ }
79
+ if (next.outputSchema) {
80
+ next.outputSchema = stripSchemaRecursively(next.outputSchema);
81
+ }
82
+ return next;
83
+ }
@@ -0,0 +1,149 @@
1
+ import { afterEach, describe, expect, it } from "@jest/globals";
2
+ import { spawn } from "node:child_process";
3
+ import { createServer } from "node:net";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import WebSocket from "ws";
7
+ async function getFreePort() {
8
+ return new Promise((resolve, reject) => {
9
+ const srv = createServer();
10
+ srv.once("error", reject);
11
+ srv.listen(0, "127.0.0.1", () => {
12
+ const addr = srv.address();
13
+ if (addr && typeof addr === "object") {
14
+ const port = addr.port;
15
+ srv.close(() => resolve(port));
16
+ }
17
+ else {
18
+ srv.close(() => reject(new Error("could not resolve free port")));
19
+ }
20
+ });
21
+ });
22
+ }
23
+ async function isPortFree(port, host = "127.0.0.1") {
24
+ return new Promise((resolve) => {
25
+ const srv = createServer();
26
+ srv.once("error", () => resolve(false));
27
+ srv.listen({ port, host }, () => {
28
+ srv.close(() => resolve(true));
29
+ });
30
+ });
31
+ }
32
+ async function waitForPortHeld(port, host, timeoutMs) {
33
+ const deadline = Date.now() + timeoutMs;
34
+ while (Date.now() < deadline) {
35
+ const free = await isPortFree(port, host);
36
+ if (!free)
37
+ return true;
38
+ await new Promise((r) => setTimeout(r, 50));
39
+ }
40
+ return false;
41
+ }
42
+ async function waitForExit(proc, timeoutMs) {
43
+ return new Promise((resolve) => {
44
+ const timer = setTimeout(() => resolve(null), timeoutMs);
45
+ proc.once("exit", (code) => {
46
+ clearTimeout(timer);
47
+ resolve(code ?? 0);
48
+ });
49
+ });
50
+ }
51
+ const here = dirname(fileURLToPath(import.meta.url));
52
+ const SERVER_BIN = join(here, "index.js");
53
+ const HOST = "127.0.0.1";
54
+ function spawnServer(extensionPort, httpPort, transport = "stdio") {
55
+ return spawn(process.execPath, [
56
+ SERVER_BIN,
57
+ "--transport",
58
+ transport,
59
+ "--extension-port",
60
+ String(extensionPort),
61
+ "--http-port",
62
+ String(httpPort),
63
+ "--host",
64
+ HOST,
65
+ ], { stdio: ["pipe", "pipe", "pipe"] });
66
+ }
67
+ describe("graceful shutdown releases WebSocket port", () => {
68
+ let proc;
69
+ afterEach(async () => {
70
+ if (proc && !proc.killed) {
71
+ proc.kill("SIGKILL");
72
+ await new Promise((r) => setTimeout(r, 100));
73
+ }
74
+ proc = undefined;
75
+ });
76
+ it("releases extension port after SIGTERM", async () => {
77
+ const extensionPort = await getFreePort();
78
+ const httpPort = await getFreePort();
79
+ proc = spawnServer(extensionPort, httpPort);
80
+ const portTaken = await waitForPortHeld(extensionPort, HOST, 5000);
81
+ expect(portTaken).toBe(true);
82
+ proc.kill("SIGTERM");
83
+ const exitCode = await waitForExit(proc, 5000);
84
+ expect(exitCode).not.toBeNull();
85
+ const free = await isPortFree(extensionPort, HOST);
86
+ expect(free).toBe(true);
87
+ }, 20000);
88
+ it("releases extension port after SIGINT", async () => {
89
+ const extensionPort = await getFreePort();
90
+ const httpPort = await getFreePort();
91
+ proc = spawnServer(extensionPort, httpPort);
92
+ const portTaken = await waitForPortHeld(extensionPort, HOST, 5000);
93
+ expect(portTaken).toBe(true);
94
+ proc.kill("SIGINT");
95
+ const exitCode = await waitForExit(proc, 5000);
96
+ expect(exitCode).not.toBeNull();
97
+ const free = await isPortFree(extensionPort, HOST);
98
+ expect(free).toBe(true);
99
+ }, 20000);
100
+ it("releases HTTP and extension ports after SIGTERM in http transport", async () => {
101
+ const extensionPort = await getFreePort();
102
+ const httpPort = await getFreePort();
103
+ proc = spawnServer(extensionPort, httpPort, "http");
104
+ const extTaken = await waitForPortHeld(extensionPort, HOST, 5000);
105
+ const httpTaken = await waitForPortHeld(httpPort, HOST, 5000);
106
+ expect(extTaken).toBe(true);
107
+ expect(httpTaken).toBe(true);
108
+ proc.kill("SIGTERM");
109
+ const exitCode = await waitForExit(proc, 5000);
110
+ expect(exitCode).not.toBeNull();
111
+ expect(await isPortFree(extensionPort, HOST)).toBe(true);
112
+ expect(await isPortFree(httpPort, HOST)).toBe(true);
113
+ }, 20000);
114
+ it("releases extension port after SIGINT with a live WebSocket client", async () => {
115
+ const extensionPort = await getFreePort();
116
+ const httpPort = await getFreePort();
117
+ proc = spawnServer(extensionPort, httpPort);
118
+ const portTaken = await waitForPortHeld(extensionPort, HOST, 5000);
119
+ expect(portTaken).toBe(true);
120
+ const ws = new WebSocket(`ws://${HOST}:${extensionPort}`);
121
+ await new Promise((resolve, reject) => {
122
+ ws.once("open", () => resolve());
123
+ ws.once("error", reject);
124
+ });
125
+ proc.kill("SIGINT");
126
+ const exitCode = await waitForExit(proc, 5000);
127
+ expect(exitCode).not.toBeNull();
128
+ try {
129
+ ws.terminate();
130
+ }
131
+ catch {
132
+ // ignore
133
+ }
134
+ const free = await isPortFree(extensionPort, HOST);
135
+ expect(free).toBe(true);
136
+ }, 20000);
137
+ it("releases extension port when stdio host closes stdin pipe", async () => {
138
+ const extensionPort = await getFreePort();
139
+ const httpPort = await getFreePort();
140
+ proc = spawnServer(extensionPort, httpPort);
141
+ const portTaken = await waitForPortHeld(extensionPort, HOST, 5000);
142
+ expect(portTaken).toBe(true);
143
+ proc.stdin.end();
144
+ const exitCode = await waitForExit(proc, 5000);
145
+ expect(exitCode).not.toBeNull();
146
+ const free = await isPortFree(extensionPort, HOST);
147
+ expect(free).toBe(true);
148
+ }, 20000);
149
+ });