@frontmcp/testing 1.8.6 → 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 +6 -1
  5. package/client/mcp-test-client.d.ts.map +1 -1
  6. package/esm/fixtures/index.mjs +487 -224
  7. package/esm/index.mjs +644 -347
  8. package/esm/package.json +6 -6
  9. package/esm/perf/index.mjs +407 -272
  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 +486 -216
  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 +652 -348
  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 +410 -285
  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) {
@@ -1422,6 +1448,15 @@ var DEFAULT_CLIENT_INFO = {
1422
1448
  version: "0.4.0"
1423
1449
  };
1424
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
+ }
1425
1460
  function extractEntryPaths(data) {
1426
1461
  let payload = data;
1427
1462
  if (typeof payload === "string") {
@@ -1508,19 +1543,22 @@ var McpTestClient = class {
1508
1543
  async connect() {
1509
1544
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1510
1545
  this.triedDiscoveredEntryPath = false;
1546
+ this.initResult = null;
1511
1547
  this.transport = this.createTransport();
1512
1548
  await this.transport.connect();
1513
1549
  const maxRetries = 3;
1514
1550
  const retryDelayMs = 500;
1515
1551
  let lastError;
1516
1552
  let reportedEntryPaths;
1553
+ let attemptsMade = 0;
1517
1554
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1555
+ attemptsMade = attempt;
1518
1556
  const initResponse = await this.initialize();
1519
1557
  if (initResponse.success && initResponse.data) {
1520
1558
  this.initResult = initResponse.data;
1521
1559
  break;
1522
1560
  }
1523
- lastError = initResponse.error?.message ?? "Unknown error";
1561
+ lastError = describeInitializeError(initResponse.error);
1524
1562
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1525
1563
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1526
1564
  this.triedDiscoveredEntryPath = true;
@@ -1532,6 +1570,7 @@ var McpTestClient = class {
1532
1570
  await this.transport.connect();
1533
1571
  continue;
1534
1572
  }
1573
+ if (isAuthFailure(initResponse.error)) break;
1535
1574
  if (attempt < maxRetries) {
1536
1575
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1537
1576
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1540,7 +1579,7 @@ var McpTestClient = class {
1540
1579
  if (!this.initResult) {
1541
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.` : "";
1542
1581
  throw new Error(
1543
- `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}`
1544
1583
  );
1545
1584
  }
1546
1585
  this._sessionId = this.transport.getSessionId();
@@ -1610,18 +1649,47 @@ var McpTestClient = class {
1610
1649
  return this._authState;
1611
1650
  }
1612
1651
  /**
1613
- * 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.
1614
1658
  */
1615
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 };
1616
1666
  this._authState = {
1617
1667
  isAnonymous: false,
1618
1668
  token,
1619
1669
  scopes: this.parseScopesFromToken(token),
1620
1670
  user: this.parseUserFromToken(token)
1621
1671
  };
1622
- if (this.transport) {
1623
- 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;
1624
1691
  }
1692
+ await previousTransport.close().catch(() => void 0);
1625
1693
  this.log("debug", "Authentication updated");
1626
1694
  }
1627
1695
  // ═══════════════════════════════════════════════════════════════════
@@ -2245,10 +2313,11 @@ var McpTestClient = class {
2245
2313
  requestId: id
2246
2314
  };
2247
2315
  } catch (err) {
2316
+ if (isInterceptedError(err)) throw err;
2248
2317
  const durationMs = Date.now() - start;
2249
2318
  const error = {
2250
2319
  code: -32603,
2251
- message: err instanceof Error ? err.message : "Unknown error"
2320
+ message: errorMessage(err)
2252
2321
  };
2253
2322
  return {
2254
2323
  success: false,
@@ -2396,12 +2465,12 @@ var McpTestClient = class {
2396
2465
  const payload = this.decodeJwtPayload(token);
2397
2466
  if (!payload) return [];
2398
2467
  const scope = payload["scope"];
2399
- const scopes = payload["scopes"];
2468
+ const scopes2 = payload["scopes"];
2400
2469
  if (typeof scope === "string") {
2401
2470
  return scope.split(" ");
2402
2471
  }
2403
- if (Array.isArray(scopes)) {
2404
- return scopes;
2472
+ if (Array.isArray(scopes2)) {
2473
+ return scopes2;
2405
2474
  }
2406
2475
  return [];
2407
2476
  } catch {
@@ -2533,14 +2602,15 @@ var TestTokenFactory = class {
2533
2602
  */
2534
2603
  async createTestToken(options) {
2535
2604
  await this.ensureKeys();
2536
- const now = Math.floor(Date.now() / 1e3);
2605
+ const nowMs = Date.now();
2606
+ const iat = Math.floor(nowMs / 1e3);
2537
2607
  const exp = options.exp ?? 3600;
2538
2608
  const payload = {
2539
2609
  iss: options.iss ?? this.issuer,
2540
2610
  sub: options.sub,
2541
2611
  aud: options.aud ?? this.audience,
2542
- iat: now,
2543
- exp: now + exp,
2612
+ iat,
2613
+ exp: Math.ceil(nowMs / 1e3) + exp,
2544
2614
  scope: options.scopes?.join(" "),
2545
2615
  ...options.claims
2546
2616
  };
@@ -2565,10 +2635,10 @@ var TestTokenFactory = class {
2565
2635
  /**
2566
2636
  * Create a regular user token
2567
2637
  */
2568
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
2638
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
2569
2639
  return this.createTestToken({
2570
2640
  sub,
2571
- scopes,
2641
+ scopes: scopes2,
2572
2642
  claims: {
2573
2643
  email: "user@test.local",
2574
2644
  name: "Test User",
@@ -2579,10 +2649,11 @@ var TestTokenFactory = class {
2579
2649
  /**
2580
2650
  * Create an anonymous user token
2581
2651
  */
2582
- async createAnonymousToken() {
2652
+ async createAnonymousToken(expiresIn) {
2583
2653
  return this.createTestToken({
2584
2654
  sub: `anon:${Date.now()}`,
2585
2655
  scopes: ["anonymous"],
2656
+ exp: expiresIn,
2586
2657
  claims: {
2587
2658
  name: "Anonymous",
2588
2659
  role: "anonymous"
@@ -2793,17 +2864,7 @@ function createTestUser(overrides) {
2793
2864
 
2794
2865
  // libs/testing/src/auth/mock-oauth-server.ts
2795
2866
  var import_http = require("http");
2796
- var _randomBytes;
2797
- var _sha256Base64url;
2798
- var _base64urlEncode;
2799
- async function loadCryptoUtils() {
2800
- if (!_randomBytes) {
2801
- const utils = await import("@frontmcp/utils");
2802
- _randomBytes = utils.randomBytes;
2803
- _sha256Base64url = utils.sha256Base64url;
2804
- _base64urlEncode = utils.base64urlEncode;
2805
- }
2806
- }
2867
+ var import_utils = require("@frontmcp/utils");
2807
2868
  var MockOAuthServer = class {
2808
2869
  tokenFactory;
2809
2870
  options;
@@ -2818,8 +2879,8 @@ var MockOAuthServer = class {
2818
2879
  accessTokenTtlSeconds;
2819
2880
  /** Refresh token TTL in seconds */
2820
2881
  refreshTokenTtlSeconds;
2821
- constructor(tokenFactory3, options = {}) {
2822
- this.tokenFactory = tokenFactory3;
2882
+ constructor(tokenFactory2, options = {}) {
2883
+ this.tokenFactory = tokenFactory2;
2823
2884
  this.options = options;
2824
2885
  this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
2825
2886
  this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
@@ -3076,14 +3137,14 @@ ${error.stack}` : String(error);
3076
3137
  return;
3077
3138
  }
3078
3139
  const code = this.generateCode();
3079
- const scopes = scope.split(" ").filter(Boolean);
3140
+ const scopes2 = scope.split(" ").filter(Boolean);
3080
3141
  this.authCodes.set(code, {
3081
3142
  code,
3082
3143
  clientId,
3083
3144
  redirectUri,
3084
3145
  codeChallenge,
3085
3146
  codeChallengeMethod,
3086
- scopes,
3147
+ scopes: scopes2,
3087
3148
  user: testUser,
3088
3149
  state,
3089
3150
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3123,7 +3184,7 @@ ${error.stack}` : String(error);
3123
3184
  }
3124
3185
  const grantType = params.get("grant_type");
3125
3186
  if (grantType === "anonymous") {
3126
- const token = await this.tokenFactory.createAnonymousToken();
3187
+ const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
3127
3188
  res.writeHead(200, { "Content-Type": "application/json" });
3128
3189
  res.end(
3129
3190
  JSON.stringify({
@@ -3255,6 +3316,8 @@ ${error.stack}` : String(error);
3255
3316
  codeRecord.used = true;
3256
3317
  const accessToken = await this.tokenFactory.createTestToken({
3257
3318
  sub: codeRecord.user.sub,
3319
+ scopes: codeRecord.scopes,
3320
+ exp: this.accessTokenTtlSeconds,
3258
3321
  claims: {
3259
3322
  email: codeRecord.user.email,
3260
3323
  name: codeRecord.user.name,
@@ -3263,6 +3326,7 @@ ${error.stack}` : String(error);
3263
3326
  });
3264
3327
  const idToken = await this.tokenFactory.createTestToken({
3265
3328
  sub: codeRecord.user.sub,
3329
+ exp: this.accessTokenTtlSeconds,
3266
3330
  claims: {
3267
3331
  email: codeRecord.user.email,
3268
3332
  name: codeRecord.user.name,
@@ -3340,6 +3404,8 @@ ${error.stack}` : String(error);
3340
3404
  }
3341
3405
  const accessToken = await this.tokenFactory.createTestToken({
3342
3406
  sub: tokenRecord.user.sub,
3407
+ scopes: tokenRecord.scopes,
3408
+ exp: this.accessTokenTtlSeconds,
3343
3409
  claims: {
3344
3410
  email: tokenRecord.user.email,
3345
3411
  name: tokenRecord.user.name,
@@ -3444,14 +3510,14 @@ ${error.stack}` : String(error);
3444
3510
  name
3445
3511
  };
3446
3512
  const code = this.generateCode();
3447
- const scopes = scope.split(" ").filter(Boolean);
3513
+ const scopes2 = scope.split(" ").filter(Boolean);
3448
3514
  this.authCodes.set(code, {
3449
3515
  code,
3450
3516
  clientId,
3451
3517
  redirectUri,
3452
3518
  codeChallenge,
3453
3519
  codeChallengeMethod,
3454
- scopes,
3520
+ scopes: scopes2,
3455
3521
  user: testUser,
3456
3522
  state,
3457
3523
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3557,8 +3623,7 @@ ${error.stack}` : String(error);
3557
3623
  * Generate a random authorization code
3558
3624
  */
3559
3625
  async generateCodeAsync() {
3560
- await loadCryptoUtils();
3561
- return _base64urlEncode(_randomBytes(32));
3626
+ return (0, import_utils.base64urlEncode)((0, import_utils.randomBytes)(32));
3562
3627
  }
3563
3628
  /**
3564
3629
  * Generate a random authorization code (sync wrapper for compatibility)
@@ -3574,8 +3639,7 @@ ${error.stack}` : String(error);
3574
3639
  */
3575
3640
  async computeCodeChallengeAsync(verifier, method) {
3576
3641
  if (method === "S256") {
3577
- await loadCryptoUtils();
3578
- return _sha256Base64url(verifier);
3642
+ return (0, import_utils.sha256Base64url)(verifier);
3579
3643
  }
3580
3644
  return verifier;
3581
3645
  }
@@ -4163,7 +4227,7 @@ var MockCimdServer = class {
4163
4227
 
4164
4228
  // libs/testing/src/server/test-server.ts
4165
4229
  var import_child_process = require("child_process");
4166
- var import_utils = require("@frontmcp/utils");
4230
+ var import_utils2 = require("@frontmcp/utils");
4167
4231
 
4168
4232
  // libs/testing/src/errors/index.ts
4169
4233
  var TestClientError = class extends Error {
@@ -4269,6 +4333,8 @@ var E2E_PORT_RANGES = {
4269
4333
  "demo-e2e-distributed": { start: 50440, size: 20 },
4270
4334
  // Protocol revision E2E tests (50460-50479)
4271
4335
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
4336
+ // Testing library E2E tests (50480-50499)
4337
+ "demo-e2e-testing": { start: 50480, size: 20 },
4272
4338
  // Mock servers and utilities (50900-50999)
4273
4339
  "mock-oauth": { start: 50900, size: 10 },
4274
4340
  "mock-api": { start: 50910, size: 10 },
@@ -4277,6 +4343,66 @@ var E2E_PORT_RANGES = {
4277
4343
  default: { start: 51e3, size: 100 }
4278
4344
  };
4279
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
+ }
4280
4406
  var projectPortIndex = /* @__PURE__ */ new Map();
4281
4407
  function getPortRange(project) {
4282
4408
  const key = project;
@@ -4287,15 +4413,22 @@ function getPortRange(project) {
4287
4413
  }
4288
4414
  async function reservePort(project, preferredPort) {
4289
4415
  const range = getPortRange(project);
4290
- 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) {
4291
4429
  const reservation = await tryReservePort(preferredPort, project);
4292
4430
  if (reservation) {
4293
- return {
4294
- port: preferredPort,
4295
- release: async () => {
4296
- await releasePort(preferredPort);
4297
- }
4298
- };
4431
+ return handle(preferredPort);
4299
4432
  }
4300
4433
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
4301
4434
  }
@@ -4304,30 +4437,19 @@ async function reservePort(project, preferredPort) {
4304
4437
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
4305
4438
  const port = range.start + index % range.size;
4306
4439
  index = (index + 1) % range.size;
4307
- if (reservedPorts.has(port)) {
4440
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
4308
4441
  continue;
4309
4442
  }
4310
4443
  const reservation = await tryReservePort(port, project);
4311
4444
  if (reservation) {
4312
4445
  projectPortIndex.set(project, index);
4313
- return {
4314
- port,
4315
- release: async () => {
4316
- await releasePort(port);
4317
- }
4318
- };
4446
+ return handle(port);
4319
4447
  }
4320
4448
  }
4321
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
4322
- if (dynamicPort) {
4323
- const reservation = await tryReservePort(dynamicPort, project);
4324
- if (reservation) {
4325
- return {
4326
- port: dynamicPort,
4327
- release: async () => {
4328
- await releasePort(dynamicPort);
4329
- }
4330
- };
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);
4331
4453
  }
4332
4454
  }
4333
4455
  throw new Error(
@@ -4335,9 +4457,13 @@ async function reservePort(project, preferredPort) {
4335
4457
  );
4336
4458
  }
4337
4459
  async function tryReservePort(port, project) {
4460
+ if (!acquirePortLock(port)) {
4461
+ return false;
4462
+ }
4338
4463
  return new Promise((resolve) => {
4339
4464
  const server = (0, import_net.createServer)();
4340
4465
  server.once("error", () => {
4466
+ releasePortLock(port);
4341
4467
  resolve(false);
4342
4468
  });
4343
4469
  server.listen(port, () => {
@@ -4363,31 +4489,6 @@ async function releasePort(port) {
4363
4489
  });
4364
4490
  });
4365
4491
  }
4366
- async function findAvailablePortInRange(start, end) {
4367
- for (let port = start; port < end; port++) {
4368
- if (reservedPorts.has(port)) {
4369
- continue;
4370
- }
4371
- const available = await isPortAvailable(port);
4372
- if (available) {
4373
- return port;
4374
- }
4375
- }
4376
- return null;
4377
- }
4378
- async function isPortAvailable(port) {
4379
- return new Promise((resolve) => {
4380
- const server = (0, import_net.createServer)();
4381
- server.once("error", () => {
4382
- resolve(false);
4383
- });
4384
- server.listen(port, () => {
4385
- server.close(() => {
4386
- resolve(true);
4387
- });
4388
- });
4389
- });
4390
- }
4391
4492
 
4392
4493
  // libs/testing/src/server/test-server.ts
4393
4494
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -4396,8 +4497,11 @@ var TestServer = class _TestServer {
4396
4497
  options;
4397
4498
  _info;
4398
4499
  logs = [];
4500
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
4399
4501
  portRelease = null;
4400
- 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) {
4401
4505
  this.options = {
4402
4506
  port,
4403
4507
  project: options.project,
@@ -4409,6 +4513,7 @@ var TestServer = class _TestServer {
4409
4513
  debug: options.debug ?? DEBUG_SERVER
4410
4514
  };
4411
4515
  this.portRelease = portRelease ?? null;
4516
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
4412
4517
  this._info = {
4413
4518
  baseUrl: `http://localhost:${port}`,
4414
4519
  port
@@ -4421,8 +4526,8 @@ var TestServer = class _TestServer {
4421
4526
  const project = options.project ?? "default";
4422
4527
  const maxAttempts = 3;
4423
4528
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4424
- const { port, release } = await reservePort(project, options.port);
4425
- 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);
4426
4531
  try {
4427
4532
  await server.startProcess();
4428
4533
  return server;
@@ -4462,7 +4567,7 @@ var TestServer = class _TestServer {
4462
4567
  }
4463
4568
  const maxAttempts = 3;
4464
4569
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4465
- const { port, release } = await reservePort(project, options.port);
4570
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4466
4571
  const serverOptions = {
4467
4572
  ...options,
4468
4573
  port,
@@ -4470,7 +4575,7 @@ var TestServer = class _TestServer {
4470
4575
  command: `npx nx serve ${project} --port ${port}`,
4471
4576
  cwd: options.cwd ?? process.cwd()
4472
4577
  };
4473
- const server = new _TestServer(serverOptions, port, release);
4578
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
4474
4579
  try {
4475
4580
  await server.startProcess();
4476
4581
  return server;
@@ -4528,10 +4633,17 @@ var TestServer = class _TestServer {
4528
4633
  * Stop the test server
4529
4634
  */
4530
4635
  async stop() {
4531
- if (this.portRelease) {
4532
- await this.portRelease();
4533
- 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;
4534
4644
  }
4645
+ }
4646
+ async stopProcess() {
4535
4647
  if (this.process) {
4536
4648
  this.log("Stopping server...");
4537
4649
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -4604,7 +4716,7 @@ var TestServer = class _TestServer {
4604
4716
  * Restart the server
4605
4717
  */
4606
4718
  async restart() {
4607
- await this.stop();
4719
+ await this.stopProcess();
4608
4720
  await this.startProcess();
4609
4721
  }
4610
4722
  /**
@@ -4634,9 +4746,9 @@ var TestServer = class _TestServer {
4634
4746
  PORT: String(this.options.port)
4635
4747
  };
4636
4748
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
4637
- if (this.portRelease) {
4638
- await this.portRelease();
4639
- this.portRelease = null;
4749
+ if (this.portReleaseSocket) {
4750
+ await this.portReleaseSocket();
4751
+ this.portReleaseSocket = null;
4640
4752
  await sleep2(300);
4641
4753
  }
4642
4754
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -4646,9 +4758,8 @@ var TestServer = class _TestServer {
4646
4758
  detached: true,
4647
4759
  stdio: ["pipe", "pipe", "pipe"]
4648
4760
  });
4649
- if (this.process.pid !== void 0) {
4650
- this._info.pid = this.process.pid;
4651
- }
4761
+ this._info.pid = this.process.pid;
4762
+ this._info.shellPid = this.process.pid;
4652
4763
  let processExited = false;
4653
4764
  let exitCode = null;
4654
4765
  let exitError = null;
@@ -4751,6 +4862,8 @@ ${allLogs}`);
4751
4862
  });
4752
4863
  if (response.ok || response.status === 404) {
4753
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;
4754
4867
  return;
4755
4868
  }
4756
4869
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -4795,6 +4908,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
4795
4908
  function sleep2(ms) {
4796
4909
  return new Promise((resolve) => setTimeout(resolve, ms));
4797
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
+ }
4798
4924
  function withWorkspaceProtocolFallback(env, cwd) {
4799
4925
  if (findInstalledProtocolPackageDir(cwd)) {
4800
4926
  return env;
@@ -4844,7 +4970,7 @@ function withProtocolNodePathAlias(env, cwd, workspacePackageDir) {
4844
4970
  const fs = require("node:fs");
4845
4971
  const os = require("node:os");
4846
4972
  const path = require("node:path");
4847
- 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));
4848
4974
  const scopeDir = path.join(aliasRoot, "@frontmcp");
4849
4975
  const aliasPackageDir = path.join(scopeDir, "protocol");
4850
4976
  fs.mkdirSync(scopeDir, { recursive: true });
@@ -5066,94 +5192,162 @@ function hasMimeType(result, mimeType) {
5066
5192
  return result.hasMimeType(mimeType);
5067
5193
  }
5068
5194
 
5069
- // libs/testing/src/fixtures/test-fixture.ts
5070
- var import_globals = require("@jest/globals");
5071
-
5072
5195
  // libs/testing/src/fixtures/gateway-token-binding.ts
5073
- var import_utils2 = require("@frontmcp/utils");
5196
+ var import_utils3 = require("@frontmcp/utils");
5074
5197
  function gatewayTokenBinding(serverUrl, entryPath) {
5075
- const base = (0, import_utils2.trimTrailing)(serverUrl, "/");
5076
- const prefix = (0, import_utils2.trimSlashes)(entryPath ?? "");
5198
+ const base = (0, import_utils3.trimTrailing)(serverUrl, "/");
5199
+ const prefix = (0, import_utils3.trimSlashes)(entryPath ?? "");
5077
5200
  const mcpUrl = prefix ? `${base}/${prefix}` : base;
5078
5201
  return { issuer: mcpUrl, audience: mcpUrl };
5079
5202
  }
5080
5203
 
5081
5204
  // libs/testing/src/fixtures/test-fixture.ts
5082
- var describe2 = import_globals.describe;
5083
- var beforeAll2 = import_globals.beforeAll;
5084
- var beforeEach2 = import_globals.beforeEach;
5085
- var afterEach2 = import_globals.afterEach;
5086
- var afterAll2 = import_globals.afterAll;
5087
- var it2 = import_globals.it;
5088
- var currentConfig = {};
5089
- var serverInstance = null;
5090
- var tokenFactory = null;
5091
- var serverStartedByUs = false;
5092
- var skipScopes = [{ skipped: false }];
5093
- function currentSkipScope() {
5094
- 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
+ }
5095
5225
  }
5096
- async function initializeSharedResources() {
5097
- if (!serverInstance) {
5098
- if (currentConfig.baseUrl && !currentConfig.server) {
5099
- serverInstance = TestServer.connect(currentConfig.baseUrl);
5100
- serverStartedByUs = false;
5101
- } else if (currentConfig.server) {
5102
- const serverCommand = resolveServerCommand(currentConfig.server);
5103
- 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;
5104
5288
  if (isDebug) {
5105
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
5289
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
5106
5290
  }
5107
- try {
5108
- serverInstance = await TestServer.start({
5109
- project: currentConfig.project,
5110
- port: currentConfig.port,
5111
- command: serverCommand,
5112
- env: currentConfig.env,
5113
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
5114
- debug: isDebug
5115
- });
5116
- serverStartedByUs = true;
5117
- if (isDebug) {
5118
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
5119
- }
5120
- } catch (error) {
5121
- const errMsg = error instanceof Error ? error.message : String(error);
5122
- throw new Error(
5123
- `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.
5124
5295
 
5125
- Server entry: ${currentConfig.server}
5126
- Project: ${currentConfig.project ?? "default"}
5296
+ Server entry: ${config.server}
5297
+ Project: ${config.project ?? "default"}
5127
5298
  Command: ${serverCommand}
5128
5299
 
5129
- Error: ${errMsg}`
5130
- );
5131
- }
5132
- } else {
5133
- throw new Error(
5134
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
5300
+ Error: ${errMsg}`,
5301
+ { cause: error }
5135
5302
  );
5136
5303
  }
5304
+ } else {
5305
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
5137
5306
  }
5138
- if (!tokenFactory) {
5139
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
5140
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
5141
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5142
- }
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 };
5143
5311
  }
5144
- async function createTestFixtures() {
5145
- await initializeSharedResources();
5146
- if (!serverInstance) {
5147
- throw new Error("Server instance not initialized");
5148
- }
5149
- if (!tokenFactory) {
5150
- 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
+ }
5151
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 };
5152
5346
  const clientInstance = McpTestClient.create({
5153
- baseUrl: resolveClientBaseUrl(serverInstance),
5154
- entryPath: currentConfig.entryPath,
5155
- transport: currentConfig.transport ?? "streamable-http",
5156
- publicMode: currentConfig.publicMode
5347
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
5348
+ entryPath: clientConfig.entryPath,
5349
+ transport: clientConfig.transport ?? "streamable-http",
5350
+ publicMode
5157
5351
  }).build();
5158
5352
  try {
5159
5353
  await clientInstance.connect();
@@ -5167,17 +5361,18 @@ async function createTestFixtures() {
5167
5361
  throw err;
5168
5362
  }
5169
5363
  }
5170
- const auth = createAuthFixture(tokenFactory);
5171
- const server = createServerFixture(serverInstance);
5172
- return {
5364
+ const context = { extraClients: [] };
5365
+ const fixtures = {
5173
5366
  mcp: clientInstance,
5174
- auth,
5175
- server
5367
+ auth: createAuthFixture(tokenFactory2),
5368
+ server: createServerFixture(server, clientConfig, clientInstance, context)
5176
5369
  };
5370
+ fixtureContexts.set(fixtures, context);
5371
+ return fixtures;
5177
5372
  }
5178
5373
  async function cleanupTestFixtures(fixtures, testFailed = false) {
5179
- if (testFailed && serverInstance) {
5180
- const logs = serverInstance.getLogs();
5374
+ if (testFailed) {
5375
+ const logs = fixtures.server.getLogs();
5181
5376
  if (logs.length > 0) {
5182
5377
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
5183
5378
  const recentLogs = logs.slice(-50);
@@ -5188,20 +5383,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
5188
5383
  console.error("[TestFixture] === End Server Logs ===\n");
5189
5384
  }
5190
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
+ }
5191
5392
  if (fixtures.mcp.isConnected()) {
5192
5393
  await fixtures.mcp.disconnect();
5193
5394
  }
5194
5395
  }
5195
- async function cleanupSharedResources() {
5196
- if (serverInstance && serverStartedByUs) {
5197
- await serverInstance.stop();
5198
- }
5199
- serverInstance = null;
5200
- tokenFactory = null;
5201
- serverStartedByUs = false;
5202
- }
5203
- function resolveClientBaseUrl(server) {
5204
- return currentConfig.baseUrl ?? server.info.baseUrl;
5396
+ function resolveClientBaseUrl(server, config) {
5397
+ return config.baseUrl ?? server.info.baseUrl;
5205
5398
  }
5206
5399
  function createAuthFixture(factory) {
5207
5400
  const users = {
@@ -5247,28 +5440,39 @@ function createAuthFixture(factory) {
5247
5440
  getAudience: () => factory.getAudience()
5248
5441
  };
5249
5442
  }
5250
- function createServerFixture(server) {
5443
+ function createServerFixture(server, config, mcp, context) {
5251
5444
  return {
5252
- 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
+ },
5253
5449
  createClient: async (opts) => {
5254
- return McpTestClient.create({
5255
- baseUrl: resolveClientBaseUrl(server),
5256
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
5450
+ const client = await McpTestClient.create({
5451
+ baseUrl: resolveClientBaseUrl(server, config),
5452
+ entryPath: opts?.entryPath ?? config.entryPath,
5257
5453
  transport: opts?.transport ?? "streamable-http",
5258
5454
  auth: opts?.token ? { token: opts.token } : void 0,
5259
5455
  clientInfo: opts?.clientInfo,
5260
- publicMode: currentConfig.publicMode
5456
+ publicMode: config.publicMode
5261
5457
  }).buildAndConnect();
5458
+ context.extraClients.push(client);
5459
+ return client;
5262
5460
  },
5263
5461
  createClientBuilder: () => {
5264
5462
  const builder = new McpTestClientBuilder({
5265
- baseUrl: resolveClientBaseUrl(server),
5266
- entryPath: currentConfig.entryPath,
5267
- publicMode: currentConfig.publicMode
5463
+ baseUrl: resolveClientBaseUrl(server, config),
5464
+ entryPath: config.entryPath,
5465
+ publicMode: config.publicMode
5268
5466
  });
5269
5467
  return builder;
5270
5468
  },
5271
- 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
+ },
5272
5476
  getLogs: () => server.getLogs(),
5273
5477
  clearLogs: () => server.clearLogs()
5274
5478
  };
@@ -5279,12 +5483,12 @@ function resolveServerCommand(server) {
5279
5483
  }
5280
5484
  return `npx tsx ${server}`;
5281
5485
  }
5282
- function runWithFixtures(fn) {
5283
- return async () => {
5284
- const fixtures = await createTestFixtures();
5486
+ function runWithFixtures(fn, chain) {
5487
+ return async (...extra) => {
5488
+ const fixtures = await buildFixtures(chain);
5285
5489
  let testFailed = false;
5286
5490
  try {
5287
- await fn(fixtures);
5491
+ await fn(fixtures, ...extra);
5288
5492
  } catch (error) {
5289
5493
  testFailed = true;
5290
5494
  throw error;
@@ -5293,24 +5497,37 @@ function runWithFixtures(fn) {
5293
5497
  }
5294
5498
  };
5295
5499
  }
5500
+ function skipTitle(name, scope) {
5501
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
5502
+ }
5296
5503
  function testWithFixtures(name, fn) {
5297
- const scope = currentSkipScope();
5504
+ const chain = scopes.slice();
5505
+ const scope = currentScope();
5298
5506
  if (scope.skipped) {
5299
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5507
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
5300
5508
  return;
5301
5509
  }
5302
- it2(name, runWithFixtures(fn));
5510
+ jestGlobals().it(name, runWithFixtures(fn, chain));
5303
5511
  }
5304
5512
  function use(config) {
5305
- currentConfig = { ...currentConfig, ...config };
5306
- afterAll2(async () => {
5307
- await cleanupSharedResources();
5308
- });
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
+ }
5309
5526
  }
5310
5527
  function skip(nameOrCondition, fnOrReason) {
5311
5528
  if (typeof nameOrCondition === "boolean") {
5312
5529
  if (nameOrCondition) {
5313
- const scope = currentSkipScope();
5530
+ const scope = currentScope();
5314
5531
  scope.skipped = true;
5315
5532
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
5316
5533
  }
@@ -5321,57 +5538,111 @@ function skip(nameOrCondition, fnOrReason) {
5321
5538
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
5322
5539
  );
5323
5540
  }
5324
- it2.skip(nameOrCondition, runWithFixtures(fnOrReason));
5541
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
5325
5542
  }
5326
5543
  function only(name, fn) {
5327
- const scope = currentSkipScope();
5544
+ const scope = currentScope();
5328
5545
  if (scope.skipped) {
5329
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5546
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
5330
5547
  return;
5331
5548
  }
5332
- it2.only(name, runWithFixtures(fn));
5549
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
5333
5550
  }
5334
5551
  function todo(name) {
5335
- it2.todo(name);
5552
+ jestGlobals().it.todo(name);
5336
5553
  }
5337
- function withSkipScope(register) {
5554
+ function each(table, ...tagged) {
5555
+ const chain = scopes.slice();
5556
+ const register = jestGlobals().it.each(table, ...tagged);
5338
5557
  return (name, fn) => {
5339
- register(name, () => {
5340
- const parent = currentSkipScope();
5341
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
5342
- try {
5343
- fn();
5344
- } finally {
5345
- skipScopes.pop();
5346
- }
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
5347
5576
  });
5577
+ try {
5578
+ fn(...args);
5579
+ } finally {
5580
+ scopes.pop();
5581
+ }
5348
5582
  };
5349
5583
  }
5350
- function eachWithSkipScope(each) {
5351
- return ((...eachArgs) => {
5352
- const register = each(...eachArgs);
5353
- return withSkipScope(register);
5354
- });
5584
+ function scopedRegister(pick) {
5585
+ return (name, fn, timeout) => {
5586
+ pick()(name, withScope(fn), timeout);
5587
+ };
5355
5588
  }
5356
- var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
5357
- only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
5358
- skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
5359
- each: eachWithSkipScope(describe2.each)
5360
- });
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
+ );
5361
5614
  var test = testWithFixtures;
5362
5615
  test.use = use;
5363
- test.describe = describeWithSkipScope;
5364
- test.beforeAll = beforeAll2;
5365
- test.beforeEach = beforeEach2;
5366
- test.afterEach = afterEach2;
5367
- 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));
5368
5621
  test.skip = skip;
5369
5622
  test.only = only;
5370
5623
  test.todo = todo;
5624
+ test.each = each;
5371
5625
 
5372
5626
  // libs/testing/src/expect.ts
5373
- var import_globals2 = require("@jest/globals");
5374
- 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
+ });
5375
5646
 
5376
5647
  // libs/testing/src/platform/platform-types.ts
5377
5648
  function getPlatformMetaNamespace(platform) {
@@ -6034,34 +6305,44 @@ var HttpInterceptorImpl = class {
6034
6305
  }
6035
6306
  };
6036
6307
  }
6037
- get(url, response) {
6308
+ get(url, response, options) {
6038
6309
  return this.mock({
6039
- match: { url, method: "GET" },
6040
- 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 }
6041
6314
  });
6042
6315
  }
6043
- post(url, response) {
6316
+ post(url, response, options) {
6044
6317
  return this.mock({
6045
- match: { url, method: "POST" },
6046
- 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 }
6047
6322
  });
6048
6323
  }
6049
- put(url, response) {
6324
+ put(url, response, options) {
6050
6325
  return this.mock({
6051
- match: { url, method: "PUT" },
6052
- 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 }
6053
6330
  });
6054
6331
  }
6055
- delete(url, response) {
6332
+ delete(url, response, options) {
6056
6333
  return this.mock({
6057
- match: { url, method: "DELETE" },
6058
- 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 }
6059
6338
  });
6060
6339
  }
6061
- any(url, response) {
6340
+ any(url, response, options) {
6062
6341
  return this.mock({
6063
- match: { url },
6064
- 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 }
6065
6346
  });
6066
6347
  }
6067
6348
  clear() {
@@ -6093,6 +6374,9 @@ ${descriptions.join("\n")}`);
6093
6374
  if (index !== -1) {
6094
6375
  activeInterceptors.splice(index, 1);
6095
6376
  }
6377
+ if (activeInterceptors.length === 0) {
6378
+ httpMock.disable();
6379
+ }
6096
6380
  }
6097
6381
  // Internal methods
6098
6382
  isActive() {
@@ -6192,43 +6476,49 @@ ${descriptions.join("\n")}`);
6192
6476
  }
6193
6477
  };
6194
6478
  async function interceptedFetch(input, init) {
6195
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
6196
- 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();
6197
6482
  const headers = {};
6198
- if (init?.headers) {
6199
- if (init.headers instanceof Headers) {
6200
- init.headers.forEach((value, key) => {
6483
+ const collectHeaders = (source) => {
6484
+ if (!source) return;
6485
+ if (source instanceof Headers) {
6486
+ source.forEach((value, key) => {
6201
6487
  headers[key.toLowerCase()] = value;
6202
6488
  });
6203
- } else if (Array.isArray(init.headers)) {
6204
- for (const [key, value] of init.headers) {
6489
+ } else if (Array.isArray(source)) {
6490
+ for (const [key, value] of source) {
6205
6491
  headers[key.toLowerCase()] = value;
6206
6492
  }
6207
6493
  } else {
6208
- for (const [key, value] of Object.entries(init.headers)) {
6494
+ for (const [key, value] of Object.entries(source)) {
6209
6495
  headers[key.toLowerCase()] = value;
6210
6496
  }
6211
6497
  }
6212
- }
6498
+ };
6499
+ collectHeaders(init?.headers ?? request?.headers);
6213
6500
  let body;
6214
6501
  let rawBody;
6215
- if (init?.body) {
6216
- if (typeof init.body === "string") {
6217
- rawBody = init.body;
6218
- try {
6219
- body = JSON.parse(init.body);
6220
- } catch {
6221
- body = init.body;
6222
- }
6223
- } else if (init.body instanceof ArrayBuffer || init.body instanceof Uint8Array) {
6224
- 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) {
6225
6515
  try {
6226
6516
  body = JSON.parse(rawBody);
6227
6517
  } catch {
6228
6518
  body = rawBody;
6229
6519
  }
6230
6520
  } else {
6231
- body = init.body;
6521
+ body = bodySource;
6232
6522
  }
6233
6523
  }
6234
6524
  const requestInfo = {
@@ -6258,8 +6548,20 @@ async function interceptedFetch(input, init) {
6258
6548
  Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
6259
6549
  );
6260
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
+ }
6261
6561
  function normalizeResponse(response) {
6262
- 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)) {
6263
6565
  return { body: response };
6264
6566
  }
6265
6567
  return response;
@@ -7593,47 +7895,48 @@ function clearGlobalMeasurements() {
7593
7895
  }
7594
7896
 
7595
7897
  // libs/testing/src/perf/perf-test.ts
7596
- var currentConfig2 = {};
7597
- var serverInstance2 = null;
7598
- var tokenFactory2 = null;
7599
- var serverStartedByUs2 = false;
7600
- async function initializeSharedResources2() {
7601
- if (!tokenFactory2) {
7602
- tokenFactory2 = new TestTokenFactory();
7603
- }
7604
- if (!serverInstance2) {
7605
- if (currentConfig2.baseUrl) {
7606
- serverInstance2 = TestServer.connect(currentConfig2.baseUrl);
7607
- serverStartedByUs2 = false;
7608
- } else if (currentConfig2.server) {
7609
- 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);
7610
7912
  const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
7611
7913
  if (isDebug) {
7612
7914
  console.log(`[PerfTest] Starting server: ${serverCommand}`);
7613
7915
  }
7614
7916
  try {
7615
- serverInstance2 = await TestServer.start({
7616
- project: currentConfig2.project,
7617
- port: currentConfig2.port,
7917
+ serverInstance = await TestServer.start({
7918
+ project: currentConfig.project,
7919
+ port: currentConfig.port,
7618
7920
  command: serverCommand,
7619
- env: currentConfig2.env,
7620
- startupTimeout: currentConfig2.startupTimeout ?? 3e4,
7921
+ env: currentConfig.env,
7922
+ startupTimeout: currentConfig.startupTimeout ?? 3e4,
7621
7923
  debug: isDebug
7622
7924
  });
7623
- serverStartedByUs2 = true;
7925
+ serverStartedByUs = true;
7624
7926
  if (isDebug) {
7625
- console.log(`[PerfTest] Server started at ${serverInstance2.info.baseUrl}`);
7927
+ console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
7626
7928
  }
7627
7929
  } catch (error) {
7628
7930
  const errMsg = error instanceof Error ? error.message : String(error);
7629
7931
  throw new Error(
7630
7932
  `Failed to start test server.
7631
7933
 
7632
- Server entry: ${currentConfig2.server}
7633
- Project: ${currentConfig2.project ?? "default"}
7934
+ Server entry: ${currentConfig.server}
7935
+ Project: ${currentConfig.project ?? "default"}
7634
7936
  Command: ${serverCommand}
7635
7937
 
7636
- Error: ${errMsg}`
7938
+ Error: ${errMsg}`,
7939
+ { cause: error }
7637
7940
  );
7638
7941
  }
7639
7942
  } else {
@@ -7641,23 +7944,23 @@ Error: ${errMsg}`
7641
7944
  }
7642
7945
  }
7643
7946
  }
7644
- async function createTestFixtures2(testName) {
7645
- await initializeSharedResources2();
7646
- if (!serverInstance2) {
7947
+ async function createTestFixtures(testName) {
7948
+ await initializeSharedResources();
7949
+ if (!serverInstance) {
7647
7950
  throw new Error("Server instance not initialized");
7648
7951
  }
7649
- if (!tokenFactory2) {
7952
+ if (!tokenFactory) {
7650
7953
  throw new Error("Token factory not initialized");
7651
7954
  }
7652
7955
  const clientInstance = await McpTestClient.create({
7653
- baseUrl: serverInstance2.info.baseUrl,
7654
- transport: currentConfig2.transport ?? "streamable-http",
7655
- publicMode: currentConfig2.publicMode
7956
+ baseUrl: serverInstance.info.baseUrl,
7957
+ transport: currentConfig.transport ?? "streamable-http",
7958
+ publicMode: currentConfig.publicMode
7656
7959
  }).buildAndConnect();
7657
- const auth = createAuthFixture2(tokenFactory2);
7658
- const server = createServerFixture2(serverInstance2);
7659
- const perfImpl = createPerfFixtures(testName, currentConfig2.project ?? "unknown");
7660
- 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) {
7661
7964
  await perfImpl.baseline();
7662
7965
  }
7663
7966
  return {
@@ -7673,8 +7976,8 @@ async function createTestFixtures2(testName) {
7673
7976
  async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7674
7977
  const measurement = perfImpl.buildMeasurement();
7675
7978
  addGlobalMeasurement(measurement);
7676
- if (testFailed && serverInstance2) {
7677
- const logs = serverInstance2.getLogs();
7979
+ if (testFailed && serverInstance) {
7980
+ const logs = serverInstance.getLogs();
7678
7981
  if (logs.length > 0) {
7679
7982
  console.error("\n[PerfTest] === Server Logs (test failed) ===");
7680
7983
  const recentLogs = logs.slice(-50);
@@ -7689,13 +7992,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7689
7992
  await fixtures.mcp.disconnect();
7690
7993
  }
7691
7994
  }
7692
- async function cleanupSharedResources2() {
7693
- if (serverInstance2 && serverStartedByUs2) {
7694
- await serverInstance2.stop();
7995
+ async function cleanupSharedResources() {
7996
+ if (serverInstance && serverStartedByUs) {
7997
+ await serverInstance.stop();
7695
7998
  }
7696
- serverInstance2 = null;
7697
- tokenFactory2 = null;
7698
- serverStartedByUs2 = false;
7999
+ serverInstance = null;
8000
+ tokenFactory = null;
8001
+ serverStartedByUs = false;
7699
8002
  }
7700
8003
  function createAuthFixture2(factory) {
7701
8004
  const users = {
@@ -7750,13 +8053,13 @@ function createServerFixture2(server) {
7750
8053
  transport: opts?.transport ?? "streamable-http",
7751
8054
  auth: opts?.token ? { token: opts.token } : void 0,
7752
8055
  clientInfo: opts?.clientInfo,
7753
- publicMode: currentConfig2.publicMode
8056
+ publicMode: currentConfig.publicMode
7754
8057
  }).buildAndConnect();
7755
8058
  },
7756
8059
  createClientBuilder: () => {
7757
8060
  return new McpTestClientBuilder({
7758
8061
  baseUrl: server.info.baseUrl,
7759
- publicMode: currentConfig2.publicMode
8062
+ publicMode: currentConfig.publicMode
7760
8063
  });
7761
8064
  },
7762
8065
  restart: () => server.restart(),
@@ -7772,7 +8075,7 @@ function resolveServerCommand2(server) {
7772
8075
  }
7773
8076
  function perfTestWithFixtures(name, fn) {
7774
8077
  it(name, async () => {
7775
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8078
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7776
8079
  let testFailed = false;
7777
8080
  try {
7778
8081
  await fn(fixtures);
@@ -7785,14 +8088,14 @@ function perfTestWithFixtures(name, fn) {
7785
8088
  });
7786
8089
  }
7787
8090
  function use2(config) {
7788
- currentConfig2 = { ...currentConfig2, ...config };
8091
+ currentConfig = { ...currentConfig, ...config };
7789
8092
  afterAll(async () => {
7790
- await cleanupSharedResources2();
8093
+ await cleanupSharedResources();
7791
8094
  });
7792
8095
  }
7793
8096
  function skip2(name, fn) {
7794
8097
  it.skip(name, async () => {
7795
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8098
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7796
8099
  let testFailed = false;
7797
8100
  try {
7798
8101
  await fn(fixtures);
@@ -7806,7 +8109,7 @@ function skip2(name, fn) {
7806
8109
  }
7807
8110
  function only2(name, fn) {
7808
8111
  it.only(name, async () => {
7809
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8112
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7810
8113
  let testFailed = false;
7811
8114
  try {
7812
8115
  await fn(fixtures);
@@ -7823,16 +8126,19 @@ function todo2(name) {
7823
8126
  }
7824
8127
  var perfTest = perfTestWithFixtures;
7825
8128
  perfTest.use = use2;
7826
- perfTest.describe = describe;
7827
- perfTest.beforeAll = beforeAll;
7828
- perfTest.beforeEach = beforeEach;
7829
- perfTest.afterEach = afterEach;
7830
- 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
+ }
7831
8136
  perfTest.skip = skip2;
7832
8137
  perfTest.only = only2;
7833
8138
  perfTest.todo = todo2;
7834
8139
 
7835
8140
  // libs/testing/src/perf/baseline-store.ts
8141
+ var import_utils4 = require("@frontmcp/utils");
7836
8142
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
7837
8143
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
7838
8144
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -7847,11 +8153,10 @@ var BaselineStore = class {
7847
8153
  */
7848
8154
  async load() {
7849
8155
  try {
7850
- const { readFile, fileExists } = await import("@frontmcp/utils");
7851
- if (!await fileExists(this.baselinePath)) {
8156
+ if (!await (0, import_utils4.fileExists)(this.baselinePath)) {
7852
8157
  return null;
7853
8158
  }
7854
- const content = await readFile(this.baselinePath);
8159
+ const content = await (0, import_utils4.readFile)(this.baselinePath);
7855
8160
  this.baseline = JSON.parse(content);
7856
8161
  return this.baseline;
7857
8162
  } catch {
@@ -7862,10 +8167,9 @@ var BaselineStore = class {
7862
8167
  * Save baseline to local file.
7863
8168
  */
7864
8169
  async save(baseline) {
7865
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
7866
8170
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
7867
- await ensureDir(dir);
7868
- 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));
7869
8173
  this.baseline = baseline;
7870
8174
  }
7871
8175
  /**
@@ -8118,6 +8422,7 @@ function getRegressionDetector(config) {
8118
8422
  }
8119
8423
 
8120
8424
  // libs/testing/src/perf/report-generator.ts
8425
+ var import_utils5 = require("@frontmcp/utils");
8121
8426
  var ReportGenerator = class {
8122
8427
  detector;
8123
8428
  constructor() {
@@ -8373,14 +8678,13 @@ var ReportGenerator = class {
8373
8678
  }
8374
8679
  };
8375
8680
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
8376
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
8377
- await ensureDir(outputDir);
8681
+ await (0, import_utils5.ensureDir)(outputDir);
8378
8682
  const generator = new ReportGenerator();
8379
8683
  const report = generator.generateReport(measurements, baseline, gitInfo);
8380
8684
  const jsonPath = `${outputDir}/report.json`;
8381
8685
  const markdownPath = `${outputDir}/report.md`;
8382
- await writeFile(jsonPath, generator.generateJsonReport(report));
8383
- 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));
8384
8688
  return { jsonPath, markdownPath };
8385
8689
  }
8386
8690
  function createReportGenerator() {