@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/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) {
@@ -2049,6 +2251,15 @@ var DEFAULT_CLIENT_INFO = {
2049
2251
  version: "0.4.0"
2050
2252
  };
2051
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
+ }
2052
2263
  function extractEntryPaths(data) {
2053
2264
  let payload = data;
2054
2265
  if (typeof payload === "string") {
@@ -2135,19 +2346,22 @@ var McpTestClient = class {
2135
2346
  async connect() {
2136
2347
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
2137
2348
  this.triedDiscoveredEntryPath = false;
2349
+ this.initResult = null;
2138
2350
  this.transport = this.createTransport();
2139
2351
  await this.transport.connect();
2140
2352
  const maxRetries = 3;
2141
2353
  const retryDelayMs = 500;
2142
2354
  let lastError;
2143
2355
  let reportedEntryPaths;
2356
+ let attemptsMade = 0;
2144
2357
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
2358
+ attemptsMade = attempt;
2145
2359
  const initResponse = await this.initialize();
2146
2360
  if (initResponse.success && initResponse.data) {
2147
2361
  this.initResult = initResponse.data;
2148
2362
  break;
2149
2363
  }
2150
- lastError = initResponse.error?.message ?? "Unknown error";
2364
+ lastError = describeInitializeError(initResponse.error);
2151
2365
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
2152
2366
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
2153
2367
  this.triedDiscoveredEntryPath = true;
@@ -2159,6 +2373,7 @@ var McpTestClient = class {
2159
2373
  await this.transport.connect();
2160
2374
  continue;
2161
2375
  }
2376
+ if (isAuthFailure(initResponse.error)) break;
2162
2377
  if (attempt < maxRetries) {
2163
2378
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
2164
2379
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -2167,7 +2382,7 @@ var McpTestClient = class {
2167
2382
  if (!this.initResult) {
2168
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.` : "";
2169
2384
  throw new Error(
2170
- `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}`
2171
2386
  );
2172
2387
  }
2173
2388
  this._sessionId = this.transport.getSessionId();
@@ -2237,18 +2452,47 @@ var McpTestClient = class {
2237
2452
  return this._authState;
2238
2453
  }
2239
2454
  /**
2240
- * 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.
2241
2461
  */
2242
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 };
2243
2469
  this._authState = {
2244
2470
  isAnonymous: false,
2245
2471
  token,
2246
2472
  scopes: this.parseScopesFromToken(token),
2247
2473
  user: this.parseUserFromToken(token)
2248
2474
  };
2249
- if (this.transport) {
2250
- this.transport.setAuthToken(token);
2475
+ const previousTransport = this.transport;
2476
+ if (!previousTransport) {
2477
+ this.log("debug", "Authentication updated (not connected yet)");
2478
+ return;
2251
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);
2252
2496
  this.log("debug", "Authentication updated");
2253
2497
  }
2254
2498
  // ═══════════════════════════════════════════════════════════════════
@@ -2872,10 +3116,11 @@ var McpTestClient = class {
2872
3116
  requestId: id
2873
3117
  };
2874
3118
  } catch (err) {
3119
+ if (isInterceptedError(err)) throw err;
2875
3120
  const durationMs = Date.now() - start;
2876
3121
  const error = {
2877
3122
  code: -32603,
2878
- message: err instanceof Error ? err.message : "Unknown error"
3123
+ message: errorMessage(err)
2879
3124
  };
2880
3125
  return {
2881
3126
  success: false,
@@ -3102,190 +3347,6 @@ var ProgressCollector = class {
3102
3347
  }
3103
3348
  };
3104
3349
 
3105
- // libs/testing/src/auth/token-factory.ts
3106
- var import_jose = require("jose");
3107
- var TestTokenFactory = class {
3108
- issuer;
3109
- audience;
3110
- privateKey = null;
3111
- publicKey = null;
3112
- jwk = null;
3113
- keyId;
3114
- /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
3115
- hmacSecret;
3116
- constructor(options = {}) {
3117
- this.issuer = options.issuer ?? "https://test.frontmcp.local";
3118
- this.audience = options.audience ?? "frontmcp-test";
3119
- this.keyId = `test-key-${Date.now()}`;
3120
- this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
3121
- }
3122
- /** True when this factory signs HS256 with a shared secret (gateway mode). */
3123
- get isHmac() {
3124
- return this.hmacSecret !== null;
3125
- }
3126
- /** The JWS `alg` this factory signs with. */
3127
- get alg() {
3128
- return this.isHmac ? "HS256" : "RS256";
3129
- }
3130
- /**
3131
- * The signing key: the shared HMAC secret in gateway mode, otherwise the
3132
- * generated RSA private key (lazily created via ensureKeys()).
3133
- */
3134
- signingKey() {
3135
- if (this.hmacSecret) return this.hmacSecret;
3136
- if (!this.privateKey) {
3137
- throw new Error("Private key not initialized");
3138
- }
3139
- return this.privateKey;
3140
- }
3141
- /**
3142
- * Initialize the key pair (called automatically on first use). No-op in HS256
3143
- * mode — there is no asymmetric keypair, just the shared secret.
3144
- */
3145
- async ensureKeys() {
3146
- if (this.isHmac) return;
3147
- if (this.privateKey && this.publicKey) return;
3148
- const { publicKey, privateKey } = await (0, import_jose.generateKeyPair)("RS256", {
3149
- extractable: true
3150
- });
3151
- this.privateKey = privateKey;
3152
- this.publicKey = publicKey;
3153
- this.jwk = await (0, import_jose.exportJWK)(publicKey);
3154
- this.jwk.kid = this.keyId;
3155
- this.jwk.use = "sig";
3156
- this.jwk.alg = "RS256";
3157
- }
3158
- /**
3159
- * Create a JWT token with the specified claims
3160
- */
3161
- async createTestToken(options) {
3162
- await this.ensureKeys();
3163
- const now = Math.floor(Date.now() / 1e3);
3164
- const exp = options.exp ?? 3600;
3165
- const payload = {
3166
- iss: options.iss ?? this.issuer,
3167
- sub: options.sub,
3168
- aud: options.aud ?? this.audience,
3169
- iat: now,
3170
- exp: now + exp,
3171
- scope: options.scopes?.join(" "),
3172
- ...options.claims
3173
- };
3174
- const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3175
- const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
3176
- return token;
3177
- }
3178
- /**
3179
- * Create an admin token with full access
3180
- */
3181
- async createAdminToken(sub = "admin-001") {
3182
- return this.createTestToken({
3183
- sub,
3184
- scopes: ["admin:*", "read", "write", "delete"],
3185
- claims: {
3186
- email: "admin@test.local",
3187
- name: "Test Admin",
3188
- role: "admin"
3189
- }
3190
- });
3191
- }
3192
- /**
3193
- * Create a regular user token
3194
- */
3195
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
3196
- return this.createTestToken({
3197
- sub,
3198
- scopes,
3199
- claims: {
3200
- email: "user@test.local",
3201
- name: "Test User",
3202
- role: "user"
3203
- }
3204
- });
3205
- }
3206
- /**
3207
- * Create an anonymous user token
3208
- */
3209
- async createAnonymousToken() {
3210
- return this.createTestToken({
3211
- sub: `anon:${Date.now()}`,
3212
- scopes: ["anonymous"],
3213
- claims: {
3214
- name: "Anonymous",
3215
- role: "anonymous"
3216
- }
3217
- });
3218
- }
3219
- /**
3220
- * Create an expired token (for testing token expiration)
3221
- */
3222
- async createExpiredToken(options) {
3223
- await this.ensureKeys();
3224
- const now = Math.floor(Date.now() / 1e3);
3225
- const payload = {
3226
- iss: this.issuer,
3227
- sub: options.sub,
3228
- aud: this.audience,
3229
- iat: now - 7200,
3230
- // 2 hours ago
3231
- exp: now - 3600
3232
- // Expired 1 hour ago
3233
- };
3234
- const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3235
- const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
3236
- return token;
3237
- }
3238
- /**
3239
- * Create a token with an invalid signature (for testing signature validation)
3240
- */
3241
- createTokenWithInvalidSignature(options) {
3242
- const now = Math.floor(Date.now() / 1e3);
3243
- const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3244
- const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
3245
- const payload = Buffer.from(
3246
- JSON.stringify({
3247
- iss: this.issuer,
3248
- sub: options.sub,
3249
- aud: this.audience,
3250
- iat: now,
3251
- exp: now + 3600
3252
- })
3253
- ).toString("base64url");
3254
- const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
3255
- return `${header}.${payload}.${signature}`;
3256
- }
3257
- /**
3258
- * Get the public JWKS for verifying tokens.
3259
- *
3260
- * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
3261
- * the shared secret, not a published public key, so there is no JWKS to serve.
3262
- */
3263
- async getPublicJwks() {
3264
- if (this.isHmac) {
3265
- throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
3266
- }
3267
- await this.ensureKeys();
3268
- if (!this.jwk) {
3269
- throw new Error("JWK not initialized");
3270
- }
3271
- return {
3272
- keys: [this.jwk]
3273
- };
3274
- }
3275
- /**
3276
- * Get the issuer URL
3277
- */
3278
- getIssuer() {
3279
- return this.issuer;
3280
- }
3281
- /**
3282
- * Get the audience
3283
- */
3284
- getAudience() {
3285
- return this.audience;
3286
- }
3287
- };
3288
-
3289
3350
  // libs/testing/src/server/test-server.ts
3290
3351
  var import_child_process = require("child_process");
3291
3352
  var import_utils = require("@frontmcp/utils");
@@ -3358,6 +3419,8 @@ var E2E_PORT_RANGES = {
3358
3419
  "demo-e2e-distributed": { start: 50440, size: 20 },
3359
3420
  // Protocol revision E2E tests (50460-50479)
3360
3421
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
3422
+ // Testing library E2E tests (50480-50499)
3423
+ "demo-e2e-testing": { start: 50480, size: 20 },
3361
3424
  // Mock servers and utilities (50900-50999)
3362
3425
  "mock-oauth": { start: 50900, size: 10 },
3363
3426
  "mock-api": { start: 50910, size: 10 },
@@ -3366,6 +3429,66 @@ var E2E_PORT_RANGES = {
3366
3429
  default: { start: 51e3, size: 100 }
3367
3430
  };
3368
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
+ }
3369
3492
  var projectPortIndex = /* @__PURE__ */ new Map();
3370
3493
  function getPortRange(project) {
3371
3494
  const key = project;
@@ -3376,15 +3499,22 @@ function getPortRange(project) {
3376
3499
  }
3377
3500
  async function reservePort(project, preferredPort) {
3378
3501
  const range = getPortRange(project);
3379
- 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) {
3380
3515
  const reservation = await tryReservePort(preferredPort, project);
3381
3516
  if (reservation) {
3382
- return {
3383
- port: preferredPort,
3384
- release: async () => {
3385
- await releasePort(preferredPort);
3386
- }
3387
- };
3517
+ return handle(preferredPort);
3388
3518
  }
3389
3519
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
3390
3520
  }
@@ -3393,30 +3523,19 @@ async function reservePort(project, preferredPort) {
3393
3523
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
3394
3524
  const port = range.start + index % range.size;
3395
3525
  index = (index + 1) % range.size;
3396
- if (reservedPorts.has(port)) {
3526
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
3397
3527
  continue;
3398
3528
  }
3399
3529
  const reservation = await tryReservePort(port, project);
3400
3530
  if (reservation) {
3401
3531
  projectPortIndex.set(project, index);
3402
- return {
3403
- port,
3404
- release: async () => {
3405
- await releasePort(port);
3406
- }
3407
- };
3532
+ return handle(port);
3408
3533
  }
3409
3534
  }
3410
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
3411
- if (dynamicPort) {
3412
- const reservation = await tryReservePort(dynamicPort, project);
3413
- if (reservation) {
3414
- return {
3415
- port: dynamicPort,
3416
- release: async () => {
3417
- await releasePort(dynamicPort);
3418
- }
3419
- };
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);
3420
3539
  }
3421
3540
  }
3422
3541
  throw new Error(
@@ -3424,9 +3543,13 @@ async function reservePort(project, preferredPort) {
3424
3543
  );
3425
3544
  }
3426
3545
  async function tryReservePort(port, project) {
3546
+ if (!acquirePortLock(port)) {
3547
+ return false;
3548
+ }
3427
3549
  return new Promise((resolve) => {
3428
3550
  const server = (0, import_net.createServer)();
3429
3551
  server.once("error", () => {
3552
+ releasePortLock(port);
3430
3553
  resolve(false);
3431
3554
  });
3432
3555
  server.listen(port, () => {
@@ -3452,31 +3575,6 @@ async function releasePort(port) {
3452
3575
  });
3453
3576
  });
3454
3577
  }
3455
- async function findAvailablePortInRange(start, end) {
3456
- for (let port = start; port < end; port++) {
3457
- if (reservedPorts.has(port)) {
3458
- continue;
3459
- }
3460
- const available = await isPortAvailable(port);
3461
- if (available) {
3462
- return port;
3463
- }
3464
- }
3465
- return null;
3466
- }
3467
- async function isPortAvailable(port) {
3468
- return new Promise((resolve) => {
3469
- const server = (0, import_net.createServer)();
3470
- server.once("error", () => {
3471
- resolve(false);
3472
- });
3473
- server.listen(port, () => {
3474
- server.close(() => {
3475
- resolve(true);
3476
- });
3477
- });
3478
- });
3479
- }
3480
3578
 
3481
3579
  // libs/testing/src/server/test-server.ts
3482
3580
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -3485,8 +3583,11 @@ var TestServer = class _TestServer {
3485
3583
  options;
3486
3584
  _info;
3487
3585
  logs = [];
3586
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
3488
3587
  portRelease = null;
3489
- 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) {
3490
3591
  this.options = {
3491
3592
  port,
3492
3593
  project: options.project,
@@ -3498,6 +3599,7 @@ var TestServer = class _TestServer {
3498
3599
  debug: options.debug ?? DEBUG_SERVER
3499
3600
  };
3500
3601
  this.portRelease = portRelease ?? null;
3602
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
3501
3603
  this._info = {
3502
3604
  baseUrl: `http://localhost:${port}`,
3503
3605
  port
@@ -3510,8 +3612,8 @@ var TestServer = class _TestServer {
3510
3612
  const project = options.project ?? "default";
3511
3613
  const maxAttempts = 3;
3512
3614
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3513
- const { port, release } = await reservePort(project, options.port);
3514
- 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);
3515
3617
  try {
3516
3618
  await server.startProcess();
3517
3619
  return server;
@@ -3551,7 +3653,7 @@ var TestServer = class _TestServer {
3551
3653
  }
3552
3654
  const maxAttempts = 3;
3553
3655
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3554
- const { port, release } = await reservePort(project, options.port);
3656
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
3555
3657
  const serverOptions = {
3556
3658
  ...options,
3557
3659
  port,
@@ -3559,7 +3661,7 @@ var TestServer = class _TestServer {
3559
3661
  command: `npx nx serve ${project} --port ${port}`,
3560
3662
  cwd: options.cwd ?? process.cwd()
3561
3663
  };
3562
- const server = new _TestServer(serverOptions, port, release);
3664
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
3563
3665
  try {
3564
3666
  await server.startProcess();
3565
3667
  return server;
@@ -3617,10 +3719,17 @@ var TestServer = class _TestServer {
3617
3719
  * Stop the test server
3618
3720
  */
3619
3721
  async stop() {
3620
- if (this.portRelease) {
3621
- await this.portRelease();
3622
- 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;
3623
3730
  }
3731
+ }
3732
+ async stopProcess() {
3624
3733
  if (this.process) {
3625
3734
  this.log("Stopping server...");
3626
3735
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -3693,7 +3802,7 @@ var TestServer = class _TestServer {
3693
3802
  * Restart the server
3694
3803
  */
3695
3804
  async restart() {
3696
- await this.stop();
3805
+ await this.stopProcess();
3697
3806
  await this.startProcess();
3698
3807
  }
3699
3808
  /**
@@ -3723,9 +3832,9 @@ var TestServer = class _TestServer {
3723
3832
  PORT: String(this.options.port)
3724
3833
  };
3725
3834
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
3726
- if (this.portRelease) {
3727
- await this.portRelease();
3728
- this.portRelease = null;
3835
+ if (this.portReleaseSocket) {
3836
+ await this.portReleaseSocket();
3837
+ this.portReleaseSocket = null;
3729
3838
  await sleep4(300);
3730
3839
  }
3731
3840
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -3735,9 +3844,8 @@ var TestServer = class _TestServer {
3735
3844
  detached: true,
3736
3845
  stdio: ["pipe", "pipe", "pipe"]
3737
3846
  });
3738
- if (this.process.pid !== void 0) {
3739
- this._info.pid = this.process.pid;
3740
- }
3847
+ this._info.pid = this.process.pid;
3848
+ this._info.shellPid = this.process.pid;
3741
3849
  let processExited = false;
3742
3850
  let exitCode = null;
3743
3851
  let exitError = null;
@@ -3840,6 +3948,8 @@ ${allLogs}`);
3840
3948
  });
3841
3949
  if (response.ok || response.status === 404) {
3842
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;
3843
3953
  return;
3844
3954
  }
3845
3955
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3884,6 +3994,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3884
3994
  function sleep4(ms) {
3885
3995
  return new Promise((resolve) => setTimeout(resolve, ms));
3886
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
+ }
3887
4010
  function withWorkspaceProtocolFallback(env, cwd) {
3888
4011
  if (findInstalledProtocolPackageDir(cwd)) {
3889
4012
  return env;
@@ -4031,7 +4154,8 @@ Server entry: ${currentConfig.server}
4031
4154
  Project: ${currentConfig.project ?? "default"}
4032
4155
  Command: ${serverCommand}
4033
4156
 
4034
- Error: ${errMsg}`
4157
+ Error: ${errMsg}`,
4158
+ { cause: error }
4035
4159
  );
4036
4160
  }
4037
4161
  } else {
@@ -4221,16 +4345,19 @@ function todo(name) {
4221
4345
  }
4222
4346
  var perfTest = perfTestWithFixtures;
4223
4347
  perfTest.use = use;
4224
- perfTest.describe = describe;
4225
- perfTest.beforeAll = beforeAll;
4226
- perfTest.beforeEach = beforeEach;
4227
- perfTest.afterEach = afterEach;
4228
- 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
+ }
4229
4355
  perfTest.skip = skip;
4230
4356
  perfTest.only = only;
4231
4357
  perfTest.todo = todo;
4232
4358
 
4233
4359
  // libs/testing/src/perf/baseline-store.ts
4360
+ var import_utils2 = require("@frontmcp/utils");
4234
4361
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
4235
4362
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
4236
4363
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -4245,11 +4372,10 @@ var BaselineStore = class {
4245
4372
  */
4246
4373
  async load() {
4247
4374
  try {
4248
- const { readFile, fileExists } = await import("@frontmcp/utils");
4249
- if (!await fileExists(this.baselinePath)) {
4375
+ if (!await (0, import_utils2.fileExists)(this.baselinePath)) {
4250
4376
  return null;
4251
4377
  }
4252
- const content = await readFile(this.baselinePath);
4378
+ const content = await (0, import_utils2.readFile)(this.baselinePath);
4253
4379
  this.baseline = JSON.parse(content);
4254
4380
  return this.baseline;
4255
4381
  } catch {
@@ -4260,10 +4386,9 @@ var BaselineStore = class {
4260
4386
  * Save baseline to local file.
4261
4387
  */
4262
4388
  async save(baseline) {
4263
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
4264
4389
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
4265
- await ensureDir(dir);
4266
- 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));
4267
4392
  this.baseline = baseline;
4268
4393
  }
4269
4394
  /**
@@ -4539,6 +4664,7 @@ function resetRegressionDetector() {
4539
4664
  }
4540
4665
 
4541
4666
  // libs/testing/src/perf/report-generator.ts
4667
+ var import_utils3 = require("@frontmcp/utils");
4542
4668
  var ReportGenerator = class {
4543
4669
  detector;
4544
4670
  constructor() {
@@ -4794,14 +4920,13 @@ var ReportGenerator = class {
4794
4920
  }
4795
4921
  };
4796
4922
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
4797
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
4798
- await ensureDir(outputDir);
4923
+ await (0, import_utils3.ensureDir)(outputDir);
4799
4924
  const generator = new ReportGenerator();
4800
4925
  const report = generator.generateReport(measurements, baseline, gitInfo);
4801
4926
  const jsonPath = `${outputDir}/report.json`;
4802
4927
  const markdownPath = `${outputDir}/report.md`;
4803
- await writeFile(jsonPath, generator.generateJsonReport(report));
4804
- 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));
4805
4930
  return { jsonPath, markdownPath };
4806
4931
  }
4807
4932
  function createReportGenerator() {