@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/fixtures/index.js CHANGED
@@ -28,9 +28,6 @@ __export(fixtures_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(fixtures_exports);
30
30
 
31
- // libs/testing/src/fixtures/test-fixture.ts
32
- var import_globals = require("@jest/globals");
33
-
34
31
  // libs/testing/src/auth/token-factory.ts
35
32
  var import_jose = require("jose");
36
33
  var TestTokenFactory = class {
@@ -89,14 +86,15 @@ var TestTokenFactory = class {
89
86
  */
90
87
  async createTestToken(options) {
91
88
  await this.ensureKeys();
92
- const now = Math.floor(Date.now() / 1e3);
89
+ const nowMs = Date.now();
90
+ const iat = Math.floor(nowMs / 1e3);
93
91
  const exp = options.exp ?? 3600;
94
92
  const payload = {
95
93
  iss: options.iss ?? this.issuer,
96
94
  sub: options.sub,
97
95
  aud: options.aud ?? this.audience,
98
- iat: now,
99
- exp: now + exp,
96
+ iat,
97
+ exp: Math.ceil(nowMs / 1e3) + exp,
100
98
  scope: options.scopes?.join(" "),
101
99
  ...options.claims
102
100
  };
@@ -121,10 +119,10 @@ var TestTokenFactory = class {
121
119
  /**
122
120
  * Create a regular user token
123
121
  */
124
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
122
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
125
123
  return this.createTestToken({
126
124
  sub,
127
- scopes,
125
+ scopes: scopes2,
128
126
  claims: {
129
127
  email: "user@test.local",
130
128
  name: "Test User",
@@ -135,10 +133,11 @@ var TestTokenFactory = class {
135
133
  /**
136
134
  * Create an anonymous user token
137
135
  */
138
- async createAnonymousToken() {
136
+ async createAnonymousToken(expiresIn) {
139
137
  return this.createTestToken({
140
138
  sub: `anon:${Date.now()}`,
141
139
  scopes: ["anonymous"],
140
+ exp: expiresIn,
142
141
  claims: {
143
142
  name: "Anonymous",
144
143
  role: "anonymous"
@@ -487,8 +486,479 @@ function sleep(ms) {
487
486
  return new Promise((resolve) => setTimeout(resolve, ms));
488
487
  }
489
488
 
490
- // libs/testing/src/transport/streamable-http.transport.ts
489
+ // libs/testing/src/transport/error-utils.ts
490
+ function errorName(err) {
491
+ if (typeof err !== "object" || err === null) return void 0;
492
+ const name = err.name;
493
+ return typeof name === "string" ? name : void 0;
494
+ }
495
+ function isAbortError(err) {
496
+ return errorName(err) === "AbortError";
497
+ }
498
+ function errorMessage(err, fallback = "Unknown error") {
499
+ if (typeof err === "string" && err) return err;
500
+ if (typeof err === "object" && err !== null) {
501
+ const message = err.message;
502
+ if (typeof message === "string" && message) return message;
503
+ }
504
+ return fallback;
505
+ }
506
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
507
+ function markInterceptedError(err) {
508
+ interceptedErrors.add(err);
509
+ return err;
510
+ }
511
+ function isInterceptedError(err) {
512
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
513
+ }
514
+
515
+ // libs/testing/src/transport/sse.transport.ts
491
516
  var DEFAULT_TIMEOUT = 3e4;
517
+ var SseTransport = class {
518
+ baseUrl;
519
+ entryPath;
520
+ timeout;
521
+ authHeaders;
522
+ publicMode;
523
+ debug;
524
+ clientInfo;
525
+ notificationHandler;
526
+ interceptors;
527
+ elicitationHandler;
528
+ state = "disconnected";
529
+ authToken;
530
+ sessionId;
531
+ messageEndpoint;
532
+ stream;
533
+ pending = /* @__PURE__ */ new Map();
534
+ connectionCount = 0;
535
+ reconnectCount = 0;
536
+ lastRequestHeaders = {};
537
+ constructor(config) {
538
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
539
+ this.entryPath = config.entryPath;
540
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
541
+ this.authHeaders = config.auth?.headers ?? {};
542
+ this.authToken = config.auth?.token;
543
+ this.publicMode = config.publicMode ?? false;
544
+ this.debug = config.debug ?? false;
545
+ this.clientInfo = config.clientInfo;
546
+ this.notificationHandler = config.notificationHandler;
547
+ this.interceptors = config.interceptors;
548
+ this.elicitationHandler = config.elicitationHandler;
549
+ }
550
+ async connect() {
551
+ this.state = "connecting";
552
+ this.connectionCount++;
553
+ try {
554
+ if (!this.publicMode && !this.authToken) {
555
+ await this.requestAnonymousToken();
556
+ }
557
+ await this.openStream();
558
+ this.state = "connected";
559
+ this.log(`Connected; messages go to ${this.messageEndpoint}`);
560
+ } catch (error) {
561
+ this.state = "error";
562
+ this.stream?.abort();
563
+ this.stream = void 0;
564
+ throw error;
565
+ }
566
+ }
567
+ async request(message) {
568
+ this.ensureConnected();
569
+ const startTime = Date.now();
570
+ if (this.interceptors) {
571
+ const intercepted = await this.interceptors.processRequest(message, {
572
+ timestamp: /* @__PURE__ */ new Date(),
573
+ transport: "sse",
574
+ sessionId: this.sessionId
575
+ });
576
+ switch (intercepted.type) {
577
+ case "mock":
578
+ return await this.interceptors.processResponse(
579
+ message,
580
+ intercepted.response,
581
+ Date.now() - startTime
582
+ );
583
+ case "error":
584
+ throw markInterceptedError(intercepted.error);
585
+ case "continue":
586
+ message = intercepted.request;
587
+ break;
588
+ }
589
+ }
590
+ let response;
591
+ if (message.id === void 0) {
592
+ await this.post(JSON.stringify(message));
593
+ response = { jsonrpc: "2.0", id: null, result: void 0 };
594
+ } else {
595
+ response = await this.postAndAwait(message.id, JSON.stringify(message));
596
+ }
597
+ if (this.interceptors) {
598
+ response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
599
+ }
600
+ return response;
601
+ }
602
+ async notify(message) {
603
+ this.ensureConnected();
604
+ const result = await this.post(JSON.stringify(message));
605
+ if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
606
+ }
607
+ async sendRaw(data) {
608
+ this.ensureConnected();
609
+ let id;
610
+ try {
611
+ const parsed = JSON.parse(data);
612
+ if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
613
+ } catch {
614
+ }
615
+ if (id !== void 0) return this.postAndAwait(id, data);
616
+ const result = await this.post(data);
617
+ if (!result.ok) {
618
+ return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
619
+ }
620
+ return { jsonrpc: "2.0", id: null, result: void 0 };
621
+ }
622
+ async close() {
623
+ this.closeStream("Transport closed");
624
+ this.state = "disconnected";
625
+ this.sessionId = void 0;
626
+ this.messageEndpoint = void 0;
627
+ this.log("SSE transport closed");
628
+ }
629
+ isConnected() {
630
+ return this.state === "connected";
631
+ }
632
+ getState() {
633
+ return this.state;
634
+ }
635
+ getSessionId() {
636
+ return this.sessionId;
637
+ }
638
+ setAuthToken(token) {
639
+ this.authToken = token;
640
+ }
641
+ setTimeout(ms) {
642
+ this.timeout = ms;
643
+ }
644
+ getMessageEndpoint() {
645
+ return this.messageEndpoint;
646
+ }
647
+ getConnectionCount() {
648
+ return this.connectionCount;
649
+ }
650
+ getReconnectCount() {
651
+ return this.reconnectCount;
652
+ }
653
+ getLastRequestHeaders() {
654
+ return { ...this.lastRequestHeaders };
655
+ }
656
+ setInterceptors(interceptors2) {
657
+ this.interceptors = interceptors2;
658
+ }
659
+ getInterceptors() {
660
+ return this.interceptors;
661
+ }
662
+ setElicitationHandler(handler) {
663
+ this.elicitationHandler = handler;
664
+ }
665
+ async simulateDisconnect() {
666
+ this.closeStream("Simulated disconnect");
667
+ this.state = "disconnected";
668
+ this.sessionId = void 0;
669
+ this.messageEndpoint = void 0;
670
+ }
671
+ async waitForReconnect(timeoutMs) {
672
+ this.reconnectCount++;
673
+ let timer;
674
+ try {
675
+ await Promise.race([
676
+ this.connect(),
677
+ new Promise((_, reject) => {
678
+ timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
679
+ })
680
+ ]);
681
+ } finally {
682
+ clearTimeout(timer);
683
+ }
684
+ }
685
+ // ═══════════════════════════════════════════════════════════════════
686
+ // STREAM
687
+ // ═══════════════════════════════════════════════════════════════════
688
+ /** Open `GET <entry>/sse` and wait for the `endpoint` event. */
689
+ async openStream() {
690
+ const controller = new AbortController();
691
+ this.stream = controller;
692
+ const url = this.sseUrl();
693
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
694
+ this.log(`GET ${url}`);
695
+ const connectTimer = setTimeout(() => controller.abort(), this.timeout);
696
+ let response;
697
+ try {
698
+ response = await fetch(url, {
699
+ method: "GET",
700
+ headers: { ...headers, Accept: "text/event-stream" },
701
+ signal: controller.signal
702
+ });
703
+ } catch (error) {
704
+ clearTimeout(connectTimer);
705
+ throw new Error(
706
+ isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
707
+ { cause: error }
708
+ );
709
+ }
710
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
711
+ if (!response.ok || !response.body || !isEventStream) {
712
+ clearTimeout(connectTimer);
713
+ const body = await response.text().catch(() => "");
714
+ throw new Error(
715
+ `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 } } })).`
716
+ );
717
+ }
718
+ const endpoint = new Promise((resolve, reject) => {
719
+ const reader = response.body?.getReader();
720
+ if (!reader) {
721
+ reject(new Error(`SSE connection to ${url} has no body`));
722
+ return;
723
+ }
724
+ void this.readStream(controller, reader, resolve, reject);
725
+ });
726
+ try {
727
+ const data = await endpoint;
728
+ const messageUrl = new URL(data, url);
729
+ this.messageEndpoint = messageUrl.toString();
730
+ this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
731
+ } finally {
732
+ clearTimeout(connectTimer);
733
+ }
734
+ }
735
+ async readStream(controller, reader, onEndpoint, onEndpointError) {
736
+ const decoder = new TextDecoder();
737
+ let buffer = "";
738
+ let endpointSeen = false;
739
+ try {
740
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
741
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
742
+ buffer = remainder;
743
+ for (const text of completeEvents) {
744
+ const event = parseSSEEvent(text);
745
+ if (!event) continue;
746
+ if (event.type === "endpoint") {
747
+ if (!endpointSeen) {
748
+ endpointSeen = true;
749
+ onEndpoint(event.data);
750
+ }
751
+ continue;
752
+ }
753
+ await this.handleMessage(event.data);
754
+ }
755
+ }
756
+ } catch (error) {
757
+ if (!controller.signal.aborted) this.log("SSE stream failed:", error);
758
+ } finally {
759
+ reader.releaseLock();
760
+ }
761
+ if (!endpointSeen) {
762
+ onEndpointError(
763
+ new Error(
764
+ 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`
765
+ )
766
+ );
767
+ }
768
+ if (this.stream === controller) {
769
+ this.stream = void 0;
770
+ this.rejectPending("SSE stream closed by the server");
771
+ if (this.state === "connected") this.state = "disconnected";
772
+ }
773
+ }
774
+ async handleMessage(data) {
775
+ let message;
776
+ try {
777
+ message = JSON.parse(data);
778
+ } catch {
779
+ this.log("Ignoring SSE event that is not JSON:", data);
780
+ return;
781
+ }
782
+ if (!message || typeof message !== "object") return;
783
+ if (typeof message.method === "string") {
784
+ const request = message;
785
+ if (request.id === void 0 || request.id === null) {
786
+ this.notificationHandler?.(request);
787
+ return;
788
+ }
789
+ await this.answerServerRequest(request);
790
+ return;
791
+ }
792
+ if (message.id !== void 0 && message.id !== null) {
793
+ const pending = this.pending.get(message.id);
794
+ if (pending) {
795
+ clearTimeout(pending.timer);
796
+ this.pending.delete(message.id);
797
+ pending.resolve(message);
798
+ }
799
+ }
800
+ }
801
+ /** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
802
+ async answerServerRequest(request) {
803
+ const id = request.id;
804
+ let reply;
805
+ if (request.method === "elicitation/create") {
806
+ reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
807
+ } else if (request.method === "ping") {
808
+ reply = { jsonrpc: "2.0", id, result: {} };
809
+ } else {
810
+ reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
811
+ }
812
+ const result = await this.post(JSON.stringify(reply));
813
+ if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
814
+ }
815
+ async elicit(params) {
816
+ if (!this.elicitationHandler) return { action: "decline" };
817
+ try {
818
+ return await this.elicitationHandler(params);
819
+ } catch (error) {
820
+ this.log("Elicitation handler error:", error);
821
+ return { action: "cancel" };
822
+ }
823
+ }
824
+ closeStream(reason) {
825
+ const controller = this.stream;
826
+ this.stream = void 0;
827
+ controller?.abort();
828
+ this.rejectPending(reason);
829
+ }
830
+ rejectPending(reason) {
831
+ for (const [id, pending] of this.pending) {
832
+ clearTimeout(pending.timer);
833
+ pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
834
+ }
835
+ this.pending.clear();
836
+ }
837
+ // ═══════════════════════════════════════════════════════════════════
838
+ // POSTING
839
+ // ═══════════════════════════════════════════════════════════════════
840
+ /** POST a message and resolve with the response the stream carries for `id`. */
841
+ async postAndAwait(id, body) {
842
+ const answer = new Promise((resolve) => {
843
+ const timer = setTimeout(() => {
844
+ this.pending.delete(id);
845
+ resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
846
+ }, this.timeout);
847
+ this.pending.set(id, { resolve, timer });
848
+ });
849
+ const result = await this.post(body);
850
+ if (!result.ok) {
851
+ const pending = this.pending.get(id);
852
+ if (pending) {
853
+ clearTimeout(pending.timer);
854
+ this.pending.delete(id);
855
+ }
856
+ return {
857
+ jsonrpc: "2.0",
858
+ id,
859
+ error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
860
+ };
861
+ }
862
+ return answer;
863
+ }
864
+ async post(body) {
865
+ const endpoint = this.messageEndpoint;
866
+ if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
867
+ const headers = this.buildHeaders();
868
+ this.lastRequestHeaders = headers;
869
+ this.log(`POST ${endpoint}`, body);
870
+ const controller = new AbortController();
871
+ const timer = setTimeout(() => controller.abort(), this.timeout);
872
+ try {
873
+ const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
874
+ const text = await response.text();
875
+ return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
876
+ } catch (error) {
877
+ if (isAbortError(error)) {
878
+ return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
879
+ }
880
+ throw error;
881
+ } finally {
882
+ clearTimeout(timer);
883
+ }
884
+ }
885
+ // ═══════════════════════════════════════════════════════════════════
886
+ // HELPERS
887
+ // ═══════════════════════════════════════════════════════════════════
888
+ async requestAnonymousToken() {
889
+ const url = new URL(this.baseUrl);
890
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
891
+ url.search = "";
892
+ const tokenUrl = url.toString();
893
+ try {
894
+ const response = await fetch(tokenUrl, {
895
+ method: "POST",
896
+ headers: { "Content-Type": "application/json" },
897
+ body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
898
+ });
899
+ if (!response.ok) {
900
+ this.log(`No anonymous token (HTTP ${response.status})`);
901
+ return;
902
+ }
903
+ const token = await response.json();
904
+ if (token.access_token) this.authToken = token.access_token;
905
+ } catch (error) {
906
+ this.log("Error requesting anonymous token:", error);
907
+ }
908
+ }
909
+ /** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
910
+ sseUrl() {
911
+ const url = new URL(this.baseUrl);
912
+ const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
913
+ const basePath = url.pathname.replace(/\/+$/, "");
914
+ url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
915
+ return url.toString();
916
+ }
917
+ buildHeaders() {
918
+ const headers = {
919
+ "Content-Type": "application/json",
920
+ Accept: "application/json, text/event-stream"
921
+ };
922
+ if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
923
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
924
+ if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
925
+ Object.assign(headers, this.authHeaders);
926
+ return headers;
927
+ }
928
+ ensureConnected() {
929
+ if (this.state !== "connected") {
930
+ throw new Error("Transport not connected. Call connect() first.");
931
+ }
932
+ }
933
+ log(message, data) {
934
+ if (this.debug) {
935
+ console.log(`[SSE] ${message}`, data ?? "");
936
+ }
937
+ }
938
+ };
939
+ function splitSSEEvents(buffer) {
940
+ const endsWithCarriageReturn = buffer.endsWith("\r");
941
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
942
+ const completeEvents = normalized.split("\n\n");
943
+ const remainder = completeEvents.pop() ?? "";
944
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
945
+ }
946
+ function parseSSEEvent(text) {
947
+ let type = "message";
948
+ const data = [];
949
+ for (const line of text.split("\n")) {
950
+ if (line.startsWith(":")) continue;
951
+ const colon = line.indexOf(":");
952
+ const field = colon === -1 ? line : line.slice(0, colon);
953
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
954
+ if (field === "event") type = value;
955
+ else if (field === "data") data.push(value);
956
+ }
957
+ return data.length > 0 ? { type, data: data.join("\n") } : void 0;
958
+ }
959
+
960
+ // libs/testing/src/transport/streamable-http.transport.ts
961
+ var DEFAULT_TIMEOUT2 = 3e4;
492
962
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
493
963
  var StreamableHttpTransport = class {
494
964
  config;
@@ -510,7 +980,7 @@ var StreamableHttpTransport = class {
510
980
  baseUrl: config.baseUrl.replace(/\/$/, ""),
511
981
  // Remove trailing slash
512
982
  entryPath: config.entryPath,
513
- timeout: config.timeout ?? DEFAULT_TIMEOUT,
983
+ timeout: config.timeout ?? DEFAULT_TIMEOUT2,
514
984
  auth: config.auth ?? {},
515
985
  publicMode: config.publicMode ?? false,
516
986
  debug: config.debug ?? false,
@@ -595,7 +1065,7 @@ var StreamableHttpTransport = class {
595
1065
  return mockResponse2;
596
1066
  }
597
1067
  case "error":
598
- throw interceptResult.error;
1068
+ throw markInterceptedError(interceptResult.error);
599
1069
  case "continue":
600
1070
  message = interceptResult.request;
601
1071
  break;
@@ -656,7 +1126,7 @@ var StreamableHttpTransport = class {
656
1126
  return jsonResponse;
657
1127
  } catch (error) {
658
1128
  clearTimeout(timeoutId);
659
- if (error instanceof Error && error.name === "AbortError") {
1129
+ if (isAbortError(error)) {
660
1130
  return {
661
1131
  jsonrpc: "2.0",
662
1132
  id: message.id ?? null,
@@ -695,7 +1165,7 @@ var StreamableHttpTransport = class {
695
1165
  }
696
1166
  } catch (error) {
697
1167
  clearTimeout(timeoutId);
698
- if (error instanceof Error && error.name !== "AbortError") {
1168
+ if (!isAbortError(error)) {
699
1169
  throw error;
700
1170
  }
701
1171
  }
@@ -736,7 +1206,7 @@ var StreamableHttpTransport = class {
736
1206
  error: {
737
1207
  code: -32700,
738
1208
  message: "Parse error",
739
- data: error instanceof Error ? error.message : "Unknown error"
1209
+ data: errorMessage(error)
740
1210
  }
741
1211
  };
742
1212
  }
@@ -850,7 +1320,7 @@ var StreamableHttpTransport = class {
850
1320
  let buffer = "";
851
1321
  try {
852
1322
  for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
853
- const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1323
+ const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
854
1324
  buffer = remainder;
855
1325
  for (const eventText of completeEvents) {
856
1326
  for (const event of this.parseSSEEvents(eventText, void 0).events) {
@@ -1106,7 +1576,7 @@ var StreamableHttpTransport = class {
1106
1576
  if (this.config.clientInfo) {
1107
1577
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1108
1578
  }
1109
- if (this.authToken && !this.publicMode) {
1579
+ if (this.authToken) {
1110
1580
  headers["Authorization"] = `Bearer ${this.authToken}`;
1111
1581
  }
1112
1582
  if (this.sessionId) {
@@ -1184,7 +1654,7 @@ var StreamableHttpTransport = class {
1184
1654
  };
1185
1655
  }
1186
1656
  };
1187
- function splitSSEEvents(buffer) {
1657
+ function splitSSEEvents2(buffer) {
1188
1658
  const endsWithCarriageReturn = buffer.endsWith("\r");
1189
1659
  const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1190
1660
  const completeEvents = normalized.split("\n\n");
@@ -1422,7 +1892,7 @@ var McpTestClientBuilder = class {
1422
1892
  };
1423
1893
 
1424
1894
  // libs/testing/src/client/mcp-test-client.ts
1425
- var DEFAULT_TIMEOUT2 = 3e4;
1895
+ var DEFAULT_TIMEOUT3 = 3e4;
1426
1896
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1427
1897
  var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1428
1898
  var DEFAULT_CLIENT_INFO = {
@@ -1430,6 +1900,15 @@ var DEFAULT_CLIENT_INFO = {
1430
1900
  version: "0.4.0"
1431
1901
  };
1432
1902
  var MAX_LIST_PAGES = 1e3;
1903
+ function isAuthFailure(error) {
1904
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1905
+ }
1906
+ function describeInitializeError(error) {
1907
+ const message = error?.message ?? "Unknown error";
1908
+ const data = error?.data;
1909
+ const detail = typeof data === "string" ? data.trim() : "";
1910
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1911
+ }
1433
1912
  function extractEntryPaths(data) {
1434
1913
  let payload = data;
1435
1914
  if (typeof payload === "string") {
@@ -1483,7 +1962,7 @@ var McpTestClient = class {
1483
1962
  transport: config.transport ?? "streamable-http",
1484
1963
  auth: config.auth ?? {},
1485
1964
  publicMode: config.publicMode ?? false,
1486
- timeout: config.timeout ?? DEFAULT_TIMEOUT2,
1965
+ timeout: config.timeout ?? DEFAULT_TIMEOUT3,
1487
1966
  debug: config.debug ?? false,
1488
1967
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
1489
1968
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
@@ -1516,19 +1995,22 @@ var McpTestClient = class {
1516
1995
  async connect() {
1517
1996
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1518
1997
  this.triedDiscoveredEntryPath = false;
1998
+ this.initResult = null;
1519
1999
  this.transport = this.createTransport();
1520
2000
  await this.transport.connect();
1521
2001
  const maxRetries = 3;
1522
2002
  const retryDelayMs = 500;
1523
2003
  let lastError;
1524
2004
  let reportedEntryPaths;
2005
+ let attemptsMade = 0;
1525
2006
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
2007
+ attemptsMade = attempt;
1526
2008
  const initResponse = await this.initialize();
1527
2009
  if (initResponse.success && initResponse.data) {
1528
2010
  this.initResult = initResponse.data;
1529
2011
  break;
1530
2012
  }
1531
- lastError = initResponse.error?.message ?? "Unknown error";
2013
+ lastError = describeInitializeError(initResponse.error);
1532
2014
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1533
2015
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1534
2016
  this.triedDiscoveredEntryPath = true;
@@ -1540,6 +2022,7 @@ var McpTestClient = class {
1540
2022
  await this.transport.connect();
1541
2023
  continue;
1542
2024
  }
2025
+ if (isAuthFailure(initResponse.error)) break;
1543
2026
  if (attempt < maxRetries) {
1544
2027
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1545
2028
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1548,7 +2031,7 @@ var McpTestClient = class {
1548
2031
  if (!this.initResult) {
1549
2032
  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.` : "";
1550
2033
  throw new Error(
1551
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
2034
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1552
2035
  );
1553
2036
  }
1554
2037
  this._sessionId = this.transport.getSessionId();
@@ -1618,18 +2101,47 @@ var McpTestClient = class {
1618
2101
  return this._authState;
1619
2102
  }
1620
2103
  /**
1621
- * Authenticate with a token
2104
+ * Authenticate with a token.
2105
+ *
2106
+ * When connected, this opens a NEW session with the token (the previous session was created
2107
+ * for the previous identity) and rejects if the server refuses it — for example because the
2108
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
2109
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1622
2110
  */
1623
2111
  async authenticate(token) {
2112
+ if (typeof token !== "string" || token.trim() === "") {
2113
+ throw new Error("authenticate() requires a non-empty token");
2114
+ }
2115
+ const previousAuthState = this._authState;
2116
+ const previousConfigAuth = this.config.auth;
2117
+ this.config.auth = { ...previousConfigAuth, token };
1624
2118
  this._authState = {
1625
2119
  isAnonymous: false,
1626
2120
  token,
1627
2121
  scopes: this.parseScopesFromToken(token),
1628
2122
  user: this.parseUserFromToken(token)
1629
2123
  };
1630
- if (this.transport) {
1631
- this.transport.setAuthToken(token);
2124
+ const previousTransport = this.transport;
2125
+ if (!previousTransport) {
2126
+ this.log("debug", "Authentication updated (not connected yet)");
2127
+ return;
1632
2128
  }
2129
+ const previousInit = this.initResult;
2130
+ const previousSessionId = this._sessionId;
2131
+ const previousSessionInfo = this._sessionInfo;
2132
+ try {
2133
+ await this.connect();
2134
+ } catch (err) {
2135
+ await this.transport?.close().catch(() => void 0);
2136
+ this.transport = previousTransport;
2137
+ this.initResult = previousInit;
2138
+ this._sessionId = previousSessionId;
2139
+ this._sessionInfo = previousSessionInfo;
2140
+ this.config.auth = previousConfigAuth;
2141
+ this._authState = previousAuthState;
2142
+ throw err;
2143
+ }
2144
+ await previousTransport.close().catch(() => void 0);
1633
2145
  this.log("debug", "Authentication updated");
1634
2146
  }
1635
2147
  // ═══════════════════════════════════════════════════════════════════
@@ -2216,7 +2728,18 @@ var McpTestClient = class {
2216
2728
  notificationHandler: this.recordNotification
2217
2729
  });
2218
2730
  case "sse":
2219
- throw new Error("SSE transport not yet implemented");
2731
+ return new SseTransport({
2732
+ baseUrl,
2733
+ entryPath: this.config.entryPath,
2734
+ timeout: this.config.timeout,
2735
+ auth: this.config.auth,
2736
+ publicMode: this.config.publicMode,
2737
+ debug: this.config.debug,
2738
+ interceptors: this._interceptors,
2739
+ clientInfo: this.config.clientInfo,
2740
+ elicitationHandler: this._elicitationHandler,
2741
+ notificationHandler: this.recordNotification
2742
+ });
2220
2743
  default:
2221
2744
  throw new Error(`Unknown transport type: ${this.config.transport}`);
2222
2745
  }
@@ -2253,10 +2776,11 @@ var McpTestClient = class {
2253
2776
  requestId: id
2254
2777
  };
2255
2778
  } catch (err) {
2779
+ if (isInterceptedError(err)) throw err;
2256
2780
  const durationMs = Date.now() - start;
2257
2781
  const error = {
2258
2782
  code: -32603,
2259
- message: err instanceof Error ? err.message : "Unknown error"
2783
+ message: errorMessage(err)
2260
2784
  };
2261
2785
  return {
2262
2786
  success: false,
@@ -2404,12 +2928,12 @@ var McpTestClient = class {
2404
2928
  const payload = this.decodeJwtPayload(token);
2405
2929
  if (!payload) return [];
2406
2930
  const scope = payload["scope"];
2407
- const scopes = payload["scopes"];
2931
+ const scopes2 = payload["scopes"];
2408
2932
  if (typeof scope === "string") {
2409
2933
  return scope.split(" ");
2410
2934
  }
2411
- if (Array.isArray(scopes)) {
2412
- return scopes;
2935
+ if (Array.isArray(scopes2)) {
2936
+ return scopes2;
2413
2937
  }
2414
2938
  return [];
2415
2939
  } catch {
@@ -2555,6 +3079,8 @@ var E2E_PORT_RANGES = {
2555
3079
  "demo-e2e-distributed": { start: 50440, size: 20 },
2556
3080
  // Protocol revision E2E tests (50460-50479)
2557
3081
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
3082
+ // Testing library E2E tests (50480-50499)
3083
+ "demo-e2e-testing": { start: 50480, size: 20 },
2558
3084
  // Mock servers and utilities (50900-50999)
2559
3085
  "mock-oauth": { start: 50900, size: 10 },
2560
3086
  "mock-api": { start: 50910, size: 10 },
@@ -2563,6 +3089,66 @@ var E2E_PORT_RANGES = {
2563
3089
  default: { start: 51e3, size: 100 }
2564
3090
  };
2565
3091
  var reservedPorts = /* @__PURE__ */ new Map();
3092
+ var lockedPorts = /* @__PURE__ */ new Set();
3093
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
3094
+ function lockDir() {
3095
+ try {
3096
+ const os = require("node:os");
3097
+ const path = require("node:path");
3098
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
3099
+ } catch {
3100
+ return void 0;
3101
+ }
3102
+ }
3103
+ function pidIsAlive(pid) {
3104
+ try {
3105
+ process.kill(pid, 0);
3106
+ return true;
3107
+ } catch (error) {
3108
+ return error.code === "EPERM";
3109
+ }
3110
+ }
3111
+ function acquirePortLock(port) {
3112
+ if (lockedPorts.has(port)) return false;
3113
+ let fs;
3114
+ let file;
3115
+ try {
3116
+ fs = require("node:fs");
3117
+ const dir = lockDir();
3118
+ if (!dir) return true;
3119
+ fs.mkdirSync(dir, { recursive: true });
3120
+ file = require("node:path").join(dir, `${port}.lock`);
3121
+ } catch {
3122
+ return true;
3123
+ }
3124
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
3125
+ for (let attempt = 0; attempt < 2; attempt++) {
3126
+ try {
3127
+ fs.writeFileSync(file, payload, { flag: "wx" });
3128
+ lockedPorts.add(port);
3129
+ return true;
3130
+ } catch (error) {
3131
+ if (error.code !== "EEXIST") return true;
3132
+ try {
3133
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
3134
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
3135
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
3136
+ fs.unlinkSync(file);
3137
+ } catch {
3138
+ }
3139
+ }
3140
+ }
3141
+ return false;
3142
+ }
3143
+ function releasePortLock(port) {
3144
+ if (!lockedPorts.delete(port)) return;
3145
+ try {
3146
+ const fs = require("node:fs");
3147
+ const dir = lockDir();
3148
+ if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
3149
+ } catch {
3150
+ }
3151
+ }
2566
3152
  var projectPortIndex = /* @__PURE__ */ new Map();
2567
3153
  function getPortRange(project) {
2568
3154
  const key = project;
@@ -2573,15 +3159,22 @@ function getPortRange(project) {
2573
3159
  }
2574
3160
  async function reservePort(project, preferredPort) {
2575
3161
  const range = getPortRange(project);
2576
- if (preferredPort !== void 0) {
3162
+ const handle = (port) => ({
3163
+ port,
3164
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
3165
+ releaseSocket: async () => {
3166
+ await releasePort(port);
3167
+ },
3168
+ release: async () => {
3169
+ await releasePort(port);
3170
+ releasePortLock(port);
3171
+ }
3172
+ });
3173
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
3174
+ if (wantsSpecificPort) {
2577
3175
  const reservation = await tryReservePort(preferredPort, project);
2578
3176
  if (reservation) {
2579
- return {
2580
- port: preferredPort,
2581
- release: async () => {
2582
- await releasePort(preferredPort);
2583
- }
2584
- };
3177
+ return handle(preferredPort);
2585
3178
  }
2586
3179
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
2587
3180
  }
@@ -2590,30 +3183,19 @@ async function reservePort(project, preferredPort) {
2590
3183
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2591
3184
  const port = range.start + index % range.size;
2592
3185
  index = (index + 1) % range.size;
2593
- if (reservedPorts.has(port)) {
3186
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
2594
3187
  continue;
2595
3188
  }
2596
3189
  const reservation = await tryReservePort(port, project);
2597
3190
  if (reservation) {
2598
3191
  projectPortIndex.set(project, index);
2599
- return {
2600
- port,
2601
- release: async () => {
2602
- await releasePort(port);
2603
- }
2604
- };
3192
+ return handle(port);
2605
3193
  }
2606
3194
  }
2607
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
2608
- if (dynamicPort) {
2609
- const reservation = await tryReservePort(dynamicPort, project);
2610
- if (reservation) {
2611
- return {
2612
- port: dynamicPort,
2613
- release: async () => {
2614
- await releasePort(dynamicPort);
2615
- }
2616
- };
3195
+ for (let port = 51e3; port < 52e3; port++) {
3196
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
3197
+ if (await tryReservePort(port, project)) {
3198
+ return handle(port);
2617
3199
  }
2618
3200
  }
2619
3201
  throw new Error(
@@ -2621,9 +3203,13 @@ async function reservePort(project, preferredPort) {
2621
3203
  );
2622
3204
  }
2623
3205
  async function tryReservePort(port, project) {
3206
+ if (!acquirePortLock(port)) {
3207
+ return false;
3208
+ }
2624
3209
  return new Promise((resolve) => {
2625
3210
  const server = (0, import_net.createServer)();
2626
3211
  server.once("error", () => {
3212
+ releasePortLock(port);
2627
3213
  resolve(false);
2628
3214
  });
2629
3215
  server.listen(port, () => {
@@ -2649,31 +3235,6 @@ async function releasePort(port) {
2649
3235
  });
2650
3236
  });
2651
3237
  }
2652
- async function findAvailablePortInRange(start, end) {
2653
- for (let port = start; port < end; port++) {
2654
- if (reservedPorts.has(port)) {
2655
- continue;
2656
- }
2657
- const available = await isPortAvailable(port);
2658
- if (available) {
2659
- return port;
2660
- }
2661
- }
2662
- return null;
2663
- }
2664
- async function isPortAvailable(port) {
2665
- return new Promise((resolve) => {
2666
- const server = (0, import_net.createServer)();
2667
- server.once("error", () => {
2668
- resolve(false);
2669
- });
2670
- server.listen(port, () => {
2671
- server.close(() => {
2672
- resolve(true);
2673
- });
2674
- });
2675
- });
2676
- }
2677
3238
 
2678
3239
  // libs/testing/src/server/test-server.ts
2679
3240
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -2682,8 +3243,11 @@ var TestServer = class _TestServer {
2682
3243
  options;
2683
3244
  _info;
2684
3245
  logs = [];
3246
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
2685
3247
  portRelease = null;
2686
- constructor(options, port, portRelease) {
3248
+ /** Frees only the reservation socket, just before the server is spawned */
3249
+ portReleaseSocket = null;
3250
+ constructor(options, port, portRelease, portReleaseSocket) {
2687
3251
  this.options = {
2688
3252
  port,
2689
3253
  project: options.project,
@@ -2695,6 +3259,7 @@ var TestServer = class _TestServer {
2695
3259
  debug: options.debug ?? DEBUG_SERVER
2696
3260
  };
2697
3261
  this.portRelease = portRelease ?? null;
3262
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
2698
3263
  this._info = {
2699
3264
  baseUrl: `http://localhost:${port}`,
2700
3265
  port
@@ -2707,8 +3272,8 @@ var TestServer = class _TestServer {
2707
3272
  const project = options.project ?? "default";
2708
3273
  const maxAttempts = 3;
2709
3274
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2710
- const { port, release } = await reservePort(project, options.port);
2711
- const server = new _TestServer(options, port, release);
3275
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
3276
+ const server = new _TestServer(options, port, release, releaseSocket);
2712
3277
  try {
2713
3278
  await server.startProcess();
2714
3279
  return server;
@@ -2748,7 +3313,7 @@ var TestServer = class _TestServer {
2748
3313
  }
2749
3314
  const maxAttempts = 3;
2750
3315
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2751
- const { port, release } = await reservePort(project, options.port);
3316
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2752
3317
  const serverOptions = {
2753
3318
  ...options,
2754
3319
  port,
@@ -2756,7 +3321,7 @@ var TestServer = class _TestServer {
2756
3321
  command: `npx nx serve ${project} --port ${port}`,
2757
3322
  cwd: options.cwd ?? process.cwd()
2758
3323
  };
2759
- const server = new _TestServer(serverOptions, port, release);
3324
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
2760
3325
  try {
2761
3326
  await server.startProcess();
2762
3327
  return server;
@@ -2814,10 +3379,17 @@ var TestServer = class _TestServer {
2814
3379
  * Stop the test server
2815
3380
  */
2816
3381
  async stop() {
2817
- if (this.portRelease) {
2818
- await this.portRelease();
2819
- this.portRelease = null;
3382
+ try {
3383
+ await this.stopProcess();
3384
+ } finally {
3385
+ if (this.portRelease) {
3386
+ await this.portRelease();
3387
+ this.portRelease = null;
3388
+ }
3389
+ this.portReleaseSocket = null;
2820
3390
  }
3391
+ }
3392
+ async stopProcess() {
2821
3393
  if (this.process) {
2822
3394
  this.log("Stopping server...");
2823
3395
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -2890,7 +3462,7 @@ var TestServer = class _TestServer {
2890
3462
  * Restart the server
2891
3463
  */
2892
3464
  async restart() {
2893
- await this.stop();
3465
+ await this.stopProcess();
2894
3466
  await this.startProcess();
2895
3467
  }
2896
3468
  /**
@@ -2920,9 +3492,9 @@ var TestServer = class _TestServer {
2920
3492
  PORT: String(this.options.port)
2921
3493
  };
2922
3494
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
2923
- if (this.portRelease) {
2924
- await this.portRelease();
2925
- this.portRelease = null;
3495
+ if (this.portReleaseSocket) {
3496
+ await this.portReleaseSocket();
3497
+ this.portReleaseSocket = null;
2926
3498
  await sleep2(300);
2927
3499
  }
2928
3500
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -2932,9 +3504,8 @@ var TestServer = class _TestServer {
2932
3504
  detached: true,
2933
3505
  stdio: ["pipe", "pipe", "pipe"]
2934
3506
  });
2935
- if (this.process.pid !== void 0) {
2936
- this._info.pid = this.process.pid;
2937
- }
3507
+ this._info.pid = this.process.pid;
3508
+ this._info.shellPid = this.process.pid;
2938
3509
  let processExited = false;
2939
3510
  let exitCode = null;
2940
3511
  let exitError = null;
@@ -3037,6 +3608,8 @@ ${allLogs}`);
3037
3608
  });
3038
3609
  if (response.ok || response.status === 404) {
3039
3610
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
3611
+ const listeningPid = await findListeningPid(this.options.port);
3612
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3040
3613
  return;
3041
3614
  }
3042
3615
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3081,6 +3654,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3081
3654
  function sleep2(ms) {
3082
3655
  return new Promise((resolve) => setTimeout(resolve, ms));
3083
3656
  }
3657
+ function findListeningPid(port) {
3658
+ if (process.platform === "win32") return Promise.resolve(void 0);
3659
+ return new Promise((resolve) => {
3660
+ (0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
3661
+ if (error) {
3662
+ resolve(void 0);
3663
+ return;
3664
+ }
3665
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
3666
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
3667
+ });
3668
+ });
3669
+ }
3084
3670
  function withWorkspaceProtocolFallback(env, cwd) {
3085
3671
  if (findInstalledProtocolPackageDir(cwd)) {
3086
3672
  return env;
@@ -3197,81 +3783,163 @@ function gatewayTokenBinding(serverUrl, entryPath) {
3197
3783
  }
3198
3784
 
3199
3785
  // libs/testing/src/fixtures/test-fixture.ts
3200
- var describe = import_globals.describe;
3201
- var beforeAll = import_globals.beforeAll;
3202
- var beforeEach = import_globals.beforeEach;
3203
- var afterEach = import_globals.afterEach;
3204
- var afterAll = import_globals.afterAll;
3205
- var it = import_globals.it;
3206
- var currentConfig = {};
3207
- var serverInstance = null;
3208
- var tokenFactory = null;
3209
- var serverStartedByUs = false;
3210
- var skipScopes = [{ skipped: false }];
3211
- function currentSkipScope() {
3212
- return skipScopes[skipScopes.length - 1];
3786
+ var jestApi;
3787
+ function jestGlobals() {
3788
+ if (jestApi) return jestApi;
3789
+ try {
3790
+ const api = require("@jest/globals");
3791
+ jestApi = {
3792
+ describe: api["describe"],
3793
+ beforeAll: api["beforeAll"],
3794
+ beforeEach: api["beforeEach"],
3795
+ afterEach: api["afterEach"],
3796
+ afterAll: api["afterAll"],
3797
+ it: api["it"]
3798
+ };
3799
+ return jestApi;
3800
+ } catch (error) {
3801
+ throw new Error(
3802
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
3803
+ { cause: error }
3804
+ );
3805
+ }
3213
3806
  }
3214
- async function initializeSharedResources() {
3215
- if (!serverInstance) {
3216
- if (currentConfig.baseUrl && !currentConfig.server) {
3217
- serverInstance = TestServer.connect(currentConfig.baseUrl);
3218
- serverStartedByUs = false;
3219
- } else if (currentConfig.server) {
3220
- const serverCommand = resolveServerCommand(currentConfig.server);
3221
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3807
+ function newScope(parent) {
3808
+ return {
3809
+ skipped: parent?.skipped ?? false,
3810
+ reason: parent?.reason,
3811
+ config: {},
3812
+ envs: [],
3813
+ configured: false,
3814
+ cleanupRegistered: false,
3815
+ beforeEachHooks: [],
3816
+ afterEachHooks: []
3817
+ };
3818
+ }
3819
+ var scopes = [newScope()];
3820
+ function currentScope() {
3821
+ return scopes[scopes.length - 1];
3822
+ }
3823
+ var serverEntries = /* @__PURE__ */ new Map();
3824
+ function resolveConfig(chain) {
3825
+ const merged = {};
3826
+ for (const scope of chain) {
3827
+ Object.assign(merged, scope.config);
3828
+ for (const env of scope.envs) merged.env = { ...merged.env, ...env };
3829
+ }
3830
+ return merged;
3831
+ }
3832
+ function ownerScopeOf(chain) {
3833
+ for (let i = chain.length - 1; i > 0; i--) {
3834
+ if (chain[i].configured) return chain[i];
3835
+ }
3836
+ return chain[0];
3837
+ }
3838
+ function serverKey(config) {
3839
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
3840
+ return JSON.stringify({
3841
+ server: config.server,
3842
+ baseUrl: config.baseUrl,
3843
+ project: config.project,
3844
+ port: config.port,
3845
+ entryPath: config.entryPath,
3846
+ startupTimeout: config.startupTimeout,
3847
+ logLevel: config.logLevel,
3848
+ auth: config.auth,
3849
+ env
3850
+ });
3851
+ }
3852
+ function serverEnv(config) {
3853
+ const extra = {};
3854
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
3855
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
3856
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
3857
+ return { ...extra, ...config.env };
3858
+ }
3859
+ async function startServerEntry(config, owner) {
3860
+ let server;
3861
+ let startedByUs = false;
3862
+ if (config.baseUrl && !config.server) {
3863
+ server = TestServer.connect(config.baseUrl);
3864
+ } else if (config.server) {
3865
+ const serverCommand = resolveServerCommand(config.server);
3866
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3867
+ if (isDebug) {
3868
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
3869
+ }
3870
+ try {
3871
+ server = await TestServer.start({
3872
+ project: config.project,
3873
+ port: config.port,
3874
+ command: serverCommand,
3875
+ env: serverEnv(config),
3876
+ startupTimeout: config.startupTimeout ?? 3e4,
3877
+ debug: isDebug
3878
+ });
3879
+ startedByUs = true;
3222
3880
  if (isDebug) {
3223
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
3881
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
3224
3882
  }
3225
- try {
3226
- serverInstance = await TestServer.start({
3227
- project: currentConfig.project,
3228
- port: currentConfig.port,
3229
- command: serverCommand,
3230
- env: currentConfig.env,
3231
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
3232
- debug: isDebug
3233
- });
3234
- serverStartedByUs = true;
3235
- if (isDebug) {
3236
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
3237
- }
3238
- } catch (error) {
3239
- const errMsg = error instanceof Error ? error.message : String(error);
3240
- throw new Error(
3241
- `Failed to start test server.
3883
+ } catch (error) {
3884
+ const errMsg = error instanceof Error ? error.message : String(error);
3885
+ throw new Error(
3886
+ `Failed to start test server.
3242
3887
 
3243
- Server entry: ${currentConfig.server}
3244
- Project: ${currentConfig.project ?? "default"}
3888
+ Server entry: ${config.server}
3889
+ Project: ${config.project ?? "default"}
3245
3890
  Command: ${serverCommand}
3246
3891
 
3247
- Error: ${errMsg}`
3248
- );
3249
- }
3250
- } else {
3251
- throw new Error(
3252
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
3892
+ Error: ${errMsg}`,
3893
+ { cause: error }
3253
3894
  );
3254
3895
  }
3896
+ } else {
3897
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
3255
3898
  }
3256
- if (!tokenFactory) {
3257
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
3258
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
3259
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3260
- }
3899
+ const sharedSecret = config.env?.["JWT_SECRET"];
3900
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
3901
+ const tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3902
+ return { server, startedByUs, tokenFactory, owner };
3261
3903
  }
3262
- async function createTestFixtures() {
3263
- await initializeSharedResources();
3264
- if (!serverInstance) {
3265
- throw new Error("Server instance not initialized");
3266
- }
3267
- if (!tokenFactory) {
3268
- throw new Error("Token factory not initialized");
3904
+ function acquireServer(chain) {
3905
+ const config = resolveConfig(chain);
3906
+ const key = serverKey(config);
3907
+ let entry = serverEntries.get(key);
3908
+ if (!entry) {
3909
+ entry = startServerEntry(config, ownerScopeOf(chain));
3910
+ serverEntries.set(key, entry);
3911
+ entry.catch(() => serverEntries.delete(key));
3912
+ }
3913
+ return entry;
3914
+ }
3915
+ async function stopServers(owner) {
3916
+ for (const [key, pending] of Array.from(serverEntries)) {
3917
+ let entry;
3918
+ try {
3919
+ entry = await pending;
3920
+ } catch {
3921
+ serverEntries.delete(key);
3922
+ continue;
3923
+ }
3924
+ if (owner && entry.owner !== owner) continue;
3925
+ serverEntries.delete(key);
3926
+ if (entry.startedByUs) {
3927
+ await entry.server.stop();
3928
+ }
3269
3929
  }
3930
+ }
3931
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
3932
+ async function buildFixtures(chain) {
3933
+ const config = resolveConfig(chain);
3934
+ const entry = await acquireServer(chain);
3935
+ const { server, tokenFactory } = entry;
3936
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
3937
+ const clientConfig = { ...config, publicMode };
3270
3938
  const clientInstance = McpTestClient.create({
3271
- baseUrl: resolveClientBaseUrl(serverInstance),
3272
- entryPath: currentConfig.entryPath,
3273
- transport: currentConfig.transport ?? "streamable-http",
3274
- publicMode: currentConfig.publicMode
3939
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
3940
+ entryPath: clientConfig.entryPath,
3941
+ transport: clientConfig.transport ?? "streamable-http",
3942
+ publicMode
3275
3943
  }).build();
3276
3944
  try {
3277
3945
  await clientInstance.connect();
@@ -3285,17 +3953,24 @@ async function createTestFixtures() {
3285
3953
  throw err;
3286
3954
  }
3287
3955
  }
3288
- const auth = createAuthFixture(tokenFactory);
3289
- const server = createServerFixture(serverInstance);
3290
- return {
3956
+ const context = { extraClients: [] };
3957
+ const fixtures = {
3291
3958
  mcp: clientInstance,
3292
- auth,
3293
- server
3959
+ auth: createAuthFixture(tokenFactory),
3960
+ server: createServerFixture(server, clientConfig, clientInstance, context)
3294
3961
  };
3962
+ fixtureContexts.set(fixtures, context);
3963
+ return fixtures;
3964
+ }
3965
+ async function createTestFixtures() {
3966
+ return buildFixtures(scopes.slice());
3967
+ }
3968
+ async function initializeSharedResources() {
3969
+ await acquireServer(scopes.slice());
3295
3970
  }
3296
3971
  async function cleanupTestFixtures(fixtures, testFailed = false) {
3297
- if (testFailed && serverInstance) {
3298
- const logs = serverInstance.getLogs();
3972
+ if (testFailed) {
3973
+ const logs = fixtures.server.getLogs();
3299
3974
  if (logs.length > 0) {
3300
3975
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
3301
3976
  const recentLogs = logs.slice(-50);
@@ -3306,20 +3981,21 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
3306
3981
  console.error("[TestFixture] === End Server Logs ===\n");
3307
3982
  }
3308
3983
  }
3984
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
3985
+ for (const client of extras) {
3986
+ if (client.isConnected()) {
3987
+ await client.disconnect().catch(() => void 0);
3988
+ }
3989
+ }
3309
3990
  if (fixtures.mcp.isConnected()) {
3310
3991
  await fixtures.mcp.disconnect();
3311
3992
  }
3312
3993
  }
3313
3994
  async function cleanupSharedResources() {
3314
- if (serverInstance && serverStartedByUs) {
3315
- await serverInstance.stop();
3316
- }
3317
- serverInstance = null;
3318
- tokenFactory = null;
3319
- serverStartedByUs = false;
3995
+ await stopServers();
3320
3996
  }
3321
- function resolveClientBaseUrl(server) {
3322
- return currentConfig.baseUrl ?? server.info.baseUrl;
3997
+ function resolveClientBaseUrl(server, config) {
3998
+ return config.baseUrl ?? server.info.baseUrl;
3323
3999
  }
3324
4000
  function createAuthFixture(factory) {
3325
4001
  const users = {
@@ -3365,28 +4041,39 @@ function createAuthFixture(factory) {
3365
4041
  getAudience: () => factory.getAudience()
3366
4042
  };
3367
4043
  }
3368
- function createServerFixture(server) {
4044
+ function createServerFixture(server, config, mcp, context) {
3369
4045
  return {
3370
- info: server.info,
4046
+ // Live view: `pid` is filled in once the server is listening and follows restarts
4047
+ get info() {
4048
+ return server.info;
4049
+ },
3371
4050
  createClient: async (opts) => {
3372
- return McpTestClient.create({
3373
- baseUrl: resolveClientBaseUrl(server),
3374
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
4051
+ const client = await McpTestClient.create({
4052
+ baseUrl: resolveClientBaseUrl(server, config),
4053
+ entryPath: opts?.entryPath ?? config.entryPath,
3375
4054
  transport: opts?.transport ?? "streamable-http",
3376
4055
  auth: opts?.token ? { token: opts.token } : void 0,
3377
4056
  clientInfo: opts?.clientInfo,
3378
- publicMode: currentConfig.publicMode
4057
+ publicMode: config.publicMode
3379
4058
  }).buildAndConnect();
4059
+ context.extraClients.push(client);
4060
+ return client;
3380
4061
  },
3381
4062
  createClientBuilder: () => {
3382
4063
  const builder = new McpTestClientBuilder({
3383
- baseUrl: resolveClientBaseUrl(server),
3384
- entryPath: currentConfig.entryPath,
3385
- publicMode: currentConfig.publicMode
4064
+ baseUrl: resolveClientBaseUrl(server, config),
4065
+ entryPath: config.entryPath,
4066
+ publicMode: config.publicMode
3386
4067
  });
3387
4068
  return builder;
3388
4069
  },
3389
- restart: () => server.restart(),
4070
+ restart: async () => {
4071
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
4072
+ await server.restart();
4073
+ for (const client of reconnect) {
4074
+ await client.reconnect();
4075
+ }
4076
+ },
3390
4077
  getLogs: () => server.getLogs(),
3391
4078
  clearLogs: () => server.clearLogs()
3392
4079
  };
@@ -3397,38 +4084,71 @@ function resolveServerCommand(server) {
3397
4084
  }
3398
4085
  return `npx tsx ${server}`;
3399
4086
  }
3400
- function runWithFixtures(fn) {
3401
- return async () => {
3402
- const fixtures = await createTestFixtures();
3403
- let testFailed = false;
4087
+ function runWithFixtures(fn, chain) {
4088
+ return async (...extra) => {
4089
+ const fixtures = await buildFixtures(chain);
4090
+ let failure;
3404
4091
  try {
3405
- await fn(fixtures);
4092
+ for (const scope of chain) {
4093
+ for (const hook of scope.beforeEachHooks) await hook(fixtures);
4094
+ }
4095
+ await fn(fixtures, ...extra);
3406
4096
  } catch (error) {
3407
- testFailed = true;
3408
- throw error;
3409
- } finally {
3410
- await cleanupTestFixtures(fixtures, testFailed);
4097
+ failure = { error };
4098
+ }
4099
+ try {
4100
+ await runAfterEachHooks(chain, fixtures);
4101
+ } catch (error) {
4102
+ failure ??= { error };
3411
4103
  }
4104
+ await cleanupTestFixtures(fixtures, failure !== void 0);
4105
+ if (failure) throw failure.error;
3412
4106
  };
3413
4107
  }
4108
+ async function runAfterEachHooks(chain, fixtures) {
4109
+ let firstError;
4110
+ let failed = false;
4111
+ for (let i = chain.length - 1; i >= 0; i--) {
4112
+ for (const hook of chain[i].afterEachHooks) {
4113
+ try {
4114
+ await hook(fixtures);
4115
+ } catch (error) {
4116
+ if (!failed) firstError = error;
4117
+ failed = true;
4118
+ }
4119
+ }
4120
+ }
4121
+ if (failed) throw firstError;
4122
+ }
4123
+ function skipTitle(name, scope) {
4124
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
4125
+ }
3414
4126
  function testWithFixtures(name, fn) {
3415
- const scope = currentSkipScope();
4127
+ const chain = scopes.slice();
4128
+ const scope = currentScope();
3416
4129
  if (scope.skipped) {
3417
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
4130
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
3418
4131
  return;
3419
4132
  }
3420
- it(name, runWithFixtures(fn));
4133
+ jestGlobals().it(name, runWithFixtures(fn, chain));
3421
4134
  }
3422
4135
  function use(config) {
3423
- currentConfig = { ...currentConfig, ...config };
3424
- afterAll(async () => {
3425
- await cleanupSharedResources();
3426
- });
4136
+ const scope = currentScope();
4137
+ const { env, ...rest } = config;
4138
+ scope.config = { ...scope.config, ...rest };
4139
+ if (env) scope.envs.push(env);
4140
+ scope.configured = true;
4141
+ if (!scope.cleanupRegistered) {
4142
+ scope.cleanupRegistered = true;
4143
+ jestGlobals().afterAll(async () => {
4144
+ await stopServers(scope === scopes[0] ? void 0 : scope);
4145
+ });
4146
+ }
3427
4147
  }
3428
4148
  function skip(nameOrCondition, fnOrReason) {
3429
4149
  if (typeof nameOrCondition === "boolean") {
3430
4150
  if (nameOrCondition) {
3431
- const scope = currentSkipScope();
4151
+ const scope = currentScope();
3432
4152
  scope.skipped = true;
3433
4153
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
3434
4154
  }
@@ -3439,53 +4159,93 @@ function skip(nameOrCondition, fnOrReason) {
3439
4159
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
3440
4160
  );
3441
4161
  }
3442
- it.skip(nameOrCondition, runWithFixtures(fnOrReason));
4162
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
3443
4163
  }
3444
4164
  function only(name, fn) {
3445
- const scope = currentSkipScope();
4165
+ const scope = currentScope();
3446
4166
  if (scope.skipped) {
3447
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
4167
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
3448
4168
  return;
3449
4169
  }
3450
- it.only(name, runWithFixtures(fn));
4170
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
3451
4171
  }
3452
4172
  function todo(name) {
3453
- it.todo(name);
4173
+ jestGlobals().it.todo(name);
3454
4174
  }
3455
- function withSkipScope(register) {
4175
+ function each(table, ...tagged) {
4176
+ const chain = scopes.slice();
4177
+ const register = jestGlobals().it.each(table, ...tagged);
3456
4178
  return (name, fn) => {
3457
- register(name, () => {
3458
- const parent = currentSkipScope();
3459
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
3460
- try {
3461
- fn();
3462
- } finally {
3463
- skipScopes.pop();
3464
- }
3465
- });
4179
+ const scope = currentScope();
4180
+ if (scope.skipped) {
4181
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
4182
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
4183
+ return;
4184
+ }
4185
+ register(name, runWithFixtures(fn, chain));
3466
4186
  };
3467
4187
  }
3468
- function eachWithSkipScope(each) {
3469
- return ((...eachArgs) => {
3470
- const register = each(...eachArgs);
3471
- return withSkipScope(register);
3472
- });
4188
+ function withScope(fn) {
4189
+ return (...args) => {
4190
+ scopes.push(newScope(currentScope()));
4191
+ try {
4192
+ fn(...args);
4193
+ } finally {
4194
+ scopes.pop();
4195
+ }
4196
+ };
4197
+ }
4198
+ function scopedRegister(pick) {
4199
+ return (name, fn, timeout) => {
4200
+ pick()(name, withScope(fn), timeout);
4201
+ };
4202
+ }
4203
+ function scopedEach(pick) {
4204
+ return (...eachArgs) => (name, fn, timeout) => {
4205
+ const register = pick()(...eachArgs);
4206
+ register(name, withScope(fn), timeout);
4207
+ };
4208
+ }
4209
+ var describeWithScope = Object.assign(
4210
+ scopedRegister(() => jestGlobals().describe),
4211
+ {
4212
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
4213
+ only: Object.assign(
4214
+ scopedRegister(() => jestGlobals().describe.only),
4215
+ {
4216
+ each: scopedEach(() => jestGlobals().describe.only.each)
4217
+ }
4218
+ ),
4219
+ skip: Object.assign(
4220
+ scopedRegister(() => jestGlobals().describe.skip),
4221
+ {
4222
+ each: scopedEach(() => jestGlobals().describe.skip.each)
4223
+ }
4224
+ ),
4225
+ each: scopedEach(() => jestGlobals().describe.each)
4226
+ }
4227
+ );
4228
+ function eachHook(kind) {
4229
+ return (fn, timeout) => {
4230
+ if (typeof fn === "function" && fn.length > 0) {
4231
+ const scope = currentScope();
4232
+ (kind === "beforeEach" ? scope.beforeEachHooks : scope.afterEachHooks).push(fn);
4233
+ return;
4234
+ }
4235
+ jestGlobals()[kind](fn, timeout);
4236
+ };
3473
4237
  }
3474
- var describeWithSkipScope = Object.assign(withSkipScope(describe), describe, {
3475
- only: Object.assign(withSkipScope(describe.only), { each: eachWithSkipScope(describe.only.each) }),
3476
- skip: Object.assign(withSkipScope(describe.skip), { each: eachWithSkipScope(describe.skip.each) }),
3477
- each: eachWithSkipScope(describe.each)
3478
- });
3479
4238
  var test = testWithFixtures;
3480
4239
  test.use = use;
3481
- test.describe = describeWithSkipScope;
3482
- test.beforeAll = beforeAll;
3483
- test.beforeEach = beforeEach;
3484
- test.afterEach = afterEach;
3485
- test.afterAll = afterAll;
4240
+ test.describe = describeWithScope;
4241
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
4242
+ test.beforeEach = eachHook("beforeEach");
4243
+ test.afterEach = eachHook("afterEach");
4244
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
3486
4245
  test.skip = skip;
3487
4246
  test.only = only;
3488
4247
  test.todo = todo;
4248
+ test.each = each;
3489
4249
  // Annotate the CommonJS export names for ESM import in node:
3490
4250
  0 && (module.exports = {
3491
4251
  cleanupSharedResources,