@frontmcp/testing 1.8.5 → 1.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/auth/mock-oauth-server.d.ts.map +1 -1
  2. package/auth/token-factory.d.ts +2 -2
  3. package/auth/token-factory.d.ts.map +1 -1
  4. package/client/mcp-test-client.d.ts +12 -1
  5. package/client/mcp-test-client.d.ts.map +1 -1
  6. package/esm/fixtures/index.mjs +545 -260
  7. package/esm/index.mjs +702 -383
  8. package/esm/package.json +6 -6
  9. package/esm/perf/index.mjs +465 -308
  10. package/expect.d.ts +12 -11
  11. package/expect.d.ts.map +1 -1
  12. package/fixtures/fixture-types.d.ts +22 -4
  13. package/fixtures/fixture-types.d.ts.map +1 -1
  14. package/fixtures/index.js +544 -252
  15. package/fixtures/test-fixture.d.ts +6 -5
  16. package/fixtures/test-fixture.d.ts.map +1 -1
  17. package/http-mock/http-mock.d.ts +5 -3
  18. package/http-mock/http-mock.d.ts.map +1 -1
  19. package/http-mock/http-mock.types.d.ts +17 -6
  20. package/http-mock/http-mock.types.d.ts.map +1 -1
  21. package/http-mock/index.d.ts +1 -1
  22. package/http-mock/index.d.ts.map +1 -1
  23. package/index.d.ts +1 -1
  24. package/index.d.ts.map +1 -1
  25. package/index.js +710 -384
  26. package/package.json +6 -6
  27. package/perf/baseline-store.d.ts +1 -1
  28. package/perf/baseline-store.d.ts.map +1 -1
  29. package/perf/index.js +468 -321
  30. package/perf/perf-test.d.ts.map +1 -1
  31. package/perf/report-generator.d.ts +1 -1
  32. package/perf/report-generator.d.ts.map +1 -1
  33. package/server/port-registry.d.ts +7 -1
  34. package/server/port-registry.d.ts.map +1 -1
  35. package/server/test-server.d.ts +10 -1
  36. package/server/test-server.d.ts.map +1 -1
  37. package/transport/error-utils.d.ts +15 -0
  38. package/transport/error-utils.d.ts.map +1 -0
  39. package/transport/streamable-http.transport.d.ts.map +1 -1
package/perf/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // libs/testing/src/perf/index.ts
@@ -834,6 +824,192 @@ function getMeasurementsForProject(project) {
834
824
  return getGlobalMeasurementsArray().filter((m) => m.project === project);
835
825
  }
836
826
 
827
+ // libs/testing/src/auth/token-factory.ts
828
+ var import_jose = require("jose");
829
+ var TestTokenFactory = class {
830
+ issuer;
831
+ audience;
832
+ privateKey = null;
833
+ publicKey = null;
834
+ jwk = null;
835
+ keyId;
836
+ /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
837
+ hmacSecret;
838
+ constructor(options = {}) {
839
+ this.issuer = options.issuer ?? "https://test.frontmcp.local";
840
+ this.audience = options.audience ?? "frontmcp-test";
841
+ this.keyId = `test-key-${Date.now()}`;
842
+ this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
843
+ }
844
+ /** True when this factory signs HS256 with a shared secret (gateway mode). */
845
+ get isHmac() {
846
+ return this.hmacSecret !== null;
847
+ }
848
+ /** The JWS `alg` this factory signs with. */
849
+ get alg() {
850
+ return this.isHmac ? "HS256" : "RS256";
851
+ }
852
+ /**
853
+ * The signing key: the shared HMAC secret in gateway mode, otherwise the
854
+ * generated RSA private key (lazily created via ensureKeys()).
855
+ */
856
+ signingKey() {
857
+ if (this.hmacSecret) return this.hmacSecret;
858
+ if (!this.privateKey) {
859
+ throw new Error("Private key not initialized");
860
+ }
861
+ return this.privateKey;
862
+ }
863
+ /**
864
+ * Initialize the key pair (called automatically on first use). No-op in HS256
865
+ * mode — there is no asymmetric keypair, just the shared secret.
866
+ */
867
+ async ensureKeys() {
868
+ if (this.isHmac) return;
869
+ if (this.privateKey && this.publicKey) return;
870
+ const { publicKey, privateKey } = await (0, import_jose.generateKeyPair)("RS256", {
871
+ extractable: true
872
+ });
873
+ this.privateKey = privateKey;
874
+ this.publicKey = publicKey;
875
+ this.jwk = await (0, import_jose.exportJWK)(publicKey);
876
+ this.jwk.kid = this.keyId;
877
+ this.jwk.use = "sig";
878
+ this.jwk.alg = "RS256";
879
+ }
880
+ /**
881
+ * Create a JWT token with the specified claims
882
+ */
883
+ async createTestToken(options) {
884
+ await this.ensureKeys();
885
+ const nowMs = Date.now();
886
+ const iat = Math.floor(nowMs / 1e3);
887
+ const exp = options.exp ?? 3600;
888
+ const payload = {
889
+ iss: options.iss ?? this.issuer,
890
+ sub: options.sub,
891
+ aud: options.aud ?? this.audience,
892
+ iat,
893
+ exp: Math.ceil(nowMs / 1e3) + exp,
894
+ scope: options.scopes?.join(" "),
895
+ ...options.claims
896
+ };
897
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
898
+ const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
899
+ return token;
900
+ }
901
+ /**
902
+ * Create an admin token with full access
903
+ */
904
+ async createAdminToken(sub = "admin-001") {
905
+ return this.createTestToken({
906
+ sub,
907
+ scopes: ["admin:*", "read", "write", "delete"],
908
+ claims: {
909
+ email: "admin@test.local",
910
+ name: "Test Admin",
911
+ role: "admin"
912
+ }
913
+ });
914
+ }
915
+ /**
916
+ * Create a regular user token
917
+ */
918
+ async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
919
+ return this.createTestToken({
920
+ sub,
921
+ scopes,
922
+ claims: {
923
+ email: "user@test.local",
924
+ name: "Test User",
925
+ role: "user"
926
+ }
927
+ });
928
+ }
929
+ /**
930
+ * Create an anonymous user token
931
+ */
932
+ async createAnonymousToken(expiresIn) {
933
+ return this.createTestToken({
934
+ sub: `anon:${Date.now()}`,
935
+ scopes: ["anonymous"],
936
+ exp: expiresIn,
937
+ claims: {
938
+ name: "Anonymous",
939
+ role: "anonymous"
940
+ }
941
+ });
942
+ }
943
+ /**
944
+ * Create an expired token (for testing token expiration)
945
+ */
946
+ async createExpiredToken(options) {
947
+ await this.ensureKeys();
948
+ const now = Math.floor(Date.now() / 1e3);
949
+ const payload = {
950
+ iss: this.issuer,
951
+ sub: options.sub,
952
+ aud: this.audience,
953
+ iat: now - 7200,
954
+ // 2 hours ago
955
+ exp: now - 3600
956
+ // Expired 1 hour ago
957
+ };
958
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
959
+ const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
960
+ return token;
961
+ }
962
+ /**
963
+ * Create a token with an invalid signature (for testing signature validation)
964
+ */
965
+ createTokenWithInvalidSignature(options) {
966
+ const now = Math.floor(Date.now() / 1e3);
967
+ const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
968
+ const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
969
+ const payload = Buffer.from(
970
+ JSON.stringify({
971
+ iss: this.issuer,
972
+ sub: options.sub,
973
+ aud: this.audience,
974
+ iat: now,
975
+ exp: now + 3600
976
+ })
977
+ ).toString("base64url");
978
+ const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
979
+ return `${header}.${payload}.${signature}`;
980
+ }
981
+ /**
982
+ * Get the public JWKS for verifying tokens.
983
+ *
984
+ * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
985
+ * the shared secret, not a published public key, so there is no JWKS to serve.
986
+ */
987
+ async getPublicJwks() {
988
+ if (this.isHmac) {
989
+ throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
990
+ }
991
+ await this.ensureKeys();
992
+ if (!this.jwk) {
993
+ throw new Error("JWK not initialized");
994
+ }
995
+ return {
996
+ keys: [this.jwk]
997
+ };
998
+ }
999
+ /**
1000
+ * Get the issuer URL
1001
+ */
1002
+ getIssuer() {
1003
+ return this.issuer;
1004
+ }
1005
+ /**
1006
+ * Get the audience
1007
+ */
1008
+ getAudience() {
1009
+ return this.audience;
1010
+ }
1011
+ };
1012
+
837
1013
  // libs/testing/src/interceptor/mock-registry.ts
838
1014
  var DefaultMockRegistry = class {
839
1015
  mocks = [];
@@ -1106,6 +1282,32 @@ function sleep3(ms) {
1106
1282
  return new Promise((resolve) => setTimeout(resolve, ms));
1107
1283
  }
1108
1284
 
1285
+ // libs/testing/src/transport/error-utils.ts
1286
+ function errorName(err) {
1287
+ if (typeof err !== "object" || err === null) return void 0;
1288
+ const name = err.name;
1289
+ return typeof name === "string" ? name : void 0;
1290
+ }
1291
+ function isAbortError(err) {
1292
+ return errorName(err) === "AbortError";
1293
+ }
1294
+ function errorMessage(err, fallback = "Unknown error") {
1295
+ if (typeof err === "string" && err) return err;
1296
+ if (typeof err === "object" && err !== null) {
1297
+ const message = err.message;
1298
+ if (typeof message === "string" && message) return message;
1299
+ }
1300
+ return fallback;
1301
+ }
1302
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
1303
+ function markInterceptedError(err) {
1304
+ interceptedErrors.add(err);
1305
+ return err;
1306
+ }
1307
+ function isInterceptedError(err) {
1308
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
1309
+ }
1310
+
1109
1311
  // libs/testing/src/transport/streamable-http.transport.ts
1110
1312
  var DEFAULT_TIMEOUT = 3e4;
1111
1313
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
@@ -1214,7 +1416,7 @@ var StreamableHttpTransport = class {
1214
1416
  return mockResponse2;
1215
1417
  }
1216
1418
  case "error":
1217
- throw interceptResult.error;
1419
+ throw markInterceptedError(interceptResult.error);
1218
1420
  case "continue":
1219
1421
  message = interceptResult.request;
1220
1422
  break;
@@ -1275,7 +1477,7 @@ var StreamableHttpTransport = class {
1275
1477
  return jsonResponse;
1276
1478
  } catch (error) {
1277
1479
  clearTimeout(timeoutId);
1278
- if (error instanceof Error && error.name === "AbortError") {
1480
+ if (isAbortError(error)) {
1279
1481
  return {
1280
1482
  jsonrpc: "2.0",
1281
1483
  id: message.id ?? null,
@@ -1314,7 +1516,7 @@ var StreamableHttpTransport = class {
1314
1516
  }
1315
1517
  } catch (error) {
1316
1518
  clearTimeout(timeoutId);
1317
- if (error instanceof Error && error.name !== "AbortError") {
1519
+ if (!isAbortError(error)) {
1318
1520
  throw error;
1319
1521
  }
1320
1522
  }
@@ -1355,7 +1557,7 @@ var StreamableHttpTransport = class {
1355
1557
  error: {
1356
1558
  code: -32700,
1357
1559
  message: "Parse error",
1358
- data: error instanceof Error ? error.message : "Unknown error"
1560
+ data: errorMessage(error)
1359
1561
  }
1360
1562
  };
1361
1563
  }
@@ -1725,7 +1927,7 @@ var StreamableHttpTransport = class {
1725
1927
  if (this.config.clientInfo) {
1726
1928
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1727
1929
  }
1728
- if (this.authToken && !this.publicMode) {
1930
+ if (this.authToken) {
1729
1931
  headers["Authorization"] = `Bearer ${this.authToken}`;
1730
1932
  }
1731
1933
  if (this.sessionId) {
@@ -2048,6 +2250,16 @@ var DEFAULT_CLIENT_INFO = {
2048
2250
  name: "@frontmcp/testing",
2049
2251
  version: "0.4.0"
2050
2252
  };
2253
+ var MAX_LIST_PAGES = 1e3;
2254
+ function isAuthFailure(error) {
2255
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
2256
+ }
2257
+ function describeInitializeError(error) {
2258
+ const message = error?.message ?? "Unknown error";
2259
+ const data = error?.data;
2260
+ const detail = typeof data === "string" ? data.trim() : "";
2261
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
2262
+ }
2051
2263
  function extractEntryPaths(data) {
2052
2264
  let payload = data;
2053
2265
  if (typeof payload === "string") {
@@ -2134,19 +2346,22 @@ var McpTestClient = class {
2134
2346
  async connect() {
2135
2347
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
2136
2348
  this.triedDiscoveredEntryPath = false;
2349
+ this.initResult = null;
2137
2350
  this.transport = this.createTransport();
2138
2351
  await this.transport.connect();
2139
2352
  const maxRetries = 3;
2140
2353
  const retryDelayMs = 500;
2141
2354
  let lastError;
2142
2355
  let reportedEntryPaths;
2356
+ let attemptsMade = 0;
2143
2357
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
2358
+ attemptsMade = attempt;
2144
2359
  const initResponse = await this.initialize();
2145
2360
  if (initResponse.success && initResponse.data) {
2146
2361
  this.initResult = initResponse.data;
2147
2362
  break;
2148
2363
  }
2149
- lastError = initResponse.error?.message ?? "Unknown error";
2364
+ lastError = describeInitializeError(initResponse.error);
2150
2365
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
2151
2366
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
2152
2367
  this.triedDiscoveredEntryPath = true;
@@ -2158,6 +2373,7 @@ var McpTestClient = class {
2158
2373
  await this.transport.connect();
2159
2374
  continue;
2160
2375
  }
2376
+ if (isAuthFailure(initResponse.error)) break;
2161
2377
  if (attempt < maxRetries) {
2162
2378
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
2163
2379
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -2166,7 +2382,7 @@ var McpTestClient = class {
2166
2382
  if (!this.initResult) {
2167
2383
  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.` : "";
2168
2384
  throw new Error(
2169
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
2385
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
2170
2386
  );
2171
2387
  }
2172
2388
  this._sessionId = this.transport.getSessionId();
@@ -2236,18 +2452,47 @@ var McpTestClient = class {
2236
2452
  return this._authState;
2237
2453
  }
2238
2454
  /**
2239
- * Authenticate with a token
2455
+ * Authenticate with a token.
2456
+ *
2457
+ * When connected, this opens a NEW session with the token (the previous session was created
2458
+ * for the previous identity) and rejects if the server refuses it — for example because the
2459
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
2460
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
2240
2461
  */
2241
2462
  async authenticate(token) {
2463
+ if (typeof token !== "string" || token.trim() === "") {
2464
+ throw new Error("authenticate() requires a non-empty token");
2465
+ }
2466
+ const previousAuthState = this._authState;
2467
+ const previousConfigAuth = this.config.auth;
2468
+ this.config.auth = { ...previousConfigAuth, token };
2242
2469
  this._authState = {
2243
2470
  isAnonymous: false,
2244
2471
  token,
2245
2472
  scopes: this.parseScopesFromToken(token),
2246
2473
  user: this.parseUserFromToken(token)
2247
2474
  };
2248
- if (this.transport) {
2249
- this.transport.setAuthToken(token);
2475
+ const previousTransport = this.transport;
2476
+ if (!previousTransport) {
2477
+ this.log("debug", "Authentication updated (not connected yet)");
2478
+ return;
2250
2479
  }
2480
+ const previousInit = this.initResult;
2481
+ const previousSessionId = this._sessionId;
2482
+ const previousSessionInfo = this._sessionInfo;
2483
+ try {
2484
+ await this.connect();
2485
+ } catch (err) {
2486
+ await this.transport?.close().catch(() => void 0);
2487
+ this.transport = previousTransport;
2488
+ this.initResult = previousInit;
2489
+ this._sessionId = previousSessionId;
2490
+ this._sessionInfo = previousSessionInfo;
2491
+ this.config.auth = previousConfigAuth;
2492
+ this._authState = previousAuthState;
2493
+ throw err;
2494
+ }
2495
+ await previousTransport.close().catch(() => void 0);
2251
2496
  this.log("debug", "Authentication updated");
2252
2497
  }
2253
2498
  // ═══════════════════════════════════════════════════════════════════
@@ -2281,13 +2526,12 @@ var McpTestClient = class {
2281
2526
  /**
2282
2527
  * List all available tools
2283
2528
  */
2284
- list: async () => {
2285
- const response = await this.listTools();
2286
- if (!response.success || !response.data) {
2287
- throw new Error(`Failed to list tools: ${response.error?.message}`);
2288
- }
2289
- return response.data.tools;
2290
- },
2529
+ list: async () => this.listAllPages(
2530
+ "tools/list",
2531
+ "tools",
2532
+ (cursor) => this.listTools(cursor),
2533
+ (page) => page.tools
2534
+ ),
2291
2535
  /**
2292
2536
  * Call a tool by name with arguments
2293
2537
  */
@@ -2303,23 +2547,21 @@ var McpTestClient = class {
2303
2547
  /**
2304
2548
  * List all static resources
2305
2549
  */
2306
- list: async () => {
2307
- const response = await this.listResources();
2308
- if (!response.success || !response.data) {
2309
- throw new Error(`Failed to list resources: ${response.error?.message}`);
2310
- }
2311
- return response.data.resources;
2312
- },
2550
+ list: async () => this.listAllPages(
2551
+ "resources/list",
2552
+ "resources",
2553
+ (cursor) => this.listResources(cursor),
2554
+ (page) => page.resources
2555
+ ),
2313
2556
  /**
2314
2557
  * List all resource templates
2315
2558
  */
2316
- listTemplates: async () => {
2317
- const response = await this.listResourceTemplates();
2318
- if (!response.success || !response.data) {
2319
- throw new Error(`Failed to list resource templates: ${response.error?.message}`);
2320
- }
2321
- return response.data.resourceTemplates;
2322
- },
2559
+ listTemplates: async () => this.listAllPages(
2560
+ "resources/templates/list",
2561
+ "resource templates",
2562
+ (cursor) => this.listResourceTemplates(cursor),
2563
+ (page) => page.resourceTemplates
2564
+ ),
2323
2565
  /**
2324
2566
  * Read a resource by URI
2325
2567
  */
@@ -2347,13 +2589,12 @@ var McpTestClient = class {
2347
2589
  /**
2348
2590
  * List all available prompts
2349
2591
  */
2350
- list: async () => {
2351
- const response = await this.listPrompts();
2352
- if (!response.success || !response.data) {
2353
- throw new Error(`Failed to list prompts: ${response.error?.message}`);
2354
- }
2355
- return response.data.prompts;
2356
- },
2592
+ list: async () => this.listAllPages(
2593
+ "prompts/list",
2594
+ "prompts",
2595
+ (cursor) => this.listPrompts(cursor),
2596
+ (page) => page.prompts
2597
+ ),
2357
2598
  /**
2358
2599
  * Get a prompt with arguments
2359
2600
  */
@@ -2737,8 +2978,33 @@ var McpTestClient = class {
2737
2978
  clientInfo: this.config.clientInfo
2738
2979
  });
2739
2980
  }
2740
- async listTools() {
2741
- return this.request("tools/list", {});
2981
+ async listTools(cursor) {
2982
+ return this.request("tools/list", cursor ? { cursor } : {});
2983
+ }
2984
+ /**
2985
+ * Follow `nextCursor` until the server stops returning one, concatenating the
2986
+ * pages. A server that hands back a cursor it already returned, or never stops
2987
+ * paging, fails the call rather than looping.
2988
+ */
2989
+ async listAllPages(method, label, fetchPage, itemsOf) {
2990
+ const items = [];
2991
+ const seenCursors = /* @__PURE__ */ new Set();
2992
+ let cursor;
2993
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
2994
+ const response = await fetchPage(cursor);
2995
+ if (!response.success || !response.data) {
2996
+ throw new Error(`Failed to list ${label}: ${response.error?.message}`);
2997
+ }
2998
+ items.push(...itemsOf(response.data) ?? []);
2999
+ const next = response.data.nextCursor;
3000
+ if (!next) return items;
3001
+ if (seenCursors.has(next)) {
3002
+ throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`);
3003
+ }
3004
+ seenCursors.add(next);
3005
+ cursor = next;
3006
+ }
3007
+ throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`);
2742
3008
  }
2743
3009
  async callTool(name, args, progressToken) {
2744
3010
  return this.request("tools/call", {
@@ -2763,17 +3029,17 @@ var McpTestClient = class {
2763
3029
  timestamp
2764
3030
  });
2765
3031
  };
2766
- async listResources() {
2767
- return this.request("resources/list", {});
3032
+ async listResources(cursor) {
3033
+ return this.request("resources/list", cursor ? { cursor } : {});
2768
3034
  }
2769
- async listResourceTemplates() {
2770
- return this.request("resources/templates/list", {});
3035
+ async listResourceTemplates(cursor) {
3036
+ return this.request("resources/templates/list", cursor ? { cursor } : {});
2771
3037
  }
2772
3038
  async readResource(uri) {
2773
3039
  return this.request("resources/read", { uri });
2774
3040
  }
2775
- async listPrompts() {
2776
- return this.request("prompts/list", {});
3041
+ async listPrompts(cursor) {
3042
+ return this.request("prompts/list", cursor ? { cursor } : {});
2777
3043
  }
2778
3044
  async getPrompt(name, args) {
2779
3045
  return this.request("prompts/get", {
@@ -2850,10 +3116,11 @@ var McpTestClient = class {
2850
3116
  requestId: id
2851
3117
  };
2852
3118
  } catch (err) {
3119
+ if (isInterceptedError(err)) throw err;
2853
3120
  const durationMs = Date.now() - start;
2854
3121
  const error = {
2855
3122
  code: -32603,
2856
- message: err instanceof Error ? err.message : "Unknown error"
3123
+ message: errorMessage(err)
2857
3124
  };
2858
3125
  return {
2859
3126
  success: false,
@@ -3080,190 +3347,6 @@ var ProgressCollector = class {
3080
3347
  }
3081
3348
  };
3082
3349
 
3083
- // libs/testing/src/auth/token-factory.ts
3084
- var import_jose = require("jose");
3085
- var TestTokenFactory = class {
3086
- issuer;
3087
- audience;
3088
- privateKey = null;
3089
- publicKey = null;
3090
- jwk = null;
3091
- keyId;
3092
- /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
3093
- hmacSecret;
3094
- constructor(options = {}) {
3095
- this.issuer = options.issuer ?? "https://test.frontmcp.local";
3096
- this.audience = options.audience ?? "frontmcp-test";
3097
- this.keyId = `test-key-${Date.now()}`;
3098
- this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
3099
- }
3100
- /** True when this factory signs HS256 with a shared secret (gateway mode). */
3101
- get isHmac() {
3102
- return this.hmacSecret !== null;
3103
- }
3104
- /** The JWS `alg` this factory signs with. */
3105
- get alg() {
3106
- return this.isHmac ? "HS256" : "RS256";
3107
- }
3108
- /**
3109
- * The signing key: the shared HMAC secret in gateway mode, otherwise the
3110
- * generated RSA private key (lazily created via ensureKeys()).
3111
- */
3112
- signingKey() {
3113
- if (this.hmacSecret) return this.hmacSecret;
3114
- if (!this.privateKey) {
3115
- throw new Error("Private key not initialized");
3116
- }
3117
- return this.privateKey;
3118
- }
3119
- /**
3120
- * Initialize the key pair (called automatically on first use). No-op in HS256
3121
- * mode — there is no asymmetric keypair, just the shared secret.
3122
- */
3123
- async ensureKeys() {
3124
- if (this.isHmac) return;
3125
- if (this.privateKey && this.publicKey) return;
3126
- const { publicKey, privateKey } = await (0, import_jose.generateKeyPair)("RS256", {
3127
- extractable: true
3128
- });
3129
- this.privateKey = privateKey;
3130
- this.publicKey = publicKey;
3131
- this.jwk = await (0, import_jose.exportJWK)(publicKey);
3132
- this.jwk.kid = this.keyId;
3133
- this.jwk.use = "sig";
3134
- this.jwk.alg = "RS256";
3135
- }
3136
- /**
3137
- * Create a JWT token with the specified claims
3138
- */
3139
- async createTestToken(options) {
3140
- await this.ensureKeys();
3141
- const now = Math.floor(Date.now() / 1e3);
3142
- const exp = options.exp ?? 3600;
3143
- const payload = {
3144
- iss: options.iss ?? this.issuer,
3145
- sub: options.sub,
3146
- aud: options.aud ?? this.audience,
3147
- iat: now,
3148
- exp: now + exp,
3149
- scope: options.scopes?.join(" "),
3150
- ...options.claims
3151
- };
3152
- const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3153
- const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
3154
- return token;
3155
- }
3156
- /**
3157
- * Create an admin token with full access
3158
- */
3159
- async createAdminToken(sub = "admin-001") {
3160
- return this.createTestToken({
3161
- sub,
3162
- scopes: ["admin:*", "read", "write", "delete"],
3163
- claims: {
3164
- email: "admin@test.local",
3165
- name: "Test Admin",
3166
- role: "admin"
3167
- }
3168
- });
3169
- }
3170
- /**
3171
- * Create a regular user token
3172
- */
3173
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
3174
- return this.createTestToken({
3175
- sub,
3176
- scopes,
3177
- claims: {
3178
- email: "user@test.local",
3179
- name: "Test User",
3180
- role: "user"
3181
- }
3182
- });
3183
- }
3184
- /**
3185
- * Create an anonymous user token
3186
- */
3187
- async createAnonymousToken() {
3188
- return this.createTestToken({
3189
- sub: `anon:${Date.now()}`,
3190
- scopes: ["anonymous"],
3191
- claims: {
3192
- name: "Anonymous",
3193
- role: "anonymous"
3194
- }
3195
- });
3196
- }
3197
- /**
3198
- * Create an expired token (for testing token expiration)
3199
- */
3200
- async createExpiredToken(options) {
3201
- await this.ensureKeys();
3202
- const now = Math.floor(Date.now() / 1e3);
3203
- const payload = {
3204
- iss: this.issuer,
3205
- sub: options.sub,
3206
- aud: this.audience,
3207
- iat: now - 7200,
3208
- // 2 hours ago
3209
- exp: now - 3600
3210
- // Expired 1 hour ago
3211
- };
3212
- const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3213
- const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
3214
- return token;
3215
- }
3216
- /**
3217
- * Create a token with an invalid signature (for testing signature validation)
3218
- */
3219
- createTokenWithInvalidSignature(options) {
3220
- const now = Math.floor(Date.now() / 1e3);
3221
- const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3222
- const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
3223
- const payload = Buffer.from(
3224
- JSON.stringify({
3225
- iss: this.issuer,
3226
- sub: options.sub,
3227
- aud: this.audience,
3228
- iat: now,
3229
- exp: now + 3600
3230
- })
3231
- ).toString("base64url");
3232
- const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
3233
- return `${header}.${payload}.${signature}`;
3234
- }
3235
- /**
3236
- * Get the public JWKS for verifying tokens.
3237
- *
3238
- * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
3239
- * the shared secret, not a published public key, so there is no JWKS to serve.
3240
- */
3241
- async getPublicJwks() {
3242
- if (this.isHmac) {
3243
- throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
3244
- }
3245
- await this.ensureKeys();
3246
- if (!this.jwk) {
3247
- throw new Error("JWK not initialized");
3248
- }
3249
- return {
3250
- keys: [this.jwk]
3251
- };
3252
- }
3253
- /**
3254
- * Get the issuer URL
3255
- */
3256
- getIssuer() {
3257
- return this.issuer;
3258
- }
3259
- /**
3260
- * Get the audience
3261
- */
3262
- getAudience() {
3263
- return this.audience;
3264
- }
3265
- };
3266
-
3267
3350
  // libs/testing/src/server/test-server.ts
3268
3351
  var import_child_process = require("child_process");
3269
3352
  var import_utils = require("@frontmcp/utils");
@@ -3336,6 +3419,8 @@ var E2E_PORT_RANGES = {
3336
3419
  "demo-e2e-distributed": { start: 50440, size: 20 },
3337
3420
  // Protocol revision E2E tests (50460-50479)
3338
3421
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
3422
+ // Testing library E2E tests (50480-50499)
3423
+ "demo-e2e-testing": { start: 50480, size: 20 },
3339
3424
  // Mock servers and utilities (50900-50999)
3340
3425
  "mock-oauth": { start: 50900, size: 10 },
3341
3426
  "mock-api": { start: 50910, size: 10 },
@@ -3344,6 +3429,66 @@ var E2E_PORT_RANGES = {
3344
3429
  default: { start: 51e3, size: 100 }
3345
3430
  };
3346
3431
  var reservedPorts = /* @__PURE__ */ new Map();
3432
+ var lockedPorts = /* @__PURE__ */ new Set();
3433
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
3434
+ function lockDir() {
3435
+ try {
3436
+ const os = require("node:os");
3437
+ const path = require("node:path");
3438
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
3439
+ } catch {
3440
+ return void 0;
3441
+ }
3442
+ }
3443
+ function pidIsAlive(pid) {
3444
+ try {
3445
+ process.kill(pid, 0);
3446
+ return true;
3447
+ } catch (error) {
3448
+ return error.code === "EPERM";
3449
+ }
3450
+ }
3451
+ function acquirePortLock(port) {
3452
+ if (lockedPorts.has(port)) return false;
3453
+ let fs;
3454
+ let file;
3455
+ try {
3456
+ fs = require("node:fs");
3457
+ const dir = lockDir();
3458
+ if (!dir) return true;
3459
+ fs.mkdirSync(dir, { recursive: true });
3460
+ file = require("node:path").join(dir, `${port}.lock`);
3461
+ } catch {
3462
+ return true;
3463
+ }
3464
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
3465
+ for (let attempt = 0; attempt < 2; attempt++) {
3466
+ try {
3467
+ fs.writeFileSync(file, payload, { flag: "wx" });
3468
+ lockedPorts.add(port);
3469
+ return true;
3470
+ } catch (error) {
3471
+ if (error.code !== "EEXIST") return true;
3472
+ try {
3473
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
3474
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
3475
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
3476
+ fs.unlinkSync(file);
3477
+ } catch {
3478
+ }
3479
+ }
3480
+ }
3481
+ return false;
3482
+ }
3483
+ function releasePortLock(port) {
3484
+ if (!lockedPorts.delete(port)) return;
3485
+ try {
3486
+ const fs = require("node:fs");
3487
+ const dir = lockDir();
3488
+ if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
3489
+ } catch {
3490
+ }
3491
+ }
3347
3492
  var projectPortIndex = /* @__PURE__ */ new Map();
3348
3493
  function getPortRange(project) {
3349
3494
  const key = project;
@@ -3354,15 +3499,22 @@ function getPortRange(project) {
3354
3499
  }
3355
3500
  async function reservePort(project, preferredPort) {
3356
3501
  const range = getPortRange(project);
3357
- if (preferredPort !== void 0) {
3502
+ const handle = (port) => ({
3503
+ port,
3504
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
3505
+ releaseSocket: async () => {
3506
+ await releasePort(port);
3507
+ },
3508
+ release: async () => {
3509
+ await releasePort(port);
3510
+ releasePortLock(port);
3511
+ }
3512
+ });
3513
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
3514
+ if (wantsSpecificPort) {
3358
3515
  const reservation = await tryReservePort(preferredPort, project);
3359
3516
  if (reservation) {
3360
- return {
3361
- port: preferredPort,
3362
- release: async () => {
3363
- await releasePort(preferredPort);
3364
- }
3365
- };
3517
+ return handle(preferredPort);
3366
3518
  }
3367
3519
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
3368
3520
  }
@@ -3371,30 +3523,19 @@ async function reservePort(project, preferredPort) {
3371
3523
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
3372
3524
  const port = range.start + index % range.size;
3373
3525
  index = (index + 1) % range.size;
3374
- if (reservedPorts.has(port)) {
3526
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
3375
3527
  continue;
3376
3528
  }
3377
3529
  const reservation = await tryReservePort(port, project);
3378
3530
  if (reservation) {
3379
3531
  projectPortIndex.set(project, index);
3380
- return {
3381
- port,
3382
- release: async () => {
3383
- await releasePort(port);
3384
- }
3385
- };
3532
+ return handle(port);
3386
3533
  }
3387
3534
  }
3388
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
3389
- if (dynamicPort) {
3390
- const reservation = await tryReservePort(dynamicPort, project);
3391
- if (reservation) {
3392
- return {
3393
- port: dynamicPort,
3394
- release: async () => {
3395
- await releasePort(dynamicPort);
3396
- }
3397
- };
3535
+ for (let port = 51e3; port < 52e3; port++) {
3536
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
3537
+ if (await tryReservePort(port, project)) {
3538
+ return handle(port);
3398
3539
  }
3399
3540
  }
3400
3541
  throw new Error(
@@ -3402,9 +3543,13 @@ async function reservePort(project, preferredPort) {
3402
3543
  );
3403
3544
  }
3404
3545
  async function tryReservePort(port, project) {
3546
+ if (!acquirePortLock(port)) {
3547
+ return false;
3548
+ }
3405
3549
  return new Promise((resolve) => {
3406
3550
  const server = (0, import_net.createServer)();
3407
3551
  server.once("error", () => {
3552
+ releasePortLock(port);
3408
3553
  resolve(false);
3409
3554
  });
3410
3555
  server.listen(port, () => {
@@ -3430,31 +3575,6 @@ async function releasePort(port) {
3430
3575
  });
3431
3576
  });
3432
3577
  }
3433
- async function findAvailablePortInRange(start, end) {
3434
- for (let port = start; port < end; port++) {
3435
- if (reservedPorts.has(port)) {
3436
- continue;
3437
- }
3438
- const available = await isPortAvailable(port);
3439
- if (available) {
3440
- return port;
3441
- }
3442
- }
3443
- return null;
3444
- }
3445
- async function isPortAvailable(port) {
3446
- return new Promise((resolve) => {
3447
- const server = (0, import_net.createServer)();
3448
- server.once("error", () => {
3449
- resolve(false);
3450
- });
3451
- server.listen(port, () => {
3452
- server.close(() => {
3453
- resolve(true);
3454
- });
3455
- });
3456
- });
3457
- }
3458
3578
 
3459
3579
  // libs/testing/src/server/test-server.ts
3460
3580
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -3463,8 +3583,11 @@ var TestServer = class _TestServer {
3463
3583
  options;
3464
3584
  _info;
3465
3585
  logs = [];
3586
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
3466
3587
  portRelease = null;
3467
- constructor(options, port, portRelease) {
3588
+ /** Frees only the reservation socket, just before the server is spawned */
3589
+ portReleaseSocket = null;
3590
+ constructor(options, port, portRelease, portReleaseSocket) {
3468
3591
  this.options = {
3469
3592
  port,
3470
3593
  project: options.project,
@@ -3476,6 +3599,7 @@ var TestServer = class _TestServer {
3476
3599
  debug: options.debug ?? DEBUG_SERVER
3477
3600
  };
3478
3601
  this.portRelease = portRelease ?? null;
3602
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
3479
3603
  this._info = {
3480
3604
  baseUrl: `http://localhost:${port}`,
3481
3605
  port
@@ -3488,8 +3612,8 @@ var TestServer = class _TestServer {
3488
3612
  const project = options.project ?? "default";
3489
3613
  const maxAttempts = 3;
3490
3614
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3491
- const { port, release } = await reservePort(project, options.port);
3492
- const server = new _TestServer(options, port, release);
3615
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
3616
+ const server = new _TestServer(options, port, release, releaseSocket);
3493
3617
  try {
3494
3618
  await server.startProcess();
3495
3619
  return server;
@@ -3529,7 +3653,7 @@ var TestServer = class _TestServer {
3529
3653
  }
3530
3654
  const maxAttempts = 3;
3531
3655
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3532
- const { port, release } = await reservePort(project, options.port);
3656
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
3533
3657
  const serverOptions = {
3534
3658
  ...options,
3535
3659
  port,
@@ -3537,7 +3661,7 @@ var TestServer = class _TestServer {
3537
3661
  command: `npx nx serve ${project} --port ${port}`,
3538
3662
  cwd: options.cwd ?? process.cwd()
3539
3663
  };
3540
- const server = new _TestServer(serverOptions, port, release);
3664
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
3541
3665
  try {
3542
3666
  await server.startProcess();
3543
3667
  return server;
@@ -3595,10 +3719,17 @@ var TestServer = class _TestServer {
3595
3719
  * Stop the test server
3596
3720
  */
3597
3721
  async stop() {
3598
- if (this.portRelease) {
3599
- await this.portRelease();
3600
- this.portRelease = null;
3722
+ try {
3723
+ await this.stopProcess();
3724
+ } finally {
3725
+ if (this.portRelease) {
3726
+ await this.portRelease();
3727
+ this.portRelease = null;
3728
+ }
3729
+ this.portReleaseSocket = null;
3601
3730
  }
3731
+ }
3732
+ async stopProcess() {
3602
3733
  if (this.process) {
3603
3734
  this.log("Stopping server...");
3604
3735
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -3671,7 +3802,7 @@ var TestServer = class _TestServer {
3671
3802
  * Restart the server
3672
3803
  */
3673
3804
  async restart() {
3674
- await this.stop();
3805
+ await this.stopProcess();
3675
3806
  await this.startProcess();
3676
3807
  }
3677
3808
  /**
@@ -3701,9 +3832,9 @@ var TestServer = class _TestServer {
3701
3832
  PORT: String(this.options.port)
3702
3833
  };
3703
3834
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
3704
- if (this.portRelease) {
3705
- await this.portRelease();
3706
- this.portRelease = null;
3835
+ if (this.portReleaseSocket) {
3836
+ await this.portReleaseSocket();
3837
+ this.portReleaseSocket = null;
3707
3838
  await sleep4(300);
3708
3839
  }
3709
3840
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -3713,9 +3844,8 @@ var TestServer = class _TestServer {
3713
3844
  detached: true,
3714
3845
  stdio: ["pipe", "pipe", "pipe"]
3715
3846
  });
3716
- if (this.process.pid !== void 0) {
3717
- this._info.pid = this.process.pid;
3718
- }
3847
+ this._info.pid = this.process.pid;
3848
+ this._info.shellPid = this.process.pid;
3719
3849
  let processExited = false;
3720
3850
  let exitCode = null;
3721
3851
  let exitError = null;
@@ -3818,6 +3948,8 @@ ${allLogs}`);
3818
3948
  });
3819
3949
  if (response.ok || response.status === 404) {
3820
3950
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
3951
+ const listeningPid = await findListeningPid(this.options.port);
3952
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3821
3953
  return;
3822
3954
  }
3823
3955
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3862,6 +3994,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3862
3994
  function sleep4(ms) {
3863
3995
  return new Promise((resolve) => setTimeout(resolve, ms));
3864
3996
  }
3997
+ function findListeningPid(port) {
3998
+ if (process.platform === "win32") return Promise.resolve(void 0);
3999
+ return new Promise((resolve) => {
4000
+ (0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
4001
+ if (error) {
4002
+ resolve(void 0);
4003
+ return;
4004
+ }
4005
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
4006
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
4007
+ });
4008
+ });
4009
+ }
3865
4010
  function withWorkspaceProtocolFallback(env, cwd) {
3866
4011
  if (findInstalledProtocolPackageDir(cwd)) {
3867
4012
  return env;
@@ -4009,7 +4154,8 @@ Server entry: ${currentConfig.server}
4009
4154
  Project: ${currentConfig.project ?? "default"}
4010
4155
  Command: ${serverCommand}
4011
4156
 
4012
- Error: ${errMsg}`
4157
+ Error: ${errMsg}`,
4158
+ { cause: error }
4013
4159
  );
4014
4160
  }
4015
4161
  } else {
@@ -4199,16 +4345,19 @@ function todo(name) {
4199
4345
  }
4200
4346
  var perfTest = perfTestWithFixtures;
4201
4347
  perfTest.use = use;
4202
- perfTest.describe = describe;
4203
- perfTest.beforeAll = beforeAll;
4204
- perfTest.beforeEach = beforeEach;
4205
- perfTest.afterEach = afterEach;
4206
- perfTest.afterAll = afterAll;
4348
+ for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
4349
+ Object.defineProperty(perfTest, hook, {
4350
+ get: () => globalThis[hook],
4351
+ enumerable: true,
4352
+ configurable: true
4353
+ });
4354
+ }
4207
4355
  perfTest.skip = skip;
4208
4356
  perfTest.only = only;
4209
4357
  perfTest.todo = todo;
4210
4358
 
4211
4359
  // libs/testing/src/perf/baseline-store.ts
4360
+ var import_utils2 = require("@frontmcp/utils");
4212
4361
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
4213
4362
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
4214
4363
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -4223,11 +4372,10 @@ var BaselineStore = class {
4223
4372
  */
4224
4373
  async load() {
4225
4374
  try {
4226
- const { readFile, fileExists } = await import("@frontmcp/utils");
4227
- if (!await fileExists(this.baselinePath)) {
4375
+ if (!await (0, import_utils2.fileExists)(this.baselinePath)) {
4228
4376
  return null;
4229
4377
  }
4230
- const content = await readFile(this.baselinePath);
4378
+ const content = await (0, import_utils2.readFile)(this.baselinePath);
4231
4379
  this.baseline = JSON.parse(content);
4232
4380
  return this.baseline;
4233
4381
  } catch {
@@ -4238,10 +4386,9 @@ var BaselineStore = class {
4238
4386
  * Save baseline to local file.
4239
4387
  */
4240
4388
  async save(baseline) {
4241
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
4242
4389
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
4243
- await ensureDir(dir);
4244
- await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
4390
+ await (0, import_utils2.ensureDir)(dir);
4391
+ await (0, import_utils2.writeFile)(this.baselinePath, JSON.stringify(baseline, null, 2));
4245
4392
  this.baseline = baseline;
4246
4393
  }
4247
4394
  /**
@@ -4517,6 +4664,7 @@ function resetRegressionDetector() {
4517
4664
  }
4518
4665
 
4519
4666
  // libs/testing/src/perf/report-generator.ts
4667
+ var import_utils3 = require("@frontmcp/utils");
4520
4668
  var ReportGenerator = class {
4521
4669
  detector;
4522
4670
  constructor() {
@@ -4772,14 +4920,13 @@ var ReportGenerator = class {
4772
4920
  }
4773
4921
  };
4774
4922
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
4775
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
4776
- await ensureDir(outputDir);
4923
+ await (0, import_utils3.ensureDir)(outputDir);
4777
4924
  const generator = new ReportGenerator();
4778
4925
  const report = generator.generateReport(measurements, baseline, gitInfo);
4779
4926
  const jsonPath = `${outputDir}/report.json`;
4780
4927
  const markdownPath = `${outputDir}/report.md`;
4781
- await writeFile(jsonPath, generator.generateJsonReport(report));
4782
- await writeFile(markdownPath, generator.generateMarkdownReport(report));
4928
+ await (0, import_utils3.writeFile)(jsonPath, generator.generateJsonReport(report));
4929
+ await (0, import_utils3.writeFile)(markdownPath, generator.generateMarkdownReport(report));
4783
4930
  return { jsonPath, markdownPath };
4784
4931
  }
4785
4932
  function createReportGenerator() {