@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
package/fixtures/index.js CHANGED
@@ -28,9 +28,6 @@ __export(fixtures_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(fixtures_exports);
30
30
 
31
- // libs/testing/src/fixtures/test-fixture.ts
32
- var import_globals = require("@jest/globals");
33
-
34
31
  // libs/testing/src/auth/token-factory.ts
35
32
  var import_jose = require("jose");
36
33
  var TestTokenFactory = class {
@@ -89,14 +86,15 @@ var TestTokenFactory = class {
89
86
  */
90
87
  async createTestToken(options) {
91
88
  await this.ensureKeys();
92
- const now = Math.floor(Date.now() / 1e3);
89
+ const nowMs = Date.now();
90
+ const iat = Math.floor(nowMs / 1e3);
93
91
  const exp = options.exp ?? 3600;
94
92
  const payload = {
95
93
  iss: options.iss ?? this.issuer,
96
94
  sub: options.sub,
97
95
  aud: options.aud ?? this.audience,
98
- iat: now,
99
- exp: now + exp,
96
+ iat,
97
+ exp: Math.ceil(nowMs / 1e3) + exp,
100
98
  scope: options.scopes?.join(" "),
101
99
  ...options.claims
102
100
  };
@@ -121,10 +119,10 @@ var TestTokenFactory = class {
121
119
  /**
122
120
  * Create a regular user token
123
121
  */
124
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
122
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
125
123
  return this.createTestToken({
126
124
  sub,
127
- scopes,
125
+ scopes: scopes2,
128
126
  claims: {
129
127
  email: "user@test.local",
130
128
  name: "Test User",
@@ -135,10 +133,11 @@ var TestTokenFactory = class {
135
133
  /**
136
134
  * Create an anonymous user token
137
135
  */
138
- async createAnonymousToken() {
136
+ async createAnonymousToken(expiresIn) {
139
137
  return this.createTestToken({
140
138
  sub: `anon:${Date.now()}`,
141
139
  scopes: ["anonymous"],
140
+ exp: expiresIn,
142
141
  claims: {
143
142
  name: "Anonymous",
144
143
  role: "anonymous"
@@ -487,6 +486,32 @@ function sleep(ms) {
487
486
  return new Promise((resolve) => setTimeout(resolve, ms));
488
487
  }
489
488
 
489
+ // libs/testing/src/transport/error-utils.ts
490
+ function errorName(err) {
491
+ if (typeof err !== "object" || err === null) return void 0;
492
+ const name = err.name;
493
+ return typeof name === "string" ? name : void 0;
494
+ }
495
+ function isAbortError(err) {
496
+ return errorName(err) === "AbortError";
497
+ }
498
+ function errorMessage(err, fallback = "Unknown error") {
499
+ if (typeof err === "string" && err) return err;
500
+ if (typeof err === "object" && err !== null) {
501
+ const message = err.message;
502
+ if (typeof message === "string" && message) return message;
503
+ }
504
+ return fallback;
505
+ }
506
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
507
+ function markInterceptedError(err) {
508
+ interceptedErrors.add(err);
509
+ return err;
510
+ }
511
+ function isInterceptedError(err) {
512
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
513
+ }
514
+
490
515
  // libs/testing/src/transport/streamable-http.transport.ts
491
516
  var DEFAULT_TIMEOUT = 3e4;
492
517
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
@@ -595,7 +620,7 @@ var StreamableHttpTransport = class {
595
620
  return mockResponse2;
596
621
  }
597
622
  case "error":
598
- throw interceptResult.error;
623
+ throw markInterceptedError(interceptResult.error);
599
624
  case "continue":
600
625
  message = interceptResult.request;
601
626
  break;
@@ -656,7 +681,7 @@ var StreamableHttpTransport = class {
656
681
  return jsonResponse;
657
682
  } catch (error) {
658
683
  clearTimeout(timeoutId);
659
- if (error instanceof Error && error.name === "AbortError") {
684
+ if (isAbortError(error)) {
660
685
  return {
661
686
  jsonrpc: "2.0",
662
687
  id: message.id ?? null,
@@ -695,7 +720,7 @@ var StreamableHttpTransport = class {
695
720
  }
696
721
  } catch (error) {
697
722
  clearTimeout(timeoutId);
698
- if (error instanceof Error && error.name !== "AbortError") {
723
+ if (!isAbortError(error)) {
699
724
  throw error;
700
725
  }
701
726
  }
@@ -736,7 +761,7 @@ var StreamableHttpTransport = class {
736
761
  error: {
737
762
  code: -32700,
738
763
  message: "Parse error",
739
- data: error instanceof Error ? error.message : "Unknown error"
764
+ data: errorMessage(error)
740
765
  }
741
766
  };
742
767
  }
@@ -1106,7 +1131,7 @@ var StreamableHttpTransport = class {
1106
1131
  if (this.config.clientInfo) {
1107
1132
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1108
1133
  }
1109
- if (this.authToken && !this.publicMode) {
1134
+ if (this.authToken) {
1110
1135
  headers["Authorization"] = `Bearer ${this.authToken}`;
1111
1136
  }
1112
1137
  if (this.sessionId) {
@@ -1429,6 +1454,16 @@ var DEFAULT_CLIENT_INFO = {
1429
1454
  name: "@frontmcp/testing",
1430
1455
  version: "0.4.0"
1431
1456
  };
1457
+ var MAX_LIST_PAGES = 1e3;
1458
+ function isAuthFailure(error) {
1459
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1460
+ }
1461
+ function describeInitializeError(error) {
1462
+ const message = error?.message ?? "Unknown error";
1463
+ const data = error?.data;
1464
+ const detail = typeof data === "string" ? data.trim() : "";
1465
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1466
+ }
1432
1467
  function extractEntryPaths(data) {
1433
1468
  let payload = data;
1434
1469
  if (typeof payload === "string") {
@@ -1515,19 +1550,22 @@ var McpTestClient = class {
1515
1550
  async connect() {
1516
1551
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1517
1552
  this.triedDiscoveredEntryPath = false;
1553
+ this.initResult = null;
1518
1554
  this.transport = this.createTransport();
1519
1555
  await this.transport.connect();
1520
1556
  const maxRetries = 3;
1521
1557
  const retryDelayMs = 500;
1522
1558
  let lastError;
1523
1559
  let reportedEntryPaths;
1560
+ let attemptsMade = 0;
1524
1561
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1562
+ attemptsMade = attempt;
1525
1563
  const initResponse = await this.initialize();
1526
1564
  if (initResponse.success && initResponse.data) {
1527
1565
  this.initResult = initResponse.data;
1528
1566
  break;
1529
1567
  }
1530
- lastError = initResponse.error?.message ?? "Unknown error";
1568
+ lastError = describeInitializeError(initResponse.error);
1531
1569
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1532
1570
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1533
1571
  this.triedDiscoveredEntryPath = true;
@@ -1539,6 +1577,7 @@ var McpTestClient = class {
1539
1577
  await this.transport.connect();
1540
1578
  continue;
1541
1579
  }
1580
+ if (isAuthFailure(initResponse.error)) break;
1542
1581
  if (attempt < maxRetries) {
1543
1582
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1544
1583
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1547,7 +1586,7 @@ var McpTestClient = class {
1547
1586
  if (!this.initResult) {
1548
1587
  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.` : "";
1549
1588
  throw new Error(
1550
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1589
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1551
1590
  );
1552
1591
  }
1553
1592
  this._sessionId = this.transport.getSessionId();
@@ -1617,18 +1656,47 @@ var McpTestClient = class {
1617
1656
  return this._authState;
1618
1657
  }
1619
1658
  /**
1620
- * Authenticate with a token
1659
+ * Authenticate with a token.
1660
+ *
1661
+ * When connected, this opens a NEW session with the token (the previous session was created
1662
+ * for the previous identity) and rejects if the server refuses it — for example because the
1663
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
1664
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1621
1665
  */
1622
1666
  async authenticate(token) {
1667
+ if (typeof token !== "string" || token.trim() === "") {
1668
+ throw new Error("authenticate() requires a non-empty token");
1669
+ }
1670
+ const previousAuthState = this._authState;
1671
+ const previousConfigAuth = this.config.auth;
1672
+ this.config.auth = { ...previousConfigAuth, token };
1623
1673
  this._authState = {
1624
1674
  isAnonymous: false,
1625
1675
  token,
1626
1676
  scopes: this.parseScopesFromToken(token),
1627
1677
  user: this.parseUserFromToken(token)
1628
1678
  };
1629
- if (this.transport) {
1630
- this.transport.setAuthToken(token);
1679
+ const previousTransport = this.transport;
1680
+ if (!previousTransport) {
1681
+ this.log("debug", "Authentication updated (not connected yet)");
1682
+ return;
1631
1683
  }
1684
+ const previousInit = this.initResult;
1685
+ const previousSessionId = this._sessionId;
1686
+ const previousSessionInfo = this._sessionInfo;
1687
+ try {
1688
+ await this.connect();
1689
+ } catch (err) {
1690
+ await this.transport?.close().catch(() => void 0);
1691
+ this.transport = previousTransport;
1692
+ this.initResult = previousInit;
1693
+ this._sessionId = previousSessionId;
1694
+ this._sessionInfo = previousSessionInfo;
1695
+ this.config.auth = previousConfigAuth;
1696
+ this._authState = previousAuthState;
1697
+ throw err;
1698
+ }
1699
+ await previousTransport.close().catch(() => void 0);
1632
1700
  this.log("debug", "Authentication updated");
1633
1701
  }
1634
1702
  // ═══════════════════════════════════════════════════════════════════
@@ -1662,13 +1730,12 @@ var McpTestClient = class {
1662
1730
  /**
1663
1731
  * List all available tools
1664
1732
  */
1665
- list: async () => {
1666
- const response = await this.listTools();
1667
- if (!response.success || !response.data) {
1668
- throw new Error(`Failed to list tools: ${response.error?.message}`);
1669
- }
1670
- return response.data.tools;
1671
- },
1733
+ list: async () => this.listAllPages(
1734
+ "tools/list",
1735
+ "tools",
1736
+ (cursor) => this.listTools(cursor),
1737
+ (page) => page.tools
1738
+ ),
1672
1739
  /**
1673
1740
  * Call a tool by name with arguments
1674
1741
  */
@@ -1684,23 +1751,21 @@ var McpTestClient = class {
1684
1751
  /**
1685
1752
  * List all static resources
1686
1753
  */
1687
- list: async () => {
1688
- const response = await this.listResources();
1689
- if (!response.success || !response.data) {
1690
- throw new Error(`Failed to list resources: ${response.error?.message}`);
1691
- }
1692
- return response.data.resources;
1693
- },
1754
+ list: async () => this.listAllPages(
1755
+ "resources/list",
1756
+ "resources",
1757
+ (cursor) => this.listResources(cursor),
1758
+ (page) => page.resources
1759
+ ),
1694
1760
  /**
1695
1761
  * List all resource templates
1696
1762
  */
1697
- listTemplates: async () => {
1698
- const response = await this.listResourceTemplates();
1699
- if (!response.success || !response.data) {
1700
- throw new Error(`Failed to list resource templates: ${response.error?.message}`);
1701
- }
1702
- return response.data.resourceTemplates;
1703
- },
1763
+ listTemplates: async () => this.listAllPages(
1764
+ "resources/templates/list",
1765
+ "resource templates",
1766
+ (cursor) => this.listResourceTemplates(cursor),
1767
+ (page) => page.resourceTemplates
1768
+ ),
1704
1769
  /**
1705
1770
  * Read a resource by URI
1706
1771
  */
@@ -1728,13 +1793,12 @@ var McpTestClient = class {
1728
1793
  /**
1729
1794
  * List all available prompts
1730
1795
  */
1731
- list: async () => {
1732
- const response = await this.listPrompts();
1733
- if (!response.success || !response.data) {
1734
- throw new Error(`Failed to list prompts: ${response.error?.message}`);
1735
- }
1736
- return response.data.prompts;
1737
- },
1796
+ list: async () => this.listAllPages(
1797
+ "prompts/list",
1798
+ "prompts",
1799
+ (cursor) => this.listPrompts(cursor),
1800
+ (page) => page.prompts
1801
+ ),
1738
1802
  /**
1739
1803
  * Get a prompt with arguments
1740
1804
  */
@@ -2118,8 +2182,33 @@ var McpTestClient = class {
2118
2182
  clientInfo: this.config.clientInfo
2119
2183
  });
2120
2184
  }
2121
- async listTools() {
2122
- return this.request("tools/list", {});
2185
+ async listTools(cursor) {
2186
+ return this.request("tools/list", cursor ? { cursor } : {});
2187
+ }
2188
+ /**
2189
+ * Follow `nextCursor` until the server stops returning one, concatenating the
2190
+ * pages. A server that hands back a cursor it already returned, or never stops
2191
+ * paging, fails the call rather than looping.
2192
+ */
2193
+ async listAllPages(method, label, fetchPage, itemsOf) {
2194
+ const items = [];
2195
+ const seenCursors = /* @__PURE__ */ new Set();
2196
+ let cursor;
2197
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
2198
+ const response = await fetchPage(cursor);
2199
+ if (!response.success || !response.data) {
2200
+ throw new Error(`Failed to list ${label}: ${response.error?.message}`);
2201
+ }
2202
+ items.push(...itemsOf(response.data) ?? []);
2203
+ const next = response.data.nextCursor;
2204
+ if (!next) return items;
2205
+ if (seenCursors.has(next)) {
2206
+ throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`);
2207
+ }
2208
+ seenCursors.add(next);
2209
+ cursor = next;
2210
+ }
2211
+ throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`);
2123
2212
  }
2124
2213
  async callTool(name, args, progressToken) {
2125
2214
  return this.request("tools/call", {
@@ -2144,17 +2233,17 @@ var McpTestClient = class {
2144
2233
  timestamp
2145
2234
  });
2146
2235
  };
2147
- async listResources() {
2148
- return this.request("resources/list", {});
2236
+ async listResources(cursor) {
2237
+ return this.request("resources/list", cursor ? { cursor } : {});
2149
2238
  }
2150
- async listResourceTemplates() {
2151
- return this.request("resources/templates/list", {});
2239
+ async listResourceTemplates(cursor) {
2240
+ return this.request("resources/templates/list", cursor ? { cursor } : {});
2152
2241
  }
2153
2242
  async readResource(uri) {
2154
2243
  return this.request("resources/read", { uri });
2155
2244
  }
2156
- async listPrompts() {
2157
- return this.request("prompts/list", {});
2245
+ async listPrompts(cursor) {
2246
+ return this.request("prompts/list", cursor ? { cursor } : {});
2158
2247
  }
2159
2248
  async getPrompt(name, args) {
2160
2249
  return this.request("prompts/get", {
@@ -2231,10 +2320,11 @@ var McpTestClient = class {
2231
2320
  requestId: id
2232
2321
  };
2233
2322
  } catch (err) {
2323
+ if (isInterceptedError(err)) throw err;
2234
2324
  const durationMs = Date.now() - start;
2235
2325
  const error = {
2236
2326
  code: -32603,
2237
- message: err instanceof Error ? err.message : "Unknown error"
2327
+ message: errorMessage(err)
2238
2328
  };
2239
2329
  return {
2240
2330
  success: false,
@@ -2382,12 +2472,12 @@ var McpTestClient = class {
2382
2472
  const payload = this.decodeJwtPayload(token);
2383
2473
  if (!payload) return [];
2384
2474
  const scope = payload["scope"];
2385
- const scopes = payload["scopes"];
2475
+ const scopes2 = payload["scopes"];
2386
2476
  if (typeof scope === "string") {
2387
2477
  return scope.split(" ");
2388
2478
  }
2389
- if (Array.isArray(scopes)) {
2390
- return scopes;
2479
+ if (Array.isArray(scopes2)) {
2480
+ return scopes2;
2391
2481
  }
2392
2482
  return [];
2393
2483
  } catch {
@@ -2533,6 +2623,8 @@ var E2E_PORT_RANGES = {
2533
2623
  "demo-e2e-distributed": { start: 50440, size: 20 },
2534
2624
  // Protocol revision E2E tests (50460-50479)
2535
2625
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
2626
+ // Testing library E2E tests (50480-50499)
2627
+ "demo-e2e-testing": { start: 50480, size: 20 },
2536
2628
  // Mock servers and utilities (50900-50999)
2537
2629
  "mock-oauth": { start: 50900, size: 10 },
2538
2630
  "mock-api": { start: 50910, size: 10 },
@@ -2541,6 +2633,66 @@ var E2E_PORT_RANGES = {
2541
2633
  default: { start: 51e3, size: 100 }
2542
2634
  };
2543
2635
  var reservedPorts = /* @__PURE__ */ new Map();
2636
+ var lockedPorts = /* @__PURE__ */ new Set();
2637
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
2638
+ function lockDir() {
2639
+ try {
2640
+ const os = require("node:os");
2641
+ const path = require("node:path");
2642
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
2643
+ } catch {
2644
+ return void 0;
2645
+ }
2646
+ }
2647
+ function pidIsAlive(pid) {
2648
+ try {
2649
+ process.kill(pid, 0);
2650
+ return true;
2651
+ } catch (error) {
2652
+ return error.code === "EPERM";
2653
+ }
2654
+ }
2655
+ function acquirePortLock(port) {
2656
+ if (lockedPorts.has(port)) return false;
2657
+ let fs;
2658
+ let file;
2659
+ try {
2660
+ fs = require("node:fs");
2661
+ const dir = lockDir();
2662
+ if (!dir) return true;
2663
+ fs.mkdirSync(dir, { recursive: true });
2664
+ file = require("node:path").join(dir, `${port}.lock`);
2665
+ } catch {
2666
+ return true;
2667
+ }
2668
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
2669
+ for (let attempt = 0; attempt < 2; attempt++) {
2670
+ try {
2671
+ fs.writeFileSync(file, payload, { flag: "wx" });
2672
+ lockedPorts.add(port);
2673
+ return true;
2674
+ } catch (error) {
2675
+ if (error.code !== "EEXIST") return true;
2676
+ try {
2677
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
2678
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
2679
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
2680
+ fs.unlinkSync(file);
2681
+ } catch {
2682
+ }
2683
+ }
2684
+ }
2685
+ return false;
2686
+ }
2687
+ function releasePortLock(port) {
2688
+ if (!lockedPorts.delete(port)) return;
2689
+ try {
2690
+ const fs = require("node:fs");
2691
+ const dir = lockDir();
2692
+ if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
2693
+ } catch {
2694
+ }
2695
+ }
2544
2696
  var projectPortIndex = /* @__PURE__ */ new Map();
2545
2697
  function getPortRange(project) {
2546
2698
  const key = project;
@@ -2551,15 +2703,22 @@ function getPortRange(project) {
2551
2703
  }
2552
2704
  async function reservePort(project, preferredPort) {
2553
2705
  const range = getPortRange(project);
2554
- if (preferredPort !== void 0) {
2706
+ const handle = (port) => ({
2707
+ port,
2708
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
2709
+ releaseSocket: async () => {
2710
+ await releasePort(port);
2711
+ },
2712
+ release: async () => {
2713
+ await releasePort(port);
2714
+ releasePortLock(port);
2715
+ }
2716
+ });
2717
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
2718
+ if (wantsSpecificPort) {
2555
2719
  const reservation = await tryReservePort(preferredPort, project);
2556
2720
  if (reservation) {
2557
- return {
2558
- port: preferredPort,
2559
- release: async () => {
2560
- await releasePort(preferredPort);
2561
- }
2562
- };
2721
+ return handle(preferredPort);
2563
2722
  }
2564
2723
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
2565
2724
  }
@@ -2568,30 +2727,19 @@ async function reservePort(project, preferredPort) {
2568
2727
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2569
2728
  const port = range.start + index % range.size;
2570
2729
  index = (index + 1) % range.size;
2571
- if (reservedPorts.has(port)) {
2730
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
2572
2731
  continue;
2573
2732
  }
2574
2733
  const reservation = await tryReservePort(port, project);
2575
2734
  if (reservation) {
2576
2735
  projectPortIndex.set(project, index);
2577
- return {
2578
- port,
2579
- release: async () => {
2580
- await releasePort(port);
2581
- }
2582
- };
2736
+ return handle(port);
2583
2737
  }
2584
2738
  }
2585
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
2586
- if (dynamicPort) {
2587
- const reservation = await tryReservePort(dynamicPort, project);
2588
- if (reservation) {
2589
- return {
2590
- port: dynamicPort,
2591
- release: async () => {
2592
- await releasePort(dynamicPort);
2593
- }
2594
- };
2739
+ for (let port = 51e3; port < 52e3; port++) {
2740
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
2741
+ if (await tryReservePort(port, project)) {
2742
+ return handle(port);
2595
2743
  }
2596
2744
  }
2597
2745
  throw new Error(
@@ -2599,9 +2747,13 @@ async function reservePort(project, preferredPort) {
2599
2747
  );
2600
2748
  }
2601
2749
  async function tryReservePort(port, project) {
2750
+ if (!acquirePortLock(port)) {
2751
+ return false;
2752
+ }
2602
2753
  return new Promise((resolve) => {
2603
2754
  const server = (0, import_net.createServer)();
2604
2755
  server.once("error", () => {
2756
+ releasePortLock(port);
2605
2757
  resolve(false);
2606
2758
  });
2607
2759
  server.listen(port, () => {
@@ -2627,31 +2779,6 @@ async function releasePort(port) {
2627
2779
  });
2628
2780
  });
2629
2781
  }
2630
- async function findAvailablePortInRange(start, end) {
2631
- for (let port = start; port < end; port++) {
2632
- if (reservedPorts.has(port)) {
2633
- continue;
2634
- }
2635
- const available = await isPortAvailable(port);
2636
- if (available) {
2637
- return port;
2638
- }
2639
- }
2640
- return null;
2641
- }
2642
- async function isPortAvailable(port) {
2643
- return new Promise((resolve) => {
2644
- const server = (0, import_net.createServer)();
2645
- server.once("error", () => {
2646
- resolve(false);
2647
- });
2648
- server.listen(port, () => {
2649
- server.close(() => {
2650
- resolve(true);
2651
- });
2652
- });
2653
- });
2654
- }
2655
2782
 
2656
2783
  // libs/testing/src/server/test-server.ts
2657
2784
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -2660,8 +2787,11 @@ var TestServer = class _TestServer {
2660
2787
  options;
2661
2788
  _info;
2662
2789
  logs = [];
2790
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
2663
2791
  portRelease = null;
2664
- constructor(options, port, portRelease) {
2792
+ /** Frees only the reservation socket, just before the server is spawned */
2793
+ portReleaseSocket = null;
2794
+ constructor(options, port, portRelease, portReleaseSocket) {
2665
2795
  this.options = {
2666
2796
  port,
2667
2797
  project: options.project,
@@ -2673,6 +2803,7 @@ var TestServer = class _TestServer {
2673
2803
  debug: options.debug ?? DEBUG_SERVER
2674
2804
  };
2675
2805
  this.portRelease = portRelease ?? null;
2806
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
2676
2807
  this._info = {
2677
2808
  baseUrl: `http://localhost:${port}`,
2678
2809
  port
@@ -2685,8 +2816,8 @@ var TestServer = class _TestServer {
2685
2816
  const project = options.project ?? "default";
2686
2817
  const maxAttempts = 3;
2687
2818
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2688
- const { port, release } = await reservePort(project, options.port);
2689
- const server = new _TestServer(options, port, release);
2819
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2820
+ const server = new _TestServer(options, port, release, releaseSocket);
2690
2821
  try {
2691
2822
  await server.startProcess();
2692
2823
  return server;
@@ -2726,7 +2857,7 @@ var TestServer = class _TestServer {
2726
2857
  }
2727
2858
  const maxAttempts = 3;
2728
2859
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2729
- const { port, release } = await reservePort(project, options.port);
2860
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2730
2861
  const serverOptions = {
2731
2862
  ...options,
2732
2863
  port,
@@ -2734,7 +2865,7 @@ var TestServer = class _TestServer {
2734
2865
  command: `npx nx serve ${project} --port ${port}`,
2735
2866
  cwd: options.cwd ?? process.cwd()
2736
2867
  };
2737
- const server = new _TestServer(serverOptions, port, release);
2868
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
2738
2869
  try {
2739
2870
  await server.startProcess();
2740
2871
  return server;
@@ -2792,10 +2923,17 @@ var TestServer = class _TestServer {
2792
2923
  * Stop the test server
2793
2924
  */
2794
2925
  async stop() {
2795
- if (this.portRelease) {
2796
- await this.portRelease();
2797
- this.portRelease = null;
2926
+ try {
2927
+ await this.stopProcess();
2928
+ } finally {
2929
+ if (this.portRelease) {
2930
+ await this.portRelease();
2931
+ this.portRelease = null;
2932
+ }
2933
+ this.portReleaseSocket = null;
2798
2934
  }
2935
+ }
2936
+ async stopProcess() {
2799
2937
  if (this.process) {
2800
2938
  this.log("Stopping server...");
2801
2939
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -2868,7 +3006,7 @@ var TestServer = class _TestServer {
2868
3006
  * Restart the server
2869
3007
  */
2870
3008
  async restart() {
2871
- await this.stop();
3009
+ await this.stopProcess();
2872
3010
  await this.startProcess();
2873
3011
  }
2874
3012
  /**
@@ -2898,9 +3036,9 @@ var TestServer = class _TestServer {
2898
3036
  PORT: String(this.options.port)
2899
3037
  };
2900
3038
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
2901
- if (this.portRelease) {
2902
- await this.portRelease();
2903
- this.portRelease = null;
3039
+ if (this.portReleaseSocket) {
3040
+ await this.portReleaseSocket();
3041
+ this.portReleaseSocket = null;
2904
3042
  await sleep2(300);
2905
3043
  }
2906
3044
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -2910,9 +3048,8 @@ var TestServer = class _TestServer {
2910
3048
  detached: true,
2911
3049
  stdio: ["pipe", "pipe", "pipe"]
2912
3050
  });
2913
- if (this.process.pid !== void 0) {
2914
- this._info.pid = this.process.pid;
2915
- }
3051
+ this._info.pid = this.process.pid;
3052
+ this._info.shellPid = this.process.pid;
2916
3053
  let processExited = false;
2917
3054
  let exitCode = null;
2918
3055
  let exitError = null;
@@ -3015,6 +3152,8 @@ ${allLogs}`);
3015
3152
  });
3016
3153
  if (response.ok || response.status === 404) {
3017
3154
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
3155
+ const listeningPid = await findListeningPid(this.options.port);
3156
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3018
3157
  return;
3019
3158
  }
3020
3159
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3059,6 +3198,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3059
3198
  function sleep2(ms) {
3060
3199
  return new Promise((resolve) => setTimeout(resolve, ms));
3061
3200
  }
3201
+ function findListeningPid(port) {
3202
+ if (process.platform === "win32") return Promise.resolve(void 0);
3203
+ return new Promise((resolve) => {
3204
+ (0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
3205
+ if (error) {
3206
+ resolve(void 0);
3207
+ return;
3208
+ }
3209
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
3210
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
3211
+ });
3212
+ });
3213
+ }
3062
3214
  function withWorkspaceProtocolFallback(env, cwd) {
3063
3215
  if (findInstalledProtocolPackageDir(cwd)) {
3064
3216
  return env;
@@ -3175,81 +3327,152 @@ function gatewayTokenBinding(serverUrl, entryPath) {
3175
3327
  }
3176
3328
 
3177
3329
  // libs/testing/src/fixtures/test-fixture.ts
3178
- var describe = import_globals.describe;
3179
- var beforeAll = import_globals.beforeAll;
3180
- var beforeEach = import_globals.beforeEach;
3181
- var afterEach = import_globals.afterEach;
3182
- var afterAll = import_globals.afterAll;
3183
- var it = import_globals.it;
3184
- var currentConfig = {};
3185
- var serverInstance = null;
3186
- var tokenFactory = null;
3187
- var serverStartedByUs = false;
3188
- var skipScopes = [{ skipped: false }];
3189
- function currentSkipScope() {
3190
- return skipScopes[skipScopes.length - 1];
3330
+ var jestApi;
3331
+ function jestGlobals() {
3332
+ if (jestApi) return jestApi;
3333
+ try {
3334
+ const api = require("@jest/globals");
3335
+ jestApi = {
3336
+ describe: api["describe"],
3337
+ beforeAll: api["beforeAll"],
3338
+ beforeEach: api["beforeEach"],
3339
+ afterEach: api["afterEach"],
3340
+ afterAll: api["afterAll"],
3341
+ it: api["it"]
3342
+ };
3343
+ return jestApi;
3344
+ } catch (error) {
3345
+ throw new Error(
3346
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
3347
+ { cause: error }
3348
+ );
3349
+ }
3191
3350
  }
3192
- async function initializeSharedResources() {
3193
- if (!serverInstance) {
3194
- if (currentConfig.baseUrl && !currentConfig.server) {
3195
- serverInstance = TestServer.connect(currentConfig.baseUrl);
3196
- serverStartedByUs = false;
3197
- } else if (currentConfig.server) {
3198
- const serverCommand = resolveServerCommand(currentConfig.server);
3199
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3351
+ var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
3352
+ function currentScope() {
3353
+ return scopes[scopes.length - 1];
3354
+ }
3355
+ var serverEntries = /* @__PURE__ */ new Map();
3356
+ function resolveConfig(chain) {
3357
+ const merged = {};
3358
+ for (const scope of chain) {
3359
+ const { env, ...rest } = scope.config;
3360
+ Object.assign(merged, rest);
3361
+ if (env) merged.env = { ...merged.env, ...env };
3362
+ }
3363
+ return merged;
3364
+ }
3365
+ function ownerScopeOf(chain) {
3366
+ for (let i = chain.length - 1; i > 0; i--) {
3367
+ if (chain[i].configured) return chain[i];
3368
+ }
3369
+ return chain[0];
3370
+ }
3371
+ function serverKey(config) {
3372
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
3373
+ return JSON.stringify({
3374
+ server: config.server,
3375
+ baseUrl: config.baseUrl,
3376
+ project: config.project,
3377
+ port: config.port,
3378
+ entryPath: config.entryPath,
3379
+ startupTimeout: config.startupTimeout,
3380
+ logLevel: config.logLevel,
3381
+ auth: config.auth,
3382
+ env
3383
+ });
3384
+ }
3385
+ function serverEnv(config) {
3386
+ const extra = {};
3387
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
3388
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
3389
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
3390
+ return { ...extra, ...config.env };
3391
+ }
3392
+ async function startServerEntry(config, owner) {
3393
+ let server;
3394
+ let startedByUs = false;
3395
+ if (config.baseUrl && !config.server) {
3396
+ server = TestServer.connect(config.baseUrl);
3397
+ } else if (config.server) {
3398
+ const serverCommand = resolveServerCommand(config.server);
3399
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3400
+ if (isDebug) {
3401
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
3402
+ }
3403
+ try {
3404
+ server = await TestServer.start({
3405
+ project: config.project,
3406
+ port: config.port,
3407
+ command: serverCommand,
3408
+ env: serverEnv(config),
3409
+ startupTimeout: config.startupTimeout ?? 3e4,
3410
+ debug: isDebug
3411
+ });
3412
+ startedByUs = true;
3200
3413
  if (isDebug) {
3201
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
3414
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
3202
3415
  }
3203
- try {
3204
- serverInstance = await TestServer.start({
3205
- project: currentConfig.project,
3206
- port: currentConfig.port,
3207
- command: serverCommand,
3208
- env: currentConfig.env,
3209
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
3210
- debug: isDebug
3211
- });
3212
- serverStartedByUs = true;
3213
- if (isDebug) {
3214
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
3215
- }
3216
- } catch (error) {
3217
- const errMsg = error instanceof Error ? error.message : String(error);
3218
- throw new Error(
3219
- `Failed to start test server.
3416
+ } catch (error) {
3417
+ const errMsg = error instanceof Error ? error.message : String(error);
3418
+ throw new Error(
3419
+ `Failed to start test server.
3220
3420
 
3221
- Server entry: ${currentConfig.server}
3222
- Project: ${currentConfig.project ?? "default"}
3421
+ Server entry: ${config.server}
3422
+ Project: ${config.project ?? "default"}
3223
3423
  Command: ${serverCommand}
3224
3424
 
3225
- Error: ${errMsg}`
3226
- );
3227
- }
3228
- } else {
3229
- throw new Error(
3230
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
3425
+ Error: ${errMsg}`,
3426
+ { cause: error }
3231
3427
  );
3232
3428
  }
3429
+ } else {
3430
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
3233
3431
  }
3234
- if (!tokenFactory) {
3235
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
3236
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
3237
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3238
- }
3432
+ const sharedSecret = config.env?.["JWT_SECRET"];
3433
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
3434
+ const tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3435
+ return { server, startedByUs, tokenFactory, owner };
3239
3436
  }
3240
- async function createTestFixtures() {
3241
- await initializeSharedResources();
3242
- if (!serverInstance) {
3243
- throw new Error("Server instance not initialized");
3244
- }
3245
- if (!tokenFactory) {
3246
- throw new Error("Token factory not initialized");
3437
+ function acquireServer(chain) {
3438
+ const config = resolveConfig(chain);
3439
+ const key = serverKey(config);
3440
+ let entry = serverEntries.get(key);
3441
+ if (!entry) {
3442
+ entry = startServerEntry(config, ownerScopeOf(chain));
3443
+ serverEntries.set(key, entry);
3444
+ entry.catch(() => serverEntries.delete(key));
3445
+ }
3446
+ return entry;
3447
+ }
3448
+ async function stopServers(owner) {
3449
+ for (const [key, pending] of Array.from(serverEntries)) {
3450
+ let entry;
3451
+ try {
3452
+ entry = await pending;
3453
+ } catch {
3454
+ serverEntries.delete(key);
3455
+ continue;
3456
+ }
3457
+ if (owner && entry.owner !== owner) continue;
3458
+ serverEntries.delete(key);
3459
+ if (entry.startedByUs) {
3460
+ await entry.server.stop();
3461
+ }
3247
3462
  }
3463
+ }
3464
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
3465
+ async function buildFixtures(chain) {
3466
+ const config = resolveConfig(chain);
3467
+ const entry = await acquireServer(chain);
3468
+ const { server, tokenFactory } = entry;
3469
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
3470
+ const clientConfig = { ...config, publicMode };
3248
3471
  const clientInstance = McpTestClient.create({
3249
- baseUrl: resolveClientBaseUrl(serverInstance),
3250
- entryPath: currentConfig.entryPath,
3251
- transport: currentConfig.transport ?? "streamable-http",
3252
- publicMode: currentConfig.publicMode
3472
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
3473
+ entryPath: clientConfig.entryPath,
3474
+ transport: clientConfig.transport ?? "streamable-http",
3475
+ publicMode
3253
3476
  }).build();
3254
3477
  try {
3255
3478
  await clientInstance.connect();
@@ -3263,17 +3486,24 @@ async function createTestFixtures() {
3263
3486
  throw err;
3264
3487
  }
3265
3488
  }
3266
- const auth = createAuthFixture(tokenFactory);
3267
- const server = createServerFixture(serverInstance);
3268
- return {
3489
+ const context = { extraClients: [] };
3490
+ const fixtures = {
3269
3491
  mcp: clientInstance,
3270
- auth,
3271
- server
3492
+ auth: createAuthFixture(tokenFactory),
3493
+ server: createServerFixture(server, clientConfig, clientInstance, context)
3272
3494
  };
3495
+ fixtureContexts.set(fixtures, context);
3496
+ return fixtures;
3497
+ }
3498
+ async function createTestFixtures() {
3499
+ return buildFixtures(scopes.slice());
3500
+ }
3501
+ async function initializeSharedResources() {
3502
+ await acquireServer(scopes.slice());
3273
3503
  }
3274
3504
  async function cleanupTestFixtures(fixtures, testFailed = false) {
3275
- if (testFailed && serverInstance) {
3276
- const logs = serverInstance.getLogs();
3505
+ if (testFailed) {
3506
+ const logs = fixtures.server.getLogs();
3277
3507
  if (logs.length > 0) {
3278
3508
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
3279
3509
  const recentLogs = logs.slice(-50);
@@ -3284,20 +3514,21 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
3284
3514
  console.error("[TestFixture] === End Server Logs ===\n");
3285
3515
  }
3286
3516
  }
3517
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
3518
+ for (const client of extras) {
3519
+ if (client.isConnected()) {
3520
+ await client.disconnect().catch(() => void 0);
3521
+ }
3522
+ }
3287
3523
  if (fixtures.mcp.isConnected()) {
3288
3524
  await fixtures.mcp.disconnect();
3289
3525
  }
3290
3526
  }
3291
3527
  async function cleanupSharedResources() {
3292
- if (serverInstance && serverStartedByUs) {
3293
- await serverInstance.stop();
3294
- }
3295
- serverInstance = null;
3296
- tokenFactory = null;
3297
- serverStartedByUs = false;
3528
+ await stopServers();
3298
3529
  }
3299
- function resolveClientBaseUrl(server) {
3300
- return currentConfig.baseUrl ?? server.info.baseUrl;
3530
+ function resolveClientBaseUrl(server, config) {
3531
+ return config.baseUrl ?? server.info.baseUrl;
3301
3532
  }
3302
3533
  function createAuthFixture(factory) {
3303
3534
  const users = {
@@ -3343,28 +3574,39 @@ function createAuthFixture(factory) {
3343
3574
  getAudience: () => factory.getAudience()
3344
3575
  };
3345
3576
  }
3346
- function createServerFixture(server) {
3577
+ function createServerFixture(server, config, mcp, context) {
3347
3578
  return {
3348
- info: server.info,
3579
+ // Live view: `pid` is filled in once the server is listening and follows restarts
3580
+ get info() {
3581
+ return server.info;
3582
+ },
3349
3583
  createClient: async (opts) => {
3350
- return McpTestClient.create({
3351
- baseUrl: resolveClientBaseUrl(server),
3352
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
3584
+ const client = await McpTestClient.create({
3585
+ baseUrl: resolveClientBaseUrl(server, config),
3586
+ entryPath: opts?.entryPath ?? config.entryPath,
3353
3587
  transport: opts?.transport ?? "streamable-http",
3354
3588
  auth: opts?.token ? { token: opts.token } : void 0,
3355
3589
  clientInfo: opts?.clientInfo,
3356
- publicMode: currentConfig.publicMode
3590
+ publicMode: config.publicMode
3357
3591
  }).buildAndConnect();
3592
+ context.extraClients.push(client);
3593
+ return client;
3358
3594
  },
3359
3595
  createClientBuilder: () => {
3360
3596
  const builder = new McpTestClientBuilder({
3361
- baseUrl: resolveClientBaseUrl(server),
3362
- entryPath: currentConfig.entryPath,
3363
- publicMode: currentConfig.publicMode
3597
+ baseUrl: resolveClientBaseUrl(server, config),
3598
+ entryPath: config.entryPath,
3599
+ publicMode: config.publicMode
3364
3600
  });
3365
3601
  return builder;
3366
3602
  },
3367
- restart: () => server.restart(),
3603
+ restart: async () => {
3604
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
3605
+ await server.restart();
3606
+ for (const client of reconnect) {
3607
+ await client.reconnect();
3608
+ }
3609
+ },
3368
3610
  getLogs: () => server.getLogs(),
3369
3611
  clearLogs: () => server.clearLogs()
3370
3612
  };
@@ -3375,12 +3617,12 @@ function resolveServerCommand(server) {
3375
3617
  }
3376
3618
  return `npx tsx ${server}`;
3377
3619
  }
3378
- function runWithFixtures(fn) {
3379
- return async () => {
3380
- const fixtures = await createTestFixtures();
3620
+ function runWithFixtures(fn, chain) {
3621
+ return async (...extra) => {
3622
+ const fixtures = await buildFixtures(chain);
3381
3623
  let testFailed = false;
3382
3624
  try {
3383
- await fn(fixtures);
3625
+ await fn(fixtures, ...extra);
3384
3626
  } catch (error) {
3385
3627
  testFailed = true;
3386
3628
  throw error;
@@ -3389,24 +3631,37 @@ function runWithFixtures(fn) {
3389
3631
  }
3390
3632
  };
3391
3633
  }
3634
+ function skipTitle(name, scope) {
3635
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
3636
+ }
3392
3637
  function testWithFixtures(name, fn) {
3393
- const scope = currentSkipScope();
3638
+ const chain = scopes.slice();
3639
+ const scope = currentScope();
3394
3640
  if (scope.skipped) {
3395
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
3641
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
3396
3642
  return;
3397
3643
  }
3398
- it(name, runWithFixtures(fn));
3644
+ jestGlobals().it(name, runWithFixtures(fn, chain));
3399
3645
  }
3400
3646
  function use(config) {
3401
- currentConfig = { ...currentConfig, ...config };
3402
- afterAll(async () => {
3403
- await cleanupSharedResources();
3404
- });
3647
+ const scope = currentScope();
3648
+ scope.config = {
3649
+ ...scope.config,
3650
+ ...config,
3651
+ env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
3652
+ };
3653
+ scope.configured = true;
3654
+ if (!scope.cleanupRegistered) {
3655
+ scope.cleanupRegistered = true;
3656
+ jestGlobals().afterAll(async () => {
3657
+ await stopServers(scope === scopes[0] ? void 0 : scope);
3658
+ });
3659
+ }
3405
3660
  }
3406
3661
  function skip(nameOrCondition, fnOrReason) {
3407
3662
  if (typeof nameOrCondition === "boolean") {
3408
3663
  if (nameOrCondition) {
3409
- const scope = currentSkipScope();
3664
+ const scope = currentScope();
3410
3665
  scope.skipped = true;
3411
3666
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
3412
3667
  }
@@ -3417,53 +3672,90 @@ function skip(nameOrCondition, fnOrReason) {
3417
3672
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
3418
3673
  );
3419
3674
  }
3420
- it.skip(nameOrCondition, runWithFixtures(fnOrReason));
3675
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
3421
3676
  }
3422
3677
  function only(name, fn) {
3423
- const scope = currentSkipScope();
3678
+ const scope = currentScope();
3424
3679
  if (scope.skipped) {
3425
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
3680
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
3426
3681
  return;
3427
3682
  }
3428
- it.only(name, runWithFixtures(fn));
3683
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
3429
3684
  }
3430
3685
  function todo(name) {
3431
- it.todo(name);
3686
+ jestGlobals().it.todo(name);
3432
3687
  }
3433
- function withSkipScope(register) {
3688
+ function each(table, ...tagged) {
3689
+ const chain = scopes.slice();
3690
+ const register = jestGlobals().it.each(table, ...tagged);
3434
3691
  return (name, fn) => {
3435
- register(name, () => {
3436
- const parent = currentSkipScope();
3437
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
3438
- try {
3439
- fn();
3440
- } finally {
3441
- skipScopes.pop();
3442
- }
3692
+ const scope = currentScope();
3693
+ if (scope.skipped) {
3694
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
3695
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
3696
+ return;
3697
+ }
3698
+ register(name, runWithFixtures(fn, chain));
3699
+ };
3700
+ }
3701
+ function withScope(fn) {
3702
+ return (...args) => {
3703
+ const parent = currentScope();
3704
+ scopes.push({
3705
+ skipped: parent.skipped,
3706
+ reason: parent.reason,
3707
+ config: {},
3708
+ configured: false,
3709
+ cleanupRegistered: false
3443
3710
  });
3711
+ try {
3712
+ fn(...args);
3713
+ } finally {
3714
+ scopes.pop();
3715
+ }
3444
3716
  };
3445
3717
  }
3446
- function eachWithSkipScope(each) {
3447
- return ((...eachArgs) => {
3448
- const register = each(...eachArgs);
3449
- return withSkipScope(register);
3450
- });
3718
+ function scopedRegister(pick) {
3719
+ return (name, fn, timeout) => {
3720
+ pick()(name, withScope(fn), timeout);
3721
+ };
3451
3722
  }
3452
- var describeWithSkipScope = Object.assign(withSkipScope(describe), describe, {
3453
- only: Object.assign(withSkipScope(describe.only), { each: eachWithSkipScope(describe.only.each) }),
3454
- skip: Object.assign(withSkipScope(describe.skip), { each: eachWithSkipScope(describe.skip.each) }),
3455
- each: eachWithSkipScope(describe.each)
3456
- });
3723
+ function scopedEach(pick) {
3724
+ return (...eachArgs) => (name, fn, timeout) => {
3725
+ const register = pick()(...eachArgs);
3726
+ register(name, withScope(fn), timeout);
3727
+ };
3728
+ }
3729
+ var describeWithScope = Object.assign(
3730
+ scopedRegister(() => jestGlobals().describe),
3731
+ {
3732
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
3733
+ only: Object.assign(
3734
+ scopedRegister(() => jestGlobals().describe.only),
3735
+ {
3736
+ each: scopedEach(() => jestGlobals().describe.only.each)
3737
+ }
3738
+ ),
3739
+ skip: Object.assign(
3740
+ scopedRegister(() => jestGlobals().describe.skip),
3741
+ {
3742
+ each: scopedEach(() => jestGlobals().describe.skip.each)
3743
+ }
3744
+ ),
3745
+ each: scopedEach(() => jestGlobals().describe.each)
3746
+ }
3747
+ );
3457
3748
  var test = testWithFixtures;
3458
3749
  test.use = use;
3459
- test.describe = describeWithSkipScope;
3460
- test.beforeAll = beforeAll;
3461
- test.beforeEach = beforeEach;
3462
- test.afterEach = afterEach;
3463
- test.afterAll = afterAll;
3750
+ test.describe = describeWithScope;
3751
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
3752
+ test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
3753
+ test.afterEach = ((...args) => jestGlobals().afterEach(...args));
3754
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
3464
3755
  test.skip = skip;
3465
3756
  test.only = only;
3466
3757
  test.todo = todo;
3758
+ test.each = each;
3467
3759
  // Annotate the CommonJS export names for ESM import in node:
3468
3760
  0 && (module.exports = {
3469
3761
  cleanupSharedResources,