@frontmcp/testing 1.8.5 → 1.8.7

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.
Files changed (39) hide show
  1. package/auth/mock-oauth-server.d.ts.map +1 -1
  2. package/auth/token-factory.d.ts +2 -2
  3. package/auth/token-factory.d.ts.map +1 -1
  4. package/client/mcp-test-client.d.ts +12 -1
  5. package/client/mcp-test-client.d.ts.map +1 -1
  6. package/esm/fixtures/index.mjs +545 -260
  7. package/esm/index.mjs +702 -383
  8. package/esm/package.json +6 -6
  9. package/esm/perf/index.mjs +465 -308
  10. package/expect.d.ts +12 -11
  11. package/expect.d.ts.map +1 -1
  12. package/fixtures/fixture-types.d.ts +22 -4
  13. package/fixtures/fixture-types.d.ts.map +1 -1
  14. package/fixtures/index.js +544 -252
  15. package/fixtures/test-fixture.d.ts +6 -5
  16. package/fixtures/test-fixture.d.ts.map +1 -1
  17. package/http-mock/http-mock.d.ts +5 -3
  18. package/http-mock/http-mock.d.ts.map +1 -1
  19. package/http-mock/http-mock.types.d.ts +17 -6
  20. package/http-mock/http-mock.types.d.ts.map +1 -1
  21. package/http-mock/index.d.ts +1 -1
  22. package/http-mock/index.d.ts.map +1 -1
  23. package/index.d.ts +1 -1
  24. package/index.d.ts.map +1 -1
  25. package/index.js +710 -384
  26. package/package.json +6 -6
  27. package/perf/baseline-store.d.ts +1 -1
  28. package/perf/baseline-store.d.ts.map +1 -1
  29. package/perf/index.js +468 -321
  30. package/perf/perf-test.d.ts.map +1 -1
  31. package/perf/report-generator.d.ts +1 -1
  32. package/perf/report-generator.d.ts.map +1 -1
  33. package/server/port-registry.d.ts +7 -1
  34. package/server/port-registry.d.ts.map +1 -1
  35. package/server/test-server.d.ts +10 -1
  36. package/server/test-server.d.ts.map +1 -1
  37. package/transport/error-utils.d.ts +15 -0
  38. package/transport/error-utils.d.ts.map +1 -0
  39. package/transport/streamable-http.transport.d.ts.map +1 -1
@@ -5,16 +5,6 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
- // libs/testing/src/fixtures/test-fixture.ts
9
- import {
10
- afterAll as _afterAll,
11
- afterEach as _afterEach,
12
- beforeAll as _beforeAll,
13
- beforeEach as _beforeEach,
14
- describe as _describe,
15
- it as _it
16
- } from "@jest/globals";
17
-
18
8
  // libs/testing/src/auth/token-factory.ts
19
9
  import { exportJWK, generateKeyPair, SignJWT } from "jose";
20
10
  var TestTokenFactory = class {
@@ -73,14 +63,15 @@ var TestTokenFactory = class {
73
63
  */
74
64
  async createTestToken(options) {
75
65
  await this.ensureKeys();
76
- const now = Math.floor(Date.now() / 1e3);
66
+ const nowMs = Date.now();
67
+ const iat = Math.floor(nowMs / 1e3);
77
68
  const exp = options.exp ?? 3600;
78
69
  const payload = {
79
70
  iss: options.iss ?? this.issuer,
80
71
  sub: options.sub,
81
72
  aud: options.aud ?? this.audience,
82
- iat: now,
83
- exp: now + exp,
73
+ iat,
74
+ exp: Math.ceil(nowMs / 1e3) + exp,
84
75
  scope: options.scopes?.join(" "),
85
76
  ...options.claims
86
77
  };
@@ -105,10 +96,10 @@ var TestTokenFactory = class {
105
96
  /**
106
97
  * Create a regular user token
107
98
  */
108
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
99
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
109
100
  return this.createTestToken({
110
101
  sub,
111
- scopes,
102
+ scopes: scopes2,
112
103
  claims: {
113
104
  email: "user@test.local",
114
105
  name: "Test User",
@@ -119,10 +110,11 @@ var TestTokenFactory = class {
119
110
  /**
120
111
  * Create an anonymous user token
121
112
  */
122
- async createAnonymousToken() {
113
+ async createAnonymousToken(expiresIn) {
123
114
  return this.createTestToken({
124
115
  sub: `anon:${Date.now()}`,
125
116
  scopes: ["anonymous"],
117
+ exp: expiresIn,
126
118
  claims: {
127
119
  name: "Anonymous",
128
120
  role: "anonymous"
@@ -471,6 +463,32 @@ function sleep(ms) {
471
463
  return new Promise((resolve) => setTimeout(resolve, ms));
472
464
  }
473
465
 
466
+ // libs/testing/src/transport/error-utils.ts
467
+ function errorName(err) {
468
+ if (typeof err !== "object" || err === null) return void 0;
469
+ const name = err.name;
470
+ return typeof name === "string" ? name : void 0;
471
+ }
472
+ function isAbortError(err) {
473
+ return errorName(err) === "AbortError";
474
+ }
475
+ function errorMessage(err, fallback = "Unknown error") {
476
+ if (typeof err === "string" && err) return err;
477
+ if (typeof err === "object" && err !== null) {
478
+ const message = err.message;
479
+ if (typeof message === "string" && message) return message;
480
+ }
481
+ return fallback;
482
+ }
483
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
484
+ function markInterceptedError(err) {
485
+ interceptedErrors.add(err);
486
+ return err;
487
+ }
488
+ function isInterceptedError(err) {
489
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
490
+ }
491
+
474
492
  // libs/testing/src/transport/streamable-http.transport.ts
475
493
  var DEFAULT_TIMEOUT = 3e4;
476
494
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
@@ -579,7 +597,7 @@ var StreamableHttpTransport = class {
579
597
  return mockResponse2;
580
598
  }
581
599
  case "error":
582
- throw interceptResult.error;
600
+ throw markInterceptedError(interceptResult.error);
583
601
  case "continue":
584
602
  message = interceptResult.request;
585
603
  break;
@@ -640,7 +658,7 @@ var StreamableHttpTransport = class {
640
658
  return jsonResponse;
641
659
  } catch (error) {
642
660
  clearTimeout(timeoutId);
643
- if (error instanceof Error && error.name === "AbortError") {
661
+ if (isAbortError(error)) {
644
662
  return {
645
663
  jsonrpc: "2.0",
646
664
  id: message.id ?? null,
@@ -679,7 +697,7 @@ var StreamableHttpTransport = class {
679
697
  }
680
698
  } catch (error) {
681
699
  clearTimeout(timeoutId);
682
- if (error instanceof Error && error.name !== "AbortError") {
700
+ if (!isAbortError(error)) {
683
701
  throw error;
684
702
  }
685
703
  }
@@ -720,7 +738,7 @@ var StreamableHttpTransport = class {
720
738
  error: {
721
739
  code: -32700,
722
740
  message: "Parse error",
723
- data: error instanceof Error ? error.message : "Unknown error"
741
+ data: errorMessage(error)
724
742
  }
725
743
  };
726
744
  }
@@ -1090,7 +1108,7 @@ var StreamableHttpTransport = class {
1090
1108
  if (this.config.clientInfo) {
1091
1109
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1092
1110
  }
1093
- if (this.authToken && !this.publicMode) {
1111
+ if (this.authToken) {
1094
1112
  headers["Authorization"] = `Bearer ${this.authToken}`;
1095
1113
  }
1096
1114
  if (this.sessionId) {
@@ -1413,6 +1431,16 @@ var DEFAULT_CLIENT_INFO = {
1413
1431
  name: "@frontmcp/testing",
1414
1432
  version: "0.4.0"
1415
1433
  };
1434
+ var MAX_LIST_PAGES = 1e3;
1435
+ function isAuthFailure(error) {
1436
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1437
+ }
1438
+ function describeInitializeError(error) {
1439
+ const message = error?.message ?? "Unknown error";
1440
+ const data = error?.data;
1441
+ const detail = typeof data === "string" ? data.trim() : "";
1442
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1443
+ }
1416
1444
  function extractEntryPaths(data) {
1417
1445
  let payload = data;
1418
1446
  if (typeof payload === "string") {
@@ -1499,19 +1527,22 @@ var McpTestClient = class {
1499
1527
  async connect() {
1500
1528
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1501
1529
  this.triedDiscoveredEntryPath = false;
1530
+ this.initResult = null;
1502
1531
  this.transport = this.createTransport();
1503
1532
  await this.transport.connect();
1504
1533
  const maxRetries = 3;
1505
1534
  const retryDelayMs = 500;
1506
1535
  let lastError;
1507
1536
  let reportedEntryPaths;
1537
+ let attemptsMade = 0;
1508
1538
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1539
+ attemptsMade = attempt;
1509
1540
  const initResponse = await this.initialize();
1510
1541
  if (initResponse.success && initResponse.data) {
1511
1542
  this.initResult = initResponse.data;
1512
1543
  break;
1513
1544
  }
1514
- lastError = initResponse.error?.message ?? "Unknown error";
1545
+ lastError = describeInitializeError(initResponse.error);
1515
1546
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1516
1547
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1517
1548
  this.triedDiscoveredEntryPath = true;
@@ -1523,6 +1554,7 @@ var McpTestClient = class {
1523
1554
  await this.transport.connect();
1524
1555
  continue;
1525
1556
  }
1557
+ if (isAuthFailure(initResponse.error)) break;
1526
1558
  if (attempt < maxRetries) {
1527
1559
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1528
1560
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1531,7 +1563,7 @@ var McpTestClient = class {
1531
1563
  if (!this.initResult) {
1532
1564
  const served = reportedEntryPaths?.length ? ` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.` : "";
1533
1565
  throw new Error(
1534
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1566
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1535
1567
  );
1536
1568
  }
1537
1569
  this._sessionId = this.transport.getSessionId();
@@ -1601,18 +1633,47 @@ var McpTestClient = class {
1601
1633
  return this._authState;
1602
1634
  }
1603
1635
  /**
1604
- * Authenticate with a token
1636
+ * Authenticate with a token.
1637
+ *
1638
+ * When connected, this opens a NEW session with the token (the previous session was created
1639
+ * for the previous identity) and rejects if the server refuses it — for example because the
1640
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
1641
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1605
1642
  */
1606
1643
  async authenticate(token) {
1644
+ if (typeof token !== "string" || token.trim() === "") {
1645
+ throw new Error("authenticate() requires a non-empty token");
1646
+ }
1647
+ const previousAuthState = this._authState;
1648
+ const previousConfigAuth = this.config.auth;
1649
+ this.config.auth = { ...previousConfigAuth, token };
1607
1650
  this._authState = {
1608
1651
  isAnonymous: false,
1609
1652
  token,
1610
1653
  scopes: this.parseScopesFromToken(token),
1611
1654
  user: this.parseUserFromToken(token)
1612
1655
  };
1613
- if (this.transport) {
1614
- this.transport.setAuthToken(token);
1656
+ const previousTransport = this.transport;
1657
+ if (!previousTransport) {
1658
+ this.log("debug", "Authentication updated (not connected yet)");
1659
+ return;
1615
1660
  }
1661
+ const previousInit = this.initResult;
1662
+ const previousSessionId = this._sessionId;
1663
+ const previousSessionInfo = this._sessionInfo;
1664
+ try {
1665
+ await this.connect();
1666
+ } catch (err) {
1667
+ await this.transport?.close().catch(() => void 0);
1668
+ this.transport = previousTransport;
1669
+ this.initResult = previousInit;
1670
+ this._sessionId = previousSessionId;
1671
+ this._sessionInfo = previousSessionInfo;
1672
+ this.config.auth = previousConfigAuth;
1673
+ this._authState = previousAuthState;
1674
+ throw err;
1675
+ }
1676
+ await previousTransport.close().catch(() => void 0);
1616
1677
  this.log("debug", "Authentication updated");
1617
1678
  }
1618
1679
  // ═══════════════════════════════════════════════════════════════════
@@ -1646,13 +1707,12 @@ var McpTestClient = class {
1646
1707
  /**
1647
1708
  * List all available tools
1648
1709
  */
1649
- list: async () => {
1650
- const response = await this.listTools();
1651
- if (!response.success || !response.data) {
1652
- throw new Error(`Failed to list tools: ${response.error?.message}`);
1653
- }
1654
- return response.data.tools;
1655
- },
1710
+ list: async () => this.listAllPages(
1711
+ "tools/list",
1712
+ "tools",
1713
+ (cursor) => this.listTools(cursor),
1714
+ (page) => page.tools
1715
+ ),
1656
1716
  /**
1657
1717
  * Call a tool by name with arguments
1658
1718
  */
@@ -1668,23 +1728,21 @@ var McpTestClient = class {
1668
1728
  /**
1669
1729
  * List all static resources
1670
1730
  */
1671
- list: async () => {
1672
- const response = await this.listResources();
1673
- if (!response.success || !response.data) {
1674
- throw new Error(`Failed to list resources: ${response.error?.message}`);
1675
- }
1676
- return response.data.resources;
1677
- },
1731
+ list: async () => this.listAllPages(
1732
+ "resources/list",
1733
+ "resources",
1734
+ (cursor) => this.listResources(cursor),
1735
+ (page) => page.resources
1736
+ ),
1678
1737
  /**
1679
1738
  * List all resource templates
1680
1739
  */
1681
- listTemplates: async () => {
1682
- const response = await this.listResourceTemplates();
1683
- if (!response.success || !response.data) {
1684
- throw new Error(`Failed to list resource templates: ${response.error?.message}`);
1685
- }
1686
- return response.data.resourceTemplates;
1687
- },
1740
+ listTemplates: async () => this.listAllPages(
1741
+ "resources/templates/list",
1742
+ "resource templates",
1743
+ (cursor) => this.listResourceTemplates(cursor),
1744
+ (page) => page.resourceTemplates
1745
+ ),
1688
1746
  /**
1689
1747
  * Read a resource by URI
1690
1748
  */
@@ -1712,13 +1770,12 @@ var McpTestClient = class {
1712
1770
  /**
1713
1771
  * List all available prompts
1714
1772
  */
1715
- list: async () => {
1716
- const response = await this.listPrompts();
1717
- if (!response.success || !response.data) {
1718
- throw new Error(`Failed to list prompts: ${response.error?.message}`);
1719
- }
1720
- return response.data.prompts;
1721
- },
1773
+ list: async () => this.listAllPages(
1774
+ "prompts/list",
1775
+ "prompts",
1776
+ (cursor) => this.listPrompts(cursor),
1777
+ (page) => page.prompts
1778
+ ),
1722
1779
  /**
1723
1780
  * Get a prompt with arguments
1724
1781
  */
@@ -2102,8 +2159,33 @@ var McpTestClient = class {
2102
2159
  clientInfo: this.config.clientInfo
2103
2160
  });
2104
2161
  }
2105
- async listTools() {
2106
- return this.request("tools/list", {});
2162
+ async listTools(cursor) {
2163
+ return this.request("tools/list", cursor ? { cursor } : {});
2164
+ }
2165
+ /**
2166
+ * Follow `nextCursor` until the server stops returning one, concatenating the
2167
+ * pages. A server that hands back a cursor it already returned, or never stops
2168
+ * paging, fails the call rather than looping.
2169
+ */
2170
+ async listAllPages(method, label, fetchPage, itemsOf) {
2171
+ const items = [];
2172
+ const seenCursors = /* @__PURE__ */ new Set();
2173
+ let cursor;
2174
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
2175
+ const response = await fetchPage(cursor);
2176
+ if (!response.success || !response.data) {
2177
+ throw new Error(`Failed to list ${label}: ${response.error?.message}`);
2178
+ }
2179
+ items.push(...itemsOf(response.data) ?? []);
2180
+ const next = response.data.nextCursor;
2181
+ if (!next) return items;
2182
+ if (seenCursors.has(next)) {
2183
+ throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`);
2184
+ }
2185
+ seenCursors.add(next);
2186
+ cursor = next;
2187
+ }
2188
+ throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`);
2107
2189
  }
2108
2190
  async callTool(name, args, progressToken) {
2109
2191
  return this.request("tools/call", {
@@ -2128,17 +2210,17 @@ var McpTestClient = class {
2128
2210
  timestamp
2129
2211
  });
2130
2212
  };
2131
- async listResources() {
2132
- return this.request("resources/list", {});
2213
+ async listResources(cursor) {
2214
+ return this.request("resources/list", cursor ? { cursor } : {});
2133
2215
  }
2134
- async listResourceTemplates() {
2135
- return this.request("resources/templates/list", {});
2216
+ async listResourceTemplates(cursor) {
2217
+ return this.request("resources/templates/list", cursor ? { cursor } : {});
2136
2218
  }
2137
2219
  async readResource(uri) {
2138
2220
  return this.request("resources/read", { uri });
2139
2221
  }
2140
- async listPrompts() {
2141
- return this.request("prompts/list", {});
2222
+ async listPrompts(cursor) {
2223
+ return this.request("prompts/list", cursor ? { cursor } : {});
2142
2224
  }
2143
2225
  async getPrompt(name, args) {
2144
2226
  return this.request("prompts/get", {
@@ -2215,10 +2297,11 @@ var McpTestClient = class {
2215
2297
  requestId: id
2216
2298
  };
2217
2299
  } catch (err) {
2300
+ if (isInterceptedError(err)) throw err;
2218
2301
  const durationMs = Date.now() - start;
2219
2302
  const error = {
2220
2303
  code: -32603,
2221
- message: err instanceof Error ? err.message : "Unknown error"
2304
+ message: errorMessage(err)
2222
2305
  };
2223
2306
  return {
2224
2307
  success: false,
@@ -2366,12 +2449,12 @@ var McpTestClient = class {
2366
2449
  const payload = this.decodeJwtPayload(token);
2367
2450
  if (!payload) return [];
2368
2451
  const scope = payload["scope"];
2369
- const scopes = payload["scopes"];
2452
+ const scopes2 = payload["scopes"];
2370
2453
  if (typeof scope === "string") {
2371
2454
  return scope.split(" ");
2372
2455
  }
2373
- if (Array.isArray(scopes)) {
2374
- return scopes;
2456
+ if (Array.isArray(scopes2)) {
2457
+ return scopes2;
2375
2458
  }
2376
2459
  return [];
2377
2460
  } catch {
@@ -2446,7 +2529,7 @@ var ProgressCollector = class {
2446
2529
  };
2447
2530
 
2448
2531
  // libs/testing/src/server/test-server.ts
2449
- import { spawn } from "child_process";
2532
+ import { execFile, spawn } from "child_process";
2450
2533
  import { sha256Hex } from "@frontmcp/utils";
2451
2534
 
2452
2535
  // libs/testing/src/errors/index.ts
@@ -2517,6 +2600,8 @@ var E2E_PORT_RANGES = {
2517
2600
  "demo-e2e-distributed": { start: 50440, size: 20 },
2518
2601
  // Protocol revision E2E tests (50460-50479)
2519
2602
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
2603
+ // Testing library E2E tests (50480-50499)
2604
+ "demo-e2e-testing": { start: 50480, size: 20 },
2520
2605
  // Mock servers and utilities (50900-50999)
2521
2606
  "mock-oauth": { start: 50900, size: 10 },
2522
2607
  "mock-api": { start: 50910, size: 10 },
@@ -2525,6 +2610,66 @@ var E2E_PORT_RANGES = {
2525
2610
  default: { start: 51e3, size: 100 }
2526
2611
  };
2527
2612
  var reservedPorts = /* @__PURE__ */ new Map();
2613
+ var lockedPorts = /* @__PURE__ */ new Set();
2614
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
2615
+ function lockDir() {
2616
+ try {
2617
+ const os = __require("node:os");
2618
+ const path = __require("node:path");
2619
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
2620
+ } catch {
2621
+ return void 0;
2622
+ }
2623
+ }
2624
+ function pidIsAlive(pid) {
2625
+ try {
2626
+ process.kill(pid, 0);
2627
+ return true;
2628
+ } catch (error) {
2629
+ return error.code === "EPERM";
2630
+ }
2631
+ }
2632
+ function acquirePortLock(port) {
2633
+ if (lockedPorts.has(port)) return false;
2634
+ let fs;
2635
+ let file;
2636
+ try {
2637
+ fs = __require("node:fs");
2638
+ const dir = lockDir();
2639
+ if (!dir) return true;
2640
+ fs.mkdirSync(dir, { recursive: true });
2641
+ file = __require("node:path").join(dir, `${port}.lock`);
2642
+ } catch {
2643
+ return true;
2644
+ }
2645
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
2646
+ for (let attempt = 0; attempt < 2; attempt++) {
2647
+ try {
2648
+ fs.writeFileSync(file, payload, { flag: "wx" });
2649
+ lockedPorts.add(port);
2650
+ return true;
2651
+ } catch (error) {
2652
+ if (error.code !== "EEXIST") return true;
2653
+ try {
2654
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
2655
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
2656
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
2657
+ fs.unlinkSync(file);
2658
+ } catch {
2659
+ }
2660
+ }
2661
+ }
2662
+ return false;
2663
+ }
2664
+ function releasePortLock(port) {
2665
+ if (!lockedPorts.delete(port)) return;
2666
+ try {
2667
+ const fs = __require("node:fs");
2668
+ const dir = lockDir();
2669
+ if (dir) fs.unlinkSync(__require("node:path").join(dir, `${port}.lock`));
2670
+ } catch {
2671
+ }
2672
+ }
2528
2673
  var projectPortIndex = /* @__PURE__ */ new Map();
2529
2674
  function getPortRange(project) {
2530
2675
  const key = project;
@@ -2535,15 +2680,22 @@ function getPortRange(project) {
2535
2680
  }
2536
2681
  async function reservePort(project, preferredPort) {
2537
2682
  const range = getPortRange(project);
2538
- if (preferredPort !== void 0) {
2683
+ const handle = (port) => ({
2684
+ port,
2685
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
2686
+ releaseSocket: async () => {
2687
+ await releasePort(port);
2688
+ },
2689
+ release: async () => {
2690
+ await releasePort(port);
2691
+ releasePortLock(port);
2692
+ }
2693
+ });
2694
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
2695
+ if (wantsSpecificPort) {
2539
2696
  const reservation = await tryReservePort(preferredPort, project);
2540
2697
  if (reservation) {
2541
- return {
2542
- port: preferredPort,
2543
- release: async () => {
2544
- await releasePort(preferredPort);
2545
- }
2546
- };
2698
+ return handle(preferredPort);
2547
2699
  }
2548
2700
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
2549
2701
  }
@@ -2552,30 +2704,19 @@ async function reservePort(project, preferredPort) {
2552
2704
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2553
2705
  const port = range.start + index % range.size;
2554
2706
  index = (index + 1) % range.size;
2555
- if (reservedPorts.has(port)) {
2707
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
2556
2708
  continue;
2557
2709
  }
2558
2710
  const reservation = await tryReservePort(port, project);
2559
2711
  if (reservation) {
2560
2712
  projectPortIndex.set(project, index);
2561
- return {
2562
- port,
2563
- release: async () => {
2564
- await releasePort(port);
2565
- }
2566
- };
2713
+ return handle(port);
2567
2714
  }
2568
2715
  }
2569
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
2570
- if (dynamicPort) {
2571
- const reservation = await tryReservePort(dynamicPort, project);
2572
- if (reservation) {
2573
- return {
2574
- port: dynamicPort,
2575
- release: async () => {
2576
- await releasePort(dynamicPort);
2577
- }
2578
- };
2716
+ for (let port = 51e3; port < 52e3; port++) {
2717
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
2718
+ if (await tryReservePort(port, project)) {
2719
+ return handle(port);
2579
2720
  }
2580
2721
  }
2581
2722
  throw new Error(
@@ -2583,9 +2724,13 @@ async function reservePort(project, preferredPort) {
2583
2724
  );
2584
2725
  }
2585
2726
  async function tryReservePort(port, project) {
2727
+ if (!acquirePortLock(port)) {
2728
+ return false;
2729
+ }
2586
2730
  return new Promise((resolve) => {
2587
2731
  const server = createServer();
2588
2732
  server.once("error", () => {
2733
+ releasePortLock(port);
2589
2734
  resolve(false);
2590
2735
  });
2591
2736
  server.listen(port, () => {
@@ -2611,31 +2756,6 @@ async function releasePort(port) {
2611
2756
  });
2612
2757
  });
2613
2758
  }
2614
- async function findAvailablePortInRange(start, end) {
2615
- for (let port = start; port < end; port++) {
2616
- if (reservedPorts.has(port)) {
2617
- continue;
2618
- }
2619
- const available = await isPortAvailable(port);
2620
- if (available) {
2621
- return port;
2622
- }
2623
- }
2624
- return null;
2625
- }
2626
- async function isPortAvailable(port) {
2627
- return new Promise((resolve) => {
2628
- const server = createServer();
2629
- server.once("error", () => {
2630
- resolve(false);
2631
- });
2632
- server.listen(port, () => {
2633
- server.close(() => {
2634
- resolve(true);
2635
- });
2636
- });
2637
- });
2638
- }
2639
2759
 
2640
2760
  // libs/testing/src/server/test-server.ts
2641
2761
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -2644,8 +2764,11 @@ var TestServer = class _TestServer {
2644
2764
  options;
2645
2765
  _info;
2646
2766
  logs = [];
2767
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
2647
2768
  portRelease = null;
2648
- constructor(options, port, portRelease) {
2769
+ /** Frees only the reservation socket, just before the server is spawned */
2770
+ portReleaseSocket = null;
2771
+ constructor(options, port, portRelease, portReleaseSocket) {
2649
2772
  this.options = {
2650
2773
  port,
2651
2774
  project: options.project,
@@ -2657,6 +2780,7 @@ var TestServer = class _TestServer {
2657
2780
  debug: options.debug ?? DEBUG_SERVER
2658
2781
  };
2659
2782
  this.portRelease = portRelease ?? null;
2783
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
2660
2784
  this._info = {
2661
2785
  baseUrl: `http://localhost:${port}`,
2662
2786
  port
@@ -2669,8 +2793,8 @@ var TestServer = class _TestServer {
2669
2793
  const project = options.project ?? "default";
2670
2794
  const maxAttempts = 3;
2671
2795
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2672
- const { port, release } = await reservePort(project, options.port);
2673
- const server = new _TestServer(options, port, release);
2796
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2797
+ const server = new _TestServer(options, port, release, releaseSocket);
2674
2798
  try {
2675
2799
  await server.startProcess();
2676
2800
  return server;
@@ -2710,7 +2834,7 @@ var TestServer = class _TestServer {
2710
2834
  }
2711
2835
  const maxAttempts = 3;
2712
2836
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2713
- const { port, release } = await reservePort(project, options.port);
2837
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2714
2838
  const serverOptions = {
2715
2839
  ...options,
2716
2840
  port,
@@ -2718,7 +2842,7 @@ var TestServer = class _TestServer {
2718
2842
  command: `npx nx serve ${project} --port ${port}`,
2719
2843
  cwd: options.cwd ?? process.cwd()
2720
2844
  };
2721
- const server = new _TestServer(serverOptions, port, release);
2845
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
2722
2846
  try {
2723
2847
  await server.startProcess();
2724
2848
  return server;
@@ -2776,10 +2900,17 @@ var TestServer = class _TestServer {
2776
2900
  * Stop the test server
2777
2901
  */
2778
2902
  async stop() {
2779
- if (this.portRelease) {
2780
- await this.portRelease();
2781
- this.portRelease = null;
2903
+ try {
2904
+ await this.stopProcess();
2905
+ } finally {
2906
+ if (this.portRelease) {
2907
+ await this.portRelease();
2908
+ this.portRelease = null;
2909
+ }
2910
+ this.portReleaseSocket = null;
2782
2911
  }
2912
+ }
2913
+ async stopProcess() {
2783
2914
  if (this.process) {
2784
2915
  this.log("Stopping server...");
2785
2916
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -2852,7 +2983,7 @@ var TestServer = class _TestServer {
2852
2983
  * Restart the server
2853
2984
  */
2854
2985
  async restart() {
2855
- await this.stop();
2986
+ await this.stopProcess();
2856
2987
  await this.startProcess();
2857
2988
  }
2858
2989
  /**
@@ -2882,9 +3013,9 @@ var TestServer = class _TestServer {
2882
3013
  PORT: String(this.options.port)
2883
3014
  };
2884
3015
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
2885
- if (this.portRelease) {
2886
- await this.portRelease();
2887
- this.portRelease = null;
3016
+ if (this.portReleaseSocket) {
3017
+ await this.portReleaseSocket();
3018
+ this.portReleaseSocket = null;
2888
3019
  await sleep2(300);
2889
3020
  }
2890
3021
  this.process = spawn(this.options.command, [], {
@@ -2894,9 +3025,8 @@ var TestServer = class _TestServer {
2894
3025
  detached: true,
2895
3026
  stdio: ["pipe", "pipe", "pipe"]
2896
3027
  });
2897
- if (this.process.pid !== void 0) {
2898
- this._info.pid = this.process.pid;
2899
- }
3028
+ this._info.pid = this.process.pid;
3029
+ this._info.shellPid = this.process.pid;
2900
3030
  let processExited = false;
2901
3031
  let exitCode = null;
2902
3032
  let exitError = null;
@@ -2999,6 +3129,8 @@ ${allLogs}`);
2999
3129
  });
3000
3130
  if (response.ok || response.status === 404) {
3001
3131
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
3132
+ const listeningPid = await findListeningPid(this.options.port);
3133
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3002
3134
  return;
3003
3135
  }
3004
3136
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3043,6 +3175,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3043
3175
  function sleep2(ms) {
3044
3176
  return new Promise((resolve) => setTimeout(resolve, ms));
3045
3177
  }
3178
+ function findListeningPid(port) {
3179
+ if (process.platform === "win32") return Promise.resolve(void 0);
3180
+ return new Promise((resolve) => {
3181
+ execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
3182
+ if (error) {
3183
+ resolve(void 0);
3184
+ return;
3185
+ }
3186
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
3187
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
3188
+ });
3189
+ });
3190
+ }
3046
3191
  function withWorkspaceProtocolFallback(env, cwd) {
3047
3192
  if (findInstalledProtocolPackageDir(cwd)) {
3048
3193
  return env;
@@ -3159,81 +3304,152 @@ function gatewayTokenBinding(serverUrl, entryPath) {
3159
3304
  }
3160
3305
 
3161
3306
  // libs/testing/src/fixtures/test-fixture.ts
3162
- var describe = _describe;
3163
- var beforeAll = _beforeAll;
3164
- var beforeEach = _beforeEach;
3165
- var afterEach = _afterEach;
3166
- var afterAll = _afterAll;
3167
- var it = _it;
3168
- var currentConfig = {};
3169
- var serverInstance = null;
3170
- var tokenFactory = null;
3171
- var serverStartedByUs = false;
3172
- var skipScopes = [{ skipped: false }];
3173
- function currentSkipScope() {
3174
- return skipScopes[skipScopes.length - 1];
3307
+ var jestApi;
3308
+ function jestGlobals() {
3309
+ if (jestApi) return jestApi;
3310
+ try {
3311
+ const api = __require("@jest/globals");
3312
+ jestApi = {
3313
+ describe: api["describe"],
3314
+ beforeAll: api["beforeAll"],
3315
+ beforeEach: api["beforeEach"],
3316
+ afterEach: api["afterEach"],
3317
+ afterAll: api["afterAll"],
3318
+ it: api["it"]
3319
+ };
3320
+ return jestApi;
3321
+ } catch (error) {
3322
+ throw new Error(
3323
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
3324
+ { cause: error }
3325
+ );
3326
+ }
3175
3327
  }
3176
- async function initializeSharedResources() {
3177
- if (!serverInstance) {
3178
- if (currentConfig.baseUrl && !currentConfig.server) {
3179
- serverInstance = TestServer.connect(currentConfig.baseUrl);
3180
- serverStartedByUs = false;
3181
- } else if (currentConfig.server) {
3182
- const serverCommand = resolveServerCommand(currentConfig.server);
3183
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3328
+ var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
3329
+ function currentScope() {
3330
+ return scopes[scopes.length - 1];
3331
+ }
3332
+ var serverEntries = /* @__PURE__ */ new Map();
3333
+ function resolveConfig(chain) {
3334
+ const merged = {};
3335
+ for (const scope of chain) {
3336
+ const { env, ...rest } = scope.config;
3337
+ Object.assign(merged, rest);
3338
+ if (env) merged.env = { ...merged.env, ...env };
3339
+ }
3340
+ return merged;
3341
+ }
3342
+ function ownerScopeOf(chain) {
3343
+ for (let i = chain.length - 1; i > 0; i--) {
3344
+ if (chain[i].configured) return chain[i];
3345
+ }
3346
+ return chain[0];
3347
+ }
3348
+ function serverKey(config) {
3349
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
3350
+ return JSON.stringify({
3351
+ server: config.server,
3352
+ baseUrl: config.baseUrl,
3353
+ project: config.project,
3354
+ port: config.port,
3355
+ entryPath: config.entryPath,
3356
+ startupTimeout: config.startupTimeout,
3357
+ logLevel: config.logLevel,
3358
+ auth: config.auth,
3359
+ env
3360
+ });
3361
+ }
3362
+ function serverEnv(config) {
3363
+ const extra = {};
3364
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
3365
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
3366
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
3367
+ return { ...extra, ...config.env };
3368
+ }
3369
+ async function startServerEntry(config, owner) {
3370
+ let server;
3371
+ let startedByUs = false;
3372
+ if (config.baseUrl && !config.server) {
3373
+ server = TestServer.connect(config.baseUrl);
3374
+ } else if (config.server) {
3375
+ const serverCommand = resolveServerCommand(config.server);
3376
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3377
+ if (isDebug) {
3378
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
3379
+ }
3380
+ try {
3381
+ server = await TestServer.start({
3382
+ project: config.project,
3383
+ port: config.port,
3384
+ command: serverCommand,
3385
+ env: serverEnv(config),
3386
+ startupTimeout: config.startupTimeout ?? 3e4,
3387
+ debug: isDebug
3388
+ });
3389
+ startedByUs = true;
3184
3390
  if (isDebug) {
3185
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
3391
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
3186
3392
  }
3187
- try {
3188
- serverInstance = await TestServer.start({
3189
- project: currentConfig.project,
3190
- port: currentConfig.port,
3191
- command: serverCommand,
3192
- env: currentConfig.env,
3193
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
3194
- debug: isDebug
3195
- });
3196
- serverStartedByUs = true;
3197
- if (isDebug) {
3198
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
3199
- }
3200
- } catch (error) {
3201
- const errMsg = error instanceof Error ? error.message : String(error);
3202
- throw new Error(
3203
- `Failed to start test server.
3393
+ } catch (error) {
3394
+ const errMsg = error instanceof Error ? error.message : String(error);
3395
+ throw new Error(
3396
+ `Failed to start test server.
3204
3397
 
3205
- Server entry: ${currentConfig.server}
3206
- Project: ${currentConfig.project ?? "default"}
3398
+ Server entry: ${config.server}
3399
+ Project: ${config.project ?? "default"}
3207
3400
  Command: ${serverCommand}
3208
3401
 
3209
- Error: ${errMsg}`
3210
- );
3211
- }
3212
- } else {
3213
- throw new Error(
3214
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
3402
+ Error: ${errMsg}`,
3403
+ { cause: error }
3215
3404
  );
3216
3405
  }
3406
+ } else {
3407
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
3217
3408
  }
3218
- if (!tokenFactory) {
3219
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
3220
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
3221
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3222
- }
3409
+ const sharedSecret = config.env?.["JWT_SECRET"];
3410
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
3411
+ const tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3412
+ return { server, startedByUs, tokenFactory, owner };
3223
3413
  }
3224
- async function createTestFixtures() {
3225
- await initializeSharedResources();
3226
- if (!serverInstance) {
3227
- throw new Error("Server instance not initialized");
3228
- }
3229
- if (!tokenFactory) {
3230
- throw new Error("Token factory not initialized");
3414
+ function acquireServer(chain) {
3415
+ const config = resolveConfig(chain);
3416
+ const key = serverKey(config);
3417
+ let entry = serverEntries.get(key);
3418
+ if (!entry) {
3419
+ entry = startServerEntry(config, ownerScopeOf(chain));
3420
+ serverEntries.set(key, entry);
3421
+ entry.catch(() => serverEntries.delete(key));
3422
+ }
3423
+ return entry;
3424
+ }
3425
+ async function stopServers(owner) {
3426
+ for (const [key, pending] of Array.from(serverEntries)) {
3427
+ let entry;
3428
+ try {
3429
+ entry = await pending;
3430
+ } catch {
3431
+ serverEntries.delete(key);
3432
+ continue;
3433
+ }
3434
+ if (owner && entry.owner !== owner) continue;
3435
+ serverEntries.delete(key);
3436
+ if (entry.startedByUs) {
3437
+ await entry.server.stop();
3438
+ }
3231
3439
  }
3440
+ }
3441
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
3442
+ async function buildFixtures(chain) {
3443
+ const config = resolveConfig(chain);
3444
+ const entry = await acquireServer(chain);
3445
+ const { server, tokenFactory } = entry;
3446
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
3447
+ const clientConfig = { ...config, publicMode };
3232
3448
  const clientInstance = McpTestClient.create({
3233
- baseUrl: resolveClientBaseUrl(serverInstance),
3234
- entryPath: currentConfig.entryPath,
3235
- transport: currentConfig.transport ?? "streamable-http",
3236
- publicMode: currentConfig.publicMode
3449
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
3450
+ entryPath: clientConfig.entryPath,
3451
+ transport: clientConfig.transport ?? "streamable-http",
3452
+ publicMode
3237
3453
  }).build();
3238
3454
  try {
3239
3455
  await clientInstance.connect();
@@ -3247,17 +3463,24 @@ async function createTestFixtures() {
3247
3463
  throw err;
3248
3464
  }
3249
3465
  }
3250
- const auth = createAuthFixture(tokenFactory);
3251
- const server = createServerFixture(serverInstance);
3252
- return {
3466
+ const context = { extraClients: [] };
3467
+ const fixtures = {
3253
3468
  mcp: clientInstance,
3254
- auth,
3255
- server
3469
+ auth: createAuthFixture(tokenFactory),
3470
+ server: createServerFixture(server, clientConfig, clientInstance, context)
3256
3471
  };
3472
+ fixtureContexts.set(fixtures, context);
3473
+ return fixtures;
3474
+ }
3475
+ async function createTestFixtures() {
3476
+ return buildFixtures(scopes.slice());
3477
+ }
3478
+ async function initializeSharedResources() {
3479
+ await acquireServer(scopes.slice());
3257
3480
  }
3258
3481
  async function cleanupTestFixtures(fixtures, testFailed = false) {
3259
- if (testFailed && serverInstance) {
3260
- const logs = serverInstance.getLogs();
3482
+ if (testFailed) {
3483
+ const logs = fixtures.server.getLogs();
3261
3484
  if (logs.length > 0) {
3262
3485
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
3263
3486
  const recentLogs = logs.slice(-50);
@@ -3268,20 +3491,21 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
3268
3491
  console.error("[TestFixture] === End Server Logs ===\n");
3269
3492
  }
3270
3493
  }
3494
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
3495
+ for (const client of extras) {
3496
+ if (client.isConnected()) {
3497
+ await client.disconnect().catch(() => void 0);
3498
+ }
3499
+ }
3271
3500
  if (fixtures.mcp.isConnected()) {
3272
3501
  await fixtures.mcp.disconnect();
3273
3502
  }
3274
3503
  }
3275
3504
  async function cleanupSharedResources() {
3276
- if (serverInstance && serverStartedByUs) {
3277
- await serverInstance.stop();
3278
- }
3279
- serverInstance = null;
3280
- tokenFactory = null;
3281
- serverStartedByUs = false;
3505
+ await stopServers();
3282
3506
  }
3283
- function resolveClientBaseUrl(server) {
3284
- return currentConfig.baseUrl ?? server.info.baseUrl;
3507
+ function resolveClientBaseUrl(server, config) {
3508
+ return config.baseUrl ?? server.info.baseUrl;
3285
3509
  }
3286
3510
  function createAuthFixture(factory) {
3287
3511
  const users = {
@@ -3327,28 +3551,39 @@ function createAuthFixture(factory) {
3327
3551
  getAudience: () => factory.getAudience()
3328
3552
  };
3329
3553
  }
3330
- function createServerFixture(server) {
3554
+ function createServerFixture(server, config, mcp, context) {
3331
3555
  return {
3332
- info: server.info,
3556
+ // Live view: `pid` is filled in once the server is listening and follows restarts
3557
+ get info() {
3558
+ return server.info;
3559
+ },
3333
3560
  createClient: async (opts) => {
3334
- return McpTestClient.create({
3335
- baseUrl: resolveClientBaseUrl(server),
3336
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
3561
+ const client = await McpTestClient.create({
3562
+ baseUrl: resolveClientBaseUrl(server, config),
3563
+ entryPath: opts?.entryPath ?? config.entryPath,
3337
3564
  transport: opts?.transport ?? "streamable-http",
3338
3565
  auth: opts?.token ? { token: opts.token } : void 0,
3339
3566
  clientInfo: opts?.clientInfo,
3340
- publicMode: currentConfig.publicMode
3567
+ publicMode: config.publicMode
3341
3568
  }).buildAndConnect();
3569
+ context.extraClients.push(client);
3570
+ return client;
3342
3571
  },
3343
3572
  createClientBuilder: () => {
3344
3573
  const builder = new McpTestClientBuilder({
3345
- baseUrl: resolveClientBaseUrl(server),
3346
- entryPath: currentConfig.entryPath,
3347
- publicMode: currentConfig.publicMode
3574
+ baseUrl: resolveClientBaseUrl(server, config),
3575
+ entryPath: config.entryPath,
3576
+ publicMode: config.publicMode
3348
3577
  });
3349
3578
  return builder;
3350
3579
  },
3351
- restart: () => server.restart(),
3580
+ restart: async () => {
3581
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
3582
+ await server.restart();
3583
+ for (const client of reconnect) {
3584
+ await client.reconnect();
3585
+ }
3586
+ },
3352
3587
  getLogs: () => server.getLogs(),
3353
3588
  clearLogs: () => server.clearLogs()
3354
3589
  };
@@ -3359,12 +3594,12 @@ function resolveServerCommand(server) {
3359
3594
  }
3360
3595
  return `npx tsx ${server}`;
3361
3596
  }
3362
- function runWithFixtures(fn) {
3363
- return async () => {
3364
- const fixtures = await createTestFixtures();
3597
+ function runWithFixtures(fn, chain) {
3598
+ return async (...extra) => {
3599
+ const fixtures = await buildFixtures(chain);
3365
3600
  let testFailed = false;
3366
3601
  try {
3367
- await fn(fixtures);
3602
+ await fn(fixtures, ...extra);
3368
3603
  } catch (error) {
3369
3604
  testFailed = true;
3370
3605
  throw error;
@@ -3373,24 +3608,37 @@ function runWithFixtures(fn) {
3373
3608
  }
3374
3609
  };
3375
3610
  }
3611
+ function skipTitle(name, scope) {
3612
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
3613
+ }
3376
3614
  function testWithFixtures(name, fn) {
3377
- const scope = currentSkipScope();
3615
+ const chain = scopes.slice();
3616
+ const scope = currentScope();
3378
3617
  if (scope.skipped) {
3379
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
3618
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
3380
3619
  return;
3381
3620
  }
3382
- it(name, runWithFixtures(fn));
3621
+ jestGlobals().it(name, runWithFixtures(fn, chain));
3383
3622
  }
3384
3623
  function use(config) {
3385
- currentConfig = { ...currentConfig, ...config };
3386
- afterAll(async () => {
3387
- await cleanupSharedResources();
3388
- });
3624
+ const scope = currentScope();
3625
+ scope.config = {
3626
+ ...scope.config,
3627
+ ...config,
3628
+ env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
3629
+ };
3630
+ scope.configured = true;
3631
+ if (!scope.cleanupRegistered) {
3632
+ scope.cleanupRegistered = true;
3633
+ jestGlobals().afterAll(async () => {
3634
+ await stopServers(scope === scopes[0] ? void 0 : scope);
3635
+ });
3636
+ }
3389
3637
  }
3390
3638
  function skip(nameOrCondition, fnOrReason) {
3391
3639
  if (typeof nameOrCondition === "boolean") {
3392
3640
  if (nameOrCondition) {
3393
- const scope = currentSkipScope();
3641
+ const scope = currentScope();
3394
3642
  scope.skipped = true;
3395
3643
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
3396
3644
  }
@@ -3401,53 +3649,90 @@ function skip(nameOrCondition, fnOrReason) {
3401
3649
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
3402
3650
  );
3403
3651
  }
3404
- it.skip(nameOrCondition, runWithFixtures(fnOrReason));
3652
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
3405
3653
  }
3406
3654
  function only(name, fn) {
3407
- const scope = currentSkipScope();
3655
+ const scope = currentScope();
3408
3656
  if (scope.skipped) {
3409
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
3657
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
3410
3658
  return;
3411
3659
  }
3412
- it.only(name, runWithFixtures(fn));
3660
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
3413
3661
  }
3414
3662
  function todo(name) {
3415
- it.todo(name);
3663
+ jestGlobals().it.todo(name);
3416
3664
  }
3417
- function withSkipScope(register) {
3665
+ function each(table, ...tagged) {
3666
+ const chain = scopes.slice();
3667
+ const register = jestGlobals().it.each(table, ...tagged);
3418
3668
  return (name, fn) => {
3419
- register(name, () => {
3420
- const parent = currentSkipScope();
3421
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
3422
- try {
3423
- fn();
3424
- } finally {
3425
- skipScopes.pop();
3426
- }
3669
+ const scope = currentScope();
3670
+ if (scope.skipped) {
3671
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
3672
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
3673
+ return;
3674
+ }
3675
+ register(name, runWithFixtures(fn, chain));
3676
+ };
3677
+ }
3678
+ function withScope(fn) {
3679
+ return (...args) => {
3680
+ const parent = currentScope();
3681
+ scopes.push({
3682
+ skipped: parent.skipped,
3683
+ reason: parent.reason,
3684
+ config: {},
3685
+ configured: false,
3686
+ cleanupRegistered: false
3427
3687
  });
3688
+ try {
3689
+ fn(...args);
3690
+ } finally {
3691
+ scopes.pop();
3692
+ }
3428
3693
  };
3429
3694
  }
3430
- function eachWithSkipScope(each) {
3431
- return ((...eachArgs) => {
3432
- const register = each(...eachArgs);
3433
- return withSkipScope(register);
3434
- });
3695
+ function scopedRegister(pick) {
3696
+ return (name, fn, timeout) => {
3697
+ pick()(name, withScope(fn), timeout);
3698
+ };
3435
3699
  }
3436
- var describeWithSkipScope = Object.assign(withSkipScope(describe), describe, {
3437
- only: Object.assign(withSkipScope(describe.only), { each: eachWithSkipScope(describe.only.each) }),
3438
- skip: Object.assign(withSkipScope(describe.skip), { each: eachWithSkipScope(describe.skip.each) }),
3439
- each: eachWithSkipScope(describe.each)
3440
- });
3700
+ function scopedEach(pick) {
3701
+ return (...eachArgs) => (name, fn, timeout) => {
3702
+ const register = pick()(...eachArgs);
3703
+ register(name, withScope(fn), timeout);
3704
+ };
3705
+ }
3706
+ var describeWithScope = Object.assign(
3707
+ scopedRegister(() => jestGlobals().describe),
3708
+ {
3709
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
3710
+ only: Object.assign(
3711
+ scopedRegister(() => jestGlobals().describe.only),
3712
+ {
3713
+ each: scopedEach(() => jestGlobals().describe.only.each)
3714
+ }
3715
+ ),
3716
+ skip: Object.assign(
3717
+ scopedRegister(() => jestGlobals().describe.skip),
3718
+ {
3719
+ each: scopedEach(() => jestGlobals().describe.skip.each)
3720
+ }
3721
+ ),
3722
+ each: scopedEach(() => jestGlobals().describe.each)
3723
+ }
3724
+ );
3441
3725
  var test = testWithFixtures;
3442
3726
  test.use = use;
3443
- test.describe = describeWithSkipScope;
3444
- test.beforeAll = beforeAll;
3445
- test.beforeEach = beforeEach;
3446
- test.afterEach = afterEach;
3447
- test.afterAll = afterAll;
3727
+ test.describe = describeWithScope;
3728
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
3729
+ test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
3730
+ test.afterEach = ((...args) => jestGlobals().afterEach(...args));
3731
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
3448
3732
  test.skip = skip;
3449
3733
  test.only = only;
3450
3734
  test.todo = todo;
3735
+ test.each = each;
3451
3736
  export {
3452
3737
  cleanupSharedResources,
3453
3738
  cleanupTestFixtures,