@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
@@ -772,6 +772,192 @@ function getMeasurementsForProject(project) {
772
772
  return getGlobalMeasurementsArray().filter((m) => m.project === project);
773
773
  }
774
774
 
775
+ // libs/testing/src/auth/token-factory.ts
776
+ import { exportJWK, generateKeyPair, SignJWT } from "jose";
777
+ var TestTokenFactory = class {
778
+ issuer;
779
+ audience;
780
+ privateKey = null;
781
+ publicKey = null;
782
+ jwk = null;
783
+ keyId;
784
+ /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
785
+ hmacSecret;
786
+ constructor(options = {}) {
787
+ this.issuer = options.issuer ?? "https://test.frontmcp.local";
788
+ this.audience = options.audience ?? "frontmcp-test";
789
+ this.keyId = `test-key-${Date.now()}`;
790
+ this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
791
+ }
792
+ /** True when this factory signs HS256 with a shared secret (gateway mode). */
793
+ get isHmac() {
794
+ return this.hmacSecret !== null;
795
+ }
796
+ /** The JWS `alg` this factory signs with. */
797
+ get alg() {
798
+ return this.isHmac ? "HS256" : "RS256";
799
+ }
800
+ /**
801
+ * The signing key: the shared HMAC secret in gateway mode, otherwise the
802
+ * generated RSA private key (lazily created via ensureKeys()).
803
+ */
804
+ signingKey() {
805
+ if (this.hmacSecret) return this.hmacSecret;
806
+ if (!this.privateKey) {
807
+ throw new Error("Private key not initialized");
808
+ }
809
+ return this.privateKey;
810
+ }
811
+ /**
812
+ * Initialize the key pair (called automatically on first use). No-op in HS256
813
+ * mode — there is no asymmetric keypair, just the shared secret.
814
+ */
815
+ async ensureKeys() {
816
+ if (this.isHmac) return;
817
+ if (this.privateKey && this.publicKey) return;
818
+ const { publicKey, privateKey } = await generateKeyPair("RS256", {
819
+ extractable: true
820
+ });
821
+ this.privateKey = privateKey;
822
+ this.publicKey = publicKey;
823
+ this.jwk = await exportJWK(publicKey);
824
+ this.jwk.kid = this.keyId;
825
+ this.jwk.use = "sig";
826
+ this.jwk.alg = "RS256";
827
+ }
828
+ /**
829
+ * Create a JWT token with the specified claims
830
+ */
831
+ async createTestToken(options) {
832
+ await this.ensureKeys();
833
+ const nowMs = Date.now();
834
+ const iat = Math.floor(nowMs / 1e3);
835
+ const exp = options.exp ?? 3600;
836
+ const payload = {
837
+ iss: options.iss ?? this.issuer,
838
+ sub: options.sub,
839
+ aud: options.aud ?? this.audience,
840
+ iat,
841
+ exp: Math.ceil(nowMs / 1e3) + exp,
842
+ scope: options.scopes?.join(" "),
843
+ ...options.claims
844
+ };
845
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
846
+ const token = await new SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
847
+ return token;
848
+ }
849
+ /**
850
+ * Create an admin token with full access
851
+ */
852
+ async createAdminToken(sub = "admin-001") {
853
+ return this.createTestToken({
854
+ sub,
855
+ scopes: ["admin:*", "read", "write", "delete"],
856
+ claims: {
857
+ email: "admin@test.local",
858
+ name: "Test Admin",
859
+ role: "admin"
860
+ }
861
+ });
862
+ }
863
+ /**
864
+ * Create a regular user token
865
+ */
866
+ async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
867
+ return this.createTestToken({
868
+ sub,
869
+ scopes,
870
+ claims: {
871
+ email: "user@test.local",
872
+ name: "Test User",
873
+ role: "user"
874
+ }
875
+ });
876
+ }
877
+ /**
878
+ * Create an anonymous user token
879
+ */
880
+ async createAnonymousToken(expiresIn) {
881
+ return this.createTestToken({
882
+ sub: `anon:${Date.now()}`,
883
+ scopes: ["anonymous"],
884
+ exp: expiresIn,
885
+ claims: {
886
+ name: "Anonymous",
887
+ role: "anonymous"
888
+ }
889
+ });
890
+ }
891
+ /**
892
+ * Create an expired token (for testing token expiration)
893
+ */
894
+ async createExpiredToken(options) {
895
+ await this.ensureKeys();
896
+ const now = Math.floor(Date.now() / 1e3);
897
+ const payload = {
898
+ iss: this.issuer,
899
+ sub: options.sub,
900
+ aud: this.audience,
901
+ iat: now - 7200,
902
+ // 2 hours ago
903
+ exp: now - 3600
904
+ // Expired 1 hour ago
905
+ };
906
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
907
+ const token = await new SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
908
+ return token;
909
+ }
910
+ /**
911
+ * Create a token with an invalid signature (for testing signature validation)
912
+ */
913
+ createTokenWithInvalidSignature(options) {
914
+ const now = Math.floor(Date.now() / 1e3);
915
+ const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
916
+ const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
917
+ const payload = Buffer.from(
918
+ JSON.stringify({
919
+ iss: this.issuer,
920
+ sub: options.sub,
921
+ aud: this.audience,
922
+ iat: now,
923
+ exp: now + 3600
924
+ })
925
+ ).toString("base64url");
926
+ const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
927
+ return `${header}.${payload}.${signature}`;
928
+ }
929
+ /**
930
+ * Get the public JWKS for verifying tokens.
931
+ *
932
+ * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
933
+ * the shared secret, not a published public key, so there is no JWKS to serve.
934
+ */
935
+ async getPublicJwks() {
936
+ if (this.isHmac) {
937
+ throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
938
+ }
939
+ await this.ensureKeys();
940
+ if (!this.jwk) {
941
+ throw new Error("JWK not initialized");
942
+ }
943
+ return {
944
+ keys: [this.jwk]
945
+ };
946
+ }
947
+ /**
948
+ * Get the issuer URL
949
+ */
950
+ getIssuer() {
951
+ return this.issuer;
952
+ }
953
+ /**
954
+ * Get the audience
955
+ */
956
+ getAudience() {
957
+ return this.audience;
958
+ }
959
+ };
960
+
775
961
  // libs/testing/src/interceptor/mock-registry.ts
776
962
  var DefaultMockRegistry = class {
777
963
  mocks = [];
@@ -1044,6 +1230,32 @@ function sleep3(ms) {
1044
1230
  return new Promise((resolve) => setTimeout(resolve, ms));
1045
1231
  }
1046
1232
 
1233
+ // libs/testing/src/transport/error-utils.ts
1234
+ function errorName(err) {
1235
+ if (typeof err !== "object" || err === null) return void 0;
1236
+ const name = err.name;
1237
+ return typeof name === "string" ? name : void 0;
1238
+ }
1239
+ function isAbortError(err) {
1240
+ return errorName(err) === "AbortError";
1241
+ }
1242
+ function errorMessage(err, fallback = "Unknown error") {
1243
+ if (typeof err === "string" && err) return err;
1244
+ if (typeof err === "object" && err !== null) {
1245
+ const message = err.message;
1246
+ if (typeof message === "string" && message) return message;
1247
+ }
1248
+ return fallback;
1249
+ }
1250
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
1251
+ function markInterceptedError(err) {
1252
+ interceptedErrors.add(err);
1253
+ return err;
1254
+ }
1255
+ function isInterceptedError(err) {
1256
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
1257
+ }
1258
+
1047
1259
  // libs/testing/src/transport/streamable-http.transport.ts
1048
1260
  var DEFAULT_TIMEOUT = 3e4;
1049
1261
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
@@ -1152,7 +1364,7 @@ var StreamableHttpTransport = class {
1152
1364
  return mockResponse2;
1153
1365
  }
1154
1366
  case "error":
1155
- throw interceptResult.error;
1367
+ throw markInterceptedError(interceptResult.error);
1156
1368
  case "continue":
1157
1369
  message = interceptResult.request;
1158
1370
  break;
@@ -1213,7 +1425,7 @@ var StreamableHttpTransport = class {
1213
1425
  return jsonResponse;
1214
1426
  } catch (error) {
1215
1427
  clearTimeout(timeoutId);
1216
- if (error instanceof Error && error.name === "AbortError") {
1428
+ if (isAbortError(error)) {
1217
1429
  return {
1218
1430
  jsonrpc: "2.0",
1219
1431
  id: message.id ?? null,
@@ -1252,7 +1464,7 @@ var StreamableHttpTransport = class {
1252
1464
  }
1253
1465
  } catch (error) {
1254
1466
  clearTimeout(timeoutId);
1255
- if (error instanceof Error && error.name !== "AbortError") {
1467
+ if (!isAbortError(error)) {
1256
1468
  throw error;
1257
1469
  }
1258
1470
  }
@@ -1293,7 +1505,7 @@ var StreamableHttpTransport = class {
1293
1505
  error: {
1294
1506
  code: -32700,
1295
1507
  message: "Parse error",
1296
- data: error instanceof Error ? error.message : "Unknown error"
1508
+ data: errorMessage(error)
1297
1509
  }
1298
1510
  };
1299
1511
  }
@@ -1663,7 +1875,7 @@ var StreamableHttpTransport = class {
1663
1875
  if (this.config.clientInfo) {
1664
1876
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1665
1877
  }
1666
- if (this.authToken && !this.publicMode) {
1878
+ if (this.authToken) {
1667
1879
  headers["Authorization"] = `Bearer ${this.authToken}`;
1668
1880
  }
1669
1881
  if (this.sessionId) {
@@ -1986,6 +2198,16 @@ var DEFAULT_CLIENT_INFO = {
1986
2198
  name: "@frontmcp/testing",
1987
2199
  version: "0.4.0"
1988
2200
  };
2201
+ var MAX_LIST_PAGES = 1e3;
2202
+ function isAuthFailure(error) {
2203
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
2204
+ }
2205
+ function describeInitializeError(error) {
2206
+ const message = error?.message ?? "Unknown error";
2207
+ const data = error?.data;
2208
+ const detail = typeof data === "string" ? data.trim() : "";
2209
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
2210
+ }
1989
2211
  function extractEntryPaths(data) {
1990
2212
  let payload = data;
1991
2213
  if (typeof payload === "string") {
@@ -2072,19 +2294,22 @@ var McpTestClient = class {
2072
2294
  async connect() {
2073
2295
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
2074
2296
  this.triedDiscoveredEntryPath = false;
2297
+ this.initResult = null;
2075
2298
  this.transport = this.createTransport();
2076
2299
  await this.transport.connect();
2077
2300
  const maxRetries = 3;
2078
2301
  const retryDelayMs = 500;
2079
2302
  let lastError;
2080
2303
  let reportedEntryPaths;
2304
+ let attemptsMade = 0;
2081
2305
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
2306
+ attemptsMade = attempt;
2082
2307
  const initResponse = await this.initialize();
2083
2308
  if (initResponse.success && initResponse.data) {
2084
2309
  this.initResult = initResponse.data;
2085
2310
  break;
2086
2311
  }
2087
- lastError = initResponse.error?.message ?? "Unknown error";
2312
+ lastError = describeInitializeError(initResponse.error);
2088
2313
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
2089
2314
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
2090
2315
  this.triedDiscoveredEntryPath = true;
@@ -2096,6 +2321,7 @@ var McpTestClient = class {
2096
2321
  await this.transport.connect();
2097
2322
  continue;
2098
2323
  }
2324
+ if (isAuthFailure(initResponse.error)) break;
2099
2325
  if (attempt < maxRetries) {
2100
2326
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
2101
2327
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -2104,7 +2330,7 @@ var McpTestClient = class {
2104
2330
  if (!this.initResult) {
2105
2331
  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.` : "";
2106
2332
  throw new Error(
2107
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
2333
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
2108
2334
  );
2109
2335
  }
2110
2336
  this._sessionId = this.transport.getSessionId();
@@ -2174,18 +2400,47 @@ var McpTestClient = class {
2174
2400
  return this._authState;
2175
2401
  }
2176
2402
  /**
2177
- * Authenticate with a token
2403
+ * Authenticate with a token.
2404
+ *
2405
+ * When connected, this opens a NEW session with the token (the previous session was created
2406
+ * for the previous identity) and rejects if the server refuses it — for example because the
2407
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
2408
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
2178
2409
  */
2179
2410
  async authenticate(token) {
2411
+ if (typeof token !== "string" || token.trim() === "") {
2412
+ throw new Error("authenticate() requires a non-empty token");
2413
+ }
2414
+ const previousAuthState = this._authState;
2415
+ const previousConfigAuth = this.config.auth;
2416
+ this.config.auth = { ...previousConfigAuth, token };
2180
2417
  this._authState = {
2181
2418
  isAnonymous: false,
2182
2419
  token,
2183
2420
  scopes: this.parseScopesFromToken(token),
2184
2421
  user: this.parseUserFromToken(token)
2185
2422
  };
2186
- if (this.transport) {
2187
- this.transport.setAuthToken(token);
2423
+ const previousTransport = this.transport;
2424
+ if (!previousTransport) {
2425
+ this.log("debug", "Authentication updated (not connected yet)");
2426
+ return;
2188
2427
  }
2428
+ const previousInit = this.initResult;
2429
+ const previousSessionId = this._sessionId;
2430
+ const previousSessionInfo = this._sessionInfo;
2431
+ try {
2432
+ await this.connect();
2433
+ } catch (err) {
2434
+ await this.transport?.close().catch(() => void 0);
2435
+ this.transport = previousTransport;
2436
+ this.initResult = previousInit;
2437
+ this._sessionId = previousSessionId;
2438
+ this._sessionInfo = previousSessionInfo;
2439
+ this.config.auth = previousConfigAuth;
2440
+ this._authState = previousAuthState;
2441
+ throw err;
2442
+ }
2443
+ await previousTransport.close().catch(() => void 0);
2189
2444
  this.log("debug", "Authentication updated");
2190
2445
  }
2191
2446
  // ═══════════════════════════════════════════════════════════════════
@@ -2219,13 +2474,12 @@ var McpTestClient = class {
2219
2474
  /**
2220
2475
  * List all available tools
2221
2476
  */
2222
- list: async () => {
2223
- const response = await this.listTools();
2224
- if (!response.success || !response.data) {
2225
- throw new Error(`Failed to list tools: ${response.error?.message}`);
2226
- }
2227
- return response.data.tools;
2228
- },
2477
+ list: async () => this.listAllPages(
2478
+ "tools/list",
2479
+ "tools",
2480
+ (cursor) => this.listTools(cursor),
2481
+ (page) => page.tools
2482
+ ),
2229
2483
  /**
2230
2484
  * Call a tool by name with arguments
2231
2485
  */
@@ -2241,23 +2495,21 @@ var McpTestClient = class {
2241
2495
  /**
2242
2496
  * List all static resources
2243
2497
  */
2244
- list: async () => {
2245
- const response = await this.listResources();
2246
- if (!response.success || !response.data) {
2247
- throw new Error(`Failed to list resources: ${response.error?.message}`);
2248
- }
2249
- return response.data.resources;
2250
- },
2498
+ list: async () => this.listAllPages(
2499
+ "resources/list",
2500
+ "resources",
2501
+ (cursor) => this.listResources(cursor),
2502
+ (page) => page.resources
2503
+ ),
2251
2504
  /**
2252
2505
  * List all resource templates
2253
2506
  */
2254
- listTemplates: async () => {
2255
- const response = await this.listResourceTemplates();
2256
- if (!response.success || !response.data) {
2257
- throw new Error(`Failed to list resource templates: ${response.error?.message}`);
2258
- }
2259
- return response.data.resourceTemplates;
2260
- },
2507
+ listTemplates: async () => this.listAllPages(
2508
+ "resources/templates/list",
2509
+ "resource templates",
2510
+ (cursor) => this.listResourceTemplates(cursor),
2511
+ (page) => page.resourceTemplates
2512
+ ),
2261
2513
  /**
2262
2514
  * Read a resource by URI
2263
2515
  */
@@ -2285,13 +2537,12 @@ var McpTestClient = class {
2285
2537
  /**
2286
2538
  * List all available prompts
2287
2539
  */
2288
- list: async () => {
2289
- const response = await this.listPrompts();
2290
- if (!response.success || !response.data) {
2291
- throw new Error(`Failed to list prompts: ${response.error?.message}`);
2292
- }
2293
- return response.data.prompts;
2294
- },
2540
+ list: async () => this.listAllPages(
2541
+ "prompts/list",
2542
+ "prompts",
2543
+ (cursor) => this.listPrompts(cursor),
2544
+ (page) => page.prompts
2545
+ ),
2295
2546
  /**
2296
2547
  * Get a prompt with arguments
2297
2548
  */
@@ -2675,8 +2926,33 @@ var McpTestClient = class {
2675
2926
  clientInfo: this.config.clientInfo
2676
2927
  });
2677
2928
  }
2678
- async listTools() {
2679
- return this.request("tools/list", {});
2929
+ async listTools(cursor) {
2930
+ return this.request("tools/list", cursor ? { cursor } : {});
2931
+ }
2932
+ /**
2933
+ * Follow `nextCursor` until the server stops returning one, concatenating the
2934
+ * pages. A server that hands back a cursor it already returned, or never stops
2935
+ * paging, fails the call rather than looping.
2936
+ */
2937
+ async listAllPages(method, label, fetchPage, itemsOf) {
2938
+ const items = [];
2939
+ const seenCursors = /* @__PURE__ */ new Set();
2940
+ let cursor;
2941
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
2942
+ const response = await fetchPage(cursor);
2943
+ if (!response.success || !response.data) {
2944
+ throw new Error(`Failed to list ${label}: ${response.error?.message}`);
2945
+ }
2946
+ items.push(...itemsOf(response.data) ?? []);
2947
+ const next = response.data.nextCursor;
2948
+ if (!next) return items;
2949
+ if (seenCursors.has(next)) {
2950
+ throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`);
2951
+ }
2952
+ seenCursors.add(next);
2953
+ cursor = next;
2954
+ }
2955
+ throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`);
2680
2956
  }
2681
2957
  async callTool(name, args, progressToken) {
2682
2958
  return this.request("tools/call", {
@@ -2701,17 +2977,17 @@ var McpTestClient = class {
2701
2977
  timestamp
2702
2978
  });
2703
2979
  };
2704
- async listResources() {
2705
- return this.request("resources/list", {});
2980
+ async listResources(cursor) {
2981
+ return this.request("resources/list", cursor ? { cursor } : {});
2706
2982
  }
2707
- async listResourceTemplates() {
2708
- return this.request("resources/templates/list", {});
2983
+ async listResourceTemplates(cursor) {
2984
+ return this.request("resources/templates/list", cursor ? { cursor } : {});
2709
2985
  }
2710
2986
  async readResource(uri) {
2711
2987
  return this.request("resources/read", { uri });
2712
2988
  }
2713
- async listPrompts() {
2714
- return this.request("prompts/list", {});
2989
+ async listPrompts(cursor) {
2990
+ return this.request("prompts/list", cursor ? { cursor } : {});
2715
2991
  }
2716
2992
  async getPrompt(name, args) {
2717
2993
  return this.request("prompts/get", {
@@ -2788,10 +3064,11 @@ var McpTestClient = class {
2788
3064
  requestId: id
2789
3065
  };
2790
3066
  } catch (err) {
3067
+ if (isInterceptedError(err)) throw err;
2791
3068
  const durationMs = Date.now() - start;
2792
3069
  const error = {
2793
3070
  code: -32603,
2794
- message: err instanceof Error ? err.message : "Unknown error"
3071
+ message: errorMessage(err)
2795
3072
  };
2796
3073
  return {
2797
3074
  success: false,
@@ -3018,192 +3295,8 @@ var ProgressCollector = class {
3018
3295
  }
3019
3296
  };
3020
3297
 
3021
- // libs/testing/src/auth/token-factory.ts
3022
- import { exportJWK, generateKeyPair, SignJWT } from "jose";
3023
- var TestTokenFactory = class {
3024
- issuer;
3025
- audience;
3026
- privateKey = null;
3027
- publicKey = null;
3028
- jwk = null;
3029
- keyId;
3030
- /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
3031
- hmacSecret;
3032
- constructor(options = {}) {
3033
- this.issuer = options.issuer ?? "https://test.frontmcp.local";
3034
- this.audience = options.audience ?? "frontmcp-test";
3035
- this.keyId = `test-key-${Date.now()}`;
3036
- this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
3037
- }
3038
- /** True when this factory signs HS256 with a shared secret (gateway mode). */
3039
- get isHmac() {
3040
- return this.hmacSecret !== null;
3041
- }
3042
- /** The JWS `alg` this factory signs with. */
3043
- get alg() {
3044
- return this.isHmac ? "HS256" : "RS256";
3045
- }
3046
- /**
3047
- * The signing key: the shared HMAC secret in gateway mode, otherwise the
3048
- * generated RSA private key (lazily created via ensureKeys()).
3049
- */
3050
- signingKey() {
3051
- if (this.hmacSecret) return this.hmacSecret;
3052
- if (!this.privateKey) {
3053
- throw new Error("Private key not initialized");
3054
- }
3055
- return this.privateKey;
3056
- }
3057
- /**
3058
- * Initialize the key pair (called automatically on first use). No-op in HS256
3059
- * mode — there is no asymmetric keypair, just the shared secret.
3060
- */
3061
- async ensureKeys() {
3062
- if (this.isHmac) return;
3063
- if (this.privateKey && this.publicKey) return;
3064
- const { publicKey, privateKey } = await generateKeyPair("RS256", {
3065
- extractable: true
3066
- });
3067
- this.privateKey = privateKey;
3068
- this.publicKey = publicKey;
3069
- this.jwk = await exportJWK(publicKey);
3070
- this.jwk.kid = this.keyId;
3071
- this.jwk.use = "sig";
3072
- this.jwk.alg = "RS256";
3073
- }
3074
- /**
3075
- * Create a JWT token with the specified claims
3076
- */
3077
- async createTestToken(options) {
3078
- await this.ensureKeys();
3079
- const now = Math.floor(Date.now() / 1e3);
3080
- const exp = options.exp ?? 3600;
3081
- const payload = {
3082
- iss: options.iss ?? this.issuer,
3083
- sub: options.sub,
3084
- aud: options.aud ?? this.audience,
3085
- iat: now,
3086
- exp: now + exp,
3087
- scope: options.scopes?.join(" "),
3088
- ...options.claims
3089
- };
3090
- const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3091
- const token = await new SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
3092
- return token;
3093
- }
3094
- /**
3095
- * Create an admin token with full access
3096
- */
3097
- async createAdminToken(sub = "admin-001") {
3098
- return this.createTestToken({
3099
- sub,
3100
- scopes: ["admin:*", "read", "write", "delete"],
3101
- claims: {
3102
- email: "admin@test.local",
3103
- name: "Test Admin",
3104
- role: "admin"
3105
- }
3106
- });
3107
- }
3108
- /**
3109
- * Create a regular user token
3110
- */
3111
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
3112
- return this.createTestToken({
3113
- sub,
3114
- scopes,
3115
- claims: {
3116
- email: "user@test.local",
3117
- name: "Test User",
3118
- role: "user"
3119
- }
3120
- });
3121
- }
3122
- /**
3123
- * Create an anonymous user token
3124
- */
3125
- async createAnonymousToken() {
3126
- return this.createTestToken({
3127
- sub: `anon:${Date.now()}`,
3128
- scopes: ["anonymous"],
3129
- claims: {
3130
- name: "Anonymous",
3131
- role: "anonymous"
3132
- }
3133
- });
3134
- }
3135
- /**
3136
- * Create an expired token (for testing token expiration)
3137
- */
3138
- async createExpiredToken(options) {
3139
- await this.ensureKeys();
3140
- const now = Math.floor(Date.now() / 1e3);
3141
- const payload = {
3142
- iss: this.issuer,
3143
- sub: options.sub,
3144
- aud: this.audience,
3145
- iat: now - 7200,
3146
- // 2 hours ago
3147
- exp: now - 3600
3148
- // Expired 1 hour ago
3149
- };
3150
- const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3151
- const token = await new SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
3152
- return token;
3153
- }
3154
- /**
3155
- * Create a token with an invalid signature (for testing signature validation)
3156
- */
3157
- createTokenWithInvalidSignature(options) {
3158
- const now = Math.floor(Date.now() / 1e3);
3159
- const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3160
- const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
3161
- const payload = Buffer.from(
3162
- JSON.stringify({
3163
- iss: this.issuer,
3164
- sub: options.sub,
3165
- aud: this.audience,
3166
- iat: now,
3167
- exp: now + 3600
3168
- })
3169
- ).toString("base64url");
3170
- const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
3171
- return `${header}.${payload}.${signature}`;
3172
- }
3173
- /**
3174
- * Get the public JWKS for verifying tokens.
3175
- *
3176
- * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
3177
- * the shared secret, not a published public key, so there is no JWKS to serve.
3178
- */
3179
- async getPublicJwks() {
3180
- if (this.isHmac) {
3181
- throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
3182
- }
3183
- await this.ensureKeys();
3184
- if (!this.jwk) {
3185
- throw new Error("JWK not initialized");
3186
- }
3187
- return {
3188
- keys: [this.jwk]
3189
- };
3190
- }
3191
- /**
3192
- * Get the issuer URL
3193
- */
3194
- getIssuer() {
3195
- return this.issuer;
3196
- }
3197
- /**
3198
- * Get the audience
3199
- */
3200
- getAudience() {
3201
- return this.audience;
3202
- }
3203
- };
3204
-
3205
3298
  // libs/testing/src/server/test-server.ts
3206
- import { spawn } from "child_process";
3299
+ import { execFile, spawn } from "child_process";
3207
3300
  import { sha256Hex } from "@frontmcp/utils";
3208
3301
 
3209
3302
  // libs/testing/src/errors/index.ts
@@ -3274,6 +3367,8 @@ var E2E_PORT_RANGES = {
3274
3367
  "demo-e2e-distributed": { start: 50440, size: 20 },
3275
3368
  // Protocol revision E2E tests (50460-50479)
3276
3369
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
3370
+ // Testing library E2E tests (50480-50499)
3371
+ "demo-e2e-testing": { start: 50480, size: 20 },
3277
3372
  // Mock servers and utilities (50900-50999)
3278
3373
  "mock-oauth": { start: 50900, size: 10 },
3279
3374
  "mock-api": { start: 50910, size: 10 },
@@ -3282,6 +3377,66 @@ var E2E_PORT_RANGES = {
3282
3377
  default: { start: 51e3, size: 100 }
3283
3378
  };
3284
3379
  var reservedPorts = /* @__PURE__ */ new Map();
3380
+ var lockedPorts = /* @__PURE__ */ new Set();
3381
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
3382
+ function lockDir() {
3383
+ try {
3384
+ const os = __require("node:os");
3385
+ const path = __require("node:path");
3386
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
3387
+ } catch {
3388
+ return void 0;
3389
+ }
3390
+ }
3391
+ function pidIsAlive(pid) {
3392
+ try {
3393
+ process.kill(pid, 0);
3394
+ return true;
3395
+ } catch (error) {
3396
+ return error.code === "EPERM";
3397
+ }
3398
+ }
3399
+ function acquirePortLock(port) {
3400
+ if (lockedPorts.has(port)) return false;
3401
+ let fs;
3402
+ let file;
3403
+ try {
3404
+ fs = __require("node:fs");
3405
+ const dir = lockDir();
3406
+ if (!dir) return true;
3407
+ fs.mkdirSync(dir, { recursive: true });
3408
+ file = __require("node:path").join(dir, `${port}.lock`);
3409
+ } catch {
3410
+ return true;
3411
+ }
3412
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
3413
+ for (let attempt = 0; attempt < 2; attempt++) {
3414
+ try {
3415
+ fs.writeFileSync(file, payload, { flag: "wx" });
3416
+ lockedPorts.add(port);
3417
+ return true;
3418
+ } catch (error) {
3419
+ if (error.code !== "EEXIST") return true;
3420
+ try {
3421
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
3422
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
3423
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
3424
+ fs.unlinkSync(file);
3425
+ } catch {
3426
+ }
3427
+ }
3428
+ }
3429
+ return false;
3430
+ }
3431
+ function releasePortLock(port) {
3432
+ if (!lockedPorts.delete(port)) return;
3433
+ try {
3434
+ const fs = __require("node:fs");
3435
+ const dir = lockDir();
3436
+ if (dir) fs.unlinkSync(__require("node:path").join(dir, `${port}.lock`));
3437
+ } catch {
3438
+ }
3439
+ }
3285
3440
  var projectPortIndex = /* @__PURE__ */ new Map();
3286
3441
  function getPortRange(project) {
3287
3442
  const key = project;
@@ -3292,15 +3447,22 @@ function getPortRange(project) {
3292
3447
  }
3293
3448
  async function reservePort(project, preferredPort) {
3294
3449
  const range = getPortRange(project);
3295
- if (preferredPort !== void 0) {
3450
+ const handle = (port) => ({
3451
+ port,
3452
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
3453
+ releaseSocket: async () => {
3454
+ await releasePort(port);
3455
+ },
3456
+ release: async () => {
3457
+ await releasePort(port);
3458
+ releasePortLock(port);
3459
+ }
3460
+ });
3461
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
3462
+ if (wantsSpecificPort) {
3296
3463
  const reservation = await tryReservePort(preferredPort, project);
3297
3464
  if (reservation) {
3298
- return {
3299
- port: preferredPort,
3300
- release: async () => {
3301
- await releasePort(preferredPort);
3302
- }
3303
- };
3465
+ return handle(preferredPort);
3304
3466
  }
3305
3467
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
3306
3468
  }
@@ -3309,30 +3471,19 @@ async function reservePort(project, preferredPort) {
3309
3471
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
3310
3472
  const port = range.start + index % range.size;
3311
3473
  index = (index + 1) % range.size;
3312
- if (reservedPorts.has(port)) {
3474
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
3313
3475
  continue;
3314
3476
  }
3315
3477
  const reservation = await tryReservePort(port, project);
3316
3478
  if (reservation) {
3317
3479
  projectPortIndex.set(project, index);
3318
- return {
3319
- port,
3320
- release: async () => {
3321
- await releasePort(port);
3322
- }
3323
- };
3480
+ return handle(port);
3324
3481
  }
3325
3482
  }
3326
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
3327
- if (dynamicPort) {
3328
- const reservation = await tryReservePort(dynamicPort, project);
3329
- if (reservation) {
3330
- return {
3331
- port: dynamicPort,
3332
- release: async () => {
3333
- await releasePort(dynamicPort);
3334
- }
3335
- };
3483
+ for (let port = 51e3; port < 52e3; port++) {
3484
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
3485
+ if (await tryReservePort(port, project)) {
3486
+ return handle(port);
3336
3487
  }
3337
3488
  }
3338
3489
  throw new Error(
@@ -3340,9 +3491,13 @@ async function reservePort(project, preferredPort) {
3340
3491
  );
3341
3492
  }
3342
3493
  async function tryReservePort(port, project) {
3494
+ if (!acquirePortLock(port)) {
3495
+ return false;
3496
+ }
3343
3497
  return new Promise((resolve) => {
3344
3498
  const server = createServer();
3345
3499
  server.once("error", () => {
3500
+ releasePortLock(port);
3346
3501
  resolve(false);
3347
3502
  });
3348
3503
  server.listen(port, () => {
@@ -3368,31 +3523,6 @@ async function releasePort(port) {
3368
3523
  });
3369
3524
  });
3370
3525
  }
3371
- async function findAvailablePortInRange(start, end) {
3372
- for (let port = start; port < end; port++) {
3373
- if (reservedPorts.has(port)) {
3374
- continue;
3375
- }
3376
- const available = await isPortAvailable(port);
3377
- if (available) {
3378
- return port;
3379
- }
3380
- }
3381
- return null;
3382
- }
3383
- async function isPortAvailable(port) {
3384
- return new Promise((resolve) => {
3385
- const server = createServer();
3386
- server.once("error", () => {
3387
- resolve(false);
3388
- });
3389
- server.listen(port, () => {
3390
- server.close(() => {
3391
- resolve(true);
3392
- });
3393
- });
3394
- });
3395
- }
3396
3526
 
3397
3527
  // libs/testing/src/server/test-server.ts
3398
3528
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -3401,8 +3531,11 @@ var TestServer = class _TestServer {
3401
3531
  options;
3402
3532
  _info;
3403
3533
  logs = [];
3534
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
3404
3535
  portRelease = null;
3405
- constructor(options, port, portRelease) {
3536
+ /** Frees only the reservation socket, just before the server is spawned */
3537
+ portReleaseSocket = null;
3538
+ constructor(options, port, portRelease, portReleaseSocket) {
3406
3539
  this.options = {
3407
3540
  port,
3408
3541
  project: options.project,
@@ -3414,6 +3547,7 @@ var TestServer = class _TestServer {
3414
3547
  debug: options.debug ?? DEBUG_SERVER
3415
3548
  };
3416
3549
  this.portRelease = portRelease ?? null;
3550
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
3417
3551
  this._info = {
3418
3552
  baseUrl: `http://localhost:${port}`,
3419
3553
  port
@@ -3426,8 +3560,8 @@ var TestServer = class _TestServer {
3426
3560
  const project = options.project ?? "default";
3427
3561
  const maxAttempts = 3;
3428
3562
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3429
- const { port, release } = await reservePort(project, options.port);
3430
- const server = new _TestServer(options, port, release);
3563
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
3564
+ const server = new _TestServer(options, port, release, releaseSocket);
3431
3565
  try {
3432
3566
  await server.startProcess();
3433
3567
  return server;
@@ -3467,7 +3601,7 @@ var TestServer = class _TestServer {
3467
3601
  }
3468
3602
  const maxAttempts = 3;
3469
3603
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3470
- const { port, release } = await reservePort(project, options.port);
3604
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
3471
3605
  const serverOptions = {
3472
3606
  ...options,
3473
3607
  port,
@@ -3475,7 +3609,7 @@ var TestServer = class _TestServer {
3475
3609
  command: `npx nx serve ${project} --port ${port}`,
3476
3610
  cwd: options.cwd ?? process.cwd()
3477
3611
  };
3478
- const server = new _TestServer(serverOptions, port, release);
3612
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
3479
3613
  try {
3480
3614
  await server.startProcess();
3481
3615
  return server;
@@ -3533,10 +3667,17 @@ var TestServer = class _TestServer {
3533
3667
  * Stop the test server
3534
3668
  */
3535
3669
  async stop() {
3536
- if (this.portRelease) {
3537
- await this.portRelease();
3538
- this.portRelease = null;
3670
+ try {
3671
+ await this.stopProcess();
3672
+ } finally {
3673
+ if (this.portRelease) {
3674
+ await this.portRelease();
3675
+ this.portRelease = null;
3676
+ }
3677
+ this.portReleaseSocket = null;
3539
3678
  }
3679
+ }
3680
+ async stopProcess() {
3540
3681
  if (this.process) {
3541
3682
  this.log("Stopping server...");
3542
3683
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -3609,7 +3750,7 @@ var TestServer = class _TestServer {
3609
3750
  * Restart the server
3610
3751
  */
3611
3752
  async restart() {
3612
- await this.stop();
3753
+ await this.stopProcess();
3613
3754
  await this.startProcess();
3614
3755
  }
3615
3756
  /**
@@ -3639,9 +3780,9 @@ var TestServer = class _TestServer {
3639
3780
  PORT: String(this.options.port)
3640
3781
  };
3641
3782
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
3642
- if (this.portRelease) {
3643
- await this.portRelease();
3644
- this.portRelease = null;
3783
+ if (this.portReleaseSocket) {
3784
+ await this.portReleaseSocket();
3785
+ this.portReleaseSocket = null;
3645
3786
  await sleep4(300);
3646
3787
  }
3647
3788
  this.process = spawn(this.options.command, [], {
@@ -3651,9 +3792,8 @@ var TestServer = class _TestServer {
3651
3792
  detached: true,
3652
3793
  stdio: ["pipe", "pipe", "pipe"]
3653
3794
  });
3654
- if (this.process.pid !== void 0) {
3655
- this._info.pid = this.process.pid;
3656
- }
3795
+ this._info.pid = this.process.pid;
3796
+ this._info.shellPid = this.process.pid;
3657
3797
  let processExited = false;
3658
3798
  let exitCode = null;
3659
3799
  let exitError = null;
@@ -3756,6 +3896,8 @@ ${allLogs}`);
3756
3896
  });
3757
3897
  if (response.ok || response.status === 404) {
3758
3898
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
3899
+ const listeningPid = await findListeningPid(this.options.port);
3900
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3759
3901
  return;
3760
3902
  }
3761
3903
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3800,6 +3942,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3800
3942
  function sleep4(ms) {
3801
3943
  return new Promise((resolve) => setTimeout(resolve, ms));
3802
3944
  }
3945
+ function findListeningPid(port) {
3946
+ if (process.platform === "win32") return Promise.resolve(void 0);
3947
+ return new Promise((resolve) => {
3948
+ execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
3949
+ if (error) {
3950
+ resolve(void 0);
3951
+ return;
3952
+ }
3953
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
3954
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
3955
+ });
3956
+ });
3957
+ }
3803
3958
  function withWorkspaceProtocolFallback(env, cwd) {
3804
3959
  if (findInstalledProtocolPackageDir(cwd)) {
3805
3960
  return env;
@@ -3947,7 +4102,8 @@ Server entry: ${currentConfig.server}
3947
4102
  Project: ${currentConfig.project ?? "default"}
3948
4103
  Command: ${serverCommand}
3949
4104
 
3950
- Error: ${errMsg}`
4105
+ Error: ${errMsg}`,
4106
+ { cause: error }
3951
4107
  );
3952
4108
  }
3953
4109
  } else {
@@ -4137,16 +4293,19 @@ function todo(name) {
4137
4293
  }
4138
4294
  var perfTest = perfTestWithFixtures;
4139
4295
  perfTest.use = use;
4140
- perfTest.describe = describe;
4141
- perfTest.beforeAll = beforeAll;
4142
- perfTest.beforeEach = beforeEach;
4143
- perfTest.afterEach = afterEach;
4144
- perfTest.afterAll = afterAll;
4296
+ for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
4297
+ Object.defineProperty(perfTest, hook, {
4298
+ get: () => globalThis[hook],
4299
+ enumerable: true,
4300
+ configurable: true
4301
+ });
4302
+ }
4145
4303
  perfTest.skip = skip;
4146
4304
  perfTest.only = only;
4147
4305
  perfTest.todo = todo;
4148
4306
 
4149
4307
  // libs/testing/src/perf/baseline-store.ts
4308
+ import { ensureDir, fileExists, readFile, writeFile } from "@frontmcp/utils";
4150
4309
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
4151
4310
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
4152
4311
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -4161,7 +4320,6 @@ var BaselineStore = class {
4161
4320
  */
4162
4321
  async load() {
4163
4322
  try {
4164
- const { readFile, fileExists } = await import("@frontmcp/utils");
4165
4323
  if (!await fileExists(this.baselinePath)) {
4166
4324
  return null;
4167
4325
  }
@@ -4176,7 +4334,6 @@ var BaselineStore = class {
4176
4334
  * Save baseline to local file.
4177
4335
  */
4178
4336
  async save(baseline) {
4179
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
4180
4337
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
4181
4338
  await ensureDir(dir);
4182
4339
  await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
@@ -4455,6 +4612,7 @@ function resetRegressionDetector() {
4455
4612
  }
4456
4613
 
4457
4614
  // libs/testing/src/perf/report-generator.ts
4615
+ import { ensureDir as ensureDir2, writeFile as writeFile2 } from "@frontmcp/utils";
4458
4616
  var ReportGenerator = class {
4459
4617
  detector;
4460
4618
  constructor() {
@@ -4710,14 +4868,13 @@ var ReportGenerator = class {
4710
4868
  }
4711
4869
  };
4712
4870
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
4713
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
4714
- await ensureDir(outputDir);
4871
+ await ensureDir2(outputDir);
4715
4872
  const generator = new ReportGenerator();
4716
4873
  const report = generator.generateReport(measurements, baseline, gitInfo);
4717
4874
  const jsonPath = `${outputDir}/report.json`;
4718
4875
  const markdownPath = `${outputDir}/report.md`;
4719
- await writeFile(jsonPath, generator.generateJsonReport(report));
4720
- await writeFile(markdownPath, generator.generateMarkdownReport(report));
4876
+ await writeFile2(jsonPath, generator.generateJsonReport(report));
4877
+ await writeFile2(markdownPath, generator.generateMarkdownReport(report));
4721
4878
  return { jsonPath, markdownPath };
4722
4879
  }
4723
4880
  function createReportGenerator() {