@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/esm/index.mjs CHANGED
@@ -341,6 +341,32 @@ var interceptors = {
341
341
  }
342
342
  };
343
343
 
344
+ // libs/testing/src/transport/error-utils.ts
345
+ function errorName(err) {
346
+ if (typeof err !== "object" || err === null) return void 0;
347
+ const name = err.name;
348
+ return typeof name === "string" ? name : void 0;
349
+ }
350
+ function isAbortError(err) {
351
+ return errorName(err) === "AbortError";
352
+ }
353
+ function errorMessage(err, fallback = "Unknown error") {
354
+ if (typeof err === "string" && err) return err;
355
+ if (typeof err === "object" && err !== null) {
356
+ const message = err.message;
357
+ if (typeof message === "string" && message) return message;
358
+ }
359
+ return fallback;
360
+ }
361
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
362
+ function markInterceptedError(err) {
363
+ interceptedErrors.add(err);
364
+ return err;
365
+ }
366
+ function isInterceptedError(err) {
367
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
368
+ }
369
+
344
370
  // libs/testing/src/transport/streamable-http.transport.ts
345
371
  var DEFAULT_TIMEOUT = 3e4;
346
372
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
@@ -449,7 +475,7 @@ var StreamableHttpTransport = class {
449
475
  return mockResponse2;
450
476
  }
451
477
  case "error":
452
- throw interceptResult.error;
478
+ throw markInterceptedError(interceptResult.error);
453
479
  case "continue":
454
480
  message = interceptResult.request;
455
481
  break;
@@ -510,7 +536,7 @@ var StreamableHttpTransport = class {
510
536
  return jsonResponse;
511
537
  } catch (error) {
512
538
  clearTimeout(timeoutId);
513
- if (error instanceof Error && error.name === "AbortError") {
539
+ if (isAbortError(error)) {
514
540
  return {
515
541
  jsonrpc: "2.0",
516
542
  id: message.id ?? null,
@@ -549,7 +575,7 @@ var StreamableHttpTransport = class {
549
575
  }
550
576
  } catch (error) {
551
577
  clearTimeout(timeoutId);
552
- if (error instanceof Error && error.name !== "AbortError") {
578
+ if (!isAbortError(error)) {
553
579
  throw error;
554
580
  }
555
581
  }
@@ -590,7 +616,7 @@ var StreamableHttpTransport = class {
590
616
  error: {
591
617
  code: -32700,
592
618
  message: "Parse error",
593
- data: error instanceof Error ? error.message : "Unknown error"
619
+ data: errorMessage(error)
594
620
  }
595
621
  };
596
622
  }
@@ -960,7 +986,7 @@ var StreamableHttpTransport = class {
960
986
  if (this.config.clientInfo) {
961
987
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
962
988
  }
963
- if (this.authToken && !this.publicMode) {
989
+ if (this.authToken) {
964
990
  headers["Authorization"] = `Bearer ${this.authToken}`;
965
991
  }
966
992
  if (this.sessionId) {
@@ -1302,6 +1328,16 @@ var DEFAULT_CLIENT_INFO = {
1302
1328
  name: "@frontmcp/testing",
1303
1329
  version: "0.4.0"
1304
1330
  };
1331
+ var MAX_LIST_PAGES = 1e3;
1332
+ function isAuthFailure(error) {
1333
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1334
+ }
1335
+ function describeInitializeError(error) {
1336
+ const message = error?.message ?? "Unknown error";
1337
+ const data = error?.data;
1338
+ const detail = typeof data === "string" ? data.trim() : "";
1339
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1340
+ }
1305
1341
  function extractEntryPaths(data) {
1306
1342
  let payload = data;
1307
1343
  if (typeof payload === "string") {
@@ -1388,19 +1424,22 @@ var McpTestClient = class {
1388
1424
  async connect() {
1389
1425
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1390
1426
  this.triedDiscoveredEntryPath = false;
1427
+ this.initResult = null;
1391
1428
  this.transport = this.createTransport();
1392
1429
  await this.transport.connect();
1393
1430
  const maxRetries = 3;
1394
1431
  const retryDelayMs = 500;
1395
1432
  let lastError;
1396
1433
  let reportedEntryPaths;
1434
+ let attemptsMade = 0;
1397
1435
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1436
+ attemptsMade = attempt;
1398
1437
  const initResponse = await this.initialize();
1399
1438
  if (initResponse.success && initResponse.data) {
1400
1439
  this.initResult = initResponse.data;
1401
1440
  break;
1402
1441
  }
1403
- lastError = initResponse.error?.message ?? "Unknown error";
1442
+ lastError = describeInitializeError(initResponse.error);
1404
1443
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1405
1444
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1406
1445
  this.triedDiscoveredEntryPath = true;
@@ -1412,6 +1451,7 @@ var McpTestClient = class {
1412
1451
  await this.transport.connect();
1413
1452
  continue;
1414
1453
  }
1454
+ if (isAuthFailure(initResponse.error)) break;
1415
1455
  if (attempt < maxRetries) {
1416
1456
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1417
1457
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1420,7 +1460,7 @@ var McpTestClient = class {
1420
1460
  if (!this.initResult) {
1421
1461
  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.` : "";
1422
1462
  throw new Error(
1423
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1463
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1424
1464
  );
1425
1465
  }
1426
1466
  this._sessionId = this.transport.getSessionId();
@@ -1490,18 +1530,47 @@ var McpTestClient = class {
1490
1530
  return this._authState;
1491
1531
  }
1492
1532
  /**
1493
- * Authenticate with a token
1533
+ * Authenticate with a token.
1534
+ *
1535
+ * When connected, this opens a NEW session with the token (the previous session was created
1536
+ * for the previous identity) and rejects if the server refuses it — for example because the
1537
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
1538
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1494
1539
  */
1495
1540
  async authenticate(token) {
1541
+ if (typeof token !== "string" || token.trim() === "") {
1542
+ throw new Error("authenticate() requires a non-empty token");
1543
+ }
1544
+ const previousAuthState = this._authState;
1545
+ const previousConfigAuth = this.config.auth;
1546
+ this.config.auth = { ...previousConfigAuth, token };
1496
1547
  this._authState = {
1497
1548
  isAnonymous: false,
1498
1549
  token,
1499
1550
  scopes: this.parseScopesFromToken(token),
1500
1551
  user: this.parseUserFromToken(token)
1501
1552
  };
1502
- if (this.transport) {
1503
- this.transport.setAuthToken(token);
1553
+ const previousTransport = this.transport;
1554
+ if (!previousTransport) {
1555
+ this.log("debug", "Authentication updated (not connected yet)");
1556
+ return;
1557
+ }
1558
+ const previousInit = this.initResult;
1559
+ const previousSessionId = this._sessionId;
1560
+ const previousSessionInfo = this._sessionInfo;
1561
+ try {
1562
+ await this.connect();
1563
+ } catch (err) {
1564
+ await this.transport?.close().catch(() => void 0);
1565
+ this.transport = previousTransport;
1566
+ this.initResult = previousInit;
1567
+ this._sessionId = previousSessionId;
1568
+ this._sessionInfo = previousSessionInfo;
1569
+ this.config.auth = previousConfigAuth;
1570
+ this._authState = previousAuthState;
1571
+ throw err;
1504
1572
  }
1573
+ await previousTransport.close().catch(() => void 0);
1505
1574
  this.log("debug", "Authentication updated");
1506
1575
  }
1507
1576
  // ═══════════════════════════════════════════════════════════════════
@@ -1535,13 +1604,12 @@ var McpTestClient = class {
1535
1604
  /**
1536
1605
  * List all available tools
1537
1606
  */
1538
- list: async () => {
1539
- const response = await this.listTools();
1540
- if (!response.success || !response.data) {
1541
- throw new Error(`Failed to list tools: ${response.error?.message}`);
1542
- }
1543
- return response.data.tools;
1544
- },
1607
+ list: async () => this.listAllPages(
1608
+ "tools/list",
1609
+ "tools",
1610
+ (cursor) => this.listTools(cursor),
1611
+ (page) => page.tools
1612
+ ),
1545
1613
  /**
1546
1614
  * Call a tool by name with arguments
1547
1615
  */
@@ -1557,23 +1625,21 @@ var McpTestClient = class {
1557
1625
  /**
1558
1626
  * List all static resources
1559
1627
  */
1560
- list: async () => {
1561
- const response = await this.listResources();
1562
- if (!response.success || !response.data) {
1563
- throw new Error(`Failed to list resources: ${response.error?.message}`);
1564
- }
1565
- return response.data.resources;
1566
- },
1628
+ list: async () => this.listAllPages(
1629
+ "resources/list",
1630
+ "resources",
1631
+ (cursor) => this.listResources(cursor),
1632
+ (page) => page.resources
1633
+ ),
1567
1634
  /**
1568
1635
  * List all resource templates
1569
1636
  */
1570
- listTemplates: async () => {
1571
- const response = await this.listResourceTemplates();
1572
- if (!response.success || !response.data) {
1573
- throw new Error(`Failed to list resource templates: ${response.error?.message}`);
1574
- }
1575
- return response.data.resourceTemplates;
1576
- },
1637
+ listTemplates: async () => this.listAllPages(
1638
+ "resources/templates/list",
1639
+ "resource templates",
1640
+ (cursor) => this.listResourceTemplates(cursor),
1641
+ (page) => page.resourceTemplates
1642
+ ),
1577
1643
  /**
1578
1644
  * Read a resource by URI
1579
1645
  */
@@ -1601,13 +1667,12 @@ var McpTestClient = class {
1601
1667
  /**
1602
1668
  * List all available prompts
1603
1669
  */
1604
- list: async () => {
1605
- const response = await this.listPrompts();
1606
- if (!response.success || !response.data) {
1607
- throw new Error(`Failed to list prompts: ${response.error?.message}`);
1608
- }
1609
- return response.data.prompts;
1610
- },
1670
+ list: async () => this.listAllPages(
1671
+ "prompts/list",
1672
+ "prompts",
1673
+ (cursor) => this.listPrompts(cursor),
1674
+ (page) => page.prompts
1675
+ ),
1611
1676
  /**
1612
1677
  * Get a prompt with arguments
1613
1678
  */
@@ -1991,8 +2056,33 @@ var McpTestClient = class {
1991
2056
  clientInfo: this.config.clientInfo
1992
2057
  });
1993
2058
  }
1994
- async listTools() {
1995
- return this.request("tools/list", {});
2059
+ async listTools(cursor) {
2060
+ return this.request("tools/list", cursor ? { cursor } : {});
2061
+ }
2062
+ /**
2063
+ * Follow `nextCursor` until the server stops returning one, concatenating the
2064
+ * pages. A server that hands back a cursor it already returned, or never stops
2065
+ * paging, fails the call rather than looping.
2066
+ */
2067
+ async listAllPages(method, label, fetchPage, itemsOf) {
2068
+ const items = [];
2069
+ const seenCursors = /* @__PURE__ */ new Set();
2070
+ let cursor;
2071
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
2072
+ const response = await fetchPage(cursor);
2073
+ if (!response.success || !response.data) {
2074
+ throw new Error(`Failed to list ${label}: ${response.error?.message}`);
2075
+ }
2076
+ items.push(...itemsOf(response.data) ?? []);
2077
+ const next = response.data.nextCursor;
2078
+ if (!next) return items;
2079
+ if (seenCursors.has(next)) {
2080
+ throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`);
2081
+ }
2082
+ seenCursors.add(next);
2083
+ cursor = next;
2084
+ }
2085
+ throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`);
1996
2086
  }
1997
2087
  async callTool(name, args, progressToken) {
1998
2088
  return this.request("tools/call", {
@@ -2017,17 +2107,17 @@ var McpTestClient = class {
2017
2107
  timestamp
2018
2108
  });
2019
2109
  };
2020
- async listResources() {
2021
- return this.request("resources/list", {});
2110
+ async listResources(cursor) {
2111
+ return this.request("resources/list", cursor ? { cursor } : {});
2022
2112
  }
2023
- async listResourceTemplates() {
2024
- return this.request("resources/templates/list", {});
2113
+ async listResourceTemplates(cursor) {
2114
+ return this.request("resources/templates/list", cursor ? { cursor } : {});
2025
2115
  }
2026
2116
  async readResource(uri) {
2027
2117
  return this.request("resources/read", { uri });
2028
2118
  }
2029
- async listPrompts() {
2030
- return this.request("prompts/list", {});
2119
+ async listPrompts(cursor) {
2120
+ return this.request("prompts/list", cursor ? { cursor } : {});
2031
2121
  }
2032
2122
  async getPrompt(name, args) {
2033
2123
  return this.request("prompts/get", {
@@ -2104,10 +2194,11 @@ var McpTestClient = class {
2104
2194
  requestId: id
2105
2195
  };
2106
2196
  } catch (err) {
2197
+ if (isInterceptedError(err)) throw err;
2107
2198
  const durationMs = Date.now() - start;
2108
2199
  const error = {
2109
2200
  code: -32603,
2110
- message: err instanceof Error ? err.message : "Unknown error"
2201
+ message: errorMessage(err)
2111
2202
  };
2112
2203
  return {
2113
2204
  success: false,
@@ -2255,12 +2346,12 @@ var McpTestClient = class {
2255
2346
  const payload = this.decodeJwtPayload(token);
2256
2347
  if (!payload) return [];
2257
2348
  const scope = payload["scope"];
2258
- const scopes = payload["scopes"];
2349
+ const scopes2 = payload["scopes"];
2259
2350
  if (typeof scope === "string") {
2260
2351
  return scope.split(" ");
2261
2352
  }
2262
- if (Array.isArray(scopes)) {
2263
- return scopes;
2353
+ if (Array.isArray(scopes2)) {
2354
+ return scopes2;
2264
2355
  }
2265
2356
  return [];
2266
2357
  } catch {
@@ -2392,14 +2483,15 @@ var TestTokenFactory = class {
2392
2483
  */
2393
2484
  async createTestToken(options) {
2394
2485
  await this.ensureKeys();
2395
- const now = Math.floor(Date.now() / 1e3);
2486
+ const nowMs = Date.now();
2487
+ const iat = Math.floor(nowMs / 1e3);
2396
2488
  const exp = options.exp ?? 3600;
2397
2489
  const payload = {
2398
2490
  iss: options.iss ?? this.issuer,
2399
2491
  sub: options.sub,
2400
2492
  aud: options.aud ?? this.audience,
2401
- iat: now,
2402
- exp: now + exp,
2493
+ iat,
2494
+ exp: Math.ceil(nowMs / 1e3) + exp,
2403
2495
  scope: options.scopes?.join(" "),
2404
2496
  ...options.claims
2405
2497
  };
@@ -2424,10 +2516,10 @@ var TestTokenFactory = class {
2424
2516
  /**
2425
2517
  * Create a regular user token
2426
2518
  */
2427
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
2519
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
2428
2520
  return this.createTestToken({
2429
2521
  sub,
2430
- scopes,
2522
+ scopes: scopes2,
2431
2523
  claims: {
2432
2524
  email: "user@test.local",
2433
2525
  name: "Test User",
@@ -2438,10 +2530,11 @@ var TestTokenFactory = class {
2438
2530
  /**
2439
2531
  * Create an anonymous user token
2440
2532
  */
2441
- async createAnonymousToken() {
2533
+ async createAnonymousToken(expiresIn) {
2442
2534
  return this.createTestToken({
2443
2535
  sub: `anon:${Date.now()}`,
2444
2536
  scopes: ["anonymous"],
2537
+ exp: expiresIn,
2445
2538
  claims: {
2446
2539
  name: "Anonymous",
2447
2540
  role: "anonymous"
@@ -2652,17 +2745,7 @@ function createTestUser(overrides) {
2652
2745
 
2653
2746
  // libs/testing/src/auth/mock-oauth-server.ts
2654
2747
  import { createServer } from "http";
2655
- var _randomBytes;
2656
- var _sha256Base64url;
2657
- var _base64urlEncode;
2658
- async function loadCryptoUtils() {
2659
- if (!_randomBytes) {
2660
- const utils = await import("@frontmcp/utils");
2661
- _randomBytes = utils.randomBytes;
2662
- _sha256Base64url = utils.sha256Base64url;
2663
- _base64urlEncode = utils.base64urlEncode;
2664
- }
2665
- }
2748
+ import { base64urlEncode, randomBytes, sha256Base64url } from "@frontmcp/utils";
2666
2749
  var MockOAuthServer = class {
2667
2750
  tokenFactory;
2668
2751
  options;
@@ -2677,8 +2760,8 @@ var MockOAuthServer = class {
2677
2760
  accessTokenTtlSeconds;
2678
2761
  /** Refresh token TTL in seconds */
2679
2762
  refreshTokenTtlSeconds;
2680
- constructor(tokenFactory3, options = {}) {
2681
- this.tokenFactory = tokenFactory3;
2763
+ constructor(tokenFactory2, options = {}) {
2764
+ this.tokenFactory = tokenFactory2;
2682
2765
  this.options = options;
2683
2766
  this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
2684
2767
  this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
@@ -2935,14 +3018,14 @@ ${error.stack}` : String(error);
2935
3018
  return;
2936
3019
  }
2937
3020
  const code = this.generateCode();
2938
- const scopes = scope.split(" ").filter(Boolean);
3021
+ const scopes2 = scope.split(" ").filter(Boolean);
2939
3022
  this.authCodes.set(code, {
2940
3023
  code,
2941
3024
  clientId,
2942
3025
  redirectUri,
2943
3026
  codeChallenge,
2944
3027
  codeChallengeMethod,
2945
- scopes,
3028
+ scopes: scopes2,
2946
3029
  user: testUser,
2947
3030
  state,
2948
3031
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -2982,7 +3065,7 @@ ${error.stack}` : String(error);
2982
3065
  }
2983
3066
  const grantType = params.get("grant_type");
2984
3067
  if (grantType === "anonymous") {
2985
- const token = await this.tokenFactory.createAnonymousToken();
3068
+ const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
2986
3069
  res.writeHead(200, { "Content-Type": "application/json" });
2987
3070
  res.end(
2988
3071
  JSON.stringify({
@@ -3114,6 +3197,8 @@ ${error.stack}` : String(error);
3114
3197
  codeRecord.used = true;
3115
3198
  const accessToken = await this.tokenFactory.createTestToken({
3116
3199
  sub: codeRecord.user.sub,
3200
+ scopes: codeRecord.scopes,
3201
+ exp: this.accessTokenTtlSeconds,
3117
3202
  claims: {
3118
3203
  email: codeRecord.user.email,
3119
3204
  name: codeRecord.user.name,
@@ -3122,6 +3207,7 @@ ${error.stack}` : String(error);
3122
3207
  });
3123
3208
  const idToken = await this.tokenFactory.createTestToken({
3124
3209
  sub: codeRecord.user.sub,
3210
+ exp: this.accessTokenTtlSeconds,
3125
3211
  claims: {
3126
3212
  email: codeRecord.user.email,
3127
3213
  name: codeRecord.user.name,
@@ -3199,6 +3285,8 @@ ${error.stack}` : String(error);
3199
3285
  }
3200
3286
  const accessToken = await this.tokenFactory.createTestToken({
3201
3287
  sub: tokenRecord.user.sub,
3288
+ scopes: tokenRecord.scopes,
3289
+ exp: this.accessTokenTtlSeconds,
3202
3290
  claims: {
3203
3291
  email: tokenRecord.user.email,
3204
3292
  name: tokenRecord.user.name,
@@ -3303,14 +3391,14 @@ ${error.stack}` : String(error);
3303
3391
  name
3304
3392
  };
3305
3393
  const code = this.generateCode();
3306
- const scopes = scope.split(" ").filter(Boolean);
3394
+ const scopes2 = scope.split(" ").filter(Boolean);
3307
3395
  this.authCodes.set(code, {
3308
3396
  code,
3309
3397
  clientId,
3310
3398
  redirectUri,
3311
3399
  codeChallenge,
3312
3400
  codeChallengeMethod,
3313
- scopes,
3401
+ scopes: scopes2,
3314
3402
  user: testUser,
3315
3403
  state,
3316
3404
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3416,8 +3504,7 @@ ${error.stack}` : String(error);
3416
3504
  * Generate a random authorization code
3417
3505
  */
3418
3506
  async generateCodeAsync() {
3419
- await loadCryptoUtils();
3420
- return _base64urlEncode(_randomBytes(32));
3507
+ return base64urlEncode(randomBytes(32));
3421
3508
  }
3422
3509
  /**
3423
3510
  * Generate a random authorization code (sync wrapper for compatibility)
@@ -3433,8 +3520,7 @@ ${error.stack}` : String(error);
3433
3520
  */
3434
3521
  async computeCodeChallengeAsync(verifier, method) {
3435
3522
  if (method === "S256") {
3436
- await loadCryptoUtils();
3437
- return _sha256Base64url(verifier);
3523
+ return sha256Base64url(verifier);
3438
3524
  }
3439
3525
  return verifier;
3440
3526
  }
@@ -4021,7 +4107,7 @@ var MockCimdServer = class {
4021
4107
  };
4022
4108
 
4023
4109
  // libs/testing/src/server/test-server.ts
4024
- import { spawn } from "child_process";
4110
+ import { execFile, spawn } from "child_process";
4025
4111
  import { sha256Hex } from "@frontmcp/utils";
4026
4112
 
4027
4113
  // libs/testing/src/errors/index.ts
@@ -4128,6 +4214,8 @@ var E2E_PORT_RANGES = {
4128
4214
  "demo-e2e-distributed": { start: 50440, size: 20 },
4129
4215
  // Protocol revision E2E tests (50460-50479)
4130
4216
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
4217
+ // Testing library E2E tests (50480-50499)
4218
+ "demo-e2e-testing": { start: 50480, size: 20 },
4131
4219
  // Mock servers and utilities (50900-50999)
4132
4220
  "mock-oauth": { start: 50900, size: 10 },
4133
4221
  "mock-api": { start: 50910, size: 10 },
@@ -4136,6 +4224,66 @@ var E2E_PORT_RANGES = {
4136
4224
  default: { start: 51e3, size: 100 }
4137
4225
  };
4138
4226
  var reservedPorts = /* @__PURE__ */ new Map();
4227
+ var lockedPorts = /* @__PURE__ */ new Set();
4228
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
4229
+ function lockDir() {
4230
+ try {
4231
+ const os = __require("node:os");
4232
+ const path = __require("node:path");
4233
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
4234
+ } catch {
4235
+ return void 0;
4236
+ }
4237
+ }
4238
+ function pidIsAlive(pid) {
4239
+ try {
4240
+ process.kill(pid, 0);
4241
+ return true;
4242
+ } catch (error) {
4243
+ return error.code === "EPERM";
4244
+ }
4245
+ }
4246
+ function acquirePortLock(port) {
4247
+ if (lockedPorts.has(port)) return false;
4248
+ let fs;
4249
+ let file;
4250
+ try {
4251
+ fs = __require("node:fs");
4252
+ const dir = lockDir();
4253
+ if (!dir) return true;
4254
+ fs.mkdirSync(dir, { recursive: true });
4255
+ file = __require("node:path").join(dir, `${port}.lock`);
4256
+ } catch {
4257
+ return true;
4258
+ }
4259
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
4260
+ for (let attempt = 0; attempt < 2; attempt++) {
4261
+ try {
4262
+ fs.writeFileSync(file, payload, { flag: "wx" });
4263
+ lockedPorts.add(port);
4264
+ return true;
4265
+ } catch (error) {
4266
+ if (error.code !== "EEXIST") return true;
4267
+ try {
4268
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
4269
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
4270
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
4271
+ fs.unlinkSync(file);
4272
+ } catch {
4273
+ }
4274
+ }
4275
+ }
4276
+ return false;
4277
+ }
4278
+ function releasePortLock(port) {
4279
+ if (!lockedPorts.delete(port)) return;
4280
+ try {
4281
+ const fs = __require("node:fs");
4282
+ const dir = lockDir();
4283
+ if (dir) fs.unlinkSync(__require("node:path").join(dir, `${port}.lock`));
4284
+ } catch {
4285
+ }
4286
+ }
4139
4287
  var projectPortIndex = /* @__PURE__ */ new Map();
4140
4288
  function getPortRange(project) {
4141
4289
  const key = project;
@@ -4146,15 +4294,22 @@ function getPortRange(project) {
4146
4294
  }
4147
4295
  async function reservePort(project, preferredPort) {
4148
4296
  const range = getPortRange(project);
4149
- if (preferredPort !== void 0) {
4297
+ const handle = (port) => ({
4298
+ port,
4299
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
4300
+ releaseSocket: async () => {
4301
+ await releasePort(port);
4302
+ },
4303
+ release: async () => {
4304
+ await releasePort(port);
4305
+ releasePortLock(port);
4306
+ }
4307
+ });
4308
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
4309
+ if (wantsSpecificPort) {
4150
4310
  const reservation = await tryReservePort(preferredPort, project);
4151
4311
  if (reservation) {
4152
- return {
4153
- port: preferredPort,
4154
- release: async () => {
4155
- await releasePort(preferredPort);
4156
- }
4157
- };
4312
+ return handle(preferredPort);
4158
4313
  }
4159
4314
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
4160
4315
  }
@@ -4163,30 +4318,19 @@ async function reservePort(project, preferredPort) {
4163
4318
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
4164
4319
  const port = range.start + index % range.size;
4165
4320
  index = (index + 1) % range.size;
4166
- if (reservedPorts.has(port)) {
4321
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
4167
4322
  continue;
4168
4323
  }
4169
4324
  const reservation = await tryReservePort(port, project);
4170
4325
  if (reservation) {
4171
4326
  projectPortIndex.set(project, index);
4172
- return {
4173
- port,
4174
- release: async () => {
4175
- await releasePort(port);
4176
- }
4177
- };
4327
+ return handle(port);
4178
4328
  }
4179
4329
  }
4180
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
4181
- if (dynamicPort) {
4182
- const reservation = await tryReservePort(dynamicPort, project);
4183
- if (reservation) {
4184
- return {
4185
- port: dynamicPort,
4186
- release: async () => {
4187
- await releasePort(dynamicPort);
4188
- }
4189
- };
4330
+ for (let port = 51e3; port < 52e3; port++) {
4331
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
4332
+ if (await tryReservePort(port, project)) {
4333
+ return handle(port);
4190
4334
  }
4191
4335
  }
4192
4336
  throw new Error(
@@ -4194,9 +4338,13 @@ async function reservePort(project, preferredPort) {
4194
4338
  );
4195
4339
  }
4196
4340
  async function tryReservePort(port, project) {
4341
+ if (!acquirePortLock(port)) {
4342
+ return false;
4343
+ }
4197
4344
  return new Promise((resolve) => {
4198
4345
  const server = createServer4();
4199
4346
  server.once("error", () => {
4347
+ releasePortLock(port);
4200
4348
  resolve(false);
4201
4349
  });
4202
4350
  server.listen(port, () => {
@@ -4222,31 +4370,6 @@ async function releasePort(port) {
4222
4370
  });
4223
4371
  });
4224
4372
  }
4225
- async function findAvailablePortInRange(start, end) {
4226
- for (let port = start; port < end; port++) {
4227
- if (reservedPorts.has(port)) {
4228
- continue;
4229
- }
4230
- const available = await isPortAvailable(port);
4231
- if (available) {
4232
- return port;
4233
- }
4234
- }
4235
- return null;
4236
- }
4237
- async function isPortAvailable(port) {
4238
- return new Promise((resolve) => {
4239
- const server = createServer4();
4240
- server.once("error", () => {
4241
- resolve(false);
4242
- });
4243
- server.listen(port, () => {
4244
- server.close(() => {
4245
- resolve(true);
4246
- });
4247
- });
4248
- });
4249
- }
4250
4373
 
4251
4374
  // libs/testing/src/server/test-server.ts
4252
4375
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -4255,8 +4378,11 @@ var TestServer = class _TestServer {
4255
4378
  options;
4256
4379
  _info;
4257
4380
  logs = [];
4381
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
4258
4382
  portRelease = null;
4259
- constructor(options, port, portRelease) {
4383
+ /** Frees only the reservation socket, just before the server is spawned */
4384
+ portReleaseSocket = null;
4385
+ constructor(options, port, portRelease, portReleaseSocket) {
4260
4386
  this.options = {
4261
4387
  port,
4262
4388
  project: options.project,
@@ -4268,6 +4394,7 @@ var TestServer = class _TestServer {
4268
4394
  debug: options.debug ?? DEBUG_SERVER
4269
4395
  };
4270
4396
  this.portRelease = portRelease ?? null;
4397
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
4271
4398
  this._info = {
4272
4399
  baseUrl: `http://localhost:${port}`,
4273
4400
  port
@@ -4280,8 +4407,8 @@ var TestServer = class _TestServer {
4280
4407
  const project = options.project ?? "default";
4281
4408
  const maxAttempts = 3;
4282
4409
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4283
- const { port, release } = await reservePort(project, options.port);
4284
- const server = new _TestServer(options, port, release);
4410
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4411
+ const server = new _TestServer(options, port, release, releaseSocket);
4285
4412
  try {
4286
4413
  await server.startProcess();
4287
4414
  return server;
@@ -4321,7 +4448,7 @@ var TestServer = class _TestServer {
4321
4448
  }
4322
4449
  const maxAttempts = 3;
4323
4450
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4324
- const { port, release } = await reservePort(project, options.port);
4451
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4325
4452
  const serverOptions = {
4326
4453
  ...options,
4327
4454
  port,
@@ -4329,7 +4456,7 @@ var TestServer = class _TestServer {
4329
4456
  command: `npx nx serve ${project} --port ${port}`,
4330
4457
  cwd: options.cwd ?? process.cwd()
4331
4458
  };
4332
- const server = new _TestServer(serverOptions, port, release);
4459
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
4333
4460
  try {
4334
4461
  await server.startProcess();
4335
4462
  return server;
@@ -4387,10 +4514,17 @@ var TestServer = class _TestServer {
4387
4514
  * Stop the test server
4388
4515
  */
4389
4516
  async stop() {
4390
- if (this.portRelease) {
4391
- await this.portRelease();
4392
- this.portRelease = null;
4517
+ try {
4518
+ await this.stopProcess();
4519
+ } finally {
4520
+ if (this.portRelease) {
4521
+ await this.portRelease();
4522
+ this.portRelease = null;
4523
+ }
4524
+ this.portReleaseSocket = null;
4393
4525
  }
4526
+ }
4527
+ async stopProcess() {
4394
4528
  if (this.process) {
4395
4529
  this.log("Stopping server...");
4396
4530
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -4463,7 +4597,7 @@ var TestServer = class _TestServer {
4463
4597
  * Restart the server
4464
4598
  */
4465
4599
  async restart() {
4466
- await this.stop();
4600
+ await this.stopProcess();
4467
4601
  await this.startProcess();
4468
4602
  }
4469
4603
  /**
@@ -4493,9 +4627,9 @@ var TestServer = class _TestServer {
4493
4627
  PORT: String(this.options.port)
4494
4628
  };
4495
4629
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
4496
- if (this.portRelease) {
4497
- await this.portRelease();
4498
- this.portRelease = null;
4630
+ if (this.portReleaseSocket) {
4631
+ await this.portReleaseSocket();
4632
+ this.portReleaseSocket = null;
4499
4633
  await sleep2(300);
4500
4634
  }
4501
4635
  this.process = spawn(this.options.command, [], {
@@ -4505,9 +4639,8 @@ var TestServer = class _TestServer {
4505
4639
  detached: true,
4506
4640
  stdio: ["pipe", "pipe", "pipe"]
4507
4641
  });
4508
- if (this.process.pid !== void 0) {
4509
- this._info.pid = this.process.pid;
4510
- }
4642
+ this._info.pid = this.process.pid;
4643
+ this._info.shellPid = this.process.pid;
4511
4644
  let processExited = false;
4512
4645
  let exitCode = null;
4513
4646
  let exitError = null;
@@ -4610,6 +4743,8 @@ ${allLogs}`);
4610
4743
  });
4611
4744
  if (response.ok || response.status === 404) {
4612
4745
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
4746
+ const listeningPid = await findListeningPid(this.options.port);
4747
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
4613
4748
  return;
4614
4749
  }
4615
4750
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -4654,6 +4789,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
4654
4789
  function sleep2(ms) {
4655
4790
  return new Promise((resolve) => setTimeout(resolve, ms));
4656
4791
  }
4792
+ function findListeningPid(port) {
4793
+ if (process.platform === "win32") return Promise.resolve(void 0);
4794
+ return new Promise((resolve) => {
4795
+ execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
4796
+ if (error) {
4797
+ resolve(void 0);
4798
+ return;
4799
+ }
4800
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
4801
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
4802
+ });
4803
+ });
4804
+ }
4657
4805
  function withWorkspaceProtocolFallback(env, cwd) {
4658
4806
  if (findInstalledProtocolPackageDir(cwd)) {
4659
4807
  return env;
@@ -4925,16 +5073,6 @@ function hasMimeType(result, mimeType) {
4925
5073
  return result.hasMimeType(mimeType);
4926
5074
  }
4927
5075
 
4928
- // libs/testing/src/fixtures/test-fixture.ts
4929
- import {
4930
- afterAll as _afterAll,
4931
- afterEach as _afterEach,
4932
- beforeAll as _beforeAll,
4933
- beforeEach as _beforeEach,
4934
- describe as _describe,
4935
- it as _it
4936
- } from "@jest/globals";
4937
-
4938
5076
  // libs/testing/src/fixtures/gateway-token-binding.ts
4939
5077
  import { trimSlashes, trimTrailing } from "@frontmcp/utils";
4940
5078
  function gatewayTokenBinding(serverUrl, entryPath) {
@@ -4945,81 +5083,152 @@ function gatewayTokenBinding(serverUrl, entryPath) {
4945
5083
  }
4946
5084
 
4947
5085
  // libs/testing/src/fixtures/test-fixture.ts
4948
- var describe2 = _describe;
4949
- var beforeAll2 = _beforeAll;
4950
- var beforeEach2 = _beforeEach;
4951
- var afterEach2 = _afterEach;
4952
- var afterAll2 = _afterAll;
4953
- var it2 = _it;
4954
- var currentConfig = {};
4955
- var serverInstance = null;
4956
- var tokenFactory = null;
4957
- var serverStartedByUs = false;
4958
- var skipScopes = [{ skipped: false }];
4959
- function currentSkipScope() {
4960
- return skipScopes[skipScopes.length - 1];
5086
+ var jestApi;
5087
+ function jestGlobals() {
5088
+ if (jestApi) return jestApi;
5089
+ try {
5090
+ const api = __require("@jest/globals");
5091
+ jestApi = {
5092
+ describe: api["describe"],
5093
+ beforeAll: api["beforeAll"],
5094
+ beforeEach: api["beforeEach"],
5095
+ afterEach: api["afterEach"],
5096
+ afterAll: api["afterAll"],
5097
+ it: api["it"]
5098
+ };
5099
+ return jestApi;
5100
+ } catch (error) {
5101
+ throw new Error(
5102
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
5103
+ { cause: error }
5104
+ );
5105
+ }
4961
5106
  }
4962
- async function initializeSharedResources() {
4963
- if (!serverInstance) {
4964
- if (currentConfig.baseUrl && !currentConfig.server) {
4965
- serverInstance = TestServer.connect(currentConfig.baseUrl);
4966
- serverStartedByUs = false;
4967
- } else if (currentConfig.server) {
4968
- const serverCommand = resolveServerCommand(currentConfig.server);
4969
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
5107
+ var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
5108
+ function currentScope() {
5109
+ return scopes[scopes.length - 1];
5110
+ }
5111
+ var serverEntries = /* @__PURE__ */ new Map();
5112
+ function resolveConfig(chain) {
5113
+ const merged = {};
5114
+ for (const scope of chain) {
5115
+ const { env, ...rest } = scope.config;
5116
+ Object.assign(merged, rest);
5117
+ if (env) merged.env = { ...merged.env, ...env };
5118
+ }
5119
+ return merged;
5120
+ }
5121
+ function ownerScopeOf(chain) {
5122
+ for (let i = chain.length - 1; i > 0; i--) {
5123
+ if (chain[i].configured) return chain[i];
5124
+ }
5125
+ return chain[0];
5126
+ }
5127
+ function serverKey(config) {
5128
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
5129
+ return JSON.stringify({
5130
+ server: config.server,
5131
+ baseUrl: config.baseUrl,
5132
+ project: config.project,
5133
+ port: config.port,
5134
+ entryPath: config.entryPath,
5135
+ startupTimeout: config.startupTimeout,
5136
+ logLevel: config.logLevel,
5137
+ auth: config.auth,
5138
+ env
5139
+ });
5140
+ }
5141
+ function serverEnv(config) {
5142
+ const extra = {};
5143
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
5144
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
5145
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
5146
+ return { ...extra, ...config.env };
5147
+ }
5148
+ async function startServerEntry(config, owner) {
5149
+ let server;
5150
+ let startedByUs = false;
5151
+ if (config.baseUrl && !config.server) {
5152
+ server = TestServer.connect(config.baseUrl);
5153
+ } else if (config.server) {
5154
+ const serverCommand = resolveServerCommand(config.server);
5155
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
5156
+ if (isDebug) {
5157
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
5158
+ }
5159
+ try {
5160
+ server = await TestServer.start({
5161
+ project: config.project,
5162
+ port: config.port,
5163
+ command: serverCommand,
5164
+ env: serverEnv(config),
5165
+ startupTimeout: config.startupTimeout ?? 3e4,
5166
+ debug: isDebug
5167
+ });
5168
+ startedByUs = true;
4970
5169
  if (isDebug) {
4971
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
5170
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
4972
5171
  }
4973
- try {
4974
- serverInstance = await TestServer.start({
4975
- project: currentConfig.project,
4976
- port: currentConfig.port,
4977
- command: serverCommand,
4978
- env: currentConfig.env,
4979
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
4980
- debug: isDebug
4981
- });
4982
- serverStartedByUs = true;
4983
- if (isDebug) {
4984
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
4985
- }
4986
- } catch (error) {
4987
- const errMsg = error instanceof Error ? error.message : String(error);
4988
- throw new Error(
4989
- `Failed to start test server.
5172
+ } catch (error) {
5173
+ const errMsg = error instanceof Error ? error.message : String(error);
5174
+ throw new Error(
5175
+ `Failed to start test server.
4990
5176
 
4991
- Server entry: ${currentConfig.server}
4992
- Project: ${currentConfig.project ?? "default"}
5177
+ Server entry: ${config.server}
5178
+ Project: ${config.project ?? "default"}
4993
5179
  Command: ${serverCommand}
4994
5180
 
4995
- Error: ${errMsg}`
4996
- );
4997
- }
4998
- } else {
4999
- throw new Error(
5000
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
5181
+ Error: ${errMsg}`,
5182
+ { cause: error }
5001
5183
  );
5002
5184
  }
5185
+ } else {
5186
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
5003
5187
  }
5004
- if (!tokenFactory) {
5005
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
5006
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
5007
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5008
- }
5188
+ const sharedSecret = config.env?.["JWT_SECRET"];
5189
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
5190
+ const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5191
+ return { server, startedByUs, tokenFactory: tokenFactory2, owner };
5009
5192
  }
5010
- async function createTestFixtures() {
5011
- await initializeSharedResources();
5012
- if (!serverInstance) {
5013
- throw new Error("Server instance not initialized");
5014
- }
5015
- if (!tokenFactory) {
5016
- throw new Error("Token factory not initialized");
5193
+ function acquireServer(chain) {
5194
+ const config = resolveConfig(chain);
5195
+ const key = serverKey(config);
5196
+ let entry = serverEntries.get(key);
5197
+ if (!entry) {
5198
+ entry = startServerEntry(config, ownerScopeOf(chain));
5199
+ serverEntries.set(key, entry);
5200
+ entry.catch(() => serverEntries.delete(key));
5201
+ }
5202
+ return entry;
5203
+ }
5204
+ async function stopServers(owner) {
5205
+ for (const [key, pending] of Array.from(serverEntries)) {
5206
+ let entry;
5207
+ try {
5208
+ entry = await pending;
5209
+ } catch {
5210
+ serverEntries.delete(key);
5211
+ continue;
5212
+ }
5213
+ if (owner && entry.owner !== owner) continue;
5214
+ serverEntries.delete(key);
5215
+ if (entry.startedByUs) {
5216
+ await entry.server.stop();
5217
+ }
5017
5218
  }
5219
+ }
5220
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
5221
+ async function buildFixtures(chain) {
5222
+ const config = resolveConfig(chain);
5223
+ const entry = await acquireServer(chain);
5224
+ const { server, tokenFactory: tokenFactory2 } = entry;
5225
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
5226
+ const clientConfig = { ...config, publicMode };
5018
5227
  const clientInstance = McpTestClient.create({
5019
- baseUrl: resolveClientBaseUrl(serverInstance),
5020
- entryPath: currentConfig.entryPath,
5021
- transport: currentConfig.transport ?? "streamable-http",
5022
- publicMode: currentConfig.publicMode
5228
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
5229
+ entryPath: clientConfig.entryPath,
5230
+ transport: clientConfig.transport ?? "streamable-http",
5231
+ publicMode
5023
5232
  }).build();
5024
5233
  try {
5025
5234
  await clientInstance.connect();
@@ -5033,17 +5242,18 @@ async function createTestFixtures() {
5033
5242
  throw err;
5034
5243
  }
5035
5244
  }
5036
- const auth = createAuthFixture(tokenFactory);
5037
- const server = createServerFixture(serverInstance);
5038
- return {
5245
+ const context = { extraClients: [] };
5246
+ const fixtures = {
5039
5247
  mcp: clientInstance,
5040
- auth,
5041
- server
5248
+ auth: createAuthFixture(tokenFactory2),
5249
+ server: createServerFixture(server, clientConfig, clientInstance, context)
5042
5250
  };
5251
+ fixtureContexts.set(fixtures, context);
5252
+ return fixtures;
5043
5253
  }
5044
5254
  async function cleanupTestFixtures(fixtures, testFailed = false) {
5045
- if (testFailed && serverInstance) {
5046
- const logs = serverInstance.getLogs();
5255
+ if (testFailed) {
5256
+ const logs = fixtures.server.getLogs();
5047
5257
  if (logs.length > 0) {
5048
5258
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
5049
5259
  const recentLogs = logs.slice(-50);
@@ -5054,20 +5264,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
5054
5264
  console.error("[TestFixture] === End Server Logs ===\n");
5055
5265
  }
5056
5266
  }
5267
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
5268
+ for (const client of extras) {
5269
+ if (client.isConnected()) {
5270
+ await client.disconnect().catch(() => void 0);
5271
+ }
5272
+ }
5057
5273
  if (fixtures.mcp.isConnected()) {
5058
5274
  await fixtures.mcp.disconnect();
5059
5275
  }
5060
5276
  }
5061
- async function cleanupSharedResources() {
5062
- if (serverInstance && serverStartedByUs) {
5063
- await serverInstance.stop();
5064
- }
5065
- serverInstance = null;
5066
- tokenFactory = null;
5067
- serverStartedByUs = false;
5068
- }
5069
- function resolveClientBaseUrl(server) {
5070
- return currentConfig.baseUrl ?? server.info.baseUrl;
5277
+ function resolveClientBaseUrl(server, config) {
5278
+ return config.baseUrl ?? server.info.baseUrl;
5071
5279
  }
5072
5280
  function createAuthFixture(factory) {
5073
5281
  const users = {
@@ -5113,28 +5321,39 @@ function createAuthFixture(factory) {
5113
5321
  getAudience: () => factory.getAudience()
5114
5322
  };
5115
5323
  }
5116
- function createServerFixture(server) {
5324
+ function createServerFixture(server, config, mcp, context) {
5117
5325
  return {
5118
- info: server.info,
5326
+ // Live view: `pid` is filled in once the server is listening and follows restarts
5327
+ get info() {
5328
+ return server.info;
5329
+ },
5119
5330
  createClient: async (opts) => {
5120
- return McpTestClient.create({
5121
- baseUrl: resolveClientBaseUrl(server),
5122
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
5331
+ const client = await McpTestClient.create({
5332
+ baseUrl: resolveClientBaseUrl(server, config),
5333
+ entryPath: opts?.entryPath ?? config.entryPath,
5123
5334
  transport: opts?.transport ?? "streamable-http",
5124
5335
  auth: opts?.token ? { token: opts.token } : void 0,
5125
5336
  clientInfo: opts?.clientInfo,
5126
- publicMode: currentConfig.publicMode
5337
+ publicMode: config.publicMode
5127
5338
  }).buildAndConnect();
5339
+ context.extraClients.push(client);
5340
+ return client;
5128
5341
  },
5129
5342
  createClientBuilder: () => {
5130
5343
  const builder = new McpTestClientBuilder({
5131
- baseUrl: resolveClientBaseUrl(server),
5132
- entryPath: currentConfig.entryPath,
5133
- publicMode: currentConfig.publicMode
5344
+ baseUrl: resolveClientBaseUrl(server, config),
5345
+ entryPath: config.entryPath,
5346
+ publicMode: config.publicMode
5134
5347
  });
5135
5348
  return builder;
5136
5349
  },
5137
- restart: () => server.restart(),
5350
+ restart: async () => {
5351
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
5352
+ await server.restart();
5353
+ for (const client of reconnect) {
5354
+ await client.reconnect();
5355
+ }
5356
+ },
5138
5357
  getLogs: () => server.getLogs(),
5139
5358
  clearLogs: () => server.clearLogs()
5140
5359
  };
@@ -5145,12 +5364,12 @@ function resolveServerCommand(server) {
5145
5364
  }
5146
5365
  return `npx tsx ${server}`;
5147
5366
  }
5148
- function runWithFixtures(fn) {
5149
- return async () => {
5150
- const fixtures = await createTestFixtures();
5367
+ function runWithFixtures(fn, chain) {
5368
+ return async (...extra) => {
5369
+ const fixtures = await buildFixtures(chain);
5151
5370
  let testFailed = false;
5152
5371
  try {
5153
- await fn(fixtures);
5372
+ await fn(fixtures, ...extra);
5154
5373
  } catch (error) {
5155
5374
  testFailed = true;
5156
5375
  throw error;
@@ -5159,24 +5378,37 @@ function runWithFixtures(fn) {
5159
5378
  }
5160
5379
  };
5161
5380
  }
5381
+ function skipTitle(name, scope) {
5382
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
5383
+ }
5162
5384
  function testWithFixtures(name, fn) {
5163
- const scope = currentSkipScope();
5385
+ const chain = scopes.slice();
5386
+ const scope = currentScope();
5164
5387
  if (scope.skipped) {
5165
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5388
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
5166
5389
  return;
5167
5390
  }
5168
- it2(name, runWithFixtures(fn));
5391
+ jestGlobals().it(name, runWithFixtures(fn, chain));
5169
5392
  }
5170
5393
  function use(config) {
5171
- currentConfig = { ...currentConfig, ...config };
5172
- afterAll2(async () => {
5173
- await cleanupSharedResources();
5174
- });
5394
+ const scope = currentScope();
5395
+ scope.config = {
5396
+ ...scope.config,
5397
+ ...config,
5398
+ env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
5399
+ };
5400
+ scope.configured = true;
5401
+ if (!scope.cleanupRegistered) {
5402
+ scope.cleanupRegistered = true;
5403
+ jestGlobals().afterAll(async () => {
5404
+ await stopServers(scope === scopes[0] ? void 0 : scope);
5405
+ });
5406
+ }
5175
5407
  }
5176
5408
  function skip(nameOrCondition, fnOrReason) {
5177
5409
  if (typeof nameOrCondition === "boolean") {
5178
5410
  if (nameOrCondition) {
5179
- const scope = currentSkipScope();
5411
+ const scope = currentScope();
5180
5412
  scope.skipped = true;
5181
5413
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
5182
5414
  }
@@ -5187,57 +5419,111 @@ function skip(nameOrCondition, fnOrReason) {
5187
5419
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
5188
5420
  );
5189
5421
  }
5190
- it2.skip(nameOrCondition, runWithFixtures(fnOrReason));
5422
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
5191
5423
  }
5192
5424
  function only(name, fn) {
5193
- const scope = currentSkipScope();
5425
+ const scope = currentScope();
5194
5426
  if (scope.skipped) {
5195
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5427
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
5196
5428
  return;
5197
5429
  }
5198
- it2.only(name, runWithFixtures(fn));
5430
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
5199
5431
  }
5200
5432
  function todo(name) {
5201
- it2.todo(name);
5433
+ jestGlobals().it.todo(name);
5202
5434
  }
5203
- function withSkipScope(register) {
5435
+ function each(table, ...tagged) {
5436
+ const chain = scopes.slice();
5437
+ const register = jestGlobals().it.each(table, ...tagged);
5204
5438
  return (name, fn) => {
5205
- register(name, () => {
5206
- const parent = currentSkipScope();
5207
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
5208
- try {
5209
- fn();
5210
- } finally {
5211
- skipScopes.pop();
5212
- }
5439
+ const scope = currentScope();
5440
+ if (scope.skipped) {
5441
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
5442
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
5443
+ return;
5444
+ }
5445
+ register(name, runWithFixtures(fn, chain));
5446
+ };
5447
+ }
5448
+ function withScope(fn) {
5449
+ return (...args) => {
5450
+ const parent = currentScope();
5451
+ scopes.push({
5452
+ skipped: parent.skipped,
5453
+ reason: parent.reason,
5454
+ config: {},
5455
+ configured: false,
5456
+ cleanupRegistered: false
5213
5457
  });
5458
+ try {
5459
+ fn(...args);
5460
+ } finally {
5461
+ scopes.pop();
5462
+ }
5214
5463
  };
5215
5464
  }
5216
- function eachWithSkipScope(each) {
5217
- return ((...eachArgs) => {
5218
- const register = each(...eachArgs);
5219
- return withSkipScope(register);
5220
- });
5465
+ function scopedRegister(pick) {
5466
+ return (name, fn, timeout) => {
5467
+ pick()(name, withScope(fn), timeout);
5468
+ };
5221
5469
  }
5222
- var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
5223
- only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
5224
- skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
5225
- each: eachWithSkipScope(describe2.each)
5226
- });
5470
+ function scopedEach(pick) {
5471
+ return (...eachArgs) => (name, fn, timeout) => {
5472
+ const register = pick()(...eachArgs);
5473
+ register(name, withScope(fn), timeout);
5474
+ };
5475
+ }
5476
+ var describeWithScope = Object.assign(
5477
+ scopedRegister(() => jestGlobals().describe),
5478
+ {
5479
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
5480
+ only: Object.assign(
5481
+ scopedRegister(() => jestGlobals().describe.only),
5482
+ {
5483
+ each: scopedEach(() => jestGlobals().describe.only.each)
5484
+ }
5485
+ ),
5486
+ skip: Object.assign(
5487
+ scopedRegister(() => jestGlobals().describe.skip),
5488
+ {
5489
+ each: scopedEach(() => jestGlobals().describe.skip.each)
5490
+ }
5491
+ ),
5492
+ each: scopedEach(() => jestGlobals().describe.each)
5493
+ }
5494
+ );
5227
5495
  var test = testWithFixtures;
5228
5496
  test.use = use;
5229
- test.describe = describeWithSkipScope;
5230
- test.beforeAll = beforeAll2;
5231
- test.beforeEach = beforeEach2;
5232
- test.afterEach = afterEach2;
5233
- test.afterAll = afterAll2;
5497
+ test.describe = describeWithScope;
5498
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
5499
+ test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
5500
+ test.afterEach = ((...args) => jestGlobals().afterEach(...args));
5501
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
5234
5502
  test.skip = skip;
5235
5503
  test.only = only;
5236
5504
  test.todo = todo;
5505
+ test.each = each;
5237
5506
 
5238
5507
  // libs/testing/src/expect.ts
5239
- import { expect as jestExpect } from "@jest/globals";
5240
- var expect = jestExpect;
5508
+ var resolvedExpect;
5509
+ function resolveJestExpect() {
5510
+ if (resolvedExpect) return resolvedExpect;
5511
+ try {
5512
+ resolvedExpect = __require("@jest/globals").expect;
5513
+ return resolvedExpect;
5514
+ } catch (error) {
5515
+ throw new Error(
5516
+ `expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
5517
+ { cause: error }
5518
+ );
5519
+ }
5520
+ }
5521
+ var expect = new Proxy(function frontmcpExpect() {
5522
+ }, {
5523
+ apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
5524
+ get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
5525
+ has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
5526
+ });
5241
5527
 
5242
5528
  // libs/testing/src/platform/platform-types.ts
5243
5529
  function getPlatformMetaNamespace(platform) {
@@ -5900,34 +6186,44 @@ var HttpInterceptorImpl = class {
5900
6186
  }
5901
6187
  };
5902
6188
  }
5903
- get(url, response) {
6189
+ get(url, response, options) {
5904
6190
  return this.mock({
5905
- match: { url, method: "GET" },
5906
- response: normalizeResponse(response)
6191
+ match: { url, method: "GET", ...options?.match },
6192
+ response: normalizeResponse(response),
6193
+ ...options?.times !== void 0 && { times: options.times },
6194
+ ...options?.name !== void 0 && { name: options.name }
5907
6195
  });
5908
6196
  }
5909
- post(url, response) {
6197
+ post(url, response, options) {
5910
6198
  return this.mock({
5911
- match: { url, method: "POST" },
5912
- response: normalizeResponse(response)
6199
+ match: { url, method: "POST", ...options?.match },
6200
+ response: normalizeResponse(response),
6201
+ ...options?.times !== void 0 && { times: options.times },
6202
+ ...options?.name !== void 0 && { name: options.name }
5913
6203
  });
5914
6204
  }
5915
- put(url, response) {
6205
+ put(url, response, options) {
5916
6206
  return this.mock({
5917
- match: { url, method: "PUT" },
5918
- response: normalizeResponse(response)
6207
+ match: { url, method: "PUT", ...options?.match },
6208
+ response: normalizeResponse(response),
6209
+ ...options?.times !== void 0 && { times: options.times },
6210
+ ...options?.name !== void 0 && { name: options.name }
5919
6211
  });
5920
6212
  }
5921
- delete(url, response) {
6213
+ delete(url, response, options) {
5922
6214
  return this.mock({
5923
- match: { url, method: "DELETE" },
5924
- response: normalizeResponse(response)
6215
+ match: { url, method: "DELETE", ...options?.match },
6216
+ response: normalizeResponse(response),
6217
+ ...options?.times !== void 0 && { times: options.times },
6218
+ ...options?.name !== void 0 && { name: options.name }
5925
6219
  });
5926
6220
  }
5927
- any(url, response) {
6221
+ any(url, response, options) {
5928
6222
  return this.mock({
5929
- match: { url },
5930
- response: normalizeResponse(response)
6223
+ match: { url, ...options?.match },
6224
+ response: normalizeResponse(response),
6225
+ ...options?.times !== void 0 && { times: options.times },
6226
+ ...options?.name !== void 0 && { name: options.name }
5931
6227
  });
5932
6228
  }
5933
6229
  clear() {
@@ -5959,6 +6255,9 @@ ${descriptions.join("\n")}`);
5959
6255
  if (index !== -1) {
5960
6256
  activeInterceptors.splice(index, 1);
5961
6257
  }
6258
+ if (activeInterceptors.length === 0) {
6259
+ httpMock.disable();
6260
+ }
5962
6261
  }
5963
6262
  // Internal methods
5964
6263
  isActive() {
@@ -6058,43 +6357,49 @@ ${descriptions.join("\n")}`);
6058
6357
  }
6059
6358
  };
6060
6359
  async function interceptedFetch(input, init) {
6061
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
6062
- const method = (init?.method ?? "GET").toUpperCase();
6360
+ const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
6361
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
6362
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
6063
6363
  const headers = {};
6064
- if (init?.headers) {
6065
- if (init.headers instanceof Headers) {
6066
- init.headers.forEach((value, key) => {
6364
+ const collectHeaders = (source) => {
6365
+ if (!source) return;
6366
+ if (source instanceof Headers) {
6367
+ source.forEach((value, key) => {
6067
6368
  headers[key.toLowerCase()] = value;
6068
6369
  });
6069
- } else if (Array.isArray(init.headers)) {
6070
- for (const [key, value] of init.headers) {
6370
+ } else if (Array.isArray(source)) {
6371
+ for (const [key, value] of source) {
6071
6372
  headers[key.toLowerCase()] = value;
6072
6373
  }
6073
6374
  } else {
6074
- for (const [key, value] of Object.entries(init.headers)) {
6375
+ for (const [key, value] of Object.entries(source)) {
6075
6376
  headers[key.toLowerCase()] = value;
6076
6377
  }
6077
6378
  }
6078
- }
6379
+ };
6380
+ collectHeaders(init?.headers ?? request?.headers);
6079
6381
  let body;
6080
6382
  let rawBody;
6081
- if (init?.body) {
6082
- if (typeof init.body === "string") {
6083
- rawBody = init.body;
6084
- try {
6085
- body = JSON.parse(init.body);
6086
- } catch {
6087
- body = init.body;
6088
- }
6089
- } else if (init.body instanceof ArrayBuffer || init.body instanceof Uint8Array) {
6090
- rawBody = new TextDecoder().decode(init.body);
6383
+ let bodySource = init?.body;
6384
+ if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
6385
+ bodySource = await request.clone().text();
6386
+ }
6387
+ if (bodySource) {
6388
+ if (typeof bodySource === "string") {
6389
+ rawBody = bodySource;
6390
+ } else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
6391
+ rawBody = new TextDecoder().decode(bodySource);
6392
+ } else if (bodySource instanceof URLSearchParams) {
6393
+ rawBody = bodySource.toString();
6394
+ }
6395
+ if (rawBody !== void 0) {
6091
6396
  try {
6092
6397
  body = JSON.parse(rawBody);
6093
6398
  } catch {
6094
6399
  body = rawBody;
6095
6400
  }
6096
6401
  } else {
6097
- body = init.body;
6402
+ body = bodySource;
6098
6403
  }
6099
6404
  }
6100
6405
  const requestInfo = {
@@ -6124,8 +6429,20 @@ async function interceptedFetch(input, init) {
6124
6429
  Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
6125
6430
  );
6126
6431
  }
6432
+ var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
6433
+ function isResponseDescriptor(value) {
6434
+ const keys = Object.keys(value);
6435
+ if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
6436
+ if ("status" in value) {
6437
+ const status = value["status"];
6438
+ if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
6439
+ }
6440
+ return true;
6441
+ }
6127
6442
  function normalizeResponse(response) {
6128
- if (!("status" in response) && !("body" in response) && !("headers" in response)) {
6443
+ const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
6444
+ if (isNetworkError) return response;
6445
+ if (Array.isArray(response) || !isResponseDescriptor(response)) {
6129
6446
  return { body: response };
6130
6447
  }
6131
6448
  return response;
@@ -7459,47 +7776,48 @@ function clearGlobalMeasurements() {
7459
7776
  }
7460
7777
 
7461
7778
  // libs/testing/src/perf/perf-test.ts
7462
- var currentConfig2 = {};
7463
- var serverInstance2 = null;
7464
- var tokenFactory2 = null;
7465
- var serverStartedByUs2 = false;
7466
- async function initializeSharedResources2() {
7467
- if (!tokenFactory2) {
7468
- tokenFactory2 = new TestTokenFactory();
7469
- }
7470
- if (!serverInstance2) {
7471
- if (currentConfig2.baseUrl) {
7472
- serverInstance2 = TestServer.connect(currentConfig2.baseUrl);
7473
- serverStartedByUs2 = false;
7474
- } else if (currentConfig2.server) {
7475
- const serverCommand = resolveServerCommand2(currentConfig2.server);
7779
+ var currentConfig = {};
7780
+ var serverInstance = null;
7781
+ var tokenFactory = null;
7782
+ var serverStartedByUs = false;
7783
+ async function initializeSharedResources() {
7784
+ if (!tokenFactory) {
7785
+ tokenFactory = new TestTokenFactory();
7786
+ }
7787
+ if (!serverInstance) {
7788
+ if (currentConfig.baseUrl) {
7789
+ serverInstance = TestServer.connect(currentConfig.baseUrl);
7790
+ serverStartedByUs = false;
7791
+ } else if (currentConfig.server) {
7792
+ const serverCommand = resolveServerCommand2(currentConfig.server);
7476
7793
  const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
7477
7794
  if (isDebug) {
7478
7795
  console.log(`[PerfTest] Starting server: ${serverCommand}`);
7479
7796
  }
7480
7797
  try {
7481
- serverInstance2 = await TestServer.start({
7482
- project: currentConfig2.project,
7483
- port: currentConfig2.port,
7798
+ serverInstance = await TestServer.start({
7799
+ project: currentConfig.project,
7800
+ port: currentConfig.port,
7484
7801
  command: serverCommand,
7485
- env: currentConfig2.env,
7486
- startupTimeout: currentConfig2.startupTimeout ?? 3e4,
7802
+ env: currentConfig.env,
7803
+ startupTimeout: currentConfig.startupTimeout ?? 3e4,
7487
7804
  debug: isDebug
7488
7805
  });
7489
- serverStartedByUs2 = true;
7806
+ serverStartedByUs = true;
7490
7807
  if (isDebug) {
7491
- console.log(`[PerfTest] Server started at ${serverInstance2.info.baseUrl}`);
7808
+ console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
7492
7809
  }
7493
7810
  } catch (error) {
7494
7811
  const errMsg = error instanceof Error ? error.message : String(error);
7495
7812
  throw new Error(
7496
7813
  `Failed to start test server.
7497
7814
 
7498
- Server entry: ${currentConfig2.server}
7499
- Project: ${currentConfig2.project ?? "default"}
7815
+ Server entry: ${currentConfig.server}
7816
+ Project: ${currentConfig.project ?? "default"}
7500
7817
  Command: ${serverCommand}
7501
7818
 
7502
- Error: ${errMsg}`
7819
+ Error: ${errMsg}`,
7820
+ { cause: error }
7503
7821
  );
7504
7822
  }
7505
7823
  } else {
@@ -7507,23 +7825,23 @@ Error: ${errMsg}`
7507
7825
  }
7508
7826
  }
7509
7827
  }
7510
- async function createTestFixtures2(testName) {
7511
- await initializeSharedResources2();
7512
- if (!serverInstance2) {
7828
+ async function createTestFixtures(testName) {
7829
+ await initializeSharedResources();
7830
+ if (!serverInstance) {
7513
7831
  throw new Error("Server instance not initialized");
7514
7832
  }
7515
- if (!tokenFactory2) {
7833
+ if (!tokenFactory) {
7516
7834
  throw new Error("Token factory not initialized");
7517
7835
  }
7518
7836
  const clientInstance = await McpTestClient.create({
7519
- baseUrl: serverInstance2.info.baseUrl,
7520
- transport: currentConfig2.transport ?? "streamable-http",
7521
- publicMode: currentConfig2.publicMode
7837
+ baseUrl: serverInstance.info.baseUrl,
7838
+ transport: currentConfig.transport ?? "streamable-http",
7839
+ publicMode: currentConfig.publicMode
7522
7840
  }).buildAndConnect();
7523
- const auth = createAuthFixture2(tokenFactory2);
7524
- const server = createServerFixture2(serverInstance2);
7525
- const perfImpl = createPerfFixtures(testName, currentConfig2.project ?? "unknown");
7526
- if (currentConfig2.forceGcOnBaseline !== false) {
7841
+ const auth = createAuthFixture2(tokenFactory);
7842
+ const server = createServerFixture2(serverInstance);
7843
+ const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
7844
+ if (currentConfig.forceGcOnBaseline !== false) {
7527
7845
  await perfImpl.baseline();
7528
7846
  }
7529
7847
  return {
@@ -7539,8 +7857,8 @@ async function createTestFixtures2(testName) {
7539
7857
  async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7540
7858
  const measurement = perfImpl.buildMeasurement();
7541
7859
  addGlobalMeasurement(measurement);
7542
- if (testFailed && serverInstance2) {
7543
- const logs = serverInstance2.getLogs();
7860
+ if (testFailed && serverInstance) {
7861
+ const logs = serverInstance.getLogs();
7544
7862
  if (logs.length > 0) {
7545
7863
  console.error("\n[PerfTest] === Server Logs (test failed) ===");
7546
7864
  const recentLogs = logs.slice(-50);
@@ -7555,13 +7873,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7555
7873
  await fixtures.mcp.disconnect();
7556
7874
  }
7557
7875
  }
7558
- async function cleanupSharedResources2() {
7559
- if (serverInstance2 && serverStartedByUs2) {
7560
- await serverInstance2.stop();
7876
+ async function cleanupSharedResources() {
7877
+ if (serverInstance && serverStartedByUs) {
7878
+ await serverInstance.stop();
7561
7879
  }
7562
- serverInstance2 = null;
7563
- tokenFactory2 = null;
7564
- serverStartedByUs2 = false;
7880
+ serverInstance = null;
7881
+ tokenFactory = null;
7882
+ serverStartedByUs = false;
7565
7883
  }
7566
7884
  function createAuthFixture2(factory) {
7567
7885
  const users = {
@@ -7616,13 +7934,13 @@ function createServerFixture2(server) {
7616
7934
  transport: opts?.transport ?? "streamable-http",
7617
7935
  auth: opts?.token ? { token: opts.token } : void 0,
7618
7936
  clientInfo: opts?.clientInfo,
7619
- publicMode: currentConfig2.publicMode
7937
+ publicMode: currentConfig.publicMode
7620
7938
  }).buildAndConnect();
7621
7939
  },
7622
7940
  createClientBuilder: () => {
7623
7941
  return new McpTestClientBuilder({
7624
7942
  baseUrl: server.info.baseUrl,
7625
- publicMode: currentConfig2.publicMode
7943
+ publicMode: currentConfig.publicMode
7626
7944
  });
7627
7945
  },
7628
7946
  restart: () => server.restart(),
@@ -7638,7 +7956,7 @@ function resolveServerCommand2(server) {
7638
7956
  }
7639
7957
  function perfTestWithFixtures(name, fn) {
7640
7958
  it(name, async () => {
7641
- const { fixtures, perfImpl } = await createTestFixtures2(name);
7959
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7642
7960
  let testFailed = false;
7643
7961
  try {
7644
7962
  await fn(fixtures);
@@ -7651,14 +7969,14 @@ function perfTestWithFixtures(name, fn) {
7651
7969
  });
7652
7970
  }
7653
7971
  function use2(config) {
7654
- currentConfig2 = { ...currentConfig2, ...config };
7972
+ currentConfig = { ...currentConfig, ...config };
7655
7973
  afterAll(async () => {
7656
- await cleanupSharedResources2();
7974
+ await cleanupSharedResources();
7657
7975
  });
7658
7976
  }
7659
7977
  function skip2(name, fn) {
7660
7978
  it.skip(name, async () => {
7661
- const { fixtures, perfImpl } = await createTestFixtures2(name);
7979
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7662
7980
  let testFailed = false;
7663
7981
  try {
7664
7982
  await fn(fixtures);
@@ -7672,7 +7990,7 @@ function skip2(name, fn) {
7672
7990
  }
7673
7991
  function only2(name, fn) {
7674
7992
  it.only(name, async () => {
7675
- const { fixtures, perfImpl } = await createTestFixtures2(name);
7993
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7676
7994
  let testFailed = false;
7677
7995
  try {
7678
7996
  await fn(fixtures);
@@ -7689,16 +8007,19 @@ function todo2(name) {
7689
8007
  }
7690
8008
  var perfTest = perfTestWithFixtures;
7691
8009
  perfTest.use = use2;
7692
- perfTest.describe = describe;
7693
- perfTest.beforeAll = beforeAll;
7694
- perfTest.beforeEach = beforeEach;
7695
- perfTest.afterEach = afterEach;
7696
- perfTest.afterAll = afterAll;
8010
+ for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
8011
+ Object.defineProperty(perfTest, hook, {
8012
+ get: () => globalThis[hook],
8013
+ enumerable: true,
8014
+ configurable: true
8015
+ });
8016
+ }
7697
8017
  perfTest.skip = skip2;
7698
8018
  perfTest.only = only2;
7699
8019
  perfTest.todo = todo2;
7700
8020
 
7701
8021
  // libs/testing/src/perf/baseline-store.ts
8022
+ import { ensureDir, fileExists, readFile, writeFile } from "@frontmcp/utils";
7702
8023
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
7703
8024
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
7704
8025
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -7713,7 +8034,6 @@ var BaselineStore = class {
7713
8034
  */
7714
8035
  async load() {
7715
8036
  try {
7716
- const { readFile, fileExists } = await import("@frontmcp/utils");
7717
8037
  if (!await fileExists(this.baselinePath)) {
7718
8038
  return null;
7719
8039
  }
@@ -7728,7 +8048,6 @@ var BaselineStore = class {
7728
8048
  * Save baseline to local file.
7729
8049
  */
7730
8050
  async save(baseline) {
7731
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
7732
8051
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
7733
8052
  await ensureDir(dir);
7734
8053
  await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
@@ -7984,6 +8303,7 @@ function getRegressionDetector(config) {
7984
8303
  }
7985
8304
 
7986
8305
  // libs/testing/src/perf/report-generator.ts
8306
+ import { ensureDir as ensureDir2, writeFile as writeFile2 } from "@frontmcp/utils";
7987
8307
  var ReportGenerator = class {
7988
8308
  detector;
7989
8309
  constructor() {
@@ -8239,14 +8559,13 @@ var ReportGenerator = class {
8239
8559
  }
8240
8560
  };
8241
8561
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
8242
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
8243
- await ensureDir(outputDir);
8562
+ await ensureDir2(outputDir);
8244
8563
  const generator = new ReportGenerator();
8245
8564
  const report = generator.generateReport(measurements, baseline, gitInfo);
8246
8565
  const jsonPath = `${outputDir}/report.json`;
8247
8566
  const markdownPath = `${outputDir}/report.md`;
8248
- await writeFile(jsonPath, generator.generateJsonReport(report));
8249
- await writeFile(markdownPath, generator.generateMarkdownReport(report));
8567
+ await writeFile2(jsonPath, generator.generateJsonReport(report));
8568
+ await writeFile2(markdownPath, generator.generateMarkdownReport(report));
8250
8569
  return { jsonPath, markdownPath };
8251
8570
  }
8252
8571
  function createReportGenerator() {