@frontmcp/testing 1.8.6 → 1.9.0

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 (49) 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 +993 -237
  7. package/esm/index.mjs +1151 -360
  8. package/esm/matchers/index.mjs +3 -0
  9. package/esm/package.json +8 -6
  10. package/esm/perf/index.mjs +874 -280
  11. package/esm/perf/perf-setup.mjs +3 -0
  12. package/esm/playwright/index.mjs +3 -0
  13. package/esm/setup.mjs +3 -0
  14. package/expect.d.ts +12 -11
  15. package/expect.d.ts.map +1 -1
  16. package/fixtures/fixture-types.d.ts +41 -10
  17. package/fixtures/fixture-types.d.ts.map +1 -1
  18. package/fixtures/index.d.ts +1 -1
  19. package/fixtures/index.d.ts.map +1 -1
  20. package/fixtures/index.js +989 -229
  21. package/fixtures/test-fixture.d.ts +6 -5
  22. package/fixtures/test-fixture.d.ts.map +1 -1
  23. package/http-mock/http-mock.d.ts +5 -3
  24. package/http-mock/http-mock.d.ts.map +1 -1
  25. package/http-mock/http-mock.types.d.ts +17 -6
  26. package/http-mock/http-mock.types.d.ts.map +1 -1
  27. package/http-mock/index.d.ts +1 -1
  28. package/http-mock/index.d.ts.map +1 -1
  29. package/index.d.ts +3 -2
  30. package/index.d.ts.map +1 -1
  31. package/index.js +1157 -361
  32. package/package.json +8 -6
  33. package/perf/baseline-store.d.ts +1 -1
  34. package/perf/baseline-store.d.ts.map +1 -1
  35. package/perf/index.js +874 -293
  36. package/perf/perf-test.d.ts.map +1 -1
  37. package/perf/report-generator.d.ts +1 -1
  38. package/perf/report-generator.d.ts.map +1 -1
  39. package/server/port-registry.d.ts +7 -1
  40. package/server/port-registry.d.ts.map +1 -1
  41. package/server/test-server.d.ts +10 -1
  42. package/server/test-server.d.ts.map +1 -1
  43. package/transport/error-utils.d.ts +15 -0
  44. package/transport/error-utils.d.ts.map +1 -0
  45. package/transport/index.d.ts +1 -0
  46. package/transport/index.d.ts.map +1 -1
  47. package/transport/sse.transport.d.ts +80 -0
  48. package/transport/sse.transport.d.ts.map +1 -0
  49. package/transport/streamable-http.transport.d.ts.map +1 -1
package/esm/index.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
5
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
6
  }) : x)(function(x) {
@@ -341,8 +344,479 @@ var interceptors = {
341
344
  }
342
345
  };
343
346
 
344
- // libs/testing/src/transport/streamable-http.transport.ts
347
+ // libs/testing/src/transport/error-utils.ts
348
+ function errorName(err) {
349
+ if (typeof err !== "object" || err === null) return void 0;
350
+ const name = err.name;
351
+ return typeof name === "string" ? name : void 0;
352
+ }
353
+ function isAbortError(err) {
354
+ return errorName(err) === "AbortError";
355
+ }
356
+ function errorMessage(err, fallback = "Unknown error") {
357
+ if (typeof err === "string" && err) return err;
358
+ if (typeof err === "object" && err !== null) {
359
+ const message = err.message;
360
+ if (typeof message === "string" && message) return message;
361
+ }
362
+ return fallback;
363
+ }
364
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
365
+ function markInterceptedError(err) {
366
+ interceptedErrors.add(err);
367
+ return err;
368
+ }
369
+ function isInterceptedError(err) {
370
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
371
+ }
372
+
373
+ // libs/testing/src/transport/sse.transport.ts
345
374
  var DEFAULT_TIMEOUT = 3e4;
375
+ var SseTransport = class {
376
+ baseUrl;
377
+ entryPath;
378
+ timeout;
379
+ authHeaders;
380
+ publicMode;
381
+ debug;
382
+ clientInfo;
383
+ notificationHandler;
384
+ interceptors;
385
+ elicitationHandler;
386
+ state = "disconnected";
387
+ authToken;
388
+ sessionId;
389
+ messageEndpoint;
390
+ stream;
391
+ pending = /* @__PURE__ */ new Map();
392
+ connectionCount = 0;
393
+ reconnectCount = 0;
394
+ lastRequestHeaders = {};
395
+ constructor(config) {
396
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
397
+ this.entryPath = config.entryPath;
398
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
399
+ this.authHeaders = config.auth?.headers ?? {};
400
+ this.authToken = config.auth?.token;
401
+ this.publicMode = config.publicMode ?? false;
402
+ this.debug = config.debug ?? false;
403
+ this.clientInfo = config.clientInfo;
404
+ this.notificationHandler = config.notificationHandler;
405
+ this.interceptors = config.interceptors;
406
+ this.elicitationHandler = config.elicitationHandler;
407
+ }
408
+ async connect() {
409
+ this.state = "connecting";
410
+ this.connectionCount++;
411
+ try {
412
+ if (!this.publicMode && !this.authToken) {
413
+ await this.requestAnonymousToken();
414
+ }
415
+ await this.openStream();
416
+ this.state = "connected";
417
+ this.log(`Connected; messages go to ${this.messageEndpoint}`);
418
+ } catch (error) {
419
+ this.state = "error";
420
+ this.stream?.abort();
421
+ this.stream = void 0;
422
+ throw error;
423
+ }
424
+ }
425
+ async request(message) {
426
+ this.ensureConnected();
427
+ const startTime = Date.now();
428
+ if (this.interceptors) {
429
+ const intercepted = await this.interceptors.processRequest(message, {
430
+ timestamp: /* @__PURE__ */ new Date(),
431
+ transport: "sse",
432
+ sessionId: this.sessionId
433
+ });
434
+ switch (intercepted.type) {
435
+ case "mock":
436
+ return await this.interceptors.processResponse(
437
+ message,
438
+ intercepted.response,
439
+ Date.now() - startTime
440
+ );
441
+ case "error":
442
+ throw markInterceptedError(intercepted.error);
443
+ case "continue":
444
+ message = intercepted.request;
445
+ break;
446
+ }
447
+ }
448
+ let response;
449
+ if (message.id === void 0) {
450
+ await this.post(JSON.stringify(message));
451
+ response = { jsonrpc: "2.0", id: null, result: void 0 };
452
+ } else {
453
+ response = await this.postAndAwait(message.id, JSON.stringify(message));
454
+ }
455
+ if (this.interceptors) {
456
+ response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
457
+ }
458
+ return response;
459
+ }
460
+ async notify(message) {
461
+ this.ensureConnected();
462
+ const result = await this.post(JSON.stringify(message));
463
+ if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
464
+ }
465
+ async sendRaw(data) {
466
+ this.ensureConnected();
467
+ let id;
468
+ try {
469
+ const parsed = JSON.parse(data);
470
+ if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
471
+ } catch {
472
+ }
473
+ if (id !== void 0) return this.postAndAwait(id, data);
474
+ const result = await this.post(data);
475
+ if (!result.ok) {
476
+ return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
477
+ }
478
+ return { jsonrpc: "2.0", id: null, result: void 0 };
479
+ }
480
+ async close() {
481
+ this.closeStream("Transport closed");
482
+ this.state = "disconnected";
483
+ this.sessionId = void 0;
484
+ this.messageEndpoint = void 0;
485
+ this.log("SSE transport closed");
486
+ }
487
+ isConnected() {
488
+ return this.state === "connected";
489
+ }
490
+ getState() {
491
+ return this.state;
492
+ }
493
+ getSessionId() {
494
+ return this.sessionId;
495
+ }
496
+ setAuthToken(token) {
497
+ this.authToken = token;
498
+ }
499
+ setTimeout(ms) {
500
+ this.timeout = ms;
501
+ }
502
+ getMessageEndpoint() {
503
+ return this.messageEndpoint;
504
+ }
505
+ getConnectionCount() {
506
+ return this.connectionCount;
507
+ }
508
+ getReconnectCount() {
509
+ return this.reconnectCount;
510
+ }
511
+ getLastRequestHeaders() {
512
+ return { ...this.lastRequestHeaders };
513
+ }
514
+ setInterceptors(interceptors2) {
515
+ this.interceptors = interceptors2;
516
+ }
517
+ getInterceptors() {
518
+ return this.interceptors;
519
+ }
520
+ setElicitationHandler(handler) {
521
+ this.elicitationHandler = handler;
522
+ }
523
+ async simulateDisconnect() {
524
+ this.closeStream("Simulated disconnect");
525
+ this.state = "disconnected";
526
+ this.sessionId = void 0;
527
+ this.messageEndpoint = void 0;
528
+ }
529
+ async waitForReconnect(timeoutMs) {
530
+ this.reconnectCount++;
531
+ let timer;
532
+ try {
533
+ await Promise.race([
534
+ this.connect(),
535
+ new Promise((_, reject) => {
536
+ timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
537
+ })
538
+ ]);
539
+ } finally {
540
+ clearTimeout(timer);
541
+ }
542
+ }
543
+ // ═══════════════════════════════════════════════════════════════════
544
+ // STREAM
545
+ // ═══════════════════════════════════════════════════════════════════
546
+ /** Open `GET <entry>/sse` and wait for the `endpoint` event. */
547
+ async openStream() {
548
+ const controller = new AbortController();
549
+ this.stream = controller;
550
+ const url = this.sseUrl();
551
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
552
+ this.log(`GET ${url}`);
553
+ const connectTimer = setTimeout(() => controller.abort(), this.timeout);
554
+ let response;
555
+ try {
556
+ response = await fetch(url, {
557
+ method: "GET",
558
+ headers: { ...headers, Accept: "text/event-stream" },
559
+ signal: controller.signal
560
+ });
561
+ } catch (error) {
562
+ clearTimeout(connectTimer);
563
+ throw new Error(
564
+ isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
565
+ { cause: error }
566
+ );
567
+ }
568
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
569
+ if (!response.ok || !response.body || !isEventStream) {
570
+ clearTimeout(connectTimer);
571
+ const body = await response.text().catch(() => "");
572
+ throw new Error(
573
+ `SSE connection to ${url} failed: HTTP ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}. The legacy HTTP+SSE transport needs a server that serves it (@FrontMcp({ transport: { protocol: { legacy: true } } })).`
574
+ );
575
+ }
576
+ const endpoint = new Promise((resolve, reject) => {
577
+ const reader = response.body?.getReader();
578
+ if (!reader) {
579
+ reject(new Error(`SSE connection to ${url} has no body`));
580
+ return;
581
+ }
582
+ void this.readStream(controller, reader, resolve, reject);
583
+ });
584
+ try {
585
+ const data = await endpoint;
586
+ const messageUrl = new URL(data, url);
587
+ this.messageEndpoint = messageUrl.toString();
588
+ this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
589
+ } finally {
590
+ clearTimeout(connectTimer);
591
+ }
592
+ }
593
+ async readStream(controller, reader, onEndpoint, onEndpointError) {
594
+ const decoder = new TextDecoder();
595
+ let buffer = "";
596
+ let endpointSeen = false;
597
+ try {
598
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
599
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
600
+ buffer = remainder;
601
+ for (const text of completeEvents) {
602
+ const event = parseSSEEvent(text);
603
+ if (!event) continue;
604
+ if (event.type === "endpoint") {
605
+ if (!endpointSeen) {
606
+ endpointSeen = true;
607
+ onEndpoint(event.data);
608
+ }
609
+ continue;
610
+ }
611
+ await this.handleMessage(event.data);
612
+ }
613
+ }
614
+ } catch (error) {
615
+ if (!controller.signal.aborted) this.log("SSE stream failed:", error);
616
+ } finally {
617
+ reader.releaseLock();
618
+ }
619
+ if (!endpointSeen) {
620
+ onEndpointError(
621
+ new Error(
622
+ controller.signal.aborted ? `SSE stream at ${this.sseUrl()} sent no endpoint event within ${this.timeout}ms` : `SSE stream at ${this.sseUrl()} ended before the server sent its endpoint`
623
+ )
624
+ );
625
+ }
626
+ if (this.stream === controller) {
627
+ this.stream = void 0;
628
+ this.rejectPending("SSE stream closed by the server");
629
+ if (this.state === "connected") this.state = "disconnected";
630
+ }
631
+ }
632
+ async handleMessage(data) {
633
+ let message;
634
+ try {
635
+ message = JSON.parse(data);
636
+ } catch {
637
+ this.log("Ignoring SSE event that is not JSON:", data);
638
+ return;
639
+ }
640
+ if (!message || typeof message !== "object") return;
641
+ if (typeof message.method === "string") {
642
+ const request = message;
643
+ if (request.id === void 0 || request.id === null) {
644
+ this.notificationHandler?.(request);
645
+ return;
646
+ }
647
+ await this.answerServerRequest(request);
648
+ return;
649
+ }
650
+ if (message.id !== void 0 && message.id !== null) {
651
+ const pending = this.pending.get(message.id);
652
+ if (pending) {
653
+ clearTimeout(pending.timer);
654
+ this.pending.delete(message.id);
655
+ pending.resolve(message);
656
+ }
657
+ }
658
+ }
659
+ /** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
660
+ async answerServerRequest(request) {
661
+ const id = request.id;
662
+ let reply;
663
+ if (request.method === "elicitation/create") {
664
+ reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
665
+ } else if (request.method === "ping") {
666
+ reply = { jsonrpc: "2.0", id, result: {} };
667
+ } else {
668
+ reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
669
+ }
670
+ const result = await this.post(JSON.stringify(reply));
671
+ if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
672
+ }
673
+ async elicit(params) {
674
+ if (!this.elicitationHandler) return { action: "decline" };
675
+ try {
676
+ return await this.elicitationHandler(params);
677
+ } catch (error) {
678
+ this.log("Elicitation handler error:", error);
679
+ return { action: "cancel" };
680
+ }
681
+ }
682
+ closeStream(reason) {
683
+ const controller = this.stream;
684
+ this.stream = void 0;
685
+ controller?.abort();
686
+ this.rejectPending(reason);
687
+ }
688
+ rejectPending(reason) {
689
+ for (const [id, pending] of this.pending) {
690
+ clearTimeout(pending.timer);
691
+ pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
692
+ }
693
+ this.pending.clear();
694
+ }
695
+ // ═══════════════════════════════════════════════════════════════════
696
+ // POSTING
697
+ // ═══════════════════════════════════════════════════════════════════
698
+ /** POST a message and resolve with the response the stream carries for `id`. */
699
+ async postAndAwait(id, body) {
700
+ const answer = new Promise((resolve) => {
701
+ const timer = setTimeout(() => {
702
+ this.pending.delete(id);
703
+ resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
704
+ }, this.timeout);
705
+ this.pending.set(id, { resolve, timer });
706
+ });
707
+ const result = await this.post(body);
708
+ if (!result.ok) {
709
+ const pending = this.pending.get(id);
710
+ if (pending) {
711
+ clearTimeout(pending.timer);
712
+ this.pending.delete(id);
713
+ }
714
+ return {
715
+ jsonrpc: "2.0",
716
+ id,
717
+ error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
718
+ };
719
+ }
720
+ return answer;
721
+ }
722
+ async post(body) {
723
+ const endpoint = this.messageEndpoint;
724
+ if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
725
+ const headers = this.buildHeaders();
726
+ this.lastRequestHeaders = headers;
727
+ this.log(`POST ${endpoint}`, body);
728
+ const controller = new AbortController();
729
+ const timer = setTimeout(() => controller.abort(), this.timeout);
730
+ try {
731
+ const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
732
+ const text = await response.text();
733
+ return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
734
+ } catch (error) {
735
+ if (isAbortError(error)) {
736
+ return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
737
+ }
738
+ throw error;
739
+ } finally {
740
+ clearTimeout(timer);
741
+ }
742
+ }
743
+ // ═══════════════════════════════════════════════════════════════════
744
+ // HELPERS
745
+ // ═══════════════════════════════════════════════════════════════════
746
+ async requestAnonymousToken() {
747
+ const url = new URL(this.baseUrl);
748
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
749
+ url.search = "";
750
+ const tokenUrl = url.toString();
751
+ try {
752
+ const response = await fetch(tokenUrl, {
753
+ method: "POST",
754
+ headers: { "Content-Type": "application/json" },
755
+ body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
756
+ });
757
+ if (!response.ok) {
758
+ this.log(`No anonymous token (HTTP ${response.status})`);
759
+ return;
760
+ }
761
+ const token = await response.json();
762
+ if (token.access_token) this.authToken = token.access_token;
763
+ } catch (error) {
764
+ this.log("Error requesting anonymous token:", error);
765
+ }
766
+ }
767
+ /** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
768
+ sseUrl() {
769
+ const url = new URL(this.baseUrl);
770
+ const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
771
+ const basePath = url.pathname.replace(/\/+$/, "");
772
+ url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
773
+ return url.toString();
774
+ }
775
+ buildHeaders() {
776
+ const headers = {
777
+ "Content-Type": "application/json",
778
+ Accept: "application/json, text/event-stream"
779
+ };
780
+ if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
781
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
782
+ if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
783
+ Object.assign(headers, this.authHeaders);
784
+ return headers;
785
+ }
786
+ ensureConnected() {
787
+ if (this.state !== "connected") {
788
+ throw new Error("Transport not connected. Call connect() first.");
789
+ }
790
+ }
791
+ log(message, data) {
792
+ if (this.debug) {
793
+ console.log(`[SSE] ${message}`, data ?? "");
794
+ }
795
+ }
796
+ };
797
+ function splitSSEEvents(buffer) {
798
+ const endsWithCarriageReturn = buffer.endsWith("\r");
799
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
800
+ const completeEvents = normalized.split("\n\n");
801
+ const remainder = completeEvents.pop() ?? "";
802
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
803
+ }
804
+ function parseSSEEvent(text) {
805
+ let type = "message";
806
+ const data = [];
807
+ for (const line of text.split("\n")) {
808
+ if (line.startsWith(":")) continue;
809
+ const colon = line.indexOf(":");
810
+ const field = colon === -1 ? line : line.slice(0, colon);
811
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
812
+ if (field === "event") type = value;
813
+ else if (field === "data") data.push(value);
814
+ }
815
+ return data.length > 0 ? { type, data: data.join("\n") } : void 0;
816
+ }
817
+
818
+ // libs/testing/src/transport/streamable-http.transport.ts
819
+ var DEFAULT_TIMEOUT2 = 3e4;
346
820
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
347
821
  var StreamableHttpTransport = class {
348
822
  config;
@@ -364,7 +838,7 @@ var StreamableHttpTransport = class {
364
838
  baseUrl: config.baseUrl.replace(/\/$/, ""),
365
839
  // Remove trailing slash
366
840
  entryPath: config.entryPath,
367
- timeout: config.timeout ?? DEFAULT_TIMEOUT,
841
+ timeout: config.timeout ?? DEFAULT_TIMEOUT2,
368
842
  auth: config.auth ?? {},
369
843
  publicMode: config.publicMode ?? false,
370
844
  debug: config.debug ?? false,
@@ -449,7 +923,7 @@ var StreamableHttpTransport = class {
449
923
  return mockResponse2;
450
924
  }
451
925
  case "error":
452
- throw interceptResult.error;
926
+ throw markInterceptedError(interceptResult.error);
453
927
  case "continue":
454
928
  message = interceptResult.request;
455
929
  break;
@@ -510,7 +984,7 @@ var StreamableHttpTransport = class {
510
984
  return jsonResponse;
511
985
  } catch (error) {
512
986
  clearTimeout(timeoutId);
513
- if (error instanceof Error && error.name === "AbortError") {
987
+ if (isAbortError(error)) {
514
988
  return {
515
989
  jsonrpc: "2.0",
516
990
  id: message.id ?? null,
@@ -549,7 +1023,7 @@ var StreamableHttpTransport = class {
549
1023
  }
550
1024
  } catch (error) {
551
1025
  clearTimeout(timeoutId);
552
- if (error instanceof Error && error.name !== "AbortError") {
1026
+ if (!isAbortError(error)) {
553
1027
  throw error;
554
1028
  }
555
1029
  }
@@ -590,7 +1064,7 @@ var StreamableHttpTransport = class {
590
1064
  error: {
591
1065
  code: -32700,
592
1066
  message: "Parse error",
593
- data: error instanceof Error ? error.message : "Unknown error"
1067
+ data: errorMessage(error)
594
1068
  }
595
1069
  };
596
1070
  }
@@ -704,7 +1178,7 @@ var StreamableHttpTransport = class {
704
1178
  let buffer = "";
705
1179
  try {
706
1180
  for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
707
- const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1181
+ const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
708
1182
  buffer = remainder;
709
1183
  for (const eventText of completeEvents) {
710
1184
  for (const event of this.parseSSEEvents(eventText, void 0).events) {
@@ -960,7 +1434,7 @@ var StreamableHttpTransport = class {
960
1434
  if (this.config.clientInfo) {
961
1435
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
962
1436
  }
963
- if (this.authToken && !this.publicMode) {
1437
+ if (this.authToken) {
964
1438
  headers["Authorization"] = `Bearer ${this.authToken}`;
965
1439
  }
966
1440
  if (this.sessionId) {
@@ -1038,7 +1512,7 @@ var StreamableHttpTransport = class {
1038
1512
  };
1039
1513
  }
1040
1514
  };
1041
- function splitSSEEvents(buffer) {
1515
+ function splitSSEEvents2(buffer) {
1042
1516
  const endsWithCarriageReturn = buffer.endsWith("\r");
1043
1517
  const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1044
1518
  const completeEvents = normalized.split("\n\n");
@@ -1295,7 +1769,7 @@ var McpTestClientBuilder = class {
1295
1769
  };
1296
1770
 
1297
1771
  // libs/testing/src/client/mcp-test-client.ts
1298
- var DEFAULT_TIMEOUT2 = 3e4;
1772
+ var DEFAULT_TIMEOUT3 = 3e4;
1299
1773
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1300
1774
  var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1301
1775
  var DEFAULT_CLIENT_INFO = {
@@ -1303,6 +1777,15 @@ var DEFAULT_CLIENT_INFO = {
1303
1777
  version: "0.4.0"
1304
1778
  };
1305
1779
  var MAX_LIST_PAGES = 1e3;
1780
+ function isAuthFailure(error) {
1781
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1782
+ }
1783
+ function describeInitializeError(error) {
1784
+ const message = error?.message ?? "Unknown error";
1785
+ const data = error?.data;
1786
+ const detail = typeof data === "string" ? data.trim() : "";
1787
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1788
+ }
1306
1789
  function extractEntryPaths(data) {
1307
1790
  let payload = data;
1308
1791
  if (typeof payload === "string") {
@@ -1356,7 +1839,7 @@ var McpTestClient = class {
1356
1839
  transport: config.transport ?? "streamable-http",
1357
1840
  auth: config.auth ?? {},
1358
1841
  publicMode: config.publicMode ?? false,
1359
- timeout: config.timeout ?? DEFAULT_TIMEOUT2,
1842
+ timeout: config.timeout ?? DEFAULT_TIMEOUT3,
1360
1843
  debug: config.debug ?? false,
1361
1844
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
1362
1845
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
@@ -1389,19 +1872,22 @@ var McpTestClient = class {
1389
1872
  async connect() {
1390
1873
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1391
1874
  this.triedDiscoveredEntryPath = false;
1875
+ this.initResult = null;
1392
1876
  this.transport = this.createTransport();
1393
1877
  await this.transport.connect();
1394
1878
  const maxRetries = 3;
1395
1879
  const retryDelayMs = 500;
1396
1880
  let lastError;
1397
1881
  let reportedEntryPaths;
1882
+ let attemptsMade = 0;
1398
1883
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1884
+ attemptsMade = attempt;
1399
1885
  const initResponse = await this.initialize();
1400
1886
  if (initResponse.success && initResponse.data) {
1401
1887
  this.initResult = initResponse.data;
1402
1888
  break;
1403
1889
  }
1404
- lastError = initResponse.error?.message ?? "Unknown error";
1890
+ lastError = describeInitializeError(initResponse.error);
1405
1891
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1406
1892
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1407
1893
  this.triedDiscoveredEntryPath = true;
@@ -1413,6 +1899,7 @@ var McpTestClient = class {
1413
1899
  await this.transport.connect();
1414
1900
  continue;
1415
1901
  }
1902
+ if (isAuthFailure(initResponse.error)) break;
1416
1903
  if (attempt < maxRetries) {
1417
1904
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1418
1905
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1421,7 +1908,7 @@ var McpTestClient = class {
1421
1908
  if (!this.initResult) {
1422
1909
  const served = reportedEntryPaths?.length ? ` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.` : "";
1423
1910
  throw new Error(
1424
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1911
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1425
1912
  );
1426
1913
  }
1427
1914
  this._sessionId = this.transport.getSessionId();
@@ -1491,18 +1978,47 @@ var McpTestClient = class {
1491
1978
  return this._authState;
1492
1979
  }
1493
1980
  /**
1494
- * Authenticate with a token
1981
+ * Authenticate with a token.
1982
+ *
1983
+ * When connected, this opens a NEW session with the token (the previous session was created
1984
+ * for the previous identity) and rejects if the server refuses it — for example because the
1985
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
1986
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1495
1987
  */
1496
1988
  async authenticate(token) {
1989
+ if (typeof token !== "string" || token.trim() === "") {
1990
+ throw new Error("authenticate() requires a non-empty token");
1991
+ }
1992
+ const previousAuthState = this._authState;
1993
+ const previousConfigAuth = this.config.auth;
1994
+ this.config.auth = { ...previousConfigAuth, token };
1497
1995
  this._authState = {
1498
1996
  isAnonymous: false,
1499
1997
  token,
1500
1998
  scopes: this.parseScopesFromToken(token),
1501
1999
  user: this.parseUserFromToken(token)
1502
2000
  };
1503
- if (this.transport) {
1504
- this.transport.setAuthToken(token);
2001
+ const previousTransport = this.transport;
2002
+ if (!previousTransport) {
2003
+ this.log("debug", "Authentication updated (not connected yet)");
2004
+ return;
2005
+ }
2006
+ const previousInit = this.initResult;
2007
+ const previousSessionId = this._sessionId;
2008
+ const previousSessionInfo = this._sessionInfo;
2009
+ try {
2010
+ await this.connect();
2011
+ } catch (err) {
2012
+ await this.transport?.close().catch(() => void 0);
2013
+ this.transport = previousTransport;
2014
+ this.initResult = previousInit;
2015
+ this._sessionId = previousSessionId;
2016
+ this._sessionInfo = previousSessionInfo;
2017
+ this.config.auth = previousConfigAuth;
2018
+ this._authState = previousAuthState;
2019
+ throw err;
1505
2020
  }
2021
+ await previousTransport.close().catch(() => void 0);
1506
2022
  this.log("debug", "Authentication updated");
1507
2023
  }
1508
2024
  // ═══════════════════════════════════════════════════════════════════
@@ -2089,7 +2605,18 @@ var McpTestClient = class {
2089
2605
  notificationHandler: this.recordNotification
2090
2606
  });
2091
2607
  case "sse":
2092
- throw new Error("SSE transport not yet implemented");
2608
+ return new SseTransport({
2609
+ baseUrl,
2610
+ entryPath: this.config.entryPath,
2611
+ timeout: this.config.timeout,
2612
+ auth: this.config.auth,
2613
+ publicMode: this.config.publicMode,
2614
+ debug: this.config.debug,
2615
+ interceptors: this._interceptors,
2616
+ clientInfo: this.config.clientInfo,
2617
+ elicitationHandler: this._elicitationHandler,
2618
+ notificationHandler: this.recordNotification
2619
+ });
2093
2620
  default:
2094
2621
  throw new Error(`Unknown transport type: ${this.config.transport}`);
2095
2622
  }
@@ -2126,10 +2653,11 @@ var McpTestClient = class {
2126
2653
  requestId: id
2127
2654
  };
2128
2655
  } catch (err) {
2656
+ if (isInterceptedError(err)) throw err;
2129
2657
  const durationMs = Date.now() - start;
2130
2658
  const error = {
2131
2659
  code: -32603,
2132
- message: err instanceof Error ? err.message : "Unknown error"
2660
+ message: errorMessage(err)
2133
2661
  };
2134
2662
  return {
2135
2663
  success: false,
@@ -2277,12 +2805,12 @@ var McpTestClient = class {
2277
2805
  const payload = this.decodeJwtPayload(token);
2278
2806
  if (!payload) return [];
2279
2807
  const scope = payload["scope"];
2280
- const scopes = payload["scopes"];
2808
+ const scopes2 = payload["scopes"];
2281
2809
  if (typeof scope === "string") {
2282
2810
  return scope.split(" ");
2283
2811
  }
2284
- if (Array.isArray(scopes)) {
2285
- return scopes;
2812
+ if (Array.isArray(scopes2)) {
2813
+ return scopes2;
2286
2814
  }
2287
2815
  return [];
2288
2816
  } catch {
@@ -2414,14 +2942,15 @@ var TestTokenFactory = class {
2414
2942
  */
2415
2943
  async createTestToken(options) {
2416
2944
  await this.ensureKeys();
2417
- const now = Math.floor(Date.now() / 1e3);
2945
+ const nowMs = Date.now();
2946
+ const iat = Math.floor(nowMs / 1e3);
2418
2947
  const exp = options.exp ?? 3600;
2419
2948
  const payload = {
2420
2949
  iss: options.iss ?? this.issuer,
2421
2950
  sub: options.sub,
2422
2951
  aud: options.aud ?? this.audience,
2423
- iat: now,
2424
- exp: now + exp,
2952
+ iat,
2953
+ exp: Math.ceil(nowMs / 1e3) + exp,
2425
2954
  scope: options.scopes?.join(" "),
2426
2955
  ...options.claims
2427
2956
  };
@@ -2446,10 +2975,10 @@ var TestTokenFactory = class {
2446
2975
  /**
2447
2976
  * Create a regular user token
2448
2977
  */
2449
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
2978
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
2450
2979
  return this.createTestToken({
2451
2980
  sub,
2452
- scopes,
2981
+ scopes: scopes2,
2453
2982
  claims: {
2454
2983
  email: "user@test.local",
2455
2984
  name: "Test User",
@@ -2460,10 +2989,11 @@ var TestTokenFactory = class {
2460
2989
  /**
2461
2990
  * Create an anonymous user token
2462
2991
  */
2463
- async createAnonymousToken() {
2992
+ async createAnonymousToken(expiresIn) {
2464
2993
  return this.createTestToken({
2465
2994
  sub: `anon:${Date.now()}`,
2466
2995
  scopes: ["anonymous"],
2996
+ exp: expiresIn,
2467
2997
  claims: {
2468
2998
  name: "Anonymous",
2469
2999
  role: "anonymous"
@@ -2674,17 +3204,7 @@ function createTestUser(overrides) {
2674
3204
 
2675
3205
  // libs/testing/src/auth/mock-oauth-server.ts
2676
3206
  import { createServer } from "http";
2677
- var _randomBytes;
2678
- var _sha256Base64url;
2679
- var _base64urlEncode;
2680
- async function loadCryptoUtils() {
2681
- if (!_randomBytes) {
2682
- const utils = await import("@frontmcp/utils");
2683
- _randomBytes = utils.randomBytes;
2684
- _sha256Base64url = utils.sha256Base64url;
2685
- _base64urlEncode = utils.base64urlEncode;
2686
- }
2687
- }
3207
+ import { base64urlEncode, randomBytes, sha256Base64url } from "@frontmcp/utils";
2688
3208
  var MockOAuthServer = class {
2689
3209
  tokenFactory;
2690
3210
  options;
@@ -2699,8 +3219,8 @@ var MockOAuthServer = class {
2699
3219
  accessTokenTtlSeconds;
2700
3220
  /** Refresh token TTL in seconds */
2701
3221
  refreshTokenTtlSeconds;
2702
- constructor(tokenFactory3, options = {}) {
2703
- this.tokenFactory = tokenFactory3;
3222
+ constructor(tokenFactory2, options = {}) {
3223
+ this.tokenFactory = tokenFactory2;
2704
3224
  this.options = options;
2705
3225
  this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
2706
3226
  this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
@@ -2957,14 +3477,14 @@ ${error.stack}` : String(error);
2957
3477
  return;
2958
3478
  }
2959
3479
  const code = this.generateCode();
2960
- const scopes = scope.split(" ").filter(Boolean);
3480
+ const scopes2 = scope.split(" ").filter(Boolean);
2961
3481
  this.authCodes.set(code, {
2962
3482
  code,
2963
3483
  clientId,
2964
3484
  redirectUri,
2965
3485
  codeChallenge,
2966
3486
  codeChallengeMethod,
2967
- scopes,
3487
+ scopes: scopes2,
2968
3488
  user: testUser,
2969
3489
  state,
2970
3490
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3004,7 +3524,7 @@ ${error.stack}` : String(error);
3004
3524
  }
3005
3525
  const grantType = params.get("grant_type");
3006
3526
  if (grantType === "anonymous") {
3007
- const token = await this.tokenFactory.createAnonymousToken();
3527
+ const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
3008
3528
  res.writeHead(200, { "Content-Type": "application/json" });
3009
3529
  res.end(
3010
3530
  JSON.stringify({
@@ -3136,6 +3656,8 @@ ${error.stack}` : String(error);
3136
3656
  codeRecord.used = true;
3137
3657
  const accessToken = await this.tokenFactory.createTestToken({
3138
3658
  sub: codeRecord.user.sub,
3659
+ scopes: codeRecord.scopes,
3660
+ exp: this.accessTokenTtlSeconds,
3139
3661
  claims: {
3140
3662
  email: codeRecord.user.email,
3141
3663
  name: codeRecord.user.name,
@@ -3144,6 +3666,7 @@ ${error.stack}` : String(error);
3144
3666
  });
3145
3667
  const idToken = await this.tokenFactory.createTestToken({
3146
3668
  sub: codeRecord.user.sub,
3669
+ exp: this.accessTokenTtlSeconds,
3147
3670
  claims: {
3148
3671
  email: codeRecord.user.email,
3149
3672
  name: codeRecord.user.name,
@@ -3221,6 +3744,8 @@ ${error.stack}` : String(error);
3221
3744
  }
3222
3745
  const accessToken = await this.tokenFactory.createTestToken({
3223
3746
  sub: tokenRecord.user.sub,
3747
+ scopes: tokenRecord.scopes,
3748
+ exp: this.accessTokenTtlSeconds,
3224
3749
  claims: {
3225
3750
  email: tokenRecord.user.email,
3226
3751
  name: tokenRecord.user.name,
@@ -3325,14 +3850,14 @@ ${error.stack}` : String(error);
3325
3850
  name
3326
3851
  };
3327
3852
  const code = this.generateCode();
3328
- const scopes = scope.split(" ").filter(Boolean);
3853
+ const scopes2 = scope.split(" ").filter(Boolean);
3329
3854
  this.authCodes.set(code, {
3330
3855
  code,
3331
3856
  clientId,
3332
3857
  redirectUri,
3333
3858
  codeChallenge,
3334
3859
  codeChallengeMethod,
3335
- scopes,
3860
+ scopes: scopes2,
3336
3861
  user: testUser,
3337
3862
  state,
3338
3863
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3438,8 +3963,7 @@ ${error.stack}` : String(error);
3438
3963
  * Generate a random authorization code
3439
3964
  */
3440
3965
  async generateCodeAsync() {
3441
- await loadCryptoUtils();
3442
- return _base64urlEncode(_randomBytes(32));
3966
+ return base64urlEncode(randomBytes(32));
3443
3967
  }
3444
3968
  /**
3445
3969
  * Generate a random authorization code (sync wrapper for compatibility)
@@ -3455,8 +3979,7 @@ ${error.stack}` : String(error);
3455
3979
  */
3456
3980
  async computeCodeChallengeAsync(verifier, method) {
3457
3981
  if (method === "S256") {
3458
- await loadCryptoUtils();
3459
- return _sha256Base64url(verifier);
3982
+ return sha256Base64url(verifier);
3460
3983
  }
3461
3984
  return verifier;
3462
3985
  }
@@ -4043,7 +4566,7 @@ var MockCimdServer = class {
4043
4566
  };
4044
4567
 
4045
4568
  // libs/testing/src/server/test-server.ts
4046
- import { spawn } from "child_process";
4569
+ import { execFile, spawn } from "child_process";
4047
4570
  import { sha256Hex } from "@frontmcp/utils";
4048
4571
 
4049
4572
  // libs/testing/src/errors/index.ts
@@ -4150,6 +4673,8 @@ var E2E_PORT_RANGES = {
4150
4673
  "demo-e2e-distributed": { start: 50440, size: 20 },
4151
4674
  // Protocol revision E2E tests (50460-50479)
4152
4675
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
4676
+ // Testing library E2E tests (50480-50499)
4677
+ "demo-e2e-testing": { start: 50480, size: 20 },
4153
4678
  // Mock servers and utilities (50900-50999)
4154
4679
  "mock-oauth": { start: 50900, size: 10 },
4155
4680
  "mock-api": { start: 50910, size: 10 },
@@ -4158,6 +4683,66 @@ var E2E_PORT_RANGES = {
4158
4683
  default: { start: 51e3, size: 100 }
4159
4684
  };
4160
4685
  var reservedPorts = /* @__PURE__ */ new Map();
4686
+ var lockedPorts = /* @__PURE__ */ new Set();
4687
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
4688
+ function lockDir() {
4689
+ try {
4690
+ const os = __require("node:os");
4691
+ const path = __require("node:path");
4692
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
4693
+ } catch {
4694
+ return void 0;
4695
+ }
4696
+ }
4697
+ function pidIsAlive(pid) {
4698
+ try {
4699
+ process.kill(pid, 0);
4700
+ return true;
4701
+ } catch (error) {
4702
+ return error.code === "EPERM";
4703
+ }
4704
+ }
4705
+ function acquirePortLock(port) {
4706
+ if (lockedPorts.has(port)) return false;
4707
+ let fs;
4708
+ let file;
4709
+ try {
4710
+ fs = __require("node:fs");
4711
+ const dir = lockDir();
4712
+ if (!dir) return true;
4713
+ fs.mkdirSync(dir, { recursive: true });
4714
+ file = __require("node:path").join(dir, `${port}.lock`);
4715
+ } catch {
4716
+ return true;
4717
+ }
4718
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
4719
+ for (let attempt = 0; attempt < 2; attempt++) {
4720
+ try {
4721
+ fs.writeFileSync(file, payload, { flag: "wx" });
4722
+ lockedPorts.add(port);
4723
+ return true;
4724
+ } catch (error) {
4725
+ if (error.code !== "EEXIST") return true;
4726
+ try {
4727
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
4728
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
4729
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
4730
+ fs.unlinkSync(file);
4731
+ } catch {
4732
+ }
4733
+ }
4734
+ }
4735
+ return false;
4736
+ }
4737
+ function releasePortLock(port) {
4738
+ if (!lockedPorts.delete(port)) return;
4739
+ try {
4740
+ const fs = __require("node:fs");
4741
+ const dir = lockDir();
4742
+ if (dir) fs.unlinkSync(__require("node:path").join(dir, `${port}.lock`));
4743
+ } catch {
4744
+ }
4745
+ }
4161
4746
  var projectPortIndex = /* @__PURE__ */ new Map();
4162
4747
  function getPortRange(project) {
4163
4748
  const key = project;
@@ -4168,15 +4753,22 @@ function getPortRange(project) {
4168
4753
  }
4169
4754
  async function reservePort(project, preferredPort) {
4170
4755
  const range = getPortRange(project);
4171
- if (preferredPort !== void 0) {
4756
+ const handle = (port) => ({
4757
+ port,
4758
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
4759
+ releaseSocket: async () => {
4760
+ await releasePort(port);
4761
+ },
4762
+ release: async () => {
4763
+ await releasePort(port);
4764
+ releasePortLock(port);
4765
+ }
4766
+ });
4767
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
4768
+ if (wantsSpecificPort) {
4172
4769
  const reservation = await tryReservePort(preferredPort, project);
4173
4770
  if (reservation) {
4174
- return {
4175
- port: preferredPort,
4176
- release: async () => {
4177
- await releasePort(preferredPort);
4178
- }
4179
- };
4771
+ return handle(preferredPort);
4180
4772
  }
4181
4773
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
4182
4774
  }
@@ -4185,30 +4777,19 @@ async function reservePort(project, preferredPort) {
4185
4777
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
4186
4778
  const port = range.start + index % range.size;
4187
4779
  index = (index + 1) % range.size;
4188
- if (reservedPorts.has(port)) {
4780
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
4189
4781
  continue;
4190
4782
  }
4191
4783
  const reservation = await tryReservePort(port, project);
4192
4784
  if (reservation) {
4193
4785
  projectPortIndex.set(project, index);
4194
- return {
4195
- port,
4196
- release: async () => {
4197
- await releasePort(port);
4198
- }
4199
- };
4786
+ return handle(port);
4200
4787
  }
4201
4788
  }
4202
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
4203
- if (dynamicPort) {
4204
- const reservation = await tryReservePort(dynamicPort, project);
4205
- if (reservation) {
4206
- return {
4207
- port: dynamicPort,
4208
- release: async () => {
4209
- await releasePort(dynamicPort);
4210
- }
4211
- };
4789
+ for (let port = 51e3; port < 52e3; port++) {
4790
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
4791
+ if (await tryReservePort(port, project)) {
4792
+ return handle(port);
4212
4793
  }
4213
4794
  }
4214
4795
  throw new Error(
@@ -4216,9 +4797,13 @@ async function reservePort(project, preferredPort) {
4216
4797
  );
4217
4798
  }
4218
4799
  async function tryReservePort(port, project) {
4800
+ if (!acquirePortLock(port)) {
4801
+ return false;
4802
+ }
4219
4803
  return new Promise((resolve) => {
4220
4804
  const server = createServer4();
4221
4805
  server.once("error", () => {
4806
+ releasePortLock(port);
4222
4807
  resolve(false);
4223
4808
  });
4224
4809
  server.listen(port, () => {
@@ -4244,31 +4829,6 @@ async function releasePort(port) {
4244
4829
  });
4245
4830
  });
4246
4831
  }
4247
- async function findAvailablePortInRange(start, end) {
4248
- for (let port = start; port < end; port++) {
4249
- if (reservedPorts.has(port)) {
4250
- continue;
4251
- }
4252
- const available = await isPortAvailable(port);
4253
- if (available) {
4254
- return port;
4255
- }
4256
- }
4257
- return null;
4258
- }
4259
- async function isPortAvailable(port) {
4260
- return new Promise((resolve) => {
4261
- const server = createServer4();
4262
- server.once("error", () => {
4263
- resolve(false);
4264
- });
4265
- server.listen(port, () => {
4266
- server.close(() => {
4267
- resolve(true);
4268
- });
4269
- });
4270
- });
4271
- }
4272
4832
 
4273
4833
  // libs/testing/src/server/test-server.ts
4274
4834
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -4277,8 +4837,11 @@ var TestServer = class _TestServer {
4277
4837
  options;
4278
4838
  _info;
4279
4839
  logs = [];
4840
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
4280
4841
  portRelease = null;
4281
- constructor(options, port, portRelease) {
4842
+ /** Frees only the reservation socket, just before the server is spawned */
4843
+ portReleaseSocket = null;
4844
+ constructor(options, port, portRelease, portReleaseSocket) {
4282
4845
  this.options = {
4283
4846
  port,
4284
4847
  project: options.project,
@@ -4290,6 +4853,7 @@ var TestServer = class _TestServer {
4290
4853
  debug: options.debug ?? DEBUG_SERVER
4291
4854
  };
4292
4855
  this.portRelease = portRelease ?? null;
4856
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
4293
4857
  this._info = {
4294
4858
  baseUrl: `http://localhost:${port}`,
4295
4859
  port
@@ -4302,8 +4866,8 @@ var TestServer = class _TestServer {
4302
4866
  const project = options.project ?? "default";
4303
4867
  const maxAttempts = 3;
4304
4868
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4305
- const { port, release } = await reservePort(project, options.port);
4306
- const server = new _TestServer(options, port, release);
4869
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4870
+ const server = new _TestServer(options, port, release, releaseSocket);
4307
4871
  try {
4308
4872
  await server.startProcess();
4309
4873
  return server;
@@ -4343,7 +4907,7 @@ var TestServer = class _TestServer {
4343
4907
  }
4344
4908
  const maxAttempts = 3;
4345
4909
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4346
- const { port, release } = await reservePort(project, options.port);
4910
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4347
4911
  const serverOptions = {
4348
4912
  ...options,
4349
4913
  port,
@@ -4351,7 +4915,7 @@ var TestServer = class _TestServer {
4351
4915
  command: `npx nx serve ${project} --port ${port}`,
4352
4916
  cwd: options.cwd ?? process.cwd()
4353
4917
  };
4354
- const server = new _TestServer(serverOptions, port, release);
4918
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
4355
4919
  try {
4356
4920
  await server.startProcess();
4357
4921
  return server;
@@ -4409,10 +4973,17 @@ var TestServer = class _TestServer {
4409
4973
  * Stop the test server
4410
4974
  */
4411
4975
  async stop() {
4412
- if (this.portRelease) {
4413
- await this.portRelease();
4414
- this.portRelease = null;
4976
+ try {
4977
+ await this.stopProcess();
4978
+ } finally {
4979
+ if (this.portRelease) {
4980
+ await this.portRelease();
4981
+ this.portRelease = null;
4982
+ }
4983
+ this.portReleaseSocket = null;
4415
4984
  }
4985
+ }
4986
+ async stopProcess() {
4416
4987
  if (this.process) {
4417
4988
  this.log("Stopping server...");
4418
4989
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -4485,7 +5056,7 @@ var TestServer = class _TestServer {
4485
5056
  * Restart the server
4486
5057
  */
4487
5058
  async restart() {
4488
- await this.stop();
5059
+ await this.stopProcess();
4489
5060
  await this.startProcess();
4490
5061
  }
4491
5062
  /**
@@ -4515,9 +5086,9 @@ var TestServer = class _TestServer {
4515
5086
  PORT: String(this.options.port)
4516
5087
  };
4517
5088
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
4518
- if (this.portRelease) {
4519
- await this.portRelease();
4520
- this.portRelease = null;
5089
+ if (this.portReleaseSocket) {
5090
+ await this.portReleaseSocket();
5091
+ this.portReleaseSocket = null;
4521
5092
  await sleep2(300);
4522
5093
  }
4523
5094
  this.process = spawn(this.options.command, [], {
@@ -4527,9 +5098,8 @@ var TestServer = class _TestServer {
4527
5098
  detached: true,
4528
5099
  stdio: ["pipe", "pipe", "pipe"]
4529
5100
  });
4530
- if (this.process.pid !== void 0) {
4531
- this._info.pid = this.process.pid;
4532
- }
5101
+ this._info.pid = this.process.pid;
5102
+ this._info.shellPid = this.process.pid;
4533
5103
  let processExited = false;
4534
5104
  let exitCode = null;
4535
5105
  let exitError = null;
@@ -4632,6 +5202,8 @@ ${allLogs}`);
4632
5202
  });
4633
5203
  if (response.ok || response.status === 404) {
4634
5204
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
5205
+ const listeningPid = await findListeningPid(this.options.port);
5206
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
4635
5207
  return;
4636
5208
  }
4637
5209
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -4676,6 +5248,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
4676
5248
  function sleep2(ms) {
4677
5249
  return new Promise((resolve) => setTimeout(resolve, ms));
4678
5250
  }
5251
+ function findListeningPid(port) {
5252
+ if (process.platform === "win32") return Promise.resolve(void 0);
5253
+ return new Promise((resolve) => {
5254
+ execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
5255
+ if (error) {
5256
+ resolve(void 0);
5257
+ return;
5258
+ }
5259
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
5260
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
5261
+ });
5262
+ });
5263
+ }
4679
5264
  function withWorkspaceProtocolFallback(env, cwd) {
4680
5265
  if (findInstalledProtocolPackageDir(cwd)) {
4681
5266
  return env;
@@ -4947,16 +5532,6 @@ function hasMimeType(result, mimeType) {
4947
5532
  return result.hasMimeType(mimeType);
4948
5533
  }
4949
5534
 
4950
- // libs/testing/src/fixtures/test-fixture.ts
4951
- import {
4952
- afterAll as _afterAll,
4953
- afterEach as _afterEach,
4954
- beforeAll as _beforeAll,
4955
- beforeEach as _beforeEach,
4956
- describe as _describe,
4957
- it as _it
4958
- } from "@jest/globals";
4959
-
4960
5535
  // libs/testing/src/fixtures/gateway-token-binding.ts
4961
5536
  import { trimSlashes, trimTrailing } from "@frontmcp/utils";
4962
5537
  function gatewayTokenBinding(serverUrl, entryPath) {
@@ -4967,81 +5542,163 @@ function gatewayTokenBinding(serverUrl, entryPath) {
4967
5542
  }
4968
5543
 
4969
5544
  // libs/testing/src/fixtures/test-fixture.ts
4970
- var describe2 = _describe;
4971
- var beforeAll2 = _beforeAll;
4972
- var beforeEach2 = _beforeEach;
4973
- var afterEach2 = _afterEach;
4974
- var afterAll2 = _afterAll;
4975
- var it2 = _it;
4976
- var currentConfig = {};
4977
- var serverInstance = null;
4978
- var tokenFactory = null;
4979
- var serverStartedByUs = false;
4980
- var skipScopes = [{ skipped: false }];
4981
- function currentSkipScope() {
4982
- return skipScopes[skipScopes.length - 1];
5545
+ var jestApi;
5546
+ function jestGlobals() {
5547
+ if (jestApi) return jestApi;
5548
+ try {
5549
+ const api = __require("@jest/globals");
5550
+ jestApi = {
5551
+ describe: api["describe"],
5552
+ beforeAll: api["beforeAll"],
5553
+ beforeEach: api["beforeEach"],
5554
+ afterEach: api["afterEach"],
5555
+ afterAll: api["afterAll"],
5556
+ it: api["it"]
5557
+ };
5558
+ return jestApi;
5559
+ } catch (error) {
5560
+ throw new Error(
5561
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
5562
+ { cause: error }
5563
+ );
5564
+ }
4983
5565
  }
4984
- async function initializeSharedResources() {
4985
- if (!serverInstance) {
4986
- if (currentConfig.baseUrl && !currentConfig.server) {
4987
- serverInstance = TestServer.connect(currentConfig.baseUrl);
4988
- serverStartedByUs = false;
4989
- } else if (currentConfig.server) {
4990
- const serverCommand = resolveServerCommand(currentConfig.server);
4991
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
5566
+ function newScope(parent) {
5567
+ return {
5568
+ skipped: parent?.skipped ?? false,
5569
+ reason: parent?.reason,
5570
+ config: {},
5571
+ envs: [],
5572
+ configured: false,
5573
+ cleanupRegistered: false,
5574
+ beforeEachHooks: [],
5575
+ afterEachHooks: []
5576
+ };
5577
+ }
5578
+ var scopes = [newScope()];
5579
+ function currentScope() {
5580
+ return scopes[scopes.length - 1];
5581
+ }
5582
+ var serverEntries = /* @__PURE__ */ new Map();
5583
+ function resolveConfig(chain) {
5584
+ const merged = {};
5585
+ for (const scope of chain) {
5586
+ Object.assign(merged, scope.config);
5587
+ for (const env of scope.envs) merged.env = { ...merged.env, ...env };
5588
+ }
5589
+ return merged;
5590
+ }
5591
+ function ownerScopeOf(chain) {
5592
+ for (let i = chain.length - 1; i > 0; i--) {
5593
+ if (chain[i].configured) return chain[i];
5594
+ }
5595
+ return chain[0];
5596
+ }
5597
+ function serverKey(config) {
5598
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
5599
+ return JSON.stringify({
5600
+ server: config.server,
5601
+ baseUrl: config.baseUrl,
5602
+ project: config.project,
5603
+ port: config.port,
5604
+ entryPath: config.entryPath,
5605
+ startupTimeout: config.startupTimeout,
5606
+ logLevel: config.logLevel,
5607
+ auth: config.auth,
5608
+ env
5609
+ });
5610
+ }
5611
+ function serverEnv(config) {
5612
+ const extra = {};
5613
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
5614
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
5615
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
5616
+ return { ...extra, ...config.env };
5617
+ }
5618
+ async function startServerEntry(config, owner) {
5619
+ let server;
5620
+ let startedByUs = false;
5621
+ if (config.baseUrl && !config.server) {
5622
+ server = TestServer.connect(config.baseUrl);
5623
+ } else if (config.server) {
5624
+ const serverCommand = resolveServerCommand(config.server);
5625
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
5626
+ if (isDebug) {
5627
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
5628
+ }
5629
+ try {
5630
+ server = await TestServer.start({
5631
+ project: config.project,
5632
+ port: config.port,
5633
+ command: serverCommand,
5634
+ env: serverEnv(config),
5635
+ startupTimeout: config.startupTimeout ?? 3e4,
5636
+ debug: isDebug
5637
+ });
5638
+ startedByUs = true;
4992
5639
  if (isDebug) {
4993
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
5640
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
4994
5641
  }
4995
- try {
4996
- serverInstance = await TestServer.start({
4997
- project: currentConfig.project,
4998
- port: currentConfig.port,
4999
- command: serverCommand,
5000
- env: currentConfig.env,
5001
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
5002
- debug: isDebug
5003
- });
5004
- serverStartedByUs = true;
5005
- if (isDebug) {
5006
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
5007
- }
5008
- } catch (error) {
5009
- const errMsg = error instanceof Error ? error.message : String(error);
5010
- throw new Error(
5011
- `Failed to start test server.
5642
+ } catch (error) {
5643
+ const errMsg = error instanceof Error ? error.message : String(error);
5644
+ throw new Error(
5645
+ `Failed to start test server.
5012
5646
 
5013
- Server entry: ${currentConfig.server}
5014
- Project: ${currentConfig.project ?? "default"}
5647
+ Server entry: ${config.server}
5648
+ Project: ${config.project ?? "default"}
5015
5649
  Command: ${serverCommand}
5016
5650
 
5017
- Error: ${errMsg}`
5018
- );
5019
- }
5020
- } else {
5021
- throw new Error(
5022
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
5651
+ Error: ${errMsg}`,
5652
+ { cause: error }
5023
5653
  );
5024
5654
  }
5655
+ } else {
5656
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
5025
5657
  }
5026
- if (!tokenFactory) {
5027
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
5028
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
5029
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5030
- }
5658
+ const sharedSecret = config.env?.["JWT_SECRET"];
5659
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
5660
+ const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5661
+ return { server, startedByUs, tokenFactory: tokenFactory2, owner };
5031
5662
  }
5032
- async function createTestFixtures() {
5033
- await initializeSharedResources();
5034
- if (!serverInstance) {
5035
- throw new Error("Server instance not initialized");
5036
- }
5037
- if (!tokenFactory) {
5038
- throw new Error("Token factory not initialized");
5663
+ function acquireServer(chain) {
5664
+ const config = resolveConfig(chain);
5665
+ const key = serverKey(config);
5666
+ let entry = serverEntries.get(key);
5667
+ if (!entry) {
5668
+ entry = startServerEntry(config, ownerScopeOf(chain));
5669
+ serverEntries.set(key, entry);
5670
+ entry.catch(() => serverEntries.delete(key));
5671
+ }
5672
+ return entry;
5673
+ }
5674
+ async function stopServers(owner) {
5675
+ for (const [key, pending] of Array.from(serverEntries)) {
5676
+ let entry;
5677
+ try {
5678
+ entry = await pending;
5679
+ } catch {
5680
+ serverEntries.delete(key);
5681
+ continue;
5682
+ }
5683
+ if (owner && entry.owner !== owner) continue;
5684
+ serverEntries.delete(key);
5685
+ if (entry.startedByUs) {
5686
+ await entry.server.stop();
5687
+ }
5039
5688
  }
5689
+ }
5690
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
5691
+ async function buildFixtures(chain) {
5692
+ const config = resolveConfig(chain);
5693
+ const entry = await acquireServer(chain);
5694
+ const { server, tokenFactory: tokenFactory2 } = entry;
5695
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
5696
+ const clientConfig = { ...config, publicMode };
5040
5697
  const clientInstance = McpTestClient.create({
5041
- baseUrl: resolveClientBaseUrl(serverInstance),
5042
- entryPath: currentConfig.entryPath,
5043
- transport: currentConfig.transport ?? "streamable-http",
5044
- publicMode: currentConfig.publicMode
5698
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
5699
+ entryPath: clientConfig.entryPath,
5700
+ transport: clientConfig.transport ?? "streamable-http",
5701
+ publicMode
5045
5702
  }).build();
5046
5703
  try {
5047
5704
  await clientInstance.connect();
@@ -5055,17 +5712,18 @@ async function createTestFixtures() {
5055
5712
  throw err;
5056
5713
  }
5057
5714
  }
5058
- const auth = createAuthFixture(tokenFactory);
5059
- const server = createServerFixture(serverInstance);
5060
- return {
5715
+ const context = { extraClients: [] };
5716
+ const fixtures = {
5061
5717
  mcp: clientInstance,
5062
- auth,
5063
- server
5718
+ auth: createAuthFixture(tokenFactory2),
5719
+ server: createServerFixture(server, clientConfig, clientInstance, context)
5064
5720
  };
5721
+ fixtureContexts.set(fixtures, context);
5722
+ return fixtures;
5065
5723
  }
5066
5724
  async function cleanupTestFixtures(fixtures, testFailed = false) {
5067
- if (testFailed && serverInstance) {
5068
- const logs = serverInstance.getLogs();
5725
+ if (testFailed) {
5726
+ const logs = fixtures.server.getLogs();
5069
5727
  if (logs.length > 0) {
5070
5728
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
5071
5729
  const recentLogs = logs.slice(-50);
@@ -5076,20 +5734,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
5076
5734
  console.error("[TestFixture] === End Server Logs ===\n");
5077
5735
  }
5078
5736
  }
5737
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
5738
+ for (const client of extras) {
5739
+ if (client.isConnected()) {
5740
+ await client.disconnect().catch(() => void 0);
5741
+ }
5742
+ }
5079
5743
  if (fixtures.mcp.isConnected()) {
5080
5744
  await fixtures.mcp.disconnect();
5081
5745
  }
5082
5746
  }
5083
- async function cleanupSharedResources() {
5084
- if (serverInstance && serverStartedByUs) {
5085
- await serverInstance.stop();
5086
- }
5087
- serverInstance = null;
5088
- tokenFactory = null;
5089
- serverStartedByUs = false;
5090
- }
5091
- function resolveClientBaseUrl(server) {
5092
- return currentConfig.baseUrl ?? server.info.baseUrl;
5747
+ function resolveClientBaseUrl(server, config) {
5748
+ return config.baseUrl ?? server.info.baseUrl;
5093
5749
  }
5094
5750
  function createAuthFixture(factory) {
5095
5751
  const users = {
@@ -5135,28 +5791,39 @@ function createAuthFixture(factory) {
5135
5791
  getAudience: () => factory.getAudience()
5136
5792
  };
5137
5793
  }
5138
- function createServerFixture(server) {
5794
+ function createServerFixture(server, config, mcp, context) {
5139
5795
  return {
5140
- info: server.info,
5796
+ // Live view: `pid` is filled in once the server is listening and follows restarts
5797
+ get info() {
5798
+ return server.info;
5799
+ },
5141
5800
  createClient: async (opts) => {
5142
- return McpTestClient.create({
5143
- baseUrl: resolveClientBaseUrl(server),
5144
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
5801
+ const client = await McpTestClient.create({
5802
+ baseUrl: resolveClientBaseUrl(server, config),
5803
+ entryPath: opts?.entryPath ?? config.entryPath,
5145
5804
  transport: opts?.transport ?? "streamable-http",
5146
5805
  auth: opts?.token ? { token: opts.token } : void 0,
5147
5806
  clientInfo: opts?.clientInfo,
5148
- publicMode: currentConfig.publicMode
5807
+ publicMode: config.publicMode
5149
5808
  }).buildAndConnect();
5809
+ context.extraClients.push(client);
5810
+ return client;
5150
5811
  },
5151
5812
  createClientBuilder: () => {
5152
5813
  const builder = new McpTestClientBuilder({
5153
- baseUrl: resolveClientBaseUrl(server),
5154
- entryPath: currentConfig.entryPath,
5155
- publicMode: currentConfig.publicMode
5814
+ baseUrl: resolveClientBaseUrl(server, config),
5815
+ entryPath: config.entryPath,
5816
+ publicMode: config.publicMode
5156
5817
  });
5157
5818
  return builder;
5158
5819
  },
5159
- restart: () => server.restart(),
5820
+ restart: async () => {
5821
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
5822
+ await server.restart();
5823
+ for (const client of reconnect) {
5824
+ await client.reconnect();
5825
+ }
5826
+ },
5160
5827
  getLogs: () => server.getLogs(),
5161
5828
  clearLogs: () => server.clearLogs()
5162
5829
  };
@@ -5167,38 +5834,71 @@ function resolveServerCommand(server) {
5167
5834
  }
5168
5835
  return `npx tsx ${server}`;
5169
5836
  }
5170
- function runWithFixtures(fn) {
5171
- return async () => {
5172
- const fixtures = await createTestFixtures();
5173
- let testFailed = false;
5837
+ function runWithFixtures(fn, chain) {
5838
+ return async (...extra) => {
5839
+ const fixtures = await buildFixtures(chain);
5840
+ let failure;
5174
5841
  try {
5175
- await fn(fixtures);
5842
+ for (const scope of chain) {
5843
+ for (const hook of scope.beforeEachHooks) await hook(fixtures);
5844
+ }
5845
+ await fn(fixtures, ...extra);
5176
5846
  } catch (error) {
5177
- testFailed = true;
5178
- throw error;
5179
- } finally {
5180
- await cleanupTestFixtures(fixtures, testFailed);
5847
+ failure = { error };
5848
+ }
5849
+ try {
5850
+ await runAfterEachHooks(chain, fixtures);
5851
+ } catch (error) {
5852
+ failure ??= { error };
5181
5853
  }
5854
+ await cleanupTestFixtures(fixtures, failure !== void 0);
5855
+ if (failure) throw failure.error;
5182
5856
  };
5183
5857
  }
5858
+ async function runAfterEachHooks(chain, fixtures) {
5859
+ let firstError;
5860
+ let failed = false;
5861
+ for (let i = chain.length - 1; i >= 0; i--) {
5862
+ for (const hook of chain[i].afterEachHooks) {
5863
+ try {
5864
+ await hook(fixtures);
5865
+ } catch (error) {
5866
+ if (!failed) firstError = error;
5867
+ failed = true;
5868
+ }
5869
+ }
5870
+ }
5871
+ if (failed) throw firstError;
5872
+ }
5873
+ function skipTitle(name, scope) {
5874
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
5875
+ }
5184
5876
  function testWithFixtures(name, fn) {
5185
- const scope = currentSkipScope();
5877
+ const chain = scopes.slice();
5878
+ const scope = currentScope();
5186
5879
  if (scope.skipped) {
5187
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5880
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
5188
5881
  return;
5189
5882
  }
5190
- it2(name, runWithFixtures(fn));
5883
+ jestGlobals().it(name, runWithFixtures(fn, chain));
5191
5884
  }
5192
5885
  function use(config) {
5193
- currentConfig = { ...currentConfig, ...config };
5194
- afterAll2(async () => {
5195
- await cleanupSharedResources();
5196
- });
5886
+ const scope = currentScope();
5887
+ const { env, ...rest } = config;
5888
+ scope.config = { ...scope.config, ...rest };
5889
+ if (env) scope.envs.push(env);
5890
+ scope.configured = true;
5891
+ if (!scope.cleanupRegistered) {
5892
+ scope.cleanupRegistered = true;
5893
+ jestGlobals().afterAll(async () => {
5894
+ await stopServers(scope === scopes[0] ? void 0 : scope);
5895
+ });
5896
+ }
5197
5897
  }
5198
5898
  function skip(nameOrCondition, fnOrReason) {
5199
5899
  if (typeof nameOrCondition === "boolean") {
5200
5900
  if (nameOrCondition) {
5201
- const scope = currentSkipScope();
5901
+ const scope = currentScope();
5202
5902
  scope.skipped = true;
5203
5903
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
5204
5904
  }
@@ -5209,57 +5909,114 @@ function skip(nameOrCondition, fnOrReason) {
5209
5909
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
5210
5910
  );
5211
5911
  }
5212
- it2.skip(nameOrCondition, runWithFixtures(fnOrReason));
5912
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
5213
5913
  }
5214
5914
  function only(name, fn) {
5215
- const scope = currentSkipScope();
5915
+ const scope = currentScope();
5216
5916
  if (scope.skipped) {
5217
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5917
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
5218
5918
  return;
5219
5919
  }
5220
- it2.only(name, runWithFixtures(fn));
5920
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
5221
5921
  }
5222
5922
  function todo(name) {
5223
- it2.todo(name);
5923
+ jestGlobals().it.todo(name);
5224
5924
  }
5225
- function withSkipScope(register) {
5925
+ function each(table, ...tagged) {
5926
+ const chain = scopes.slice();
5927
+ const register = jestGlobals().it.each(table, ...tagged);
5226
5928
  return (name, fn) => {
5227
- register(name, () => {
5228
- const parent = currentSkipScope();
5229
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
5230
- try {
5231
- fn();
5232
- } finally {
5233
- skipScopes.pop();
5234
- }
5235
- });
5929
+ const scope = currentScope();
5930
+ if (scope.skipped) {
5931
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
5932
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
5933
+ return;
5934
+ }
5935
+ register(name, runWithFixtures(fn, chain));
5236
5936
  };
5237
5937
  }
5238
- function eachWithSkipScope(each) {
5239
- return ((...eachArgs) => {
5240
- const register = each(...eachArgs);
5241
- return withSkipScope(register);
5242
- });
5938
+ function withScope(fn) {
5939
+ return (...args) => {
5940
+ scopes.push(newScope(currentScope()));
5941
+ try {
5942
+ fn(...args);
5943
+ } finally {
5944
+ scopes.pop();
5945
+ }
5946
+ };
5947
+ }
5948
+ function scopedRegister(pick) {
5949
+ return (name, fn, timeout) => {
5950
+ pick()(name, withScope(fn), timeout);
5951
+ };
5952
+ }
5953
+ function scopedEach(pick) {
5954
+ return (...eachArgs) => (name, fn, timeout) => {
5955
+ const register = pick()(...eachArgs);
5956
+ register(name, withScope(fn), timeout);
5957
+ };
5958
+ }
5959
+ var describeWithScope = Object.assign(
5960
+ scopedRegister(() => jestGlobals().describe),
5961
+ {
5962
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
5963
+ only: Object.assign(
5964
+ scopedRegister(() => jestGlobals().describe.only),
5965
+ {
5966
+ each: scopedEach(() => jestGlobals().describe.only.each)
5967
+ }
5968
+ ),
5969
+ skip: Object.assign(
5970
+ scopedRegister(() => jestGlobals().describe.skip),
5971
+ {
5972
+ each: scopedEach(() => jestGlobals().describe.skip.each)
5973
+ }
5974
+ ),
5975
+ each: scopedEach(() => jestGlobals().describe.each)
5976
+ }
5977
+ );
5978
+ function eachHook(kind) {
5979
+ return (fn, timeout) => {
5980
+ if (typeof fn === "function" && fn.length > 0) {
5981
+ const scope = currentScope();
5982
+ (kind === "beforeEach" ? scope.beforeEachHooks : scope.afterEachHooks).push(fn);
5983
+ return;
5984
+ }
5985
+ jestGlobals()[kind](fn, timeout);
5986
+ };
5243
5987
  }
5244
- var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
5245
- only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
5246
- skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
5247
- each: eachWithSkipScope(describe2.each)
5248
- });
5249
5988
  var test = testWithFixtures;
5250
5989
  test.use = use;
5251
- test.describe = describeWithSkipScope;
5252
- test.beforeAll = beforeAll2;
5253
- test.beforeEach = beforeEach2;
5254
- test.afterEach = afterEach2;
5255
- test.afterAll = afterAll2;
5990
+ test.describe = describeWithScope;
5991
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
5992
+ test.beforeEach = eachHook("beforeEach");
5993
+ test.afterEach = eachHook("afterEach");
5994
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
5256
5995
  test.skip = skip;
5257
5996
  test.only = only;
5258
5997
  test.todo = todo;
5998
+ test.each = each;
5259
5999
 
5260
6000
  // libs/testing/src/expect.ts
5261
- import { expect as jestExpect } from "@jest/globals";
5262
- var expect = jestExpect;
6001
+ var resolvedExpect;
6002
+ function resolveJestExpect() {
6003
+ if (resolvedExpect) return resolvedExpect;
6004
+ try {
6005
+ resolvedExpect = __require("@jest/globals").expect;
6006
+ return resolvedExpect;
6007
+ } catch (error) {
6008
+ throw new Error(
6009
+ `expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
6010
+ { cause: error }
6011
+ );
6012
+ }
6013
+ }
6014
+ var expect = new Proxy(function frontmcpExpect() {
6015
+ }, {
6016
+ apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
6017
+ get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
6018
+ has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
6019
+ });
5263
6020
 
5264
6021
  // libs/testing/src/platform/platform-types.ts
5265
6022
  function getPlatformMetaNamespace(platform) {
@@ -5922,34 +6679,44 @@ var HttpInterceptorImpl = class {
5922
6679
  }
5923
6680
  };
5924
6681
  }
5925
- get(url, response) {
6682
+ get(url, response, options) {
5926
6683
  return this.mock({
5927
- match: { url, method: "GET" },
5928
- response: normalizeResponse(response)
6684
+ match: { url, method: "GET", ...options?.match },
6685
+ response: normalizeResponse(response),
6686
+ ...options?.times !== void 0 && { times: options.times },
6687
+ ...options?.name !== void 0 && { name: options.name }
5929
6688
  });
5930
6689
  }
5931
- post(url, response) {
6690
+ post(url, response, options) {
5932
6691
  return this.mock({
5933
- match: { url, method: "POST" },
5934
- response: normalizeResponse(response)
6692
+ match: { url, method: "POST", ...options?.match },
6693
+ response: normalizeResponse(response),
6694
+ ...options?.times !== void 0 && { times: options.times },
6695
+ ...options?.name !== void 0 && { name: options.name }
5935
6696
  });
5936
6697
  }
5937
- put(url, response) {
6698
+ put(url, response, options) {
5938
6699
  return this.mock({
5939
- match: { url, method: "PUT" },
5940
- response: normalizeResponse(response)
6700
+ match: { url, method: "PUT", ...options?.match },
6701
+ response: normalizeResponse(response),
6702
+ ...options?.times !== void 0 && { times: options.times },
6703
+ ...options?.name !== void 0 && { name: options.name }
5941
6704
  });
5942
6705
  }
5943
- delete(url, response) {
6706
+ delete(url, response, options) {
5944
6707
  return this.mock({
5945
- match: { url, method: "DELETE" },
5946
- response: normalizeResponse(response)
6708
+ match: { url, method: "DELETE", ...options?.match },
6709
+ response: normalizeResponse(response),
6710
+ ...options?.times !== void 0 && { times: options.times },
6711
+ ...options?.name !== void 0 && { name: options.name }
5947
6712
  });
5948
6713
  }
5949
- any(url, response) {
6714
+ any(url, response, options) {
5950
6715
  return this.mock({
5951
- match: { url },
5952
- response: normalizeResponse(response)
6716
+ match: { url, ...options?.match },
6717
+ response: normalizeResponse(response),
6718
+ ...options?.times !== void 0 && { times: options.times },
6719
+ ...options?.name !== void 0 && { name: options.name }
5953
6720
  });
5954
6721
  }
5955
6722
  clear() {
@@ -5981,6 +6748,9 @@ ${descriptions.join("\n")}`);
5981
6748
  if (index !== -1) {
5982
6749
  activeInterceptors.splice(index, 1);
5983
6750
  }
6751
+ if (activeInterceptors.length === 0) {
6752
+ httpMock.disable();
6753
+ }
5984
6754
  }
5985
6755
  // Internal methods
5986
6756
  isActive() {
@@ -6080,43 +6850,49 @@ ${descriptions.join("\n")}`);
6080
6850
  }
6081
6851
  };
6082
6852
  async function interceptedFetch(input, init) {
6083
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
6084
- const method = (init?.method ?? "GET").toUpperCase();
6853
+ const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
6854
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
6855
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
6085
6856
  const headers = {};
6086
- if (init?.headers) {
6087
- if (init.headers instanceof Headers) {
6088
- init.headers.forEach((value, key) => {
6857
+ const collectHeaders = (source) => {
6858
+ if (!source) return;
6859
+ if (source instanceof Headers) {
6860
+ source.forEach((value, key) => {
6089
6861
  headers[key.toLowerCase()] = value;
6090
6862
  });
6091
- } else if (Array.isArray(init.headers)) {
6092
- for (const [key, value] of init.headers) {
6863
+ } else if (Array.isArray(source)) {
6864
+ for (const [key, value] of source) {
6093
6865
  headers[key.toLowerCase()] = value;
6094
6866
  }
6095
6867
  } else {
6096
- for (const [key, value] of Object.entries(init.headers)) {
6868
+ for (const [key, value] of Object.entries(source)) {
6097
6869
  headers[key.toLowerCase()] = value;
6098
6870
  }
6099
6871
  }
6100
- }
6872
+ };
6873
+ collectHeaders(init?.headers ?? request?.headers);
6101
6874
  let body;
6102
6875
  let rawBody;
6103
- if (init?.body) {
6104
- if (typeof init.body === "string") {
6105
- rawBody = init.body;
6106
- try {
6107
- body = JSON.parse(init.body);
6108
- } catch {
6109
- body = init.body;
6110
- }
6111
- } else if (init.body instanceof ArrayBuffer || init.body instanceof Uint8Array) {
6112
- rawBody = new TextDecoder().decode(init.body);
6876
+ let bodySource = init?.body;
6877
+ if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
6878
+ bodySource = await request.clone().text();
6879
+ }
6880
+ if (bodySource) {
6881
+ if (typeof bodySource === "string") {
6882
+ rawBody = bodySource;
6883
+ } else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
6884
+ rawBody = new TextDecoder().decode(bodySource);
6885
+ } else if (bodySource instanceof URLSearchParams) {
6886
+ rawBody = bodySource.toString();
6887
+ }
6888
+ if (rawBody !== void 0) {
6113
6889
  try {
6114
6890
  body = JSON.parse(rawBody);
6115
6891
  } catch {
6116
6892
  body = rawBody;
6117
6893
  }
6118
6894
  } else {
6119
- body = init.body;
6895
+ body = bodySource;
6120
6896
  }
6121
6897
  }
6122
6898
  const requestInfo = {
@@ -6146,8 +6922,20 @@ async function interceptedFetch(input, init) {
6146
6922
  Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
6147
6923
  );
6148
6924
  }
6925
+ var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
6926
+ function isResponseDescriptor(value) {
6927
+ const keys = Object.keys(value);
6928
+ if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
6929
+ if ("status" in value) {
6930
+ const status = value["status"];
6931
+ if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
6932
+ }
6933
+ return true;
6934
+ }
6149
6935
  function normalizeResponse(response) {
6150
- if (!("status" in response) && !("body" in response) && !("headers" in response)) {
6936
+ const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
6937
+ if (isNetworkError) return response;
6938
+ if (Array.isArray(response) || !isResponseDescriptor(response)) {
6151
6939
  return { body: response };
6152
6940
  }
6153
6941
  return response;
@@ -7481,47 +8269,48 @@ function clearGlobalMeasurements() {
7481
8269
  }
7482
8270
 
7483
8271
  // libs/testing/src/perf/perf-test.ts
7484
- var currentConfig2 = {};
7485
- var serverInstance2 = null;
7486
- var tokenFactory2 = null;
7487
- var serverStartedByUs2 = false;
7488
- async function initializeSharedResources2() {
7489
- if (!tokenFactory2) {
7490
- tokenFactory2 = new TestTokenFactory();
7491
- }
7492
- if (!serverInstance2) {
7493
- if (currentConfig2.baseUrl) {
7494
- serverInstance2 = TestServer.connect(currentConfig2.baseUrl);
7495
- serverStartedByUs2 = false;
7496
- } else if (currentConfig2.server) {
7497
- const serverCommand = resolveServerCommand2(currentConfig2.server);
8272
+ var currentConfig = {};
8273
+ var serverInstance = null;
8274
+ var tokenFactory = null;
8275
+ var serverStartedByUs = false;
8276
+ async function initializeSharedResources() {
8277
+ if (!tokenFactory) {
8278
+ tokenFactory = new TestTokenFactory();
8279
+ }
8280
+ if (!serverInstance) {
8281
+ if (currentConfig.baseUrl) {
8282
+ serverInstance = TestServer.connect(currentConfig.baseUrl);
8283
+ serverStartedByUs = false;
8284
+ } else if (currentConfig.server) {
8285
+ const serverCommand = resolveServerCommand2(currentConfig.server);
7498
8286
  const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
7499
8287
  if (isDebug) {
7500
8288
  console.log(`[PerfTest] Starting server: ${serverCommand}`);
7501
8289
  }
7502
8290
  try {
7503
- serverInstance2 = await TestServer.start({
7504
- project: currentConfig2.project,
7505
- port: currentConfig2.port,
8291
+ serverInstance = await TestServer.start({
8292
+ project: currentConfig.project,
8293
+ port: currentConfig.port,
7506
8294
  command: serverCommand,
7507
- env: currentConfig2.env,
7508
- startupTimeout: currentConfig2.startupTimeout ?? 3e4,
8295
+ env: currentConfig.env,
8296
+ startupTimeout: currentConfig.startupTimeout ?? 3e4,
7509
8297
  debug: isDebug
7510
8298
  });
7511
- serverStartedByUs2 = true;
8299
+ serverStartedByUs = true;
7512
8300
  if (isDebug) {
7513
- console.log(`[PerfTest] Server started at ${serverInstance2.info.baseUrl}`);
8301
+ console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
7514
8302
  }
7515
8303
  } catch (error) {
7516
8304
  const errMsg = error instanceof Error ? error.message : String(error);
7517
8305
  throw new Error(
7518
8306
  `Failed to start test server.
7519
8307
 
7520
- Server entry: ${currentConfig2.server}
7521
- Project: ${currentConfig2.project ?? "default"}
8308
+ Server entry: ${currentConfig.server}
8309
+ Project: ${currentConfig.project ?? "default"}
7522
8310
  Command: ${serverCommand}
7523
8311
 
7524
- Error: ${errMsg}`
8312
+ Error: ${errMsg}`,
8313
+ { cause: error }
7525
8314
  );
7526
8315
  }
7527
8316
  } else {
@@ -7529,23 +8318,23 @@ Error: ${errMsg}`
7529
8318
  }
7530
8319
  }
7531
8320
  }
7532
- async function createTestFixtures2(testName) {
7533
- await initializeSharedResources2();
7534
- if (!serverInstance2) {
8321
+ async function createTestFixtures(testName) {
8322
+ await initializeSharedResources();
8323
+ if (!serverInstance) {
7535
8324
  throw new Error("Server instance not initialized");
7536
8325
  }
7537
- if (!tokenFactory2) {
8326
+ if (!tokenFactory) {
7538
8327
  throw new Error("Token factory not initialized");
7539
8328
  }
7540
8329
  const clientInstance = await McpTestClient.create({
7541
- baseUrl: serverInstance2.info.baseUrl,
7542
- transport: currentConfig2.transport ?? "streamable-http",
7543
- publicMode: currentConfig2.publicMode
8330
+ baseUrl: serverInstance.info.baseUrl,
8331
+ transport: currentConfig.transport ?? "streamable-http",
8332
+ publicMode: currentConfig.publicMode
7544
8333
  }).buildAndConnect();
7545
- const auth = createAuthFixture2(tokenFactory2);
7546
- const server = createServerFixture2(serverInstance2);
7547
- const perfImpl = createPerfFixtures(testName, currentConfig2.project ?? "unknown");
7548
- if (currentConfig2.forceGcOnBaseline !== false) {
8334
+ const auth = createAuthFixture2(tokenFactory);
8335
+ const server = createServerFixture2(serverInstance);
8336
+ const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
8337
+ if (currentConfig.forceGcOnBaseline !== false) {
7549
8338
  await perfImpl.baseline();
7550
8339
  }
7551
8340
  return {
@@ -7561,8 +8350,8 @@ async function createTestFixtures2(testName) {
7561
8350
  async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7562
8351
  const measurement = perfImpl.buildMeasurement();
7563
8352
  addGlobalMeasurement(measurement);
7564
- if (testFailed && serverInstance2) {
7565
- const logs = serverInstance2.getLogs();
8353
+ if (testFailed && serverInstance) {
8354
+ const logs = serverInstance.getLogs();
7566
8355
  if (logs.length > 0) {
7567
8356
  console.error("\n[PerfTest] === Server Logs (test failed) ===");
7568
8357
  const recentLogs = logs.slice(-50);
@@ -7577,13 +8366,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7577
8366
  await fixtures.mcp.disconnect();
7578
8367
  }
7579
8368
  }
7580
- async function cleanupSharedResources2() {
7581
- if (serverInstance2 && serverStartedByUs2) {
7582
- await serverInstance2.stop();
8369
+ async function cleanupSharedResources() {
8370
+ if (serverInstance && serverStartedByUs) {
8371
+ await serverInstance.stop();
7583
8372
  }
7584
- serverInstance2 = null;
7585
- tokenFactory2 = null;
7586
- serverStartedByUs2 = false;
8373
+ serverInstance = null;
8374
+ tokenFactory = null;
8375
+ serverStartedByUs = false;
7587
8376
  }
7588
8377
  function createAuthFixture2(factory) {
7589
8378
  const users = {
@@ -7638,13 +8427,13 @@ function createServerFixture2(server) {
7638
8427
  transport: opts?.transport ?? "streamable-http",
7639
8428
  auth: opts?.token ? { token: opts.token } : void 0,
7640
8429
  clientInfo: opts?.clientInfo,
7641
- publicMode: currentConfig2.publicMode
8430
+ publicMode: currentConfig.publicMode
7642
8431
  }).buildAndConnect();
7643
8432
  },
7644
8433
  createClientBuilder: () => {
7645
8434
  return new McpTestClientBuilder({
7646
8435
  baseUrl: server.info.baseUrl,
7647
- publicMode: currentConfig2.publicMode
8436
+ publicMode: currentConfig.publicMode
7648
8437
  });
7649
8438
  },
7650
8439
  restart: () => server.restart(),
@@ -7660,7 +8449,7 @@ function resolveServerCommand2(server) {
7660
8449
  }
7661
8450
  function perfTestWithFixtures(name, fn) {
7662
8451
  it(name, async () => {
7663
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8452
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7664
8453
  let testFailed = false;
7665
8454
  try {
7666
8455
  await fn(fixtures);
@@ -7673,14 +8462,14 @@ function perfTestWithFixtures(name, fn) {
7673
8462
  });
7674
8463
  }
7675
8464
  function use2(config) {
7676
- currentConfig2 = { ...currentConfig2, ...config };
8465
+ currentConfig = { ...currentConfig, ...config };
7677
8466
  afterAll(async () => {
7678
- await cleanupSharedResources2();
8467
+ await cleanupSharedResources();
7679
8468
  });
7680
8469
  }
7681
8470
  function skip2(name, fn) {
7682
8471
  it.skip(name, async () => {
7683
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8472
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7684
8473
  let testFailed = false;
7685
8474
  try {
7686
8475
  await fn(fixtures);
@@ -7694,7 +8483,7 @@ function skip2(name, fn) {
7694
8483
  }
7695
8484
  function only2(name, fn) {
7696
8485
  it.only(name, async () => {
7697
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8486
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7698
8487
  let testFailed = false;
7699
8488
  try {
7700
8489
  await fn(fixtures);
@@ -7711,16 +8500,19 @@ function todo2(name) {
7711
8500
  }
7712
8501
  var perfTest = perfTestWithFixtures;
7713
8502
  perfTest.use = use2;
7714
- perfTest.describe = describe;
7715
- perfTest.beforeAll = beforeAll;
7716
- perfTest.beforeEach = beforeEach;
7717
- perfTest.afterEach = afterEach;
7718
- perfTest.afterAll = afterAll;
8503
+ for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
8504
+ Object.defineProperty(perfTest, hook, {
8505
+ get: () => globalThis[hook],
8506
+ enumerable: true,
8507
+ configurable: true
8508
+ });
8509
+ }
7719
8510
  perfTest.skip = skip2;
7720
8511
  perfTest.only = only2;
7721
8512
  perfTest.todo = todo2;
7722
8513
 
7723
8514
  // libs/testing/src/perf/baseline-store.ts
8515
+ import { ensureDir, fileExists, readFile, writeFile } from "@frontmcp/utils";
7724
8516
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
7725
8517
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
7726
8518
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -7735,7 +8527,6 @@ var BaselineStore = class {
7735
8527
  */
7736
8528
  async load() {
7737
8529
  try {
7738
- const { readFile, fileExists } = await import("@frontmcp/utils");
7739
8530
  if (!await fileExists(this.baselinePath)) {
7740
8531
  return null;
7741
8532
  }
@@ -7750,7 +8541,6 @@ var BaselineStore = class {
7750
8541
  * Save baseline to local file.
7751
8542
  */
7752
8543
  async save(baseline) {
7753
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
7754
8544
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
7755
8545
  await ensureDir(dir);
7756
8546
  await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
@@ -8006,6 +8796,7 @@ function getRegressionDetector(config) {
8006
8796
  }
8007
8797
 
8008
8798
  // libs/testing/src/perf/report-generator.ts
8799
+ import { ensureDir as ensureDir2, writeFile as writeFile2 } from "@frontmcp/utils";
8009
8800
  var ReportGenerator = class {
8010
8801
  detector;
8011
8802
  constructor() {
@@ -8261,14 +9052,13 @@ var ReportGenerator = class {
8261
9052
  }
8262
9053
  };
8263
9054
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
8264
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
8265
- await ensureDir(outputDir);
9055
+ await ensureDir2(outputDir);
8266
9056
  const generator = new ReportGenerator();
8267
9057
  const report = generator.generateReport(measurements, baseline, gitInfo);
8268
9058
  const jsonPath = `${outputDir}/report.json`;
8269
9059
  const markdownPath = `${outputDir}/report.md`;
8270
- await writeFile(jsonPath, generator.generateJsonReport(report));
8271
- await writeFile(markdownPath, generator.generateMarkdownReport(report));
9060
+ await writeFile2(jsonPath, generator.generateJsonReport(report));
9061
+ await writeFile2(markdownPath, generator.generateMarkdownReport(report));
8272
9062
  return { jsonPath, markdownPath };
8273
9063
  }
8274
9064
  function createReportGenerator() {
@@ -8329,6 +9119,7 @@ export {
8329
9119
  RegressionDetector,
8330
9120
  ReportGenerator,
8331
9121
  ServerStartError,
9122
+ SseTransport,
8332
9123
  StreamableHttpTransport,
8333
9124
  TestClientError,
8334
9125
  TestServer,