@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/index.js CHANGED
@@ -460,6 +460,32 @@ var interceptors = {
460
460
  }
461
461
  };
462
462
 
463
+ // libs/testing/src/transport/error-utils.ts
464
+ function errorName(err) {
465
+ if (typeof err !== "object" || err === null) return void 0;
466
+ const name = err.name;
467
+ return typeof name === "string" ? name : void 0;
468
+ }
469
+ function isAbortError(err) {
470
+ return errorName(err) === "AbortError";
471
+ }
472
+ function errorMessage(err, fallback = "Unknown error") {
473
+ if (typeof err === "string" && err) return err;
474
+ if (typeof err === "object" && err !== null) {
475
+ const message = err.message;
476
+ if (typeof message === "string" && message) return message;
477
+ }
478
+ return fallback;
479
+ }
480
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
481
+ function markInterceptedError(err) {
482
+ interceptedErrors.add(err);
483
+ return err;
484
+ }
485
+ function isInterceptedError(err) {
486
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
487
+ }
488
+
463
489
  // libs/testing/src/transport/streamable-http.transport.ts
464
490
  var DEFAULT_TIMEOUT = 3e4;
465
491
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
@@ -568,7 +594,7 @@ var StreamableHttpTransport = class {
568
594
  return mockResponse2;
569
595
  }
570
596
  case "error":
571
- throw interceptResult.error;
597
+ throw markInterceptedError(interceptResult.error);
572
598
  case "continue":
573
599
  message = interceptResult.request;
574
600
  break;
@@ -629,7 +655,7 @@ var StreamableHttpTransport = class {
629
655
  return jsonResponse;
630
656
  } catch (error) {
631
657
  clearTimeout(timeoutId);
632
- if (error instanceof Error && error.name === "AbortError") {
658
+ if (isAbortError(error)) {
633
659
  return {
634
660
  jsonrpc: "2.0",
635
661
  id: message.id ?? null,
@@ -668,7 +694,7 @@ var StreamableHttpTransport = class {
668
694
  }
669
695
  } catch (error) {
670
696
  clearTimeout(timeoutId);
671
- if (error instanceof Error && error.name !== "AbortError") {
697
+ if (!isAbortError(error)) {
672
698
  throw error;
673
699
  }
674
700
  }
@@ -709,7 +735,7 @@ var StreamableHttpTransport = class {
709
735
  error: {
710
736
  code: -32700,
711
737
  message: "Parse error",
712
- data: error instanceof Error ? error.message : "Unknown error"
738
+ data: errorMessage(error)
713
739
  }
714
740
  };
715
741
  }
@@ -1079,7 +1105,7 @@ var StreamableHttpTransport = class {
1079
1105
  if (this.config.clientInfo) {
1080
1106
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1081
1107
  }
1082
- if (this.authToken && !this.publicMode) {
1108
+ if (this.authToken) {
1083
1109
  headers["Authorization"] = `Bearer ${this.authToken}`;
1084
1110
  }
1085
1111
  if (this.sessionId) {
@@ -1421,6 +1447,16 @@ var DEFAULT_CLIENT_INFO = {
1421
1447
  name: "@frontmcp/testing",
1422
1448
  version: "0.4.0"
1423
1449
  };
1450
+ var MAX_LIST_PAGES = 1e3;
1451
+ function isAuthFailure(error) {
1452
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1453
+ }
1454
+ function describeInitializeError(error) {
1455
+ const message = error?.message ?? "Unknown error";
1456
+ const data = error?.data;
1457
+ const detail = typeof data === "string" ? data.trim() : "";
1458
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1459
+ }
1424
1460
  function extractEntryPaths(data) {
1425
1461
  let payload = data;
1426
1462
  if (typeof payload === "string") {
@@ -1507,19 +1543,22 @@ var McpTestClient = class {
1507
1543
  async connect() {
1508
1544
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1509
1545
  this.triedDiscoveredEntryPath = false;
1546
+ this.initResult = null;
1510
1547
  this.transport = this.createTransport();
1511
1548
  await this.transport.connect();
1512
1549
  const maxRetries = 3;
1513
1550
  const retryDelayMs = 500;
1514
1551
  let lastError;
1515
1552
  let reportedEntryPaths;
1553
+ let attemptsMade = 0;
1516
1554
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1555
+ attemptsMade = attempt;
1517
1556
  const initResponse = await this.initialize();
1518
1557
  if (initResponse.success && initResponse.data) {
1519
1558
  this.initResult = initResponse.data;
1520
1559
  break;
1521
1560
  }
1522
- lastError = initResponse.error?.message ?? "Unknown error";
1561
+ lastError = describeInitializeError(initResponse.error);
1523
1562
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1524
1563
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1525
1564
  this.triedDiscoveredEntryPath = true;
@@ -1531,6 +1570,7 @@ var McpTestClient = class {
1531
1570
  await this.transport.connect();
1532
1571
  continue;
1533
1572
  }
1573
+ if (isAuthFailure(initResponse.error)) break;
1534
1574
  if (attempt < maxRetries) {
1535
1575
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1536
1576
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1539,7 +1579,7 @@ var McpTestClient = class {
1539
1579
  if (!this.initResult) {
1540
1580
  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.` : "";
1541
1581
  throw new Error(
1542
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1582
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1543
1583
  );
1544
1584
  }
1545
1585
  this._sessionId = this.transport.getSessionId();
@@ -1609,18 +1649,47 @@ var McpTestClient = class {
1609
1649
  return this._authState;
1610
1650
  }
1611
1651
  /**
1612
- * Authenticate with a token
1652
+ * Authenticate with a token.
1653
+ *
1654
+ * When connected, this opens a NEW session with the token (the previous session was created
1655
+ * for the previous identity) and rejects if the server refuses it — for example because the
1656
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
1657
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1613
1658
  */
1614
1659
  async authenticate(token) {
1660
+ if (typeof token !== "string" || token.trim() === "") {
1661
+ throw new Error("authenticate() requires a non-empty token");
1662
+ }
1663
+ const previousAuthState = this._authState;
1664
+ const previousConfigAuth = this.config.auth;
1665
+ this.config.auth = { ...previousConfigAuth, token };
1615
1666
  this._authState = {
1616
1667
  isAnonymous: false,
1617
1668
  token,
1618
1669
  scopes: this.parseScopesFromToken(token),
1619
1670
  user: this.parseUserFromToken(token)
1620
1671
  };
1621
- if (this.transport) {
1622
- this.transport.setAuthToken(token);
1672
+ const previousTransport = this.transport;
1673
+ if (!previousTransport) {
1674
+ this.log("debug", "Authentication updated (not connected yet)");
1675
+ return;
1676
+ }
1677
+ const previousInit = this.initResult;
1678
+ const previousSessionId = this._sessionId;
1679
+ const previousSessionInfo = this._sessionInfo;
1680
+ try {
1681
+ await this.connect();
1682
+ } catch (err) {
1683
+ await this.transport?.close().catch(() => void 0);
1684
+ this.transport = previousTransport;
1685
+ this.initResult = previousInit;
1686
+ this._sessionId = previousSessionId;
1687
+ this._sessionInfo = previousSessionInfo;
1688
+ this.config.auth = previousConfigAuth;
1689
+ this._authState = previousAuthState;
1690
+ throw err;
1623
1691
  }
1692
+ await previousTransport.close().catch(() => void 0);
1624
1693
  this.log("debug", "Authentication updated");
1625
1694
  }
1626
1695
  // ═══════════════════════════════════════════════════════════════════
@@ -1654,13 +1723,12 @@ var McpTestClient = class {
1654
1723
  /**
1655
1724
  * List all available tools
1656
1725
  */
1657
- list: async () => {
1658
- const response = await this.listTools();
1659
- if (!response.success || !response.data) {
1660
- throw new Error(`Failed to list tools: ${response.error?.message}`);
1661
- }
1662
- return response.data.tools;
1663
- },
1726
+ list: async () => this.listAllPages(
1727
+ "tools/list",
1728
+ "tools",
1729
+ (cursor) => this.listTools(cursor),
1730
+ (page) => page.tools
1731
+ ),
1664
1732
  /**
1665
1733
  * Call a tool by name with arguments
1666
1734
  */
@@ -1676,23 +1744,21 @@ var McpTestClient = class {
1676
1744
  /**
1677
1745
  * List all static resources
1678
1746
  */
1679
- list: async () => {
1680
- const response = await this.listResources();
1681
- if (!response.success || !response.data) {
1682
- throw new Error(`Failed to list resources: ${response.error?.message}`);
1683
- }
1684
- return response.data.resources;
1685
- },
1747
+ list: async () => this.listAllPages(
1748
+ "resources/list",
1749
+ "resources",
1750
+ (cursor) => this.listResources(cursor),
1751
+ (page) => page.resources
1752
+ ),
1686
1753
  /**
1687
1754
  * List all resource templates
1688
1755
  */
1689
- listTemplates: async () => {
1690
- const response = await this.listResourceTemplates();
1691
- if (!response.success || !response.data) {
1692
- throw new Error(`Failed to list resource templates: ${response.error?.message}`);
1693
- }
1694
- return response.data.resourceTemplates;
1695
- },
1756
+ listTemplates: async () => this.listAllPages(
1757
+ "resources/templates/list",
1758
+ "resource templates",
1759
+ (cursor) => this.listResourceTemplates(cursor),
1760
+ (page) => page.resourceTemplates
1761
+ ),
1696
1762
  /**
1697
1763
  * Read a resource by URI
1698
1764
  */
@@ -1720,13 +1786,12 @@ var McpTestClient = class {
1720
1786
  /**
1721
1787
  * List all available prompts
1722
1788
  */
1723
- list: async () => {
1724
- const response = await this.listPrompts();
1725
- if (!response.success || !response.data) {
1726
- throw new Error(`Failed to list prompts: ${response.error?.message}`);
1727
- }
1728
- return response.data.prompts;
1729
- },
1789
+ list: async () => this.listAllPages(
1790
+ "prompts/list",
1791
+ "prompts",
1792
+ (cursor) => this.listPrompts(cursor),
1793
+ (page) => page.prompts
1794
+ ),
1730
1795
  /**
1731
1796
  * Get a prompt with arguments
1732
1797
  */
@@ -2110,8 +2175,33 @@ var McpTestClient = class {
2110
2175
  clientInfo: this.config.clientInfo
2111
2176
  });
2112
2177
  }
2113
- async listTools() {
2114
- return this.request("tools/list", {});
2178
+ async listTools(cursor) {
2179
+ return this.request("tools/list", cursor ? { cursor } : {});
2180
+ }
2181
+ /**
2182
+ * Follow `nextCursor` until the server stops returning one, concatenating the
2183
+ * pages. A server that hands back a cursor it already returned, or never stops
2184
+ * paging, fails the call rather than looping.
2185
+ */
2186
+ async listAllPages(method, label, fetchPage, itemsOf) {
2187
+ const items = [];
2188
+ const seenCursors = /* @__PURE__ */ new Set();
2189
+ let cursor;
2190
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
2191
+ const response = await fetchPage(cursor);
2192
+ if (!response.success || !response.data) {
2193
+ throw new Error(`Failed to list ${label}: ${response.error?.message}`);
2194
+ }
2195
+ items.push(...itemsOf(response.data) ?? []);
2196
+ const next = response.data.nextCursor;
2197
+ if (!next) return items;
2198
+ if (seenCursors.has(next)) {
2199
+ throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`);
2200
+ }
2201
+ seenCursors.add(next);
2202
+ cursor = next;
2203
+ }
2204
+ throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`);
2115
2205
  }
2116
2206
  async callTool(name, args, progressToken) {
2117
2207
  return this.request("tools/call", {
@@ -2136,17 +2226,17 @@ var McpTestClient = class {
2136
2226
  timestamp
2137
2227
  });
2138
2228
  };
2139
- async listResources() {
2140
- return this.request("resources/list", {});
2229
+ async listResources(cursor) {
2230
+ return this.request("resources/list", cursor ? { cursor } : {});
2141
2231
  }
2142
- async listResourceTemplates() {
2143
- return this.request("resources/templates/list", {});
2232
+ async listResourceTemplates(cursor) {
2233
+ return this.request("resources/templates/list", cursor ? { cursor } : {});
2144
2234
  }
2145
2235
  async readResource(uri) {
2146
2236
  return this.request("resources/read", { uri });
2147
2237
  }
2148
- async listPrompts() {
2149
- return this.request("prompts/list", {});
2238
+ async listPrompts(cursor) {
2239
+ return this.request("prompts/list", cursor ? { cursor } : {});
2150
2240
  }
2151
2241
  async getPrompt(name, args) {
2152
2242
  return this.request("prompts/get", {
@@ -2223,10 +2313,11 @@ var McpTestClient = class {
2223
2313
  requestId: id
2224
2314
  };
2225
2315
  } catch (err) {
2316
+ if (isInterceptedError(err)) throw err;
2226
2317
  const durationMs = Date.now() - start;
2227
2318
  const error = {
2228
2319
  code: -32603,
2229
- message: err instanceof Error ? err.message : "Unknown error"
2320
+ message: errorMessage(err)
2230
2321
  };
2231
2322
  return {
2232
2323
  success: false,
@@ -2374,12 +2465,12 @@ var McpTestClient = class {
2374
2465
  const payload = this.decodeJwtPayload(token);
2375
2466
  if (!payload) return [];
2376
2467
  const scope = payload["scope"];
2377
- const scopes = payload["scopes"];
2468
+ const scopes2 = payload["scopes"];
2378
2469
  if (typeof scope === "string") {
2379
2470
  return scope.split(" ");
2380
2471
  }
2381
- if (Array.isArray(scopes)) {
2382
- return scopes;
2472
+ if (Array.isArray(scopes2)) {
2473
+ return scopes2;
2383
2474
  }
2384
2475
  return [];
2385
2476
  } catch {
@@ -2511,14 +2602,15 @@ var TestTokenFactory = class {
2511
2602
  */
2512
2603
  async createTestToken(options) {
2513
2604
  await this.ensureKeys();
2514
- const now = Math.floor(Date.now() / 1e3);
2605
+ const nowMs = Date.now();
2606
+ const iat = Math.floor(nowMs / 1e3);
2515
2607
  const exp = options.exp ?? 3600;
2516
2608
  const payload = {
2517
2609
  iss: options.iss ?? this.issuer,
2518
2610
  sub: options.sub,
2519
2611
  aud: options.aud ?? this.audience,
2520
- iat: now,
2521
- exp: now + exp,
2612
+ iat,
2613
+ exp: Math.ceil(nowMs / 1e3) + exp,
2522
2614
  scope: options.scopes?.join(" "),
2523
2615
  ...options.claims
2524
2616
  };
@@ -2543,10 +2635,10 @@ var TestTokenFactory = class {
2543
2635
  /**
2544
2636
  * Create a regular user token
2545
2637
  */
2546
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
2638
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
2547
2639
  return this.createTestToken({
2548
2640
  sub,
2549
- scopes,
2641
+ scopes: scopes2,
2550
2642
  claims: {
2551
2643
  email: "user@test.local",
2552
2644
  name: "Test User",
@@ -2557,10 +2649,11 @@ var TestTokenFactory = class {
2557
2649
  /**
2558
2650
  * Create an anonymous user token
2559
2651
  */
2560
- async createAnonymousToken() {
2652
+ async createAnonymousToken(expiresIn) {
2561
2653
  return this.createTestToken({
2562
2654
  sub: `anon:${Date.now()}`,
2563
2655
  scopes: ["anonymous"],
2656
+ exp: expiresIn,
2564
2657
  claims: {
2565
2658
  name: "Anonymous",
2566
2659
  role: "anonymous"
@@ -2771,17 +2864,7 @@ function createTestUser(overrides) {
2771
2864
 
2772
2865
  // libs/testing/src/auth/mock-oauth-server.ts
2773
2866
  var import_http = require("http");
2774
- var _randomBytes;
2775
- var _sha256Base64url;
2776
- var _base64urlEncode;
2777
- async function loadCryptoUtils() {
2778
- if (!_randomBytes) {
2779
- const utils = await import("@frontmcp/utils");
2780
- _randomBytes = utils.randomBytes;
2781
- _sha256Base64url = utils.sha256Base64url;
2782
- _base64urlEncode = utils.base64urlEncode;
2783
- }
2784
- }
2867
+ var import_utils = require("@frontmcp/utils");
2785
2868
  var MockOAuthServer = class {
2786
2869
  tokenFactory;
2787
2870
  options;
@@ -2796,8 +2879,8 @@ var MockOAuthServer = class {
2796
2879
  accessTokenTtlSeconds;
2797
2880
  /** Refresh token TTL in seconds */
2798
2881
  refreshTokenTtlSeconds;
2799
- constructor(tokenFactory3, options = {}) {
2800
- this.tokenFactory = tokenFactory3;
2882
+ constructor(tokenFactory2, options = {}) {
2883
+ this.tokenFactory = tokenFactory2;
2801
2884
  this.options = options;
2802
2885
  this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
2803
2886
  this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
@@ -3054,14 +3137,14 @@ ${error.stack}` : String(error);
3054
3137
  return;
3055
3138
  }
3056
3139
  const code = this.generateCode();
3057
- const scopes = scope.split(" ").filter(Boolean);
3140
+ const scopes2 = scope.split(" ").filter(Boolean);
3058
3141
  this.authCodes.set(code, {
3059
3142
  code,
3060
3143
  clientId,
3061
3144
  redirectUri,
3062
3145
  codeChallenge,
3063
3146
  codeChallengeMethod,
3064
- scopes,
3147
+ scopes: scopes2,
3065
3148
  user: testUser,
3066
3149
  state,
3067
3150
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3101,7 +3184,7 @@ ${error.stack}` : String(error);
3101
3184
  }
3102
3185
  const grantType = params.get("grant_type");
3103
3186
  if (grantType === "anonymous") {
3104
- const token = await this.tokenFactory.createAnonymousToken();
3187
+ const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
3105
3188
  res.writeHead(200, { "Content-Type": "application/json" });
3106
3189
  res.end(
3107
3190
  JSON.stringify({
@@ -3233,6 +3316,8 @@ ${error.stack}` : String(error);
3233
3316
  codeRecord.used = true;
3234
3317
  const accessToken = await this.tokenFactory.createTestToken({
3235
3318
  sub: codeRecord.user.sub,
3319
+ scopes: codeRecord.scopes,
3320
+ exp: this.accessTokenTtlSeconds,
3236
3321
  claims: {
3237
3322
  email: codeRecord.user.email,
3238
3323
  name: codeRecord.user.name,
@@ -3241,6 +3326,7 @@ ${error.stack}` : String(error);
3241
3326
  });
3242
3327
  const idToken = await this.tokenFactory.createTestToken({
3243
3328
  sub: codeRecord.user.sub,
3329
+ exp: this.accessTokenTtlSeconds,
3244
3330
  claims: {
3245
3331
  email: codeRecord.user.email,
3246
3332
  name: codeRecord.user.name,
@@ -3318,6 +3404,8 @@ ${error.stack}` : String(error);
3318
3404
  }
3319
3405
  const accessToken = await this.tokenFactory.createTestToken({
3320
3406
  sub: tokenRecord.user.sub,
3407
+ scopes: tokenRecord.scopes,
3408
+ exp: this.accessTokenTtlSeconds,
3321
3409
  claims: {
3322
3410
  email: tokenRecord.user.email,
3323
3411
  name: tokenRecord.user.name,
@@ -3422,14 +3510,14 @@ ${error.stack}` : String(error);
3422
3510
  name
3423
3511
  };
3424
3512
  const code = this.generateCode();
3425
- const scopes = scope.split(" ").filter(Boolean);
3513
+ const scopes2 = scope.split(" ").filter(Boolean);
3426
3514
  this.authCodes.set(code, {
3427
3515
  code,
3428
3516
  clientId,
3429
3517
  redirectUri,
3430
3518
  codeChallenge,
3431
3519
  codeChallengeMethod,
3432
- scopes,
3520
+ scopes: scopes2,
3433
3521
  user: testUser,
3434
3522
  state,
3435
3523
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3535,8 +3623,7 @@ ${error.stack}` : String(error);
3535
3623
  * Generate a random authorization code
3536
3624
  */
3537
3625
  async generateCodeAsync() {
3538
- await loadCryptoUtils();
3539
- return _base64urlEncode(_randomBytes(32));
3626
+ return (0, import_utils.base64urlEncode)((0, import_utils.randomBytes)(32));
3540
3627
  }
3541
3628
  /**
3542
3629
  * Generate a random authorization code (sync wrapper for compatibility)
@@ -3552,8 +3639,7 @@ ${error.stack}` : String(error);
3552
3639
  */
3553
3640
  async computeCodeChallengeAsync(verifier, method) {
3554
3641
  if (method === "S256") {
3555
- await loadCryptoUtils();
3556
- return _sha256Base64url(verifier);
3642
+ return (0, import_utils.sha256Base64url)(verifier);
3557
3643
  }
3558
3644
  return verifier;
3559
3645
  }
@@ -4141,7 +4227,7 @@ var MockCimdServer = class {
4141
4227
 
4142
4228
  // libs/testing/src/server/test-server.ts
4143
4229
  var import_child_process = require("child_process");
4144
- var import_utils = require("@frontmcp/utils");
4230
+ var import_utils2 = require("@frontmcp/utils");
4145
4231
 
4146
4232
  // libs/testing/src/errors/index.ts
4147
4233
  var TestClientError = class extends Error {
@@ -4247,6 +4333,8 @@ var E2E_PORT_RANGES = {
4247
4333
  "demo-e2e-distributed": { start: 50440, size: 20 },
4248
4334
  // Protocol revision E2E tests (50460-50479)
4249
4335
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
4336
+ // Testing library E2E tests (50480-50499)
4337
+ "demo-e2e-testing": { start: 50480, size: 20 },
4250
4338
  // Mock servers and utilities (50900-50999)
4251
4339
  "mock-oauth": { start: 50900, size: 10 },
4252
4340
  "mock-api": { start: 50910, size: 10 },
@@ -4255,6 +4343,66 @@ var E2E_PORT_RANGES = {
4255
4343
  default: { start: 51e3, size: 100 }
4256
4344
  };
4257
4345
  var reservedPorts = /* @__PURE__ */ new Map();
4346
+ var lockedPorts = /* @__PURE__ */ new Set();
4347
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
4348
+ function lockDir() {
4349
+ try {
4350
+ const os = require("node:os");
4351
+ const path = require("node:path");
4352
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
4353
+ } catch {
4354
+ return void 0;
4355
+ }
4356
+ }
4357
+ function pidIsAlive(pid) {
4358
+ try {
4359
+ process.kill(pid, 0);
4360
+ return true;
4361
+ } catch (error) {
4362
+ return error.code === "EPERM";
4363
+ }
4364
+ }
4365
+ function acquirePortLock(port) {
4366
+ if (lockedPorts.has(port)) return false;
4367
+ let fs;
4368
+ let file;
4369
+ try {
4370
+ fs = require("node:fs");
4371
+ const dir = lockDir();
4372
+ if (!dir) return true;
4373
+ fs.mkdirSync(dir, { recursive: true });
4374
+ file = require("node:path").join(dir, `${port}.lock`);
4375
+ } catch {
4376
+ return true;
4377
+ }
4378
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
4379
+ for (let attempt = 0; attempt < 2; attempt++) {
4380
+ try {
4381
+ fs.writeFileSync(file, payload, { flag: "wx" });
4382
+ lockedPorts.add(port);
4383
+ return true;
4384
+ } catch (error) {
4385
+ if (error.code !== "EEXIST") return true;
4386
+ try {
4387
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
4388
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
4389
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
4390
+ fs.unlinkSync(file);
4391
+ } catch {
4392
+ }
4393
+ }
4394
+ }
4395
+ return false;
4396
+ }
4397
+ function releasePortLock(port) {
4398
+ if (!lockedPorts.delete(port)) return;
4399
+ try {
4400
+ const fs = require("node:fs");
4401
+ const dir = lockDir();
4402
+ if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
4403
+ } catch {
4404
+ }
4405
+ }
4258
4406
  var projectPortIndex = /* @__PURE__ */ new Map();
4259
4407
  function getPortRange(project) {
4260
4408
  const key = project;
@@ -4265,15 +4413,22 @@ function getPortRange(project) {
4265
4413
  }
4266
4414
  async function reservePort(project, preferredPort) {
4267
4415
  const range = getPortRange(project);
4268
- if (preferredPort !== void 0) {
4416
+ const handle = (port) => ({
4417
+ port,
4418
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
4419
+ releaseSocket: async () => {
4420
+ await releasePort(port);
4421
+ },
4422
+ release: async () => {
4423
+ await releasePort(port);
4424
+ releasePortLock(port);
4425
+ }
4426
+ });
4427
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
4428
+ if (wantsSpecificPort) {
4269
4429
  const reservation = await tryReservePort(preferredPort, project);
4270
4430
  if (reservation) {
4271
- return {
4272
- port: preferredPort,
4273
- release: async () => {
4274
- await releasePort(preferredPort);
4275
- }
4276
- };
4431
+ return handle(preferredPort);
4277
4432
  }
4278
4433
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
4279
4434
  }
@@ -4282,30 +4437,19 @@ async function reservePort(project, preferredPort) {
4282
4437
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
4283
4438
  const port = range.start + index % range.size;
4284
4439
  index = (index + 1) % range.size;
4285
- if (reservedPorts.has(port)) {
4440
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
4286
4441
  continue;
4287
4442
  }
4288
4443
  const reservation = await tryReservePort(port, project);
4289
4444
  if (reservation) {
4290
4445
  projectPortIndex.set(project, index);
4291
- return {
4292
- port,
4293
- release: async () => {
4294
- await releasePort(port);
4295
- }
4296
- };
4446
+ return handle(port);
4297
4447
  }
4298
4448
  }
4299
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
4300
- if (dynamicPort) {
4301
- const reservation = await tryReservePort(dynamicPort, project);
4302
- if (reservation) {
4303
- return {
4304
- port: dynamicPort,
4305
- release: async () => {
4306
- await releasePort(dynamicPort);
4307
- }
4308
- };
4449
+ for (let port = 51e3; port < 52e3; port++) {
4450
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
4451
+ if (await tryReservePort(port, project)) {
4452
+ return handle(port);
4309
4453
  }
4310
4454
  }
4311
4455
  throw new Error(
@@ -4313,9 +4457,13 @@ async function reservePort(project, preferredPort) {
4313
4457
  );
4314
4458
  }
4315
4459
  async function tryReservePort(port, project) {
4460
+ if (!acquirePortLock(port)) {
4461
+ return false;
4462
+ }
4316
4463
  return new Promise((resolve) => {
4317
4464
  const server = (0, import_net.createServer)();
4318
4465
  server.once("error", () => {
4466
+ releasePortLock(port);
4319
4467
  resolve(false);
4320
4468
  });
4321
4469
  server.listen(port, () => {
@@ -4341,31 +4489,6 @@ async function releasePort(port) {
4341
4489
  });
4342
4490
  });
4343
4491
  }
4344
- async function findAvailablePortInRange(start, end) {
4345
- for (let port = start; port < end; port++) {
4346
- if (reservedPorts.has(port)) {
4347
- continue;
4348
- }
4349
- const available = await isPortAvailable(port);
4350
- if (available) {
4351
- return port;
4352
- }
4353
- }
4354
- return null;
4355
- }
4356
- async function isPortAvailable(port) {
4357
- return new Promise((resolve) => {
4358
- const server = (0, import_net.createServer)();
4359
- server.once("error", () => {
4360
- resolve(false);
4361
- });
4362
- server.listen(port, () => {
4363
- server.close(() => {
4364
- resolve(true);
4365
- });
4366
- });
4367
- });
4368
- }
4369
4492
 
4370
4493
  // libs/testing/src/server/test-server.ts
4371
4494
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -4374,8 +4497,11 @@ var TestServer = class _TestServer {
4374
4497
  options;
4375
4498
  _info;
4376
4499
  logs = [];
4500
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
4377
4501
  portRelease = null;
4378
- constructor(options, port, portRelease) {
4502
+ /** Frees only the reservation socket, just before the server is spawned */
4503
+ portReleaseSocket = null;
4504
+ constructor(options, port, portRelease, portReleaseSocket) {
4379
4505
  this.options = {
4380
4506
  port,
4381
4507
  project: options.project,
@@ -4387,6 +4513,7 @@ var TestServer = class _TestServer {
4387
4513
  debug: options.debug ?? DEBUG_SERVER
4388
4514
  };
4389
4515
  this.portRelease = portRelease ?? null;
4516
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
4390
4517
  this._info = {
4391
4518
  baseUrl: `http://localhost:${port}`,
4392
4519
  port
@@ -4399,8 +4526,8 @@ var TestServer = class _TestServer {
4399
4526
  const project = options.project ?? "default";
4400
4527
  const maxAttempts = 3;
4401
4528
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4402
- const { port, release } = await reservePort(project, options.port);
4403
- const server = new _TestServer(options, port, release);
4529
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4530
+ const server = new _TestServer(options, port, release, releaseSocket);
4404
4531
  try {
4405
4532
  await server.startProcess();
4406
4533
  return server;
@@ -4440,7 +4567,7 @@ var TestServer = class _TestServer {
4440
4567
  }
4441
4568
  const maxAttempts = 3;
4442
4569
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4443
- const { port, release } = await reservePort(project, options.port);
4570
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4444
4571
  const serverOptions = {
4445
4572
  ...options,
4446
4573
  port,
@@ -4448,7 +4575,7 @@ var TestServer = class _TestServer {
4448
4575
  command: `npx nx serve ${project} --port ${port}`,
4449
4576
  cwd: options.cwd ?? process.cwd()
4450
4577
  };
4451
- const server = new _TestServer(serverOptions, port, release);
4578
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
4452
4579
  try {
4453
4580
  await server.startProcess();
4454
4581
  return server;
@@ -4506,10 +4633,17 @@ var TestServer = class _TestServer {
4506
4633
  * Stop the test server
4507
4634
  */
4508
4635
  async stop() {
4509
- if (this.portRelease) {
4510
- await this.portRelease();
4511
- this.portRelease = null;
4636
+ try {
4637
+ await this.stopProcess();
4638
+ } finally {
4639
+ if (this.portRelease) {
4640
+ await this.portRelease();
4641
+ this.portRelease = null;
4642
+ }
4643
+ this.portReleaseSocket = null;
4512
4644
  }
4645
+ }
4646
+ async stopProcess() {
4513
4647
  if (this.process) {
4514
4648
  this.log("Stopping server...");
4515
4649
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -4582,7 +4716,7 @@ var TestServer = class _TestServer {
4582
4716
  * Restart the server
4583
4717
  */
4584
4718
  async restart() {
4585
- await this.stop();
4719
+ await this.stopProcess();
4586
4720
  await this.startProcess();
4587
4721
  }
4588
4722
  /**
@@ -4612,9 +4746,9 @@ var TestServer = class _TestServer {
4612
4746
  PORT: String(this.options.port)
4613
4747
  };
4614
4748
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
4615
- if (this.portRelease) {
4616
- await this.portRelease();
4617
- this.portRelease = null;
4749
+ if (this.portReleaseSocket) {
4750
+ await this.portReleaseSocket();
4751
+ this.portReleaseSocket = null;
4618
4752
  await sleep2(300);
4619
4753
  }
4620
4754
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -4624,9 +4758,8 @@ var TestServer = class _TestServer {
4624
4758
  detached: true,
4625
4759
  stdio: ["pipe", "pipe", "pipe"]
4626
4760
  });
4627
- if (this.process.pid !== void 0) {
4628
- this._info.pid = this.process.pid;
4629
- }
4761
+ this._info.pid = this.process.pid;
4762
+ this._info.shellPid = this.process.pid;
4630
4763
  let processExited = false;
4631
4764
  let exitCode = null;
4632
4765
  let exitError = null;
@@ -4729,6 +4862,8 @@ ${allLogs}`);
4729
4862
  });
4730
4863
  if (response.ok || response.status === 404) {
4731
4864
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
4865
+ const listeningPid = await findListeningPid(this.options.port);
4866
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
4732
4867
  return;
4733
4868
  }
4734
4869
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -4773,6 +4908,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
4773
4908
  function sleep2(ms) {
4774
4909
  return new Promise((resolve) => setTimeout(resolve, ms));
4775
4910
  }
4911
+ function findListeningPid(port) {
4912
+ if (process.platform === "win32") return Promise.resolve(void 0);
4913
+ return new Promise((resolve) => {
4914
+ (0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
4915
+ if (error) {
4916
+ resolve(void 0);
4917
+ return;
4918
+ }
4919
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
4920
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
4921
+ });
4922
+ });
4923
+ }
4776
4924
  function withWorkspaceProtocolFallback(env, cwd) {
4777
4925
  if (findInstalledProtocolPackageDir(cwd)) {
4778
4926
  return env;
@@ -4822,7 +4970,7 @@ function withProtocolNodePathAlias(env, cwd, workspacePackageDir) {
4822
4970
  const fs = require("node:fs");
4823
4971
  const os = require("node:os");
4824
4972
  const path = require("node:path");
4825
- const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0, import_utils.sha256Hex)(cwd).slice(0, 12));
4973
+ const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0, import_utils2.sha256Hex)(cwd).slice(0, 12));
4826
4974
  const scopeDir = path.join(aliasRoot, "@frontmcp");
4827
4975
  const aliasPackageDir = path.join(scopeDir, "protocol");
4828
4976
  fs.mkdirSync(scopeDir, { recursive: true });
@@ -5044,94 +5192,162 @@ function hasMimeType(result, mimeType) {
5044
5192
  return result.hasMimeType(mimeType);
5045
5193
  }
5046
5194
 
5047
- // libs/testing/src/fixtures/test-fixture.ts
5048
- var import_globals = require("@jest/globals");
5049
-
5050
5195
  // libs/testing/src/fixtures/gateway-token-binding.ts
5051
- var import_utils2 = require("@frontmcp/utils");
5196
+ var import_utils3 = require("@frontmcp/utils");
5052
5197
  function gatewayTokenBinding(serverUrl, entryPath) {
5053
- const base = (0, import_utils2.trimTrailing)(serverUrl, "/");
5054
- const prefix = (0, import_utils2.trimSlashes)(entryPath ?? "");
5198
+ const base = (0, import_utils3.trimTrailing)(serverUrl, "/");
5199
+ const prefix = (0, import_utils3.trimSlashes)(entryPath ?? "");
5055
5200
  const mcpUrl = prefix ? `${base}/${prefix}` : base;
5056
5201
  return { issuer: mcpUrl, audience: mcpUrl };
5057
5202
  }
5058
5203
 
5059
5204
  // libs/testing/src/fixtures/test-fixture.ts
5060
- var describe2 = import_globals.describe;
5061
- var beforeAll2 = import_globals.beforeAll;
5062
- var beforeEach2 = import_globals.beforeEach;
5063
- var afterEach2 = import_globals.afterEach;
5064
- var afterAll2 = import_globals.afterAll;
5065
- var it2 = import_globals.it;
5066
- var currentConfig = {};
5067
- var serverInstance = null;
5068
- var tokenFactory = null;
5069
- var serverStartedByUs = false;
5070
- var skipScopes = [{ skipped: false }];
5071
- function currentSkipScope() {
5072
- return skipScopes[skipScopes.length - 1];
5205
+ var jestApi;
5206
+ function jestGlobals() {
5207
+ if (jestApi) return jestApi;
5208
+ try {
5209
+ const api = require("@jest/globals");
5210
+ jestApi = {
5211
+ describe: api["describe"],
5212
+ beforeAll: api["beforeAll"],
5213
+ beforeEach: api["beforeEach"],
5214
+ afterEach: api["afterEach"],
5215
+ afterAll: api["afterAll"],
5216
+ it: api["it"]
5217
+ };
5218
+ return jestApi;
5219
+ } catch (error) {
5220
+ throw new Error(
5221
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
5222
+ { cause: error }
5223
+ );
5224
+ }
5073
5225
  }
5074
- async function initializeSharedResources() {
5075
- if (!serverInstance) {
5076
- if (currentConfig.baseUrl && !currentConfig.server) {
5077
- serverInstance = TestServer.connect(currentConfig.baseUrl);
5078
- serverStartedByUs = false;
5079
- } else if (currentConfig.server) {
5080
- const serverCommand = resolveServerCommand(currentConfig.server);
5081
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
5226
+ var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
5227
+ function currentScope() {
5228
+ return scopes[scopes.length - 1];
5229
+ }
5230
+ var serverEntries = /* @__PURE__ */ new Map();
5231
+ function resolveConfig(chain) {
5232
+ const merged = {};
5233
+ for (const scope of chain) {
5234
+ const { env, ...rest } = scope.config;
5235
+ Object.assign(merged, rest);
5236
+ if (env) merged.env = { ...merged.env, ...env };
5237
+ }
5238
+ return merged;
5239
+ }
5240
+ function ownerScopeOf(chain) {
5241
+ for (let i = chain.length - 1; i > 0; i--) {
5242
+ if (chain[i].configured) return chain[i];
5243
+ }
5244
+ return chain[0];
5245
+ }
5246
+ function serverKey(config) {
5247
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
5248
+ return JSON.stringify({
5249
+ server: config.server,
5250
+ baseUrl: config.baseUrl,
5251
+ project: config.project,
5252
+ port: config.port,
5253
+ entryPath: config.entryPath,
5254
+ startupTimeout: config.startupTimeout,
5255
+ logLevel: config.logLevel,
5256
+ auth: config.auth,
5257
+ env
5258
+ });
5259
+ }
5260
+ function serverEnv(config) {
5261
+ const extra = {};
5262
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
5263
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
5264
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
5265
+ return { ...extra, ...config.env };
5266
+ }
5267
+ async function startServerEntry(config, owner) {
5268
+ let server;
5269
+ let startedByUs = false;
5270
+ if (config.baseUrl && !config.server) {
5271
+ server = TestServer.connect(config.baseUrl);
5272
+ } else if (config.server) {
5273
+ const serverCommand = resolveServerCommand(config.server);
5274
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
5275
+ if (isDebug) {
5276
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
5277
+ }
5278
+ try {
5279
+ server = await TestServer.start({
5280
+ project: config.project,
5281
+ port: config.port,
5282
+ command: serverCommand,
5283
+ env: serverEnv(config),
5284
+ startupTimeout: config.startupTimeout ?? 3e4,
5285
+ debug: isDebug
5286
+ });
5287
+ startedByUs = true;
5082
5288
  if (isDebug) {
5083
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
5289
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
5084
5290
  }
5085
- try {
5086
- serverInstance = await TestServer.start({
5087
- project: currentConfig.project,
5088
- port: currentConfig.port,
5089
- command: serverCommand,
5090
- env: currentConfig.env,
5091
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
5092
- debug: isDebug
5093
- });
5094
- serverStartedByUs = true;
5095
- if (isDebug) {
5096
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
5097
- }
5098
- } catch (error) {
5099
- const errMsg = error instanceof Error ? error.message : String(error);
5100
- throw new Error(
5101
- `Failed to start test server.
5291
+ } catch (error) {
5292
+ const errMsg = error instanceof Error ? error.message : String(error);
5293
+ throw new Error(
5294
+ `Failed to start test server.
5102
5295
 
5103
- Server entry: ${currentConfig.server}
5104
- Project: ${currentConfig.project ?? "default"}
5296
+ Server entry: ${config.server}
5297
+ Project: ${config.project ?? "default"}
5105
5298
  Command: ${serverCommand}
5106
5299
 
5107
- Error: ${errMsg}`
5108
- );
5109
- }
5110
- } else {
5111
- throw new Error(
5112
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
5300
+ Error: ${errMsg}`,
5301
+ { cause: error }
5113
5302
  );
5114
5303
  }
5304
+ } else {
5305
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
5115
5306
  }
5116
- if (!tokenFactory) {
5117
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
5118
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
5119
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5120
- }
5307
+ const sharedSecret = config.env?.["JWT_SECRET"];
5308
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
5309
+ const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5310
+ return { server, startedByUs, tokenFactory: tokenFactory2, owner };
5121
5311
  }
5122
- async function createTestFixtures() {
5123
- await initializeSharedResources();
5124
- if (!serverInstance) {
5125
- throw new Error("Server instance not initialized");
5126
- }
5127
- if (!tokenFactory) {
5128
- throw new Error("Token factory not initialized");
5312
+ function acquireServer(chain) {
5313
+ const config = resolveConfig(chain);
5314
+ const key = serverKey(config);
5315
+ let entry = serverEntries.get(key);
5316
+ if (!entry) {
5317
+ entry = startServerEntry(config, ownerScopeOf(chain));
5318
+ serverEntries.set(key, entry);
5319
+ entry.catch(() => serverEntries.delete(key));
5320
+ }
5321
+ return entry;
5322
+ }
5323
+ async function stopServers(owner) {
5324
+ for (const [key, pending] of Array.from(serverEntries)) {
5325
+ let entry;
5326
+ try {
5327
+ entry = await pending;
5328
+ } catch {
5329
+ serverEntries.delete(key);
5330
+ continue;
5331
+ }
5332
+ if (owner && entry.owner !== owner) continue;
5333
+ serverEntries.delete(key);
5334
+ if (entry.startedByUs) {
5335
+ await entry.server.stop();
5336
+ }
5129
5337
  }
5338
+ }
5339
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
5340
+ async function buildFixtures(chain) {
5341
+ const config = resolveConfig(chain);
5342
+ const entry = await acquireServer(chain);
5343
+ const { server, tokenFactory: tokenFactory2 } = entry;
5344
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
5345
+ const clientConfig = { ...config, publicMode };
5130
5346
  const clientInstance = McpTestClient.create({
5131
- baseUrl: resolveClientBaseUrl(serverInstance),
5132
- entryPath: currentConfig.entryPath,
5133
- transport: currentConfig.transport ?? "streamable-http",
5134
- publicMode: currentConfig.publicMode
5347
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
5348
+ entryPath: clientConfig.entryPath,
5349
+ transport: clientConfig.transport ?? "streamable-http",
5350
+ publicMode
5135
5351
  }).build();
5136
5352
  try {
5137
5353
  await clientInstance.connect();
@@ -5145,17 +5361,18 @@ async function createTestFixtures() {
5145
5361
  throw err;
5146
5362
  }
5147
5363
  }
5148
- const auth = createAuthFixture(tokenFactory);
5149
- const server = createServerFixture(serverInstance);
5150
- return {
5364
+ const context = { extraClients: [] };
5365
+ const fixtures = {
5151
5366
  mcp: clientInstance,
5152
- auth,
5153
- server
5367
+ auth: createAuthFixture(tokenFactory2),
5368
+ server: createServerFixture(server, clientConfig, clientInstance, context)
5154
5369
  };
5370
+ fixtureContexts.set(fixtures, context);
5371
+ return fixtures;
5155
5372
  }
5156
5373
  async function cleanupTestFixtures(fixtures, testFailed = false) {
5157
- if (testFailed && serverInstance) {
5158
- const logs = serverInstance.getLogs();
5374
+ if (testFailed) {
5375
+ const logs = fixtures.server.getLogs();
5159
5376
  if (logs.length > 0) {
5160
5377
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
5161
5378
  const recentLogs = logs.slice(-50);
@@ -5166,20 +5383,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
5166
5383
  console.error("[TestFixture] === End Server Logs ===\n");
5167
5384
  }
5168
5385
  }
5386
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
5387
+ for (const client of extras) {
5388
+ if (client.isConnected()) {
5389
+ await client.disconnect().catch(() => void 0);
5390
+ }
5391
+ }
5169
5392
  if (fixtures.mcp.isConnected()) {
5170
5393
  await fixtures.mcp.disconnect();
5171
5394
  }
5172
5395
  }
5173
- async function cleanupSharedResources() {
5174
- if (serverInstance && serverStartedByUs) {
5175
- await serverInstance.stop();
5176
- }
5177
- serverInstance = null;
5178
- tokenFactory = null;
5179
- serverStartedByUs = false;
5180
- }
5181
- function resolveClientBaseUrl(server) {
5182
- return currentConfig.baseUrl ?? server.info.baseUrl;
5396
+ function resolveClientBaseUrl(server, config) {
5397
+ return config.baseUrl ?? server.info.baseUrl;
5183
5398
  }
5184
5399
  function createAuthFixture(factory) {
5185
5400
  const users = {
@@ -5225,28 +5440,39 @@ function createAuthFixture(factory) {
5225
5440
  getAudience: () => factory.getAudience()
5226
5441
  };
5227
5442
  }
5228
- function createServerFixture(server) {
5443
+ function createServerFixture(server, config, mcp, context) {
5229
5444
  return {
5230
- info: server.info,
5445
+ // Live view: `pid` is filled in once the server is listening and follows restarts
5446
+ get info() {
5447
+ return server.info;
5448
+ },
5231
5449
  createClient: async (opts) => {
5232
- return McpTestClient.create({
5233
- baseUrl: resolveClientBaseUrl(server),
5234
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
5450
+ const client = await McpTestClient.create({
5451
+ baseUrl: resolveClientBaseUrl(server, config),
5452
+ entryPath: opts?.entryPath ?? config.entryPath,
5235
5453
  transport: opts?.transport ?? "streamable-http",
5236
5454
  auth: opts?.token ? { token: opts.token } : void 0,
5237
5455
  clientInfo: opts?.clientInfo,
5238
- publicMode: currentConfig.publicMode
5456
+ publicMode: config.publicMode
5239
5457
  }).buildAndConnect();
5458
+ context.extraClients.push(client);
5459
+ return client;
5240
5460
  },
5241
5461
  createClientBuilder: () => {
5242
5462
  const builder = new McpTestClientBuilder({
5243
- baseUrl: resolveClientBaseUrl(server),
5244
- entryPath: currentConfig.entryPath,
5245
- publicMode: currentConfig.publicMode
5463
+ baseUrl: resolveClientBaseUrl(server, config),
5464
+ entryPath: config.entryPath,
5465
+ publicMode: config.publicMode
5246
5466
  });
5247
5467
  return builder;
5248
5468
  },
5249
- restart: () => server.restart(),
5469
+ restart: async () => {
5470
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
5471
+ await server.restart();
5472
+ for (const client of reconnect) {
5473
+ await client.reconnect();
5474
+ }
5475
+ },
5250
5476
  getLogs: () => server.getLogs(),
5251
5477
  clearLogs: () => server.clearLogs()
5252
5478
  };
@@ -5257,12 +5483,12 @@ function resolveServerCommand(server) {
5257
5483
  }
5258
5484
  return `npx tsx ${server}`;
5259
5485
  }
5260
- function runWithFixtures(fn) {
5261
- return async () => {
5262
- const fixtures = await createTestFixtures();
5486
+ function runWithFixtures(fn, chain) {
5487
+ return async (...extra) => {
5488
+ const fixtures = await buildFixtures(chain);
5263
5489
  let testFailed = false;
5264
5490
  try {
5265
- await fn(fixtures);
5491
+ await fn(fixtures, ...extra);
5266
5492
  } catch (error) {
5267
5493
  testFailed = true;
5268
5494
  throw error;
@@ -5271,24 +5497,37 @@ function runWithFixtures(fn) {
5271
5497
  }
5272
5498
  };
5273
5499
  }
5500
+ function skipTitle(name, scope) {
5501
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
5502
+ }
5274
5503
  function testWithFixtures(name, fn) {
5275
- const scope = currentSkipScope();
5504
+ const chain = scopes.slice();
5505
+ const scope = currentScope();
5276
5506
  if (scope.skipped) {
5277
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5507
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
5278
5508
  return;
5279
5509
  }
5280
- it2(name, runWithFixtures(fn));
5510
+ jestGlobals().it(name, runWithFixtures(fn, chain));
5281
5511
  }
5282
5512
  function use(config) {
5283
- currentConfig = { ...currentConfig, ...config };
5284
- afterAll2(async () => {
5285
- await cleanupSharedResources();
5286
- });
5513
+ const scope = currentScope();
5514
+ scope.config = {
5515
+ ...scope.config,
5516
+ ...config,
5517
+ env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
5518
+ };
5519
+ scope.configured = true;
5520
+ if (!scope.cleanupRegistered) {
5521
+ scope.cleanupRegistered = true;
5522
+ jestGlobals().afterAll(async () => {
5523
+ await stopServers(scope === scopes[0] ? void 0 : scope);
5524
+ });
5525
+ }
5287
5526
  }
5288
5527
  function skip(nameOrCondition, fnOrReason) {
5289
5528
  if (typeof nameOrCondition === "boolean") {
5290
5529
  if (nameOrCondition) {
5291
- const scope = currentSkipScope();
5530
+ const scope = currentScope();
5292
5531
  scope.skipped = true;
5293
5532
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
5294
5533
  }
@@ -5299,57 +5538,111 @@ function skip(nameOrCondition, fnOrReason) {
5299
5538
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
5300
5539
  );
5301
5540
  }
5302
- it2.skip(nameOrCondition, runWithFixtures(fnOrReason));
5541
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
5303
5542
  }
5304
5543
  function only(name, fn) {
5305
- const scope = currentSkipScope();
5544
+ const scope = currentScope();
5306
5545
  if (scope.skipped) {
5307
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5546
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
5308
5547
  return;
5309
5548
  }
5310
- it2.only(name, runWithFixtures(fn));
5549
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
5311
5550
  }
5312
5551
  function todo(name) {
5313
- it2.todo(name);
5552
+ jestGlobals().it.todo(name);
5314
5553
  }
5315
- function withSkipScope(register) {
5554
+ function each(table, ...tagged) {
5555
+ const chain = scopes.slice();
5556
+ const register = jestGlobals().it.each(table, ...tagged);
5316
5557
  return (name, fn) => {
5317
- register(name, () => {
5318
- const parent = currentSkipScope();
5319
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
5320
- try {
5321
- fn();
5322
- } finally {
5323
- skipScopes.pop();
5324
- }
5558
+ const scope = currentScope();
5559
+ if (scope.skipped) {
5560
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
5561
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
5562
+ return;
5563
+ }
5564
+ register(name, runWithFixtures(fn, chain));
5565
+ };
5566
+ }
5567
+ function withScope(fn) {
5568
+ return (...args) => {
5569
+ const parent = currentScope();
5570
+ scopes.push({
5571
+ skipped: parent.skipped,
5572
+ reason: parent.reason,
5573
+ config: {},
5574
+ configured: false,
5575
+ cleanupRegistered: false
5325
5576
  });
5577
+ try {
5578
+ fn(...args);
5579
+ } finally {
5580
+ scopes.pop();
5581
+ }
5326
5582
  };
5327
5583
  }
5328
- function eachWithSkipScope(each) {
5329
- return ((...eachArgs) => {
5330
- const register = each(...eachArgs);
5331
- return withSkipScope(register);
5332
- });
5584
+ function scopedRegister(pick) {
5585
+ return (name, fn, timeout) => {
5586
+ pick()(name, withScope(fn), timeout);
5587
+ };
5333
5588
  }
5334
- var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
5335
- only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
5336
- skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
5337
- each: eachWithSkipScope(describe2.each)
5338
- });
5589
+ function scopedEach(pick) {
5590
+ return (...eachArgs) => (name, fn, timeout) => {
5591
+ const register = pick()(...eachArgs);
5592
+ register(name, withScope(fn), timeout);
5593
+ };
5594
+ }
5595
+ var describeWithScope = Object.assign(
5596
+ scopedRegister(() => jestGlobals().describe),
5597
+ {
5598
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
5599
+ only: Object.assign(
5600
+ scopedRegister(() => jestGlobals().describe.only),
5601
+ {
5602
+ each: scopedEach(() => jestGlobals().describe.only.each)
5603
+ }
5604
+ ),
5605
+ skip: Object.assign(
5606
+ scopedRegister(() => jestGlobals().describe.skip),
5607
+ {
5608
+ each: scopedEach(() => jestGlobals().describe.skip.each)
5609
+ }
5610
+ ),
5611
+ each: scopedEach(() => jestGlobals().describe.each)
5612
+ }
5613
+ );
5339
5614
  var test = testWithFixtures;
5340
5615
  test.use = use;
5341
- test.describe = describeWithSkipScope;
5342
- test.beforeAll = beforeAll2;
5343
- test.beforeEach = beforeEach2;
5344
- test.afterEach = afterEach2;
5345
- test.afterAll = afterAll2;
5616
+ test.describe = describeWithScope;
5617
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
5618
+ test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
5619
+ test.afterEach = ((...args) => jestGlobals().afterEach(...args));
5620
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
5346
5621
  test.skip = skip;
5347
5622
  test.only = only;
5348
5623
  test.todo = todo;
5624
+ test.each = each;
5349
5625
 
5350
5626
  // libs/testing/src/expect.ts
5351
- var import_globals2 = require("@jest/globals");
5352
- var expect = import_globals2.expect;
5627
+ var resolvedExpect;
5628
+ function resolveJestExpect() {
5629
+ if (resolvedExpect) return resolvedExpect;
5630
+ try {
5631
+ resolvedExpect = require("@jest/globals").expect;
5632
+ return resolvedExpect;
5633
+ } catch (error) {
5634
+ throw new Error(
5635
+ `expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
5636
+ { cause: error }
5637
+ );
5638
+ }
5639
+ }
5640
+ var expect = new Proxy(function frontmcpExpect() {
5641
+ }, {
5642
+ apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
5643
+ get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
5644
+ has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
5645
+ });
5353
5646
 
5354
5647
  // libs/testing/src/platform/platform-types.ts
5355
5648
  function getPlatformMetaNamespace(platform) {
@@ -6012,34 +6305,44 @@ var HttpInterceptorImpl = class {
6012
6305
  }
6013
6306
  };
6014
6307
  }
6015
- get(url, response) {
6308
+ get(url, response, options) {
6016
6309
  return this.mock({
6017
- match: { url, method: "GET" },
6018
- response: normalizeResponse(response)
6310
+ match: { url, method: "GET", ...options?.match },
6311
+ response: normalizeResponse(response),
6312
+ ...options?.times !== void 0 && { times: options.times },
6313
+ ...options?.name !== void 0 && { name: options.name }
6019
6314
  });
6020
6315
  }
6021
- post(url, response) {
6316
+ post(url, response, options) {
6022
6317
  return this.mock({
6023
- match: { url, method: "POST" },
6024
- response: normalizeResponse(response)
6318
+ match: { url, method: "POST", ...options?.match },
6319
+ response: normalizeResponse(response),
6320
+ ...options?.times !== void 0 && { times: options.times },
6321
+ ...options?.name !== void 0 && { name: options.name }
6025
6322
  });
6026
6323
  }
6027
- put(url, response) {
6324
+ put(url, response, options) {
6028
6325
  return this.mock({
6029
- match: { url, method: "PUT" },
6030
- response: normalizeResponse(response)
6326
+ match: { url, method: "PUT", ...options?.match },
6327
+ response: normalizeResponse(response),
6328
+ ...options?.times !== void 0 && { times: options.times },
6329
+ ...options?.name !== void 0 && { name: options.name }
6031
6330
  });
6032
6331
  }
6033
- delete(url, response) {
6332
+ delete(url, response, options) {
6034
6333
  return this.mock({
6035
- match: { url, method: "DELETE" },
6036
- response: normalizeResponse(response)
6334
+ match: { url, method: "DELETE", ...options?.match },
6335
+ response: normalizeResponse(response),
6336
+ ...options?.times !== void 0 && { times: options.times },
6337
+ ...options?.name !== void 0 && { name: options.name }
6037
6338
  });
6038
6339
  }
6039
- any(url, response) {
6340
+ any(url, response, options) {
6040
6341
  return this.mock({
6041
- match: { url },
6042
- response: normalizeResponse(response)
6342
+ match: { url, ...options?.match },
6343
+ response: normalizeResponse(response),
6344
+ ...options?.times !== void 0 && { times: options.times },
6345
+ ...options?.name !== void 0 && { name: options.name }
6043
6346
  });
6044
6347
  }
6045
6348
  clear() {
@@ -6071,6 +6374,9 @@ ${descriptions.join("\n")}`);
6071
6374
  if (index !== -1) {
6072
6375
  activeInterceptors.splice(index, 1);
6073
6376
  }
6377
+ if (activeInterceptors.length === 0) {
6378
+ httpMock.disable();
6379
+ }
6074
6380
  }
6075
6381
  // Internal methods
6076
6382
  isActive() {
@@ -6170,43 +6476,49 @@ ${descriptions.join("\n")}`);
6170
6476
  }
6171
6477
  };
6172
6478
  async function interceptedFetch(input, init) {
6173
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
6174
- const method = (init?.method ?? "GET").toUpperCase();
6479
+ const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
6480
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
6481
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
6175
6482
  const headers = {};
6176
- if (init?.headers) {
6177
- if (init.headers instanceof Headers) {
6178
- init.headers.forEach((value, key) => {
6483
+ const collectHeaders = (source) => {
6484
+ if (!source) return;
6485
+ if (source instanceof Headers) {
6486
+ source.forEach((value, key) => {
6179
6487
  headers[key.toLowerCase()] = value;
6180
6488
  });
6181
- } else if (Array.isArray(init.headers)) {
6182
- for (const [key, value] of init.headers) {
6489
+ } else if (Array.isArray(source)) {
6490
+ for (const [key, value] of source) {
6183
6491
  headers[key.toLowerCase()] = value;
6184
6492
  }
6185
6493
  } else {
6186
- for (const [key, value] of Object.entries(init.headers)) {
6494
+ for (const [key, value] of Object.entries(source)) {
6187
6495
  headers[key.toLowerCase()] = value;
6188
6496
  }
6189
6497
  }
6190
- }
6498
+ };
6499
+ collectHeaders(init?.headers ?? request?.headers);
6191
6500
  let body;
6192
6501
  let rawBody;
6193
- if (init?.body) {
6194
- if (typeof init.body === "string") {
6195
- rawBody = init.body;
6196
- try {
6197
- body = JSON.parse(init.body);
6198
- } catch {
6199
- body = init.body;
6200
- }
6201
- } else if (init.body instanceof ArrayBuffer || init.body instanceof Uint8Array) {
6202
- rawBody = new TextDecoder().decode(init.body);
6502
+ let bodySource = init?.body;
6503
+ if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
6504
+ bodySource = await request.clone().text();
6505
+ }
6506
+ if (bodySource) {
6507
+ if (typeof bodySource === "string") {
6508
+ rawBody = bodySource;
6509
+ } else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
6510
+ rawBody = new TextDecoder().decode(bodySource);
6511
+ } else if (bodySource instanceof URLSearchParams) {
6512
+ rawBody = bodySource.toString();
6513
+ }
6514
+ if (rawBody !== void 0) {
6203
6515
  try {
6204
6516
  body = JSON.parse(rawBody);
6205
6517
  } catch {
6206
6518
  body = rawBody;
6207
6519
  }
6208
6520
  } else {
6209
- body = init.body;
6521
+ body = bodySource;
6210
6522
  }
6211
6523
  }
6212
6524
  const requestInfo = {
@@ -6236,8 +6548,20 @@ async function interceptedFetch(input, init) {
6236
6548
  Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
6237
6549
  );
6238
6550
  }
6551
+ var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
6552
+ function isResponseDescriptor(value) {
6553
+ const keys = Object.keys(value);
6554
+ if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
6555
+ if ("status" in value) {
6556
+ const status = value["status"];
6557
+ if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
6558
+ }
6559
+ return true;
6560
+ }
6239
6561
  function normalizeResponse(response) {
6240
- if (!("status" in response) && !("body" in response) && !("headers" in response)) {
6562
+ const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
6563
+ if (isNetworkError) return response;
6564
+ if (Array.isArray(response) || !isResponseDescriptor(response)) {
6241
6565
  return { body: response };
6242
6566
  }
6243
6567
  return response;
@@ -7571,47 +7895,48 @@ function clearGlobalMeasurements() {
7571
7895
  }
7572
7896
 
7573
7897
  // libs/testing/src/perf/perf-test.ts
7574
- var currentConfig2 = {};
7575
- var serverInstance2 = null;
7576
- var tokenFactory2 = null;
7577
- var serverStartedByUs2 = false;
7578
- async function initializeSharedResources2() {
7579
- if (!tokenFactory2) {
7580
- tokenFactory2 = new TestTokenFactory();
7581
- }
7582
- if (!serverInstance2) {
7583
- if (currentConfig2.baseUrl) {
7584
- serverInstance2 = TestServer.connect(currentConfig2.baseUrl);
7585
- serverStartedByUs2 = false;
7586
- } else if (currentConfig2.server) {
7587
- const serverCommand = resolveServerCommand2(currentConfig2.server);
7898
+ var currentConfig = {};
7899
+ var serverInstance = null;
7900
+ var tokenFactory = null;
7901
+ var serverStartedByUs = false;
7902
+ async function initializeSharedResources() {
7903
+ if (!tokenFactory) {
7904
+ tokenFactory = new TestTokenFactory();
7905
+ }
7906
+ if (!serverInstance) {
7907
+ if (currentConfig.baseUrl) {
7908
+ serverInstance = TestServer.connect(currentConfig.baseUrl);
7909
+ serverStartedByUs = false;
7910
+ } else if (currentConfig.server) {
7911
+ const serverCommand = resolveServerCommand2(currentConfig.server);
7588
7912
  const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
7589
7913
  if (isDebug) {
7590
7914
  console.log(`[PerfTest] Starting server: ${serverCommand}`);
7591
7915
  }
7592
7916
  try {
7593
- serverInstance2 = await TestServer.start({
7594
- project: currentConfig2.project,
7595
- port: currentConfig2.port,
7917
+ serverInstance = await TestServer.start({
7918
+ project: currentConfig.project,
7919
+ port: currentConfig.port,
7596
7920
  command: serverCommand,
7597
- env: currentConfig2.env,
7598
- startupTimeout: currentConfig2.startupTimeout ?? 3e4,
7921
+ env: currentConfig.env,
7922
+ startupTimeout: currentConfig.startupTimeout ?? 3e4,
7599
7923
  debug: isDebug
7600
7924
  });
7601
- serverStartedByUs2 = true;
7925
+ serverStartedByUs = true;
7602
7926
  if (isDebug) {
7603
- console.log(`[PerfTest] Server started at ${serverInstance2.info.baseUrl}`);
7927
+ console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
7604
7928
  }
7605
7929
  } catch (error) {
7606
7930
  const errMsg = error instanceof Error ? error.message : String(error);
7607
7931
  throw new Error(
7608
7932
  `Failed to start test server.
7609
7933
 
7610
- Server entry: ${currentConfig2.server}
7611
- Project: ${currentConfig2.project ?? "default"}
7934
+ Server entry: ${currentConfig.server}
7935
+ Project: ${currentConfig.project ?? "default"}
7612
7936
  Command: ${serverCommand}
7613
7937
 
7614
- Error: ${errMsg}`
7938
+ Error: ${errMsg}`,
7939
+ { cause: error }
7615
7940
  );
7616
7941
  }
7617
7942
  } else {
@@ -7619,23 +7944,23 @@ Error: ${errMsg}`
7619
7944
  }
7620
7945
  }
7621
7946
  }
7622
- async function createTestFixtures2(testName) {
7623
- await initializeSharedResources2();
7624
- if (!serverInstance2) {
7947
+ async function createTestFixtures(testName) {
7948
+ await initializeSharedResources();
7949
+ if (!serverInstance) {
7625
7950
  throw new Error("Server instance not initialized");
7626
7951
  }
7627
- if (!tokenFactory2) {
7952
+ if (!tokenFactory) {
7628
7953
  throw new Error("Token factory not initialized");
7629
7954
  }
7630
7955
  const clientInstance = await McpTestClient.create({
7631
- baseUrl: serverInstance2.info.baseUrl,
7632
- transport: currentConfig2.transport ?? "streamable-http",
7633
- publicMode: currentConfig2.publicMode
7956
+ baseUrl: serverInstance.info.baseUrl,
7957
+ transport: currentConfig.transport ?? "streamable-http",
7958
+ publicMode: currentConfig.publicMode
7634
7959
  }).buildAndConnect();
7635
- const auth = createAuthFixture2(tokenFactory2);
7636
- const server = createServerFixture2(serverInstance2);
7637
- const perfImpl = createPerfFixtures(testName, currentConfig2.project ?? "unknown");
7638
- if (currentConfig2.forceGcOnBaseline !== false) {
7960
+ const auth = createAuthFixture2(tokenFactory);
7961
+ const server = createServerFixture2(serverInstance);
7962
+ const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
7963
+ if (currentConfig.forceGcOnBaseline !== false) {
7639
7964
  await perfImpl.baseline();
7640
7965
  }
7641
7966
  return {
@@ -7651,8 +7976,8 @@ async function createTestFixtures2(testName) {
7651
7976
  async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7652
7977
  const measurement = perfImpl.buildMeasurement();
7653
7978
  addGlobalMeasurement(measurement);
7654
- if (testFailed && serverInstance2) {
7655
- const logs = serverInstance2.getLogs();
7979
+ if (testFailed && serverInstance) {
7980
+ const logs = serverInstance.getLogs();
7656
7981
  if (logs.length > 0) {
7657
7982
  console.error("\n[PerfTest] === Server Logs (test failed) ===");
7658
7983
  const recentLogs = logs.slice(-50);
@@ -7667,13 +7992,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7667
7992
  await fixtures.mcp.disconnect();
7668
7993
  }
7669
7994
  }
7670
- async function cleanupSharedResources2() {
7671
- if (serverInstance2 && serverStartedByUs2) {
7672
- await serverInstance2.stop();
7995
+ async function cleanupSharedResources() {
7996
+ if (serverInstance && serverStartedByUs) {
7997
+ await serverInstance.stop();
7673
7998
  }
7674
- serverInstance2 = null;
7675
- tokenFactory2 = null;
7676
- serverStartedByUs2 = false;
7999
+ serverInstance = null;
8000
+ tokenFactory = null;
8001
+ serverStartedByUs = false;
7677
8002
  }
7678
8003
  function createAuthFixture2(factory) {
7679
8004
  const users = {
@@ -7728,13 +8053,13 @@ function createServerFixture2(server) {
7728
8053
  transport: opts?.transport ?? "streamable-http",
7729
8054
  auth: opts?.token ? { token: opts.token } : void 0,
7730
8055
  clientInfo: opts?.clientInfo,
7731
- publicMode: currentConfig2.publicMode
8056
+ publicMode: currentConfig.publicMode
7732
8057
  }).buildAndConnect();
7733
8058
  },
7734
8059
  createClientBuilder: () => {
7735
8060
  return new McpTestClientBuilder({
7736
8061
  baseUrl: server.info.baseUrl,
7737
- publicMode: currentConfig2.publicMode
8062
+ publicMode: currentConfig.publicMode
7738
8063
  });
7739
8064
  },
7740
8065
  restart: () => server.restart(),
@@ -7750,7 +8075,7 @@ function resolveServerCommand2(server) {
7750
8075
  }
7751
8076
  function perfTestWithFixtures(name, fn) {
7752
8077
  it(name, async () => {
7753
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8078
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7754
8079
  let testFailed = false;
7755
8080
  try {
7756
8081
  await fn(fixtures);
@@ -7763,14 +8088,14 @@ function perfTestWithFixtures(name, fn) {
7763
8088
  });
7764
8089
  }
7765
8090
  function use2(config) {
7766
- currentConfig2 = { ...currentConfig2, ...config };
8091
+ currentConfig = { ...currentConfig, ...config };
7767
8092
  afterAll(async () => {
7768
- await cleanupSharedResources2();
8093
+ await cleanupSharedResources();
7769
8094
  });
7770
8095
  }
7771
8096
  function skip2(name, fn) {
7772
8097
  it.skip(name, async () => {
7773
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8098
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7774
8099
  let testFailed = false;
7775
8100
  try {
7776
8101
  await fn(fixtures);
@@ -7784,7 +8109,7 @@ function skip2(name, fn) {
7784
8109
  }
7785
8110
  function only2(name, fn) {
7786
8111
  it.only(name, async () => {
7787
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8112
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7788
8113
  let testFailed = false;
7789
8114
  try {
7790
8115
  await fn(fixtures);
@@ -7801,16 +8126,19 @@ function todo2(name) {
7801
8126
  }
7802
8127
  var perfTest = perfTestWithFixtures;
7803
8128
  perfTest.use = use2;
7804
- perfTest.describe = describe;
7805
- perfTest.beforeAll = beforeAll;
7806
- perfTest.beforeEach = beforeEach;
7807
- perfTest.afterEach = afterEach;
7808
- perfTest.afterAll = afterAll;
8129
+ for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
8130
+ Object.defineProperty(perfTest, hook, {
8131
+ get: () => globalThis[hook],
8132
+ enumerable: true,
8133
+ configurable: true
8134
+ });
8135
+ }
7809
8136
  perfTest.skip = skip2;
7810
8137
  perfTest.only = only2;
7811
8138
  perfTest.todo = todo2;
7812
8139
 
7813
8140
  // libs/testing/src/perf/baseline-store.ts
8141
+ var import_utils4 = require("@frontmcp/utils");
7814
8142
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
7815
8143
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
7816
8144
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -7825,11 +8153,10 @@ var BaselineStore = class {
7825
8153
  */
7826
8154
  async load() {
7827
8155
  try {
7828
- const { readFile, fileExists } = await import("@frontmcp/utils");
7829
- if (!await fileExists(this.baselinePath)) {
8156
+ if (!await (0, import_utils4.fileExists)(this.baselinePath)) {
7830
8157
  return null;
7831
8158
  }
7832
- const content = await readFile(this.baselinePath);
8159
+ const content = await (0, import_utils4.readFile)(this.baselinePath);
7833
8160
  this.baseline = JSON.parse(content);
7834
8161
  return this.baseline;
7835
8162
  } catch {
@@ -7840,10 +8167,9 @@ var BaselineStore = class {
7840
8167
  * Save baseline to local file.
7841
8168
  */
7842
8169
  async save(baseline) {
7843
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
7844
8170
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
7845
- await ensureDir(dir);
7846
- await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
8171
+ await (0, import_utils4.ensureDir)(dir);
8172
+ await (0, import_utils4.writeFile)(this.baselinePath, JSON.stringify(baseline, null, 2));
7847
8173
  this.baseline = baseline;
7848
8174
  }
7849
8175
  /**
@@ -8096,6 +8422,7 @@ function getRegressionDetector(config) {
8096
8422
  }
8097
8423
 
8098
8424
  // libs/testing/src/perf/report-generator.ts
8425
+ var import_utils5 = require("@frontmcp/utils");
8099
8426
  var ReportGenerator = class {
8100
8427
  detector;
8101
8428
  constructor() {
@@ -8351,14 +8678,13 @@ var ReportGenerator = class {
8351
8678
  }
8352
8679
  };
8353
8680
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
8354
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
8355
- await ensureDir(outputDir);
8681
+ await (0, import_utils5.ensureDir)(outputDir);
8356
8682
  const generator = new ReportGenerator();
8357
8683
  const report = generator.generateReport(measurements, baseline, gitInfo);
8358
8684
  const jsonPath = `${outputDir}/report.json`;
8359
8685
  const markdownPath = `${outputDir}/report.md`;
8360
- await writeFile(jsonPath, generator.generateJsonReport(report));
8361
- await writeFile(markdownPath, generator.generateMarkdownReport(report));
8686
+ await (0, import_utils5.writeFile)(jsonPath, generator.generateJsonReport(report));
8687
+ await (0, import_utils5.writeFile)(markdownPath, generator.generateMarkdownReport(report));
8362
8688
  return { jsonPath, markdownPath };
8363
8689
  }
8364
8690
  function createReportGenerator() {