@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/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) {
@@ -1303,6 +1329,15 @@ var DEFAULT_CLIENT_INFO = {
1303
1329
  version: "0.4.0"
1304
1330
  };
1305
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
+ }
1306
1341
  function extractEntryPaths(data) {
1307
1342
  let payload = data;
1308
1343
  if (typeof payload === "string") {
@@ -1389,19 +1424,22 @@ var McpTestClient = class {
1389
1424
  async connect() {
1390
1425
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1391
1426
  this.triedDiscoveredEntryPath = false;
1427
+ this.initResult = null;
1392
1428
  this.transport = this.createTransport();
1393
1429
  await this.transport.connect();
1394
1430
  const maxRetries = 3;
1395
1431
  const retryDelayMs = 500;
1396
1432
  let lastError;
1397
1433
  let reportedEntryPaths;
1434
+ let attemptsMade = 0;
1398
1435
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1436
+ attemptsMade = attempt;
1399
1437
  const initResponse = await this.initialize();
1400
1438
  if (initResponse.success && initResponse.data) {
1401
1439
  this.initResult = initResponse.data;
1402
1440
  break;
1403
1441
  }
1404
- lastError = initResponse.error?.message ?? "Unknown error";
1442
+ lastError = describeInitializeError(initResponse.error);
1405
1443
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1406
1444
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1407
1445
  this.triedDiscoveredEntryPath = true;
@@ -1413,6 +1451,7 @@ var McpTestClient = class {
1413
1451
  await this.transport.connect();
1414
1452
  continue;
1415
1453
  }
1454
+ if (isAuthFailure(initResponse.error)) break;
1416
1455
  if (attempt < maxRetries) {
1417
1456
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1418
1457
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1421,7 +1460,7 @@ var McpTestClient = class {
1421
1460
  if (!this.initResult) {
1422
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.` : "";
1423
1462
  throw new Error(
1424
- `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}`
1425
1464
  );
1426
1465
  }
1427
1466
  this._sessionId = this.transport.getSessionId();
@@ -1491,18 +1530,47 @@ var McpTestClient = class {
1491
1530
  return this._authState;
1492
1531
  }
1493
1532
  /**
1494
- * 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.
1495
1539
  */
1496
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 };
1497
1547
  this._authState = {
1498
1548
  isAnonymous: false,
1499
1549
  token,
1500
1550
  scopes: this.parseScopesFromToken(token),
1501
1551
  user: this.parseUserFromToken(token)
1502
1552
  };
1503
- if (this.transport) {
1504
- this.transport.setAuthToken(token);
1553
+ const previousTransport = this.transport;
1554
+ if (!previousTransport) {
1555
+ this.log("debug", "Authentication updated (not connected yet)");
1556
+ return;
1505
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;
1572
+ }
1573
+ await previousTransport.close().catch(() => void 0);
1506
1574
  this.log("debug", "Authentication updated");
1507
1575
  }
1508
1576
  // ═══════════════════════════════════════════════════════════════════
@@ -2126,10 +2194,11 @@ var McpTestClient = class {
2126
2194
  requestId: id
2127
2195
  };
2128
2196
  } catch (err) {
2197
+ if (isInterceptedError(err)) throw err;
2129
2198
  const durationMs = Date.now() - start;
2130
2199
  const error = {
2131
2200
  code: -32603,
2132
- message: err instanceof Error ? err.message : "Unknown error"
2201
+ message: errorMessage(err)
2133
2202
  };
2134
2203
  return {
2135
2204
  success: false,
@@ -2277,12 +2346,12 @@ var McpTestClient = class {
2277
2346
  const payload = this.decodeJwtPayload(token);
2278
2347
  if (!payload) return [];
2279
2348
  const scope = payload["scope"];
2280
- const scopes = payload["scopes"];
2349
+ const scopes2 = payload["scopes"];
2281
2350
  if (typeof scope === "string") {
2282
2351
  return scope.split(" ");
2283
2352
  }
2284
- if (Array.isArray(scopes)) {
2285
- return scopes;
2353
+ if (Array.isArray(scopes2)) {
2354
+ return scopes2;
2286
2355
  }
2287
2356
  return [];
2288
2357
  } catch {
@@ -2414,14 +2483,15 @@ var TestTokenFactory = class {
2414
2483
  */
2415
2484
  async createTestToken(options) {
2416
2485
  await this.ensureKeys();
2417
- const now = Math.floor(Date.now() / 1e3);
2486
+ const nowMs = Date.now();
2487
+ const iat = Math.floor(nowMs / 1e3);
2418
2488
  const exp = options.exp ?? 3600;
2419
2489
  const payload = {
2420
2490
  iss: options.iss ?? this.issuer,
2421
2491
  sub: options.sub,
2422
2492
  aud: options.aud ?? this.audience,
2423
- iat: now,
2424
- exp: now + exp,
2493
+ iat,
2494
+ exp: Math.ceil(nowMs / 1e3) + exp,
2425
2495
  scope: options.scopes?.join(" "),
2426
2496
  ...options.claims
2427
2497
  };
@@ -2446,10 +2516,10 @@ var TestTokenFactory = class {
2446
2516
  /**
2447
2517
  * Create a regular user token
2448
2518
  */
2449
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
2519
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
2450
2520
  return this.createTestToken({
2451
2521
  sub,
2452
- scopes,
2522
+ scopes: scopes2,
2453
2523
  claims: {
2454
2524
  email: "user@test.local",
2455
2525
  name: "Test User",
@@ -2460,10 +2530,11 @@ var TestTokenFactory = class {
2460
2530
  /**
2461
2531
  * Create an anonymous user token
2462
2532
  */
2463
- async createAnonymousToken() {
2533
+ async createAnonymousToken(expiresIn) {
2464
2534
  return this.createTestToken({
2465
2535
  sub: `anon:${Date.now()}`,
2466
2536
  scopes: ["anonymous"],
2537
+ exp: expiresIn,
2467
2538
  claims: {
2468
2539
  name: "Anonymous",
2469
2540
  role: "anonymous"
@@ -2674,17 +2745,7 @@ function createTestUser(overrides) {
2674
2745
 
2675
2746
  // libs/testing/src/auth/mock-oauth-server.ts
2676
2747
  import { createServer } from "http";
2677
- var _randomBytes;
2678
- var _sha256Base64url;
2679
- var _base64urlEncode;
2680
- async function loadCryptoUtils() {
2681
- if (!_randomBytes) {
2682
- const utils = await import("@frontmcp/utils");
2683
- _randomBytes = utils.randomBytes;
2684
- _sha256Base64url = utils.sha256Base64url;
2685
- _base64urlEncode = utils.base64urlEncode;
2686
- }
2687
- }
2748
+ import { base64urlEncode, randomBytes, sha256Base64url } from "@frontmcp/utils";
2688
2749
  var MockOAuthServer = class {
2689
2750
  tokenFactory;
2690
2751
  options;
@@ -2699,8 +2760,8 @@ var MockOAuthServer = class {
2699
2760
  accessTokenTtlSeconds;
2700
2761
  /** Refresh token TTL in seconds */
2701
2762
  refreshTokenTtlSeconds;
2702
- constructor(tokenFactory3, options = {}) {
2703
- this.tokenFactory = tokenFactory3;
2763
+ constructor(tokenFactory2, options = {}) {
2764
+ this.tokenFactory = tokenFactory2;
2704
2765
  this.options = options;
2705
2766
  this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
2706
2767
  this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
@@ -2957,14 +3018,14 @@ ${error.stack}` : String(error);
2957
3018
  return;
2958
3019
  }
2959
3020
  const code = this.generateCode();
2960
- const scopes = scope.split(" ").filter(Boolean);
3021
+ const scopes2 = scope.split(" ").filter(Boolean);
2961
3022
  this.authCodes.set(code, {
2962
3023
  code,
2963
3024
  clientId,
2964
3025
  redirectUri,
2965
3026
  codeChallenge,
2966
3027
  codeChallengeMethod,
2967
- scopes,
3028
+ scopes: scopes2,
2968
3029
  user: testUser,
2969
3030
  state,
2970
3031
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3004,7 +3065,7 @@ ${error.stack}` : String(error);
3004
3065
  }
3005
3066
  const grantType = params.get("grant_type");
3006
3067
  if (grantType === "anonymous") {
3007
- const token = await this.tokenFactory.createAnonymousToken();
3068
+ const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
3008
3069
  res.writeHead(200, { "Content-Type": "application/json" });
3009
3070
  res.end(
3010
3071
  JSON.stringify({
@@ -3136,6 +3197,8 @@ ${error.stack}` : String(error);
3136
3197
  codeRecord.used = true;
3137
3198
  const accessToken = await this.tokenFactory.createTestToken({
3138
3199
  sub: codeRecord.user.sub,
3200
+ scopes: codeRecord.scopes,
3201
+ exp: this.accessTokenTtlSeconds,
3139
3202
  claims: {
3140
3203
  email: codeRecord.user.email,
3141
3204
  name: codeRecord.user.name,
@@ -3144,6 +3207,7 @@ ${error.stack}` : String(error);
3144
3207
  });
3145
3208
  const idToken = await this.tokenFactory.createTestToken({
3146
3209
  sub: codeRecord.user.sub,
3210
+ exp: this.accessTokenTtlSeconds,
3147
3211
  claims: {
3148
3212
  email: codeRecord.user.email,
3149
3213
  name: codeRecord.user.name,
@@ -3221,6 +3285,8 @@ ${error.stack}` : String(error);
3221
3285
  }
3222
3286
  const accessToken = await this.tokenFactory.createTestToken({
3223
3287
  sub: tokenRecord.user.sub,
3288
+ scopes: tokenRecord.scopes,
3289
+ exp: this.accessTokenTtlSeconds,
3224
3290
  claims: {
3225
3291
  email: tokenRecord.user.email,
3226
3292
  name: tokenRecord.user.name,
@@ -3325,14 +3391,14 @@ ${error.stack}` : String(error);
3325
3391
  name
3326
3392
  };
3327
3393
  const code = this.generateCode();
3328
- const scopes = scope.split(" ").filter(Boolean);
3394
+ const scopes2 = scope.split(" ").filter(Boolean);
3329
3395
  this.authCodes.set(code, {
3330
3396
  code,
3331
3397
  clientId,
3332
3398
  redirectUri,
3333
3399
  codeChallenge,
3334
3400
  codeChallengeMethod,
3335
- scopes,
3401
+ scopes: scopes2,
3336
3402
  user: testUser,
3337
3403
  state,
3338
3404
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3438,8 +3504,7 @@ ${error.stack}` : String(error);
3438
3504
  * Generate a random authorization code
3439
3505
  */
3440
3506
  async generateCodeAsync() {
3441
- await loadCryptoUtils();
3442
- return _base64urlEncode(_randomBytes(32));
3507
+ return base64urlEncode(randomBytes(32));
3443
3508
  }
3444
3509
  /**
3445
3510
  * Generate a random authorization code (sync wrapper for compatibility)
@@ -3455,8 +3520,7 @@ ${error.stack}` : String(error);
3455
3520
  */
3456
3521
  async computeCodeChallengeAsync(verifier, method) {
3457
3522
  if (method === "S256") {
3458
- await loadCryptoUtils();
3459
- return _sha256Base64url(verifier);
3523
+ return sha256Base64url(verifier);
3460
3524
  }
3461
3525
  return verifier;
3462
3526
  }
@@ -4043,7 +4107,7 @@ var MockCimdServer = class {
4043
4107
  };
4044
4108
 
4045
4109
  // libs/testing/src/server/test-server.ts
4046
- import { spawn } from "child_process";
4110
+ import { execFile, spawn } from "child_process";
4047
4111
  import { sha256Hex } from "@frontmcp/utils";
4048
4112
 
4049
4113
  // libs/testing/src/errors/index.ts
@@ -4150,6 +4214,8 @@ var E2E_PORT_RANGES = {
4150
4214
  "demo-e2e-distributed": { start: 50440, size: 20 },
4151
4215
  // Protocol revision E2E tests (50460-50479)
4152
4216
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
4217
+ // Testing library E2E tests (50480-50499)
4218
+ "demo-e2e-testing": { start: 50480, size: 20 },
4153
4219
  // Mock servers and utilities (50900-50999)
4154
4220
  "mock-oauth": { start: 50900, size: 10 },
4155
4221
  "mock-api": { start: 50910, size: 10 },
@@ -4158,6 +4224,66 @@ var E2E_PORT_RANGES = {
4158
4224
  default: { start: 51e3, size: 100 }
4159
4225
  };
4160
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
+ }
4161
4287
  var projectPortIndex = /* @__PURE__ */ new Map();
4162
4288
  function getPortRange(project) {
4163
4289
  const key = project;
@@ -4168,15 +4294,22 @@ function getPortRange(project) {
4168
4294
  }
4169
4295
  async function reservePort(project, preferredPort) {
4170
4296
  const range = getPortRange(project);
4171
- 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) {
4172
4310
  const reservation = await tryReservePort(preferredPort, project);
4173
4311
  if (reservation) {
4174
- return {
4175
- port: preferredPort,
4176
- release: async () => {
4177
- await releasePort(preferredPort);
4178
- }
4179
- };
4312
+ return handle(preferredPort);
4180
4313
  }
4181
4314
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
4182
4315
  }
@@ -4185,30 +4318,19 @@ async function reservePort(project, preferredPort) {
4185
4318
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
4186
4319
  const port = range.start + index % range.size;
4187
4320
  index = (index + 1) % range.size;
4188
- if (reservedPorts.has(port)) {
4321
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
4189
4322
  continue;
4190
4323
  }
4191
4324
  const reservation = await tryReservePort(port, project);
4192
4325
  if (reservation) {
4193
4326
  projectPortIndex.set(project, index);
4194
- return {
4195
- port,
4196
- release: async () => {
4197
- await releasePort(port);
4198
- }
4199
- };
4327
+ return handle(port);
4200
4328
  }
4201
4329
  }
4202
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
4203
- if (dynamicPort) {
4204
- const reservation = await tryReservePort(dynamicPort, project);
4205
- if (reservation) {
4206
- return {
4207
- port: dynamicPort,
4208
- release: async () => {
4209
- await releasePort(dynamicPort);
4210
- }
4211
- };
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);
4212
4334
  }
4213
4335
  }
4214
4336
  throw new Error(
@@ -4216,9 +4338,13 @@ async function reservePort(project, preferredPort) {
4216
4338
  );
4217
4339
  }
4218
4340
  async function tryReservePort(port, project) {
4341
+ if (!acquirePortLock(port)) {
4342
+ return false;
4343
+ }
4219
4344
  return new Promise((resolve) => {
4220
4345
  const server = createServer4();
4221
4346
  server.once("error", () => {
4347
+ releasePortLock(port);
4222
4348
  resolve(false);
4223
4349
  });
4224
4350
  server.listen(port, () => {
@@ -4244,31 +4370,6 @@ async function releasePort(port) {
4244
4370
  });
4245
4371
  });
4246
4372
  }
4247
- async function findAvailablePortInRange(start, end) {
4248
- for (let port = start; port < end; port++) {
4249
- if (reservedPorts.has(port)) {
4250
- continue;
4251
- }
4252
- const available = await isPortAvailable(port);
4253
- if (available) {
4254
- return port;
4255
- }
4256
- }
4257
- return null;
4258
- }
4259
- async function isPortAvailable(port) {
4260
- return new Promise((resolve) => {
4261
- const server = createServer4();
4262
- server.once("error", () => {
4263
- resolve(false);
4264
- });
4265
- server.listen(port, () => {
4266
- server.close(() => {
4267
- resolve(true);
4268
- });
4269
- });
4270
- });
4271
- }
4272
4373
 
4273
4374
  // libs/testing/src/server/test-server.ts
4274
4375
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -4277,8 +4378,11 @@ var TestServer = class _TestServer {
4277
4378
  options;
4278
4379
  _info;
4279
4380
  logs = [];
4381
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
4280
4382
  portRelease = null;
4281
- 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) {
4282
4386
  this.options = {
4283
4387
  port,
4284
4388
  project: options.project,
@@ -4290,6 +4394,7 @@ var TestServer = class _TestServer {
4290
4394
  debug: options.debug ?? DEBUG_SERVER
4291
4395
  };
4292
4396
  this.portRelease = portRelease ?? null;
4397
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
4293
4398
  this._info = {
4294
4399
  baseUrl: `http://localhost:${port}`,
4295
4400
  port
@@ -4302,8 +4407,8 @@ var TestServer = class _TestServer {
4302
4407
  const project = options.project ?? "default";
4303
4408
  const maxAttempts = 3;
4304
4409
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4305
- const { port, release } = await reservePort(project, options.port);
4306
- 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);
4307
4412
  try {
4308
4413
  await server.startProcess();
4309
4414
  return server;
@@ -4343,7 +4448,7 @@ var TestServer = class _TestServer {
4343
4448
  }
4344
4449
  const maxAttempts = 3;
4345
4450
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4346
- const { port, release } = await reservePort(project, options.port);
4451
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4347
4452
  const serverOptions = {
4348
4453
  ...options,
4349
4454
  port,
@@ -4351,7 +4456,7 @@ var TestServer = class _TestServer {
4351
4456
  command: `npx nx serve ${project} --port ${port}`,
4352
4457
  cwd: options.cwd ?? process.cwd()
4353
4458
  };
4354
- const server = new _TestServer(serverOptions, port, release);
4459
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
4355
4460
  try {
4356
4461
  await server.startProcess();
4357
4462
  return server;
@@ -4409,10 +4514,17 @@ var TestServer = class _TestServer {
4409
4514
  * Stop the test server
4410
4515
  */
4411
4516
  async stop() {
4412
- if (this.portRelease) {
4413
- await this.portRelease();
4414
- 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;
4415
4525
  }
4526
+ }
4527
+ async stopProcess() {
4416
4528
  if (this.process) {
4417
4529
  this.log("Stopping server...");
4418
4530
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -4485,7 +4597,7 @@ var TestServer = class _TestServer {
4485
4597
  * Restart the server
4486
4598
  */
4487
4599
  async restart() {
4488
- await this.stop();
4600
+ await this.stopProcess();
4489
4601
  await this.startProcess();
4490
4602
  }
4491
4603
  /**
@@ -4515,9 +4627,9 @@ var TestServer = class _TestServer {
4515
4627
  PORT: String(this.options.port)
4516
4628
  };
4517
4629
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
4518
- if (this.portRelease) {
4519
- await this.portRelease();
4520
- this.portRelease = null;
4630
+ if (this.portReleaseSocket) {
4631
+ await this.portReleaseSocket();
4632
+ this.portReleaseSocket = null;
4521
4633
  await sleep2(300);
4522
4634
  }
4523
4635
  this.process = spawn(this.options.command, [], {
@@ -4527,9 +4639,8 @@ var TestServer = class _TestServer {
4527
4639
  detached: true,
4528
4640
  stdio: ["pipe", "pipe", "pipe"]
4529
4641
  });
4530
- if (this.process.pid !== void 0) {
4531
- this._info.pid = this.process.pid;
4532
- }
4642
+ this._info.pid = this.process.pid;
4643
+ this._info.shellPid = this.process.pid;
4533
4644
  let processExited = false;
4534
4645
  let exitCode = null;
4535
4646
  let exitError = null;
@@ -4632,6 +4743,8 @@ ${allLogs}`);
4632
4743
  });
4633
4744
  if (response.ok || response.status === 404) {
4634
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;
4635
4748
  return;
4636
4749
  }
4637
4750
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -4676,6 +4789,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
4676
4789
  function sleep2(ms) {
4677
4790
  return new Promise((resolve) => setTimeout(resolve, ms));
4678
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
+ }
4679
4805
  function withWorkspaceProtocolFallback(env, cwd) {
4680
4806
  if (findInstalledProtocolPackageDir(cwd)) {
4681
4807
  return env;
@@ -4947,16 +5073,6 @@ function hasMimeType(result, mimeType) {
4947
5073
  return result.hasMimeType(mimeType);
4948
5074
  }
4949
5075
 
4950
- // libs/testing/src/fixtures/test-fixture.ts
4951
- import {
4952
- afterAll as _afterAll,
4953
- afterEach as _afterEach,
4954
- beforeAll as _beforeAll,
4955
- beforeEach as _beforeEach,
4956
- describe as _describe,
4957
- it as _it
4958
- } from "@jest/globals";
4959
-
4960
5076
  // libs/testing/src/fixtures/gateway-token-binding.ts
4961
5077
  import { trimSlashes, trimTrailing } from "@frontmcp/utils";
4962
5078
  function gatewayTokenBinding(serverUrl, entryPath) {
@@ -4967,81 +5083,152 @@ function gatewayTokenBinding(serverUrl, entryPath) {
4967
5083
  }
4968
5084
 
4969
5085
  // libs/testing/src/fixtures/test-fixture.ts
4970
- var describe2 = _describe;
4971
- var beforeAll2 = _beforeAll;
4972
- var beforeEach2 = _beforeEach;
4973
- var afterEach2 = _afterEach;
4974
- var afterAll2 = _afterAll;
4975
- var it2 = _it;
4976
- var currentConfig = {};
4977
- var serverInstance = null;
4978
- var tokenFactory = null;
4979
- var serverStartedByUs = false;
4980
- var skipScopes = [{ skipped: false }];
4981
- function currentSkipScope() {
4982
- 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
+ }
4983
5106
  }
4984
- async function initializeSharedResources() {
4985
- if (!serverInstance) {
4986
- if (currentConfig.baseUrl && !currentConfig.server) {
4987
- serverInstance = TestServer.connect(currentConfig.baseUrl);
4988
- serverStartedByUs = false;
4989
- } else if (currentConfig.server) {
4990
- const serverCommand = resolveServerCommand(currentConfig.server);
4991
- 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;
4992
5169
  if (isDebug) {
4993
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
5170
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
4994
5171
  }
4995
- try {
4996
- serverInstance = await TestServer.start({
4997
- project: currentConfig.project,
4998
- port: currentConfig.port,
4999
- command: serverCommand,
5000
- env: currentConfig.env,
5001
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
5002
- debug: isDebug
5003
- });
5004
- serverStartedByUs = true;
5005
- if (isDebug) {
5006
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
5007
- }
5008
- } catch (error) {
5009
- const errMsg = error instanceof Error ? error.message : String(error);
5010
- throw new Error(
5011
- `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.
5012
5176
 
5013
- Server entry: ${currentConfig.server}
5014
- Project: ${currentConfig.project ?? "default"}
5177
+ Server entry: ${config.server}
5178
+ Project: ${config.project ?? "default"}
5015
5179
  Command: ${serverCommand}
5016
5180
 
5017
- Error: ${errMsg}`
5018
- );
5019
- }
5020
- } else {
5021
- throw new Error(
5022
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
5181
+ Error: ${errMsg}`,
5182
+ { cause: error }
5023
5183
  );
5024
5184
  }
5185
+ } else {
5186
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
5025
5187
  }
5026
- if (!tokenFactory) {
5027
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
5028
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
5029
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5030
- }
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 };
5031
5192
  }
5032
- async function createTestFixtures() {
5033
- await initializeSharedResources();
5034
- if (!serverInstance) {
5035
- throw new Error("Server instance not initialized");
5036
- }
5037
- if (!tokenFactory) {
5038
- 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
+ }
5039
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 };
5040
5227
  const clientInstance = McpTestClient.create({
5041
- baseUrl: resolveClientBaseUrl(serverInstance),
5042
- entryPath: currentConfig.entryPath,
5043
- transport: currentConfig.transport ?? "streamable-http",
5044
- publicMode: currentConfig.publicMode
5228
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
5229
+ entryPath: clientConfig.entryPath,
5230
+ transport: clientConfig.transport ?? "streamable-http",
5231
+ publicMode
5045
5232
  }).build();
5046
5233
  try {
5047
5234
  await clientInstance.connect();
@@ -5055,17 +5242,18 @@ async function createTestFixtures() {
5055
5242
  throw err;
5056
5243
  }
5057
5244
  }
5058
- const auth = createAuthFixture(tokenFactory);
5059
- const server = createServerFixture(serverInstance);
5060
- return {
5245
+ const context = { extraClients: [] };
5246
+ const fixtures = {
5061
5247
  mcp: clientInstance,
5062
- auth,
5063
- server
5248
+ auth: createAuthFixture(tokenFactory2),
5249
+ server: createServerFixture(server, clientConfig, clientInstance, context)
5064
5250
  };
5251
+ fixtureContexts.set(fixtures, context);
5252
+ return fixtures;
5065
5253
  }
5066
5254
  async function cleanupTestFixtures(fixtures, testFailed = false) {
5067
- if (testFailed && serverInstance) {
5068
- const logs = serverInstance.getLogs();
5255
+ if (testFailed) {
5256
+ const logs = fixtures.server.getLogs();
5069
5257
  if (logs.length > 0) {
5070
5258
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
5071
5259
  const recentLogs = logs.slice(-50);
@@ -5076,20 +5264,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
5076
5264
  console.error("[TestFixture] === End Server Logs ===\n");
5077
5265
  }
5078
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
+ }
5079
5273
  if (fixtures.mcp.isConnected()) {
5080
5274
  await fixtures.mcp.disconnect();
5081
5275
  }
5082
5276
  }
5083
- async function cleanupSharedResources() {
5084
- if (serverInstance && serverStartedByUs) {
5085
- await serverInstance.stop();
5086
- }
5087
- serverInstance = null;
5088
- tokenFactory = null;
5089
- serverStartedByUs = false;
5090
- }
5091
- function resolveClientBaseUrl(server) {
5092
- return currentConfig.baseUrl ?? server.info.baseUrl;
5277
+ function resolveClientBaseUrl(server, config) {
5278
+ return config.baseUrl ?? server.info.baseUrl;
5093
5279
  }
5094
5280
  function createAuthFixture(factory) {
5095
5281
  const users = {
@@ -5135,28 +5321,39 @@ function createAuthFixture(factory) {
5135
5321
  getAudience: () => factory.getAudience()
5136
5322
  };
5137
5323
  }
5138
- function createServerFixture(server) {
5324
+ function createServerFixture(server, config, mcp, context) {
5139
5325
  return {
5140
- 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
+ },
5141
5330
  createClient: async (opts) => {
5142
- return McpTestClient.create({
5143
- baseUrl: resolveClientBaseUrl(server),
5144
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
5331
+ const client = await McpTestClient.create({
5332
+ baseUrl: resolveClientBaseUrl(server, config),
5333
+ entryPath: opts?.entryPath ?? config.entryPath,
5145
5334
  transport: opts?.transport ?? "streamable-http",
5146
5335
  auth: opts?.token ? { token: opts.token } : void 0,
5147
5336
  clientInfo: opts?.clientInfo,
5148
- publicMode: currentConfig.publicMode
5337
+ publicMode: config.publicMode
5149
5338
  }).buildAndConnect();
5339
+ context.extraClients.push(client);
5340
+ return client;
5150
5341
  },
5151
5342
  createClientBuilder: () => {
5152
5343
  const builder = new McpTestClientBuilder({
5153
- baseUrl: resolveClientBaseUrl(server),
5154
- entryPath: currentConfig.entryPath,
5155
- publicMode: currentConfig.publicMode
5344
+ baseUrl: resolveClientBaseUrl(server, config),
5345
+ entryPath: config.entryPath,
5346
+ publicMode: config.publicMode
5156
5347
  });
5157
5348
  return builder;
5158
5349
  },
5159
- 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
+ },
5160
5357
  getLogs: () => server.getLogs(),
5161
5358
  clearLogs: () => server.clearLogs()
5162
5359
  };
@@ -5167,12 +5364,12 @@ function resolveServerCommand(server) {
5167
5364
  }
5168
5365
  return `npx tsx ${server}`;
5169
5366
  }
5170
- function runWithFixtures(fn) {
5171
- return async () => {
5172
- const fixtures = await createTestFixtures();
5367
+ function runWithFixtures(fn, chain) {
5368
+ return async (...extra) => {
5369
+ const fixtures = await buildFixtures(chain);
5173
5370
  let testFailed = false;
5174
5371
  try {
5175
- await fn(fixtures);
5372
+ await fn(fixtures, ...extra);
5176
5373
  } catch (error) {
5177
5374
  testFailed = true;
5178
5375
  throw error;
@@ -5181,24 +5378,37 @@ function runWithFixtures(fn) {
5181
5378
  }
5182
5379
  };
5183
5380
  }
5381
+ function skipTitle(name, scope) {
5382
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
5383
+ }
5184
5384
  function testWithFixtures(name, fn) {
5185
- const scope = currentSkipScope();
5385
+ const chain = scopes.slice();
5386
+ const scope = currentScope();
5186
5387
  if (scope.skipped) {
5187
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5388
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
5188
5389
  return;
5189
5390
  }
5190
- it2(name, runWithFixtures(fn));
5391
+ jestGlobals().it(name, runWithFixtures(fn, chain));
5191
5392
  }
5192
5393
  function use(config) {
5193
- currentConfig = { ...currentConfig, ...config };
5194
- afterAll2(async () => {
5195
- await cleanupSharedResources();
5196
- });
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
+ }
5197
5407
  }
5198
5408
  function skip(nameOrCondition, fnOrReason) {
5199
5409
  if (typeof nameOrCondition === "boolean") {
5200
5410
  if (nameOrCondition) {
5201
- const scope = currentSkipScope();
5411
+ const scope = currentScope();
5202
5412
  scope.skipped = true;
5203
5413
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
5204
5414
  }
@@ -5209,57 +5419,111 @@ function skip(nameOrCondition, fnOrReason) {
5209
5419
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
5210
5420
  );
5211
5421
  }
5212
- it2.skip(nameOrCondition, runWithFixtures(fnOrReason));
5422
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
5213
5423
  }
5214
5424
  function only(name, fn) {
5215
- const scope = currentSkipScope();
5425
+ const scope = currentScope();
5216
5426
  if (scope.skipped) {
5217
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5427
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
5218
5428
  return;
5219
5429
  }
5220
- it2.only(name, runWithFixtures(fn));
5430
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
5221
5431
  }
5222
5432
  function todo(name) {
5223
- it2.todo(name);
5433
+ jestGlobals().it.todo(name);
5224
5434
  }
5225
- function withSkipScope(register) {
5435
+ function each(table, ...tagged) {
5436
+ const chain = scopes.slice();
5437
+ const register = jestGlobals().it.each(table, ...tagged);
5226
5438
  return (name, fn) => {
5227
- register(name, () => {
5228
- const parent = currentSkipScope();
5229
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
5230
- try {
5231
- fn();
5232
- } finally {
5233
- skipScopes.pop();
5234
- }
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
5235
5457
  });
5458
+ try {
5459
+ fn(...args);
5460
+ } finally {
5461
+ scopes.pop();
5462
+ }
5236
5463
  };
5237
5464
  }
5238
- function eachWithSkipScope(each) {
5239
- return ((...eachArgs) => {
5240
- const register = each(...eachArgs);
5241
- return withSkipScope(register);
5242
- });
5465
+ function scopedRegister(pick) {
5466
+ return (name, fn, timeout) => {
5467
+ pick()(name, withScope(fn), timeout);
5468
+ };
5243
5469
  }
5244
- var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
5245
- only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
5246
- skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
5247
- each: eachWithSkipScope(describe2.each)
5248
- });
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
+ );
5249
5495
  var test = testWithFixtures;
5250
5496
  test.use = use;
5251
- test.describe = describeWithSkipScope;
5252
- test.beforeAll = beforeAll2;
5253
- test.beforeEach = beforeEach2;
5254
- test.afterEach = afterEach2;
5255
- 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));
5256
5502
  test.skip = skip;
5257
5503
  test.only = only;
5258
5504
  test.todo = todo;
5505
+ test.each = each;
5259
5506
 
5260
5507
  // libs/testing/src/expect.ts
5261
- import { expect as jestExpect } from "@jest/globals";
5262
- 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
+ });
5263
5527
 
5264
5528
  // libs/testing/src/platform/platform-types.ts
5265
5529
  function getPlatformMetaNamespace(platform) {
@@ -5922,34 +6186,44 @@ var HttpInterceptorImpl = class {
5922
6186
  }
5923
6187
  };
5924
6188
  }
5925
- get(url, response) {
6189
+ get(url, response, options) {
5926
6190
  return this.mock({
5927
- match: { url, method: "GET" },
5928
- 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 }
5929
6195
  });
5930
6196
  }
5931
- post(url, response) {
6197
+ post(url, response, options) {
5932
6198
  return this.mock({
5933
- match: { url, method: "POST" },
5934
- 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 }
5935
6203
  });
5936
6204
  }
5937
- put(url, response) {
6205
+ put(url, response, options) {
5938
6206
  return this.mock({
5939
- match: { url, method: "PUT" },
5940
- 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 }
5941
6211
  });
5942
6212
  }
5943
- delete(url, response) {
6213
+ delete(url, response, options) {
5944
6214
  return this.mock({
5945
- match: { url, method: "DELETE" },
5946
- 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 }
5947
6219
  });
5948
6220
  }
5949
- any(url, response) {
6221
+ any(url, response, options) {
5950
6222
  return this.mock({
5951
- match: { url },
5952
- 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 }
5953
6227
  });
5954
6228
  }
5955
6229
  clear() {
@@ -5981,6 +6255,9 @@ ${descriptions.join("\n")}`);
5981
6255
  if (index !== -1) {
5982
6256
  activeInterceptors.splice(index, 1);
5983
6257
  }
6258
+ if (activeInterceptors.length === 0) {
6259
+ httpMock.disable();
6260
+ }
5984
6261
  }
5985
6262
  // Internal methods
5986
6263
  isActive() {
@@ -6080,43 +6357,49 @@ ${descriptions.join("\n")}`);
6080
6357
  }
6081
6358
  };
6082
6359
  async function interceptedFetch(input, init) {
6083
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
6084
- 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();
6085
6363
  const headers = {};
6086
- if (init?.headers) {
6087
- if (init.headers instanceof Headers) {
6088
- init.headers.forEach((value, key) => {
6364
+ const collectHeaders = (source) => {
6365
+ if (!source) return;
6366
+ if (source instanceof Headers) {
6367
+ source.forEach((value, key) => {
6089
6368
  headers[key.toLowerCase()] = value;
6090
6369
  });
6091
- } else if (Array.isArray(init.headers)) {
6092
- for (const [key, value] of init.headers) {
6370
+ } else if (Array.isArray(source)) {
6371
+ for (const [key, value] of source) {
6093
6372
  headers[key.toLowerCase()] = value;
6094
6373
  }
6095
6374
  } else {
6096
- for (const [key, value] of Object.entries(init.headers)) {
6375
+ for (const [key, value] of Object.entries(source)) {
6097
6376
  headers[key.toLowerCase()] = value;
6098
6377
  }
6099
6378
  }
6100
- }
6379
+ };
6380
+ collectHeaders(init?.headers ?? request?.headers);
6101
6381
  let body;
6102
6382
  let rawBody;
6103
- if (init?.body) {
6104
- if (typeof init.body === "string") {
6105
- rawBody = init.body;
6106
- try {
6107
- body = JSON.parse(init.body);
6108
- } catch {
6109
- body = init.body;
6110
- }
6111
- } else if (init.body instanceof ArrayBuffer || init.body instanceof Uint8Array) {
6112
- 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) {
6113
6396
  try {
6114
6397
  body = JSON.parse(rawBody);
6115
6398
  } catch {
6116
6399
  body = rawBody;
6117
6400
  }
6118
6401
  } else {
6119
- body = init.body;
6402
+ body = bodySource;
6120
6403
  }
6121
6404
  }
6122
6405
  const requestInfo = {
@@ -6146,8 +6429,20 @@ async function interceptedFetch(input, init) {
6146
6429
  Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
6147
6430
  );
6148
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
+ }
6149
6442
  function normalizeResponse(response) {
6150
- 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)) {
6151
6446
  return { body: response };
6152
6447
  }
6153
6448
  return response;
@@ -7481,47 +7776,48 @@ function clearGlobalMeasurements() {
7481
7776
  }
7482
7777
 
7483
7778
  // libs/testing/src/perf/perf-test.ts
7484
- var currentConfig2 = {};
7485
- var serverInstance2 = null;
7486
- var tokenFactory2 = null;
7487
- var serverStartedByUs2 = false;
7488
- async function initializeSharedResources2() {
7489
- if (!tokenFactory2) {
7490
- tokenFactory2 = new TestTokenFactory();
7491
- }
7492
- if (!serverInstance2) {
7493
- if (currentConfig2.baseUrl) {
7494
- serverInstance2 = TestServer.connect(currentConfig2.baseUrl);
7495
- serverStartedByUs2 = false;
7496
- } else if (currentConfig2.server) {
7497
- 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);
7498
7793
  const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
7499
7794
  if (isDebug) {
7500
7795
  console.log(`[PerfTest] Starting server: ${serverCommand}`);
7501
7796
  }
7502
7797
  try {
7503
- serverInstance2 = await TestServer.start({
7504
- project: currentConfig2.project,
7505
- port: currentConfig2.port,
7798
+ serverInstance = await TestServer.start({
7799
+ project: currentConfig.project,
7800
+ port: currentConfig.port,
7506
7801
  command: serverCommand,
7507
- env: currentConfig2.env,
7508
- startupTimeout: currentConfig2.startupTimeout ?? 3e4,
7802
+ env: currentConfig.env,
7803
+ startupTimeout: currentConfig.startupTimeout ?? 3e4,
7509
7804
  debug: isDebug
7510
7805
  });
7511
- serverStartedByUs2 = true;
7806
+ serverStartedByUs = true;
7512
7807
  if (isDebug) {
7513
- console.log(`[PerfTest] Server started at ${serverInstance2.info.baseUrl}`);
7808
+ console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
7514
7809
  }
7515
7810
  } catch (error) {
7516
7811
  const errMsg = error instanceof Error ? error.message : String(error);
7517
7812
  throw new Error(
7518
7813
  `Failed to start test server.
7519
7814
 
7520
- Server entry: ${currentConfig2.server}
7521
- Project: ${currentConfig2.project ?? "default"}
7815
+ Server entry: ${currentConfig.server}
7816
+ Project: ${currentConfig.project ?? "default"}
7522
7817
  Command: ${serverCommand}
7523
7818
 
7524
- Error: ${errMsg}`
7819
+ Error: ${errMsg}`,
7820
+ { cause: error }
7525
7821
  );
7526
7822
  }
7527
7823
  } else {
@@ -7529,23 +7825,23 @@ Error: ${errMsg}`
7529
7825
  }
7530
7826
  }
7531
7827
  }
7532
- async function createTestFixtures2(testName) {
7533
- await initializeSharedResources2();
7534
- if (!serverInstance2) {
7828
+ async function createTestFixtures(testName) {
7829
+ await initializeSharedResources();
7830
+ if (!serverInstance) {
7535
7831
  throw new Error("Server instance not initialized");
7536
7832
  }
7537
- if (!tokenFactory2) {
7833
+ if (!tokenFactory) {
7538
7834
  throw new Error("Token factory not initialized");
7539
7835
  }
7540
7836
  const clientInstance = await McpTestClient.create({
7541
- baseUrl: serverInstance2.info.baseUrl,
7542
- transport: currentConfig2.transport ?? "streamable-http",
7543
- publicMode: currentConfig2.publicMode
7837
+ baseUrl: serverInstance.info.baseUrl,
7838
+ transport: currentConfig.transport ?? "streamable-http",
7839
+ publicMode: currentConfig.publicMode
7544
7840
  }).buildAndConnect();
7545
- const auth = createAuthFixture2(tokenFactory2);
7546
- const server = createServerFixture2(serverInstance2);
7547
- const perfImpl = createPerfFixtures(testName, currentConfig2.project ?? "unknown");
7548
- 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) {
7549
7845
  await perfImpl.baseline();
7550
7846
  }
7551
7847
  return {
@@ -7561,8 +7857,8 @@ async function createTestFixtures2(testName) {
7561
7857
  async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7562
7858
  const measurement = perfImpl.buildMeasurement();
7563
7859
  addGlobalMeasurement(measurement);
7564
- if (testFailed && serverInstance2) {
7565
- const logs = serverInstance2.getLogs();
7860
+ if (testFailed && serverInstance) {
7861
+ const logs = serverInstance.getLogs();
7566
7862
  if (logs.length > 0) {
7567
7863
  console.error("\n[PerfTest] === Server Logs (test failed) ===");
7568
7864
  const recentLogs = logs.slice(-50);
@@ -7577,13 +7873,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7577
7873
  await fixtures.mcp.disconnect();
7578
7874
  }
7579
7875
  }
7580
- async function cleanupSharedResources2() {
7581
- if (serverInstance2 && serverStartedByUs2) {
7582
- await serverInstance2.stop();
7876
+ async function cleanupSharedResources() {
7877
+ if (serverInstance && serverStartedByUs) {
7878
+ await serverInstance.stop();
7583
7879
  }
7584
- serverInstance2 = null;
7585
- tokenFactory2 = null;
7586
- serverStartedByUs2 = false;
7880
+ serverInstance = null;
7881
+ tokenFactory = null;
7882
+ serverStartedByUs = false;
7587
7883
  }
7588
7884
  function createAuthFixture2(factory) {
7589
7885
  const users = {
@@ -7638,13 +7934,13 @@ function createServerFixture2(server) {
7638
7934
  transport: opts?.transport ?? "streamable-http",
7639
7935
  auth: opts?.token ? { token: opts.token } : void 0,
7640
7936
  clientInfo: opts?.clientInfo,
7641
- publicMode: currentConfig2.publicMode
7937
+ publicMode: currentConfig.publicMode
7642
7938
  }).buildAndConnect();
7643
7939
  },
7644
7940
  createClientBuilder: () => {
7645
7941
  return new McpTestClientBuilder({
7646
7942
  baseUrl: server.info.baseUrl,
7647
- publicMode: currentConfig2.publicMode
7943
+ publicMode: currentConfig.publicMode
7648
7944
  });
7649
7945
  },
7650
7946
  restart: () => server.restart(),
@@ -7660,7 +7956,7 @@ function resolveServerCommand2(server) {
7660
7956
  }
7661
7957
  function perfTestWithFixtures(name, fn) {
7662
7958
  it(name, async () => {
7663
- const { fixtures, perfImpl } = await createTestFixtures2(name);
7959
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7664
7960
  let testFailed = false;
7665
7961
  try {
7666
7962
  await fn(fixtures);
@@ -7673,14 +7969,14 @@ function perfTestWithFixtures(name, fn) {
7673
7969
  });
7674
7970
  }
7675
7971
  function use2(config) {
7676
- currentConfig2 = { ...currentConfig2, ...config };
7972
+ currentConfig = { ...currentConfig, ...config };
7677
7973
  afterAll(async () => {
7678
- await cleanupSharedResources2();
7974
+ await cleanupSharedResources();
7679
7975
  });
7680
7976
  }
7681
7977
  function skip2(name, fn) {
7682
7978
  it.skip(name, async () => {
7683
- const { fixtures, perfImpl } = await createTestFixtures2(name);
7979
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7684
7980
  let testFailed = false;
7685
7981
  try {
7686
7982
  await fn(fixtures);
@@ -7694,7 +7990,7 @@ function skip2(name, fn) {
7694
7990
  }
7695
7991
  function only2(name, fn) {
7696
7992
  it.only(name, async () => {
7697
- const { fixtures, perfImpl } = await createTestFixtures2(name);
7993
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7698
7994
  let testFailed = false;
7699
7995
  try {
7700
7996
  await fn(fixtures);
@@ -7711,16 +8007,19 @@ function todo2(name) {
7711
8007
  }
7712
8008
  var perfTest = perfTestWithFixtures;
7713
8009
  perfTest.use = use2;
7714
- perfTest.describe = describe;
7715
- perfTest.beforeAll = beforeAll;
7716
- perfTest.beforeEach = beforeEach;
7717
- perfTest.afterEach = afterEach;
7718
- 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
+ }
7719
8017
  perfTest.skip = skip2;
7720
8018
  perfTest.only = only2;
7721
8019
  perfTest.todo = todo2;
7722
8020
 
7723
8021
  // libs/testing/src/perf/baseline-store.ts
8022
+ import { ensureDir, fileExists, readFile, writeFile } from "@frontmcp/utils";
7724
8023
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
7725
8024
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
7726
8025
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -7735,7 +8034,6 @@ var BaselineStore = class {
7735
8034
  */
7736
8035
  async load() {
7737
8036
  try {
7738
- const { readFile, fileExists } = await import("@frontmcp/utils");
7739
8037
  if (!await fileExists(this.baselinePath)) {
7740
8038
  return null;
7741
8039
  }
@@ -7750,7 +8048,6 @@ var BaselineStore = class {
7750
8048
  * Save baseline to local file.
7751
8049
  */
7752
8050
  async save(baseline) {
7753
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
7754
8051
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
7755
8052
  await ensureDir(dir);
7756
8053
  await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
@@ -8006,6 +8303,7 @@ function getRegressionDetector(config) {
8006
8303
  }
8007
8304
 
8008
8305
  // libs/testing/src/perf/report-generator.ts
8306
+ import { ensureDir as ensureDir2, writeFile as writeFile2 } from "@frontmcp/utils";
8009
8307
  var ReportGenerator = class {
8010
8308
  detector;
8011
8309
  constructor() {
@@ -8261,14 +8559,13 @@ var ReportGenerator = class {
8261
8559
  }
8262
8560
  };
8263
8561
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
8264
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
8265
- await ensureDir(outputDir);
8562
+ await ensureDir2(outputDir);
8266
8563
  const generator = new ReportGenerator();
8267
8564
  const report = generator.generateReport(measurements, baseline, gitInfo);
8268
8565
  const jsonPath = `${outputDir}/report.json`;
8269
8566
  const markdownPath = `${outputDir}/report.md`;
8270
- await writeFile(jsonPath, generator.generateJsonReport(report));
8271
- await writeFile(markdownPath, generator.generateMarkdownReport(report));
8567
+ await writeFile2(jsonPath, generator.generateJsonReport(report));
8568
+ await writeFile2(markdownPath, generator.generateMarkdownReport(report));
8272
8569
  return { jsonPath, markdownPath };
8273
8570
  }
8274
8571
  function createReportGenerator() {