@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
@@ -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) {
@@ -5,16 +8,6 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
8
  throw Error('Dynamic require of "' + x + '" is not supported');
6
9
  });
7
10
 
8
- // libs/testing/src/fixtures/test-fixture.ts
9
- import {
10
- afterAll as _afterAll,
11
- afterEach as _afterEach,
12
- beforeAll as _beforeAll,
13
- beforeEach as _beforeEach,
14
- describe as _describe,
15
- it as _it
16
- } from "@jest/globals";
17
-
18
11
  // libs/testing/src/auth/token-factory.ts
19
12
  import { exportJWK, generateKeyPair, SignJWT } from "jose";
20
13
  var TestTokenFactory = class {
@@ -73,14 +66,15 @@ var TestTokenFactory = class {
73
66
  */
74
67
  async createTestToken(options) {
75
68
  await this.ensureKeys();
76
- const now = Math.floor(Date.now() / 1e3);
69
+ const nowMs = Date.now();
70
+ const iat = Math.floor(nowMs / 1e3);
77
71
  const exp = options.exp ?? 3600;
78
72
  const payload = {
79
73
  iss: options.iss ?? this.issuer,
80
74
  sub: options.sub,
81
75
  aud: options.aud ?? this.audience,
82
- iat: now,
83
- exp: now + exp,
76
+ iat,
77
+ exp: Math.ceil(nowMs / 1e3) + exp,
84
78
  scope: options.scopes?.join(" "),
85
79
  ...options.claims
86
80
  };
@@ -105,10 +99,10 @@ var TestTokenFactory = class {
105
99
  /**
106
100
  * Create a regular user token
107
101
  */
108
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
102
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
109
103
  return this.createTestToken({
110
104
  sub,
111
- scopes,
105
+ scopes: scopes2,
112
106
  claims: {
113
107
  email: "user@test.local",
114
108
  name: "Test User",
@@ -119,10 +113,11 @@ var TestTokenFactory = class {
119
113
  /**
120
114
  * Create an anonymous user token
121
115
  */
122
- async createAnonymousToken() {
116
+ async createAnonymousToken(expiresIn) {
123
117
  return this.createTestToken({
124
118
  sub: `anon:${Date.now()}`,
125
119
  scopes: ["anonymous"],
120
+ exp: expiresIn,
126
121
  claims: {
127
122
  name: "Anonymous",
128
123
  role: "anonymous"
@@ -471,8 +466,479 @@ function sleep(ms) {
471
466
  return new Promise((resolve) => setTimeout(resolve, ms));
472
467
  }
473
468
 
474
- // libs/testing/src/transport/streamable-http.transport.ts
469
+ // libs/testing/src/transport/error-utils.ts
470
+ function errorName(err) {
471
+ if (typeof err !== "object" || err === null) return void 0;
472
+ const name = err.name;
473
+ return typeof name === "string" ? name : void 0;
474
+ }
475
+ function isAbortError(err) {
476
+ return errorName(err) === "AbortError";
477
+ }
478
+ function errorMessage(err, fallback = "Unknown error") {
479
+ if (typeof err === "string" && err) return err;
480
+ if (typeof err === "object" && err !== null) {
481
+ const message = err.message;
482
+ if (typeof message === "string" && message) return message;
483
+ }
484
+ return fallback;
485
+ }
486
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
487
+ function markInterceptedError(err) {
488
+ interceptedErrors.add(err);
489
+ return err;
490
+ }
491
+ function isInterceptedError(err) {
492
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
493
+ }
494
+
495
+ // libs/testing/src/transport/sse.transport.ts
475
496
  var DEFAULT_TIMEOUT = 3e4;
497
+ var SseTransport = class {
498
+ baseUrl;
499
+ entryPath;
500
+ timeout;
501
+ authHeaders;
502
+ publicMode;
503
+ debug;
504
+ clientInfo;
505
+ notificationHandler;
506
+ interceptors;
507
+ elicitationHandler;
508
+ state = "disconnected";
509
+ authToken;
510
+ sessionId;
511
+ messageEndpoint;
512
+ stream;
513
+ pending = /* @__PURE__ */ new Map();
514
+ connectionCount = 0;
515
+ reconnectCount = 0;
516
+ lastRequestHeaders = {};
517
+ constructor(config) {
518
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
519
+ this.entryPath = config.entryPath;
520
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
521
+ this.authHeaders = config.auth?.headers ?? {};
522
+ this.authToken = config.auth?.token;
523
+ this.publicMode = config.publicMode ?? false;
524
+ this.debug = config.debug ?? false;
525
+ this.clientInfo = config.clientInfo;
526
+ this.notificationHandler = config.notificationHandler;
527
+ this.interceptors = config.interceptors;
528
+ this.elicitationHandler = config.elicitationHandler;
529
+ }
530
+ async connect() {
531
+ this.state = "connecting";
532
+ this.connectionCount++;
533
+ try {
534
+ if (!this.publicMode && !this.authToken) {
535
+ await this.requestAnonymousToken();
536
+ }
537
+ await this.openStream();
538
+ this.state = "connected";
539
+ this.log(`Connected; messages go to ${this.messageEndpoint}`);
540
+ } catch (error) {
541
+ this.state = "error";
542
+ this.stream?.abort();
543
+ this.stream = void 0;
544
+ throw error;
545
+ }
546
+ }
547
+ async request(message) {
548
+ this.ensureConnected();
549
+ const startTime = Date.now();
550
+ if (this.interceptors) {
551
+ const intercepted = await this.interceptors.processRequest(message, {
552
+ timestamp: /* @__PURE__ */ new Date(),
553
+ transport: "sse",
554
+ sessionId: this.sessionId
555
+ });
556
+ switch (intercepted.type) {
557
+ case "mock":
558
+ return await this.interceptors.processResponse(
559
+ message,
560
+ intercepted.response,
561
+ Date.now() - startTime
562
+ );
563
+ case "error":
564
+ throw markInterceptedError(intercepted.error);
565
+ case "continue":
566
+ message = intercepted.request;
567
+ break;
568
+ }
569
+ }
570
+ let response;
571
+ if (message.id === void 0) {
572
+ await this.post(JSON.stringify(message));
573
+ response = { jsonrpc: "2.0", id: null, result: void 0 };
574
+ } else {
575
+ response = await this.postAndAwait(message.id, JSON.stringify(message));
576
+ }
577
+ if (this.interceptors) {
578
+ response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
579
+ }
580
+ return response;
581
+ }
582
+ async notify(message) {
583
+ this.ensureConnected();
584
+ const result = await this.post(JSON.stringify(message));
585
+ if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
586
+ }
587
+ async sendRaw(data) {
588
+ this.ensureConnected();
589
+ let id;
590
+ try {
591
+ const parsed = JSON.parse(data);
592
+ if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
593
+ } catch {
594
+ }
595
+ if (id !== void 0) return this.postAndAwait(id, data);
596
+ const result = await this.post(data);
597
+ if (!result.ok) {
598
+ return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
599
+ }
600
+ return { jsonrpc: "2.0", id: null, result: void 0 };
601
+ }
602
+ async close() {
603
+ this.closeStream("Transport closed");
604
+ this.state = "disconnected";
605
+ this.sessionId = void 0;
606
+ this.messageEndpoint = void 0;
607
+ this.log("SSE transport closed");
608
+ }
609
+ isConnected() {
610
+ return this.state === "connected";
611
+ }
612
+ getState() {
613
+ return this.state;
614
+ }
615
+ getSessionId() {
616
+ return this.sessionId;
617
+ }
618
+ setAuthToken(token) {
619
+ this.authToken = token;
620
+ }
621
+ setTimeout(ms) {
622
+ this.timeout = ms;
623
+ }
624
+ getMessageEndpoint() {
625
+ return this.messageEndpoint;
626
+ }
627
+ getConnectionCount() {
628
+ return this.connectionCount;
629
+ }
630
+ getReconnectCount() {
631
+ return this.reconnectCount;
632
+ }
633
+ getLastRequestHeaders() {
634
+ return { ...this.lastRequestHeaders };
635
+ }
636
+ setInterceptors(interceptors2) {
637
+ this.interceptors = interceptors2;
638
+ }
639
+ getInterceptors() {
640
+ return this.interceptors;
641
+ }
642
+ setElicitationHandler(handler) {
643
+ this.elicitationHandler = handler;
644
+ }
645
+ async simulateDisconnect() {
646
+ this.closeStream("Simulated disconnect");
647
+ this.state = "disconnected";
648
+ this.sessionId = void 0;
649
+ this.messageEndpoint = void 0;
650
+ }
651
+ async waitForReconnect(timeoutMs) {
652
+ this.reconnectCount++;
653
+ let timer;
654
+ try {
655
+ await Promise.race([
656
+ this.connect(),
657
+ new Promise((_, reject) => {
658
+ timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
659
+ })
660
+ ]);
661
+ } finally {
662
+ clearTimeout(timer);
663
+ }
664
+ }
665
+ // ═══════════════════════════════════════════════════════════════════
666
+ // STREAM
667
+ // ═══════════════════════════════════════════════════════════════════
668
+ /** Open `GET <entry>/sse` and wait for the `endpoint` event. */
669
+ async openStream() {
670
+ const controller = new AbortController();
671
+ this.stream = controller;
672
+ const url = this.sseUrl();
673
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
674
+ this.log(`GET ${url}`);
675
+ const connectTimer = setTimeout(() => controller.abort(), this.timeout);
676
+ let response;
677
+ try {
678
+ response = await fetch(url, {
679
+ method: "GET",
680
+ headers: { ...headers, Accept: "text/event-stream" },
681
+ signal: controller.signal
682
+ });
683
+ } catch (error) {
684
+ clearTimeout(connectTimer);
685
+ throw new Error(
686
+ isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
687
+ { cause: error }
688
+ );
689
+ }
690
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
691
+ if (!response.ok || !response.body || !isEventStream) {
692
+ clearTimeout(connectTimer);
693
+ const body = await response.text().catch(() => "");
694
+ throw new Error(
695
+ `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 } } })).`
696
+ );
697
+ }
698
+ const endpoint = new Promise((resolve, reject) => {
699
+ const reader = response.body?.getReader();
700
+ if (!reader) {
701
+ reject(new Error(`SSE connection to ${url} has no body`));
702
+ return;
703
+ }
704
+ void this.readStream(controller, reader, resolve, reject);
705
+ });
706
+ try {
707
+ const data = await endpoint;
708
+ const messageUrl = new URL(data, url);
709
+ this.messageEndpoint = messageUrl.toString();
710
+ this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
711
+ } finally {
712
+ clearTimeout(connectTimer);
713
+ }
714
+ }
715
+ async readStream(controller, reader, onEndpoint, onEndpointError) {
716
+ const decoder = new TextDecoder();
717
+ let buffer = "";
718
+ let endpointSeen = false;
719
+ try {
720
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
721
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
722
+ buffer = remainder;
723
+ for (const text of completeEvents) {
724
+ const event = parseSSEEvent(text);
725
+ if (!event) continue;
726
+ if (event.type === "endpoint") {
727
+ if (!endpointSeen) {
728
+ endpointSeen = true;
729
+ onEndpoint(event.data);
730
+ }
731
+ continue;
732
+ }
733
+ await this.handleMessage(event.data);
734
+ }
735
+ }
736
+ } catch (error) {
737
+ if (!controller.signal.aborted) this.log("SSE stream failed:", error);
738
+ } finally {
739
+ reader.releaseLock();
740
+ }
741
+ if (!endpointSeen) {
742
+ onEndpointError(
743
+ new Error(
744
+ 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`
745
+ )
746
+ );
747
+ }
748
+ if (this.stream === controller) {
749
+ this.stream = void 0;
750
+ this.rejectPending("SSE stream closed by the server");
751
+ if (this.state === "connected") this.state = "disconnected";
752
+ }
753
+ }
754
+ async handleMessage(data) {
755
+ let message;
756
+ try {
757
+ message = JSON.parse(data);
758
+ } catch {
759
+ this.log("Ignoring SSE event that is not JSON:", data);
760
+ return;
761
+ }
762
+ if (!message || typeof message !== "object") return;
763
+ if (typeof message.method === "string") {
764
+ const request = message;
765
+ if (request.id === void 0 || request.id === null) {
766
+ this.notificationHandler?.(request);
767
+ return;
768
+ }
769
+ await this.answerServerRequest(request);
770
+ return;
771
+ }
772
+ if (message.id !== void 0 && message.id !== null) {
773
+ const pending = this.pending.get(message.id);
774
+ if (pending) {
775
+ clearTimeout(pending.timer);
776
+ this.pending.delete(message.id);
777
+ pending.resolve(message);
778
+ }
779
+ }
780
+ }
781
+ /** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
782
+ async answerServerRequest(request) {
783
+ const id = request.id;
784
+ let reply;
785
+ if (request.method === "elicitation/create") {
786
+ reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
787
+ } else if (request.method === "ping") {
788
+ reply = { jsonrpc: "2.0", id, result: {} };
789
+ } else {
790
+ reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
791
+ }
792
+ const result = await this.post(JSON.stringify(reply));
793
+ if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
794
+ }
795
+ async elicit(params) {
796
+ if (!this.elicitationHandler) return { action: "decline" };
797
+ try {
798
+ return await this.elicitationHandler(params);
799
+ } catch (error) {
800
+ this.log("Elicitation handler error:", error);
801
+ return { action: "cancel" };
802
+ }
803
+ }
804
+ closeStream(reason) {
805
+ const controller = this.stream;
806
+ this.stream = void 0;
807
+ controller?.abort();
808
+ this.rejectPending(reason);
809
+ }
810
+ rejectPending(reason) {
811
+ for (const [id, pending] of this.pending) {
812
+ clearTimeout(pending.timer);
813
+ pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
814
+ }
815
+ this.pending.clear();
816
+ }
817
+ // ═══════════════════════════════════════════════════════════════════
818
+ // POSTING
819
+ // ═══════════════════════════════════════════════════════════════════
820
+ /** POST a message and resolve with the response the stream carries for `id`. */
821
+ async postAndAwait(id, body) {
822
+ const answer = new Promise((resolve) => {
823
+ const timer = setTimeout(() => {
824
+ this.pending.delete(id);
825
+ resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
826
+ }, this.timeout);
827
+ this.pending.set(id, { resolve, timer });
828
+ });
829
+ const result = await this.post(body);
830
+ if (!result.ok) {
831
+ const pending = this.pending.get(id);
832
+ if (pending) {
833
+ clearTimeout(pending.timer);
834
+ this.pending.delete(id);
835
+ }
836
+ return {
837
+ jsonrpc: "2.0",
838
+ id,
839
+ error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
840
+ };
841
+ }
842
+ return answer;
843
+ }
844
+ async post(body) {
845
+ const endpoint = this.messageEndpoint;
846
+ if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
847
+ const headers = this.buildHeaders();
848
+ this.lastRequestHeaders = headers;
849
+ this.log(`POST ${endpoint}`, body);
850
+ const controller = new AbortController();
851
+ const timer = setTimeout(() => controller.abort(), this.timeout);
852
+ try {
853
+ const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
854
+ const text = await response.text();
855
+ return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
856
+ } catch (error) {
857
+ if (isAbortError(error)) {
858
+ return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
859
+ }
860
+ throw error;
861
+ } finally {
862
+ clearTimeout(timer);
863
+ }
864
+ }
865
+ // ═══════════════════════════════════════════════════════════════════
866
+ // HELPERS
867
+ // ═══════════════════════════════════════════════════════════════════
868
+ async requestAnonymousToken() {
869
+ const url = new URL(this.baseUrl);
870
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
871
+ url.search = "";
872
+ const tokenUrl = url.toString();
873
+ try {
874
+ const response = await fetch(tokenUrl, {
875
+ method: "POST",
876
+ headers: { "Content-Type": "application/json" },
877
+ body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
878
+ });
879
+ if (!response.ok) {
880
+ this.log(`No anonymous token (HTTP ${response.status})`);
881
+ return;
882
+ }
883
+ const token = await response.json();
884
+ if (token.access_token) this.authToken = token.access_token;
885
+ } catch (error) {
886
+ this.log("Error requesting anonymous token:", error);
887
+ }
888
+ }
889
+ /** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
890
+ sseUrl() {
891
+ const url = new URL(this.baseUrl);
892
+ const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
893
+ const basePath = url.pathname.replace(/\/+$/, "");
894
+ url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
895
+ return url.toString();
896
+ }
897
+ buildHeaders() {
898
+ const headers = {
899
+ "Content-Type": "application/json",
900
+ Accept: "application/json, text/event-stream"
901
+ };
902
+ if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
903
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
904
+ if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
905
+ Object.assign(headers, this.authHeaders);
906
+ return headers;
907
+ }
908
+ ensureConnected() {
909
+ if (this.state !== "connected") {
910
+ throw new Error("Transport not connected. Call connect() first.");
911
+ }
912
+ }
913
+ log(message, data) {
914
+ if (this.debug) {
915
+ console.log(`[SSE] ${message}`, data ?? "");
916
+ }
917
+ }
918
+ };
919
+ function splitSSEEvents(buffer) {
920
+ const endsWithCarriageReturn = buffer.endsWith("\r");
921
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
922
+ const completeEvents = normalized.split("\n\n");
923
+ const remainder = completeEvents.pop() ?? "";
924
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
925
+ }
926
+ function parseSSEEvent(text) {
927
+ let type = "message";
928
+ const data = [];
929
+ for (const line of text.split("\n")) {
930
+ if (line.startsWith(":")) continue;
931
+ const colon = line.indexOf(":");
932
+ const field = colon === -1 ? line : line.slice(0, colon);
933
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
934
+ if (field === "event") type = value;
935
+ else if (field === "data") data.push(value);
936
+ }
937
+ return data.length > 0 ? { type, data: data.join("\n") } : void 0;
938
+ }
939
+
940
+ // libs/testing/src/transport/streamable-http.transport.ts
941
+ var DEFAULT_TIMEOUT2 = 3e4;
476
942
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
477
943
  var StreamableHttpTransport = class {
478
944
  config;
@@ -494,7 +960,7 @@ var StreamableHttpTransport = class {
494
960
  baseUrl: config.baseUrl.replace(/\/$/, ""),
495
961
  // Remove trailing slash
496
962
  entryPath: config.entryPath,
497
- timeout: config.timeout ?? DEFAULT_TIMEOUT,
963
+ timeout: config.timeout ?? DEFAULT_TIMEOUT2,
498
964
  auth: config.auth ?? {},
499
965
  publicMode: config.publicMode ?? false,
500
966
  debug: config.debug ?? false,
@@ -579,7 +1045,7 @@ var StreamableHttpTransport = class {
579
1045
  return mockResponse2;
580
1046
  }
581
1047
  case "error":
582
- throw interceptResult.error;
1048
+ throw markInterceptedError(interceptResult.error);
583
1049
  case "continue":
584
1050
  message = interceptResult.request;
585
1051
  break;
@@ -640,7 +1106,7 @@ var StreamableHttpTransport = class {
640
1106
  return jsonResponse;
641
1107
  } catch (error) {
642
1108
  clearTimeout(timeoutId);
643
- if (error instanceof Error && error.name === "AbortError") {
1109
+ if (isAbortError(error)) {
644
1110
  return {
645
1111
  jsonrpc: "2.0",
646
1112
  id: message.id ?? null,
@@ -679,7 +1145,7 @@ var StreamableHttpTransport = class {
679
1145
  }
680
1146
  } catch (error) {
681
1147
  clearTimeout(timeoutId);
682
- if (error instanceof Error && error.name !== "AbortError") {
1148
+ if (!isAbortError(error)) {
683
1149
  throw error;
684
1150
  }
685
1151
  }
@@ -720,7 +1186,7 @@ var StreamableHttpTransport = class {
720
1186
  error: {
721
1187
  code: -32700,
722
1188
  message: "Parse error",
723
- data: error instanceof Error ? error.message : "Unknown error"
1189
+ data: errorMessage(error)
724
1190
  }
725
1191
  };
726
1192
  }
@@ -834,7 +1300,7 @@ var StreamableHttpTransport = class {
834
1300
  let buffer = "";
835
1301
  try {
836
1302
  for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
837
- const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1303
+ const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
838
1304
  buffer = remainder;
839
1305
  for (const eventText of completeEvents) {
840
1306
  for (const event of this.parseSSEEvents(eventText, void 0).events) {
@@ -1090,7 +1556,7 @@ var StreamableHttpTransport = class {
1090
1556
  if (this.config.clientInfo) {
1091
1557
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1092
1558
  }
1093
- if (this.authToken && !this.publicMode) {
1559
+ if (this.authToken) {
1094
1560
  headers["Authorization"] = `Bearer ${this.authToken}`;
1095
1561
  }
1096
1562
  if (this.sessionId) {
@@ -1168,7 +1634,7 @@ var StreamableHttpTransport = class {
1168
1634
  };
1169
1635
  }
1170
1636
  };
1171
- function splitSSEEvents(buffer) {
1637
+ function splitSSEEvents2(buffer) {
1172
1638
  const endsWithCarriageReturn = buffer.endsWith("\r");
1173
1639
  const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1174
1640
  const completeEvents = normalized.split("\n\n");
@@ -1406,7 +1872,7 @@ var McpTestClientBuilder = class {
1406
1872
  };
1407
1873
 
1408
1874
  // libs/testing/src/client/mcp-test-client.ts
1409
- var DEFAULT_TIMEOUT2 = 3e4;
1875
+ var DEFAULT_TIMEOUT3 = 3e4;
1410
1876
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1411
1877
  var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1412
1878
  var DEFAULT_CLIENT_INFO = {
@@ -1414,6 +1880,15 @@ var DEFAULT_CLIENT_INFO = {
1414
1880
  version: "0.4.0"
1415
1881
  };
1416
1882
  var MAX_LIST_PAGES = 1e3;
1883
+ function isAuthFailure(error) {
1884
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1885
+ }
1886
+ function describeInitializeError(error) {
1887
+ const message = error?.message ?? "Unknown error";
1888
+ const data = error?.data;
1889
+ const detail = typeof data === "string" ? data.trim() : "";
1890
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1891
+ }
1417
1892
  function extractEntryPaths(data) {
1418
1893
  let payload = data;
1419
1894
  if (typeof payload === "string") {
@@ -1467,7 +1942,7 @@ var McpTestClient = class {
1467
1942
  transport: config.transport ?? "streamable-http",
1468
1943
  auth: config.auth ?? {},
1469
1944
  publicMode: config.publicMode ?? false,
1470
- timeout: config.timeout ?? DEFAULT_TIMEOUT2,
1945
+ timeout: config.timeout ?? DEFAULT_TIMEOUT3,
1471
1946
  debug: config.debug ?? false,
1472
1947
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
1473
1948
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
@@ -1500,19 +1975,22 @@ var McpTestClient = class {
1500
1975
  async connect() {
1501
1976
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1502
1977
  this.triedDiscoveredEntryPath = false;
1978
+ this.initResult = null;
1503
1979
  this.transport = this.createTransport();
1504
1980
  await this.transport.connect();
1505
1981
  const maxRetries = 3;
1506
1982
  const retryDelayMs = 500;
1507
1983
  let lastError;
1508
1984
  let reportedEntryPaths;
1985
+ let attemptsMade = 0;
1509
1986
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1987
+ attemptsMade = attempt;
1510
1988
  const initResponse = await this.initialize();
1511
1989
  if (initResponse.success && initResponse.data) {
1512
1990
  this.initResult = initResponse.data;
1513
1991
  break;
1514
1992
  }
1515
- lastError = initResponse.error?.message ?? "Unknown error";
1993
+ lastError = describeInitializeError(initResponse.error);
1516
1994
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1517
1995
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1518
1996
  this.triedDiscoveredEntryPath = true;
@@ -1524,6 +2002,7 @@ var McpTestClient = class {
1524
2002
  await this.transport.connect();
1525
2003
  continue;
1526
2004
  }
2005
+ if (isAuthFailure(initResponse.error)) break;
1527
2006
  if (attempt < maxRetries) {
1528
2007
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1529
2008
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1532,7 +2011,7 @@ var McpTestClient = class {
1532
2011
  if (!this.initResult) {
1533
2012
  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.` : "";
1534
2013
  throw new Error(
1535
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
2014
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1536
2015
  );
1537
2016
  }
1538
2017
  this._sessionId = this.transport.getSessionId();
@@ -1602,18 +2081,47 @@ var McpTestClient = class {
1602
2081
  return this._authState;
1603
2082
  }
1604
2083
  /**
1605
- * Authenticate with a token
2084
+ * Authenticate with a token.
2085
+ *
2086
+ * When connected, this opens a NEW session with the token (the previous session was created
2087
+ * for the previous identity) and rejects if the server refuses it — for example because the
2088
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
2089
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1606
2090
  */
1607
2091
  async authenticate(token) {
2092
+ if (typeof token !== "string" || token.trim() === "") {
2093
+ throw new Error("authenticate() requires a non-empty token");
2094
+ }
2095
+ const previousAuthState = this._authState;
2096
+ const previousConfigAuth = this.config.auth;
2097
+ this.config.auth = { ...previousConfigAuth, token };
1608
2098
  this._authState = {
1609
2099
  isAnonymous: false,
1610
2100
  token,
1611
2101
  scopes: this.parseScopesFromToken(token),
1612
2102
  user: this.parseUserFromToken(token)
1613
2103
  };
1614
- if (this.transport) {
1615
- this.transport.setAuthToken(token);
2104
+ const previousTransport = this.transport;
2105
+ if (!previousTransport) {
2106
+ this.log("debug", "Authentication updated (not connected yet)");
2107
+ return;
1616
2108
  }
2109
+ const previousInit = this.initResult;
2110
+ const previousSessionId = this._sessionId;
2111
+ const previousSessionInfo = this._sessionInfo;
2112
+ try {
2113
+ await this.connect();
2114
+ } catch (err) {
2115
+ await this.transport?.close().catch(() => void 0);
2116
+ this.transport = previousTransport;
2117
+ this.initResult = previousInit;
2118
+ this._sessionId = previousSessionId;
2119
+ this._sessionInfo = previousSessionInfo;
2120
+ this.config.auth = previousConfigAuth;
2121
+ this._authState = previousAuthState;
2122
+ throw err;
2123
+ }
2124
+ await previousTransport.close().catch(() => void 0);
1617
2125
  this.log("debug", "Authentication updated");
1618
2126
  }
1619
2127
  // ═══════════════════════════════════════════════════════════════════
@@ -2200,7 +2708,18 @@ var McpTestClient = class {
2200
2708
  notificationHandler: this.recordNotification
2201
2709
  });
2202
2710
  case "sse":
2203
- throw new Error("SSE transport not yet implemented");
2711
+ return new SseTransport({
2712
+ baseUrl,
2713
+ entryPath: this.config.entryPath,
2714
+ timeout: this.config.timeout,
2715
+ auth: this.config.auth,
2716
+ publicMode: this.config.publicMode,
2717
+ debug: this.config.debug,
2718
+ interceptors: this._interceptors,
2719
+ clientInfo: this.config.clientInfo,
2720
+ elicitationHandler: this._elicitationHandler,
2721
+ notificationHandler: this.recordNotification
2722
+ });
2204
2723
  default:
2205
2724
  throw new Error(`Unknown transport type: ${this.config.transport}`);
2206
2725
  }
@@ -2237,10 +2756,11 @@ var McpTestClient = class {
2237
2756
  requestId: id
2238
2757
  };
2239
2758
  } catch (err) {
2759
+ if (isInterceptedError(err)) throw err;
2240
2760
  const durationMs = Date.now() - start;
2241
2761
  const error = {
2242
2762
  code: -32603,
2243
- message: err instanceof Error ? err.message : "Unknown error"
2763
+ message: errorMessage(err)
2244
2764
  };
2245
2765
  return {
2246
2766
  success: false,
@@ -2388,12 +2908,12 @@ var McpTestClient = class {
2388
2908
  const payload = this.decodeJwtPayload(token);
2389
2909
  if (!payload) return [];
2390
2910
  const scope = payload["scope"];
2391
- const scopes = payload["scopes"];
2911
+ const scopes2 = payload["scopes"];
2392
2912
  if (typeof scope === "string") {
2393
2913
  return scope.split(" ");
2394
2914
  }
2395
- if (Array.isArray(scopes)) {
2396
- return scopes;
2915
+ if (Array.isArray(scopes2)) {
2916
+ return scopes2;
2397
2917
  }
2398
2918
  return [];
2399
2919
  } catch {
@@ -2468,7 +2988,7 @@ var ProgressCollector = class {
2468
2988
  };
2469
2989
 
2470
2990
  // libs/testing/src/server/test-server.ts
2471
- import { spawn } from "child_process";
2991
+ import { execFile, spawn } from "child_process";
2472
2992
  import { sha256Hex } from "@frontmcp/utils";
2473
2993
 
2474
2994
  // libs/testing/src/errors/index.ts
@@ -2539,6 +3059,8 @@ var E2E_PORT_RANGES = {
2539
3059
  "demo-e2e-distributed": { start: 50440, size: 20 },
2540
3060
  // Protocol revision E2E tests (50460-50479)
2541
3061
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
3062
+ // Testing library E2E tests (50480-50499)
3063
+ "demo-e2e-testing": { start: 50480, size: 20 },
2542
3064
  // Mock servers and utilities (50900-50999)
2543
3065
  "mock-oauth": { start: 50900, size: 10 },
2544
3066
  "mock-api": { start: 50910, size: 10 },
@@ -2547,6 +3069,66 @@ var E2E_PORT_RANGES = {
2547
3069
  default: { start: 51e3, size: 100 }
2548
3070
  };
2549
3071
  var reservedPorts = /* @__PURE__ */ new Map();
3072
+ var lockedPorts = /* @__PURE__ */ new Set();
3073
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
3074
+ function lockDir() {
3075
+ try {
3076
+ const os = __require("node:os");
3077
+ const path = __require("node:path");
3078
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
3079
+ } catch {
3080
+ return void 0;
3081
+ }
3082
+ }
3083
+ function pidIsAlive(pid) {
3084
+ try {
3085
+ process.kill(pid, 0);
3086
+ return true;
3087
+ } catch (error) {
3088
+ return error.code === "EPERM";
3089
+ }
3090
+ }
3091
+ function acquirePortLock(port) {
3092
+ if (lockedPorts.has(port)) return false;
3093
+ let fs;
3094
+ let file;
3095
+ try {
3096
+ fs = __require("node:fs");
3097
+ const dir = lockDir();
3098
+ if (!dir) return true;
3099
+ fs.mkdirSync(dir, { recursive: true });
3100
+ file = __require("node:path").join(dir, `${port}.lock`);
3101
+ } catch {
3102
+ return true;
3103
+ }
3104
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
3105
+ for (let attempt = 0; attempt < 2; attempt++) {
3106
+ try {
3107
+ fs.writeFileSync(file, payload, { flag: "wx" });
3108
+ lockedPorts.add(port);
3109
+ return true;
3110
+ } catch (error) {
3111
+ if (error.code !== "EEXIST") return true;
3112
+ try {
3113
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
3114
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
3115
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
3116
+ fs.unlinkSync(file);
3117
+ } catch {
3118
+ }
3119
+ }
3120
+ }
3121
+ return false;
3122
+ }
3123
+ function releasePortLock(port) {
3124
+ if (!lockedPorts.delete(port)) return;
3125
+ try {
3126
+ const fs = __require("node:fs");
3127
+ const dir = lockDir();
3128
+ if (dir) fs.unlinkSync(__require("node:path").join(dir, `${port}.lock`));
3129
+ } catch {
3130
+ }
3131
+ }
2550
3132
  var projectPortIndex = /* @__PURE__ */ new Map();
2551
3133
  function getPortRange(project) {
2552
3134
  const key = project;
@@ -2557,15 +3139,22 @@ function getPortRange(project) {
2557
3139
  }
2558
3140
  async function reservePort(project, preferredPort) {
2559
3141
  const range = getPortRange(project);
2560
- if (preferredPort !== void 0) {
3142
+ const handle = (port) => ({
3143
+ port,
3144
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
3145
+ releaseSocket: async () => {
3146
+ await releasePort(port);
3147
+ },
3148
+ release: async () => {
3149
+ await releasePort(port);
3150
+ releasePortLock(port);
3151
+ }
3152
+ });
3153
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
3154
+ if (wantsSpecificPort) {
2561
3155
  const reservation = await tryReservePort(preferredPort, project);
2562
3156
  if (reservation) {
2563
- return {
2564
- port: preferredPort,
2565
- release: async () => {
2566
- await releasePort(preferredPort);
2567
- }
2568
- };
3157
+ return handle(preferredPort);
2569
3158
  }
2570
3159
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
2571
3160
  }
@@ -2574,30 +3163,19 @@ async function reservePort(project, preferredPort) {
2574
3163
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2575
3164
  const port = range.start + index % range.size;
2576
3165
  index = (index + 1) % range.size;
2577
- if (reservedPorts.has(port)) {
3166
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
2578
3167
  continue;
2579
3168
  }
2580
3169
  const reservation = await tryReservePort(port, project);
2581
3170
  if (reservation) {
2582
3171
  projectPortIndex.set(project, index);
2583
- return {
2584
- port,
2585
- release: async () => {
2586
- await releasePort(port);
2587
- }
2588
- };
3172
+ return handle(port);
2589
3173
  }
2590
3174
  }
2591
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
2592
- if (dynamicPort) {
2593
- const reservation = await tryReservePort(dynamicPort, project);
2594
- if (reservation) {
2595
- return {
2596
- port: dynamicPort,
2597
- release: async () => {
2598
- await releasePort(dynamicPort);
2599
- }
2600
- };
3175
+ for (let port = 51e3; port < 52e3; port++) {
3176
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
3177
+ if (await tryReservePort(port, project)) {
3178
+ return handle(port);
2601
3179
  }
2602
3180
  }
2603
3181
  throw new Error(
@@ -2605,9 +3183,13 @@ async function reservePort(project, preferredPort) {
2605
3183
  );
2606
3184
  }
2607
3185
  async function tryReservePort(port, project) {
3186
+ if (!acquirePortLock(port)) {
3187
+ return false;
3188
+ }
2608
3189
  return new Promise((resolve) => {
2609
3190
  const server = createServer();
2610
3191
  server.once("error", () => {
3192
+ releasePortLock(port);
2611
3193
  resolve(false);
2612
3194
  });
2613
3195
  server.listen(port, () => {
@@ -2633,31 +3215,6 @@ async function releasePort(port) {
2633
3215
  });
2634
3216
  });
2635
3217
  }
2636
- async function findAvailablePortInRange(start, end) {
2637
- for (let port = start; port < end; port++) {
2638
- if (reservedPorts.has(port)) {
2639
- continue;
2640
- }
2641
- const available = await isPortAvailable(port);
2642
- if (available) {
2643
- return port;
2644
- }
2645
- }
2646
- return null;
2647
- }
2648
- async function isPortAvailable(port) {
2649
- return new Promise((resolve) => {
2650
- const server = createServer();
2651
- server.once("error", () => {
2652
- resolve(false);
2653
- });
2654
- server.listen(port, () => {
2655
- server.close(() => {
2656
- resolve(true);
2657
- });
2658
- });
2659
- });
2660
- }
2661
3218
 
2662
3219
  // libs/testing/src/server/test-server.ts
2663
3220
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -2666,8 +3223,11 @@ var TestServer = class _TestServer {
2666
3223
  options;
2667
3224
  _info;
2668
3225
  logs = [];
3226
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
2669
3227
  portRelease = null;
2670
- constructor(options, port, portRelease) {
3228
+ /** Frees only the reservation socket, just before the server is spawned */
3229
+ portReleaseSocket = null;
3230
+ constructor(options, port, portRelease, portReleaseSocket) {
2671
3231
  this.options = {
2672
3232
  port,
2673
3233
  project: options.project,
@@ -2679,6 +3239,7 @@ var TestServer = class _TestServer {
2679
3239
  debug: options.debug ?? DEBUG_SERVER
2680
3240
  };
2681
3241
  this.portRelease = portRelease ?? null;
3242
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
2682
3243
  this._info = {
2683
3244
  baseUrl: `http://localhost:${port}`,
2684
3245
  port
@@ -2691,8 +3252,8 @@ var TestServer = class _TestServer {
2691
3252
  const project = options.project ?? "default";
2692
3253
  const maxAttempts = 3;
2693
3254
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2694
- const { port, release } = await reservePort(project, options.port);
2695
- const server = new _TestServer(options, port, release);
3255
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
3256
+ const server = new _TestServer(options, port, release, releaseSocket);
2696
3257
  try {
2697
3258
  await server.startProcess();
2698
3259
  return server;
@@ -2732,7 +3293,7 @@ var TestServer = class _TestServer {
2732
3293
  }
2733
3294
  const maxAttempts = 3;
2734
3295
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2735
- const { port, release } = await reservePort(project, options.port);
3296
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2736
3297
  const serverOptions = {
2737
3298
  ...options,
2738
3299
  port,
@@ -2740,7 +3301,7 @@ var TestServer = class _TestServer {
2740
3301
  command: `npx nx serve ${project} --port ${port}`,
2741
3302
  cwd: options.cwd ?? process.cwd()
2742
3303
  };
2743
- const server = new _TestServer(serverOptions, port, release);
3304
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
2744
3305
  try {
2745
3306
  await server.startProcess();
2746
3307
  return server;
@@ -2798,10 +3359,17 @@ var TestServer = class _TestServer {
2798
3359
  * Stop the test server
2799
3360
  */
2800
3361
  async stop() {
2801
- if (this.portRelease) {
2802
- await this.portRelease();
2803
- this.portRelease = null;
3362
+ try {
3363
+ await this.stopProcess();
3364
+ } finally {
3365
+ if (this.portRelease) {
3366
+ await this.portRelease();
3367
+ this.portRelease = null;
3368
+ }
3369
+ this.portReleaseSocket = null;
2804
3370
  }
3371
+ }
3372
+ async stopProcess() {
2805
3373
  if (this.process) {
2806
3374
  this.log("Stopping server...");
2807
3375
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -2874,7 +3442,7 @@ var TestServer = class _TestServer {
2874
3442
  * Restart the server
2875
3443
  */
2876
3444
  async restart() {
2877
- await this.stop();
3445
+ await this.stopProcess();
2878
3446
  await this.startProcess();
2879
3447
  }
2880
3448
  /**
@@ -2904,9 +3472,9 @@ var TestServer = class _TestServer {
2904
3472
  PORT: String(this.options.port)
2905
3473
  };
2906
3474
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
2907
- if (this.portRelease) {
2908
- await this.portRelease();
2909
- this.portRelease = null;
3475
+ if (this.portReleaseSocket) {
3476
+ await this.portReleaseSocket();
3477
+ this.portReleaseSocket = null;
2910
3478
  await sleep2(300);
2911
3479
  }
2912
3480
  this.process = spawn(this.options.command, [], {
@@ -2916,9 +3484,8 @@ var TestServer = class _TestServer {
2916
3484
  detached: true,
2917
3485
  stdio: ["pipe", "pipe", "pipe"]
2918
3486
  });
2919
- if (this.process.pid !== void 0) {
2920
- this._info.pid = this.process.pid;
2921
- }
3487
+ this._info.pid = this.process.pid;
3488
+ this._info.shellPid = this.process.pid;
2922
3489
  let processExited = false;
2923
3490
  let exitCode = null;
2924
3491
  let exitError = null;
@@ -3021,6 +3588,8 @@ ${allLogs}`);
3021
3588
  });
3022
3589
  if (response.ok || response.status === 404) {
3023
3590
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
3591
+ const listeningPid = await findListeningPid(this.options.port);
3592
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3024
3593
  return;
3025
3594
  }
3026
3595
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3065,6 +3634,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3065
3634
  function sleep2(ms) {
3066
3635
  return new Promise((resolve) => setTimeout(resolve, ms));
3067
3636
  }
3637
+ function findListeningPid(port) {
3638
+ if (process.platform === "win32") return Promise.resolve(void 0);
3639
+ return new Promise((resolve) => {
3640
+ execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
3641
+ if (error) {
3642
+ resolve(void 0);
3643
+ return;
3644
+ }
3645
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
3646
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
3647
+ });
3648
+ });
3649
+ }
3068
3650
  function withWorkspaceProtocolFallback(env, cwd) {
3069
3651
  if (findInstalledProtocolPackageDir(cwd)) {
3070
3652
  return env;
@@ -3181,81 +3763,163 @@ function gatewayTokenBinding(serverUrl, entryPath) {
3181
3763
  }
3182
3764
 
3183
3765
  // libs/testing/src/fixtures/test-fixture.ts
3184
- var describe = _describe;
3185
- var beforeAll = _beforeAll;
3186
- var beforeEach = _beforeEach;
3187
- var afterEach = _afterEach;
3188
- var afterAll = _afterAll;
3189
- var it = _it;
3190
- var currentConfig = {};
3191
- var serverInstance = null;
3192
- var tokenFactory = null;
3193
- var serverStartedByUs = false;
3194
- var skipScopes = [{ skipped: false }];
3195
- function currentSkipScope() {
3196
- return skipScopes[skipScopes.length - 1];
3766
+ var jestApi;
3767
+ function jestGlobals() {
3768
+ if (jestApi) return jestApi;
3769
+ try {
3770
+ const api = __require("@jest/globals");
3771
+ jestApi = {
3772
+ describe: api["describe"],
3773
+ beforeAll: api["beforeAll"],
3774
+ beforeEach: api["beforeEach"],
3775
+ afterEach: api["afterEach"],
3776
+ afterAll: api["afterAll"],
3777
+ it: api["it"]
3778
+ };
3779
+ return jestApi;
3780
+ } catch (error) {
3781
+ throw new Error(
3782
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
3783
+ { cause: error }
3784
+ );
3785
+ }
3197
3786
  }
3198
- async function initializeSharedResources() {
3199
- if (!serverInstance) {
3200
- if (currentConfig.baseUrl && !currentConfig.server) {
3201
- serverInstance = TestServer.connect(currentConfig.baseUrl);
3202
- serverStartedByUs = false;
3203
- } else if (currentConfig.server) {
3204
- const serverCommand = resolveServerCommand(currentConfig.server);
3205
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3787
+ function newScope(parent) {
3788
+ return {
3789
+ skipped: parent?.skipped ?? false,
3790
+ reason: parent?.reason,
3791
+ config: {},
3792
+ envs: [],
3793
+ configured: false,
3794
+ cleanupRegistered: false,
3795
+ beforeEachHooks: [],
3796
+ afterEachHooks: []
3797
+ };
3798
+ }
3799
+ var scopes = [newScope()];
3800
+ function currentScope() {
3801
+ return scopes[scopes.length - 1];
3802
+ }
3803
+ var serverEntries = /* @__PURE__ */ new Map();
3804
+ function resolveConfig(chain) {
3805
+ const merged = {};
3806
+ for (const scope of chain) {
3807
+ Object.assign(merged, scope.config);
3808
+ for (const env of scope.envs) merged.env = { ...merged.env, ...env };
3809
+ }
3810
+ return merged;
3811
+ }
3812
+ function ownerScopeOf(chain) {
3813
+ for (let i = chain.length - 1; i > 0; i--) {
3814
+ if (chain[i].configured) return chain[i];
3815
+ }
3816
+ return chain[0];
3817
+ }
3818
+ function serverKey(config) {
3819
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
3820
+ return JSON.stringify({
3821
+ server: config.server,
3822
+ baseUrl: config.baseUrl,
3823
+ project: config.project,
3824
+ port: config.port,
3825
+ entryPath: config.entryPath,
3826
+ startupTimeout: config.startupTimeout,
3827
+ logLevel: config.logLevel,
3828
+ auth: config.auth,
3829
+ env
3830
+ });
3831
+ }
3832
+ function serverEnv(config) {
3833
+ const extra = {};
3834
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
3835
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
3836
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
3837
+ return { ...extra, ...config.env };
3838
+ }
3839
+ async function startServerEntry(config, owner) {
3840
+ let server;
3841
+ let startedByUs = false;
3842
+ if (config.baseUrl && !config.server) {
3843
+ server = TestServer.connect(config.baseUrl);
3844
+ } else if (config.server) {
3845
+ const serverCommand = resolveServerCommand(config.server);
3846
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3847
+ if (isDebug) {
3848
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
3849
+ }
3850
+ try {
3851
+ server = await TestServer.start({
3852
+ project: config.project,
3853
+ port: config.port,
3854
+ command: serverCommand,
3855
+ env: serverEnv(config),
3856
+ startupTimeout: config.startupTimeout ?? 3e4,
3857
+ debug: isDebug
3858
+ });
3859
+ startedByUs = true;
3206
3860
  if (isDebug) {
3207
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
3861
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
3208
3862
  }
3209
- try {
3210
- serverInstance = await TestServer.start({
3211
- project: currentConfig.project,
3212
- port: currentConfig.port,
3213
- command: serverCommand,
3214
- env: currentConfig.env,
3215
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
3216
- debug: isDebug
3217
- });
3218
- serverStartedByUs = true;
3219
- if (isDebug) {
3220
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
3221
- }
3222
- } catch (error) {
3223
- const errMsg = error instanceof Error ? error.message : String(error);
3224
- throw new Error(
3225
- `Failed to start test server.
3863
+ } catch (error) {
3864
+ const errMsg = error instanceof Error ? error.message : String(error);
3865
+ throw new Error(
3866
+ `Failed to start test server.
3226
3867
 
3227
- Server entry: ${currentConfig.server}
3228
- Project: ${currentConfig.project ?? "default"}
3868
+ Server entry: ${config.server}
3869
+ Project: ${config.project ?? "default"}
3229
3870
  Command: ${serverCommand}
3230
3871
 
3231
- Error: ${errMsg}`
3232
- );
3233
- }
3234
- } else {
3235
- throw new Error(
3236
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
3872
+ Error: ${errMsg}`,
3873
+ { cause: error }
3237
3874
  );
3238
3875
  }
3876
+ } else {
3877
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
3239
3878
  }
3240
- if (!tokenFactory) {
3241
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
3242
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
3243
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3244
- }
3879
+ const sharedSecret = config.env?.["JWT_SECRET"];
3880
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
3881
+ const tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3882
+ return { server, startedByUs, tokenFactory, owner };
3245
3883
  }
3246
- async function createTestFixtures() {
3247
- await initializeSharedResources();
3248
- if (!serverInstance) {
3249
- throw new Error("Server instance not initialized");
3250
- }
3251
- if (!tokenFactory) {
3252
- throw new Error("Token factory not initialized");
3884
+ function acquireServer(chain) {
3885
+ const config = resolveConfig(chain);
3886
+ const key = serverKey(config);
3887
+ let entry = serverEntries.get(key);
3888
+ if (!entry) {
3889
+ entry = startServerEntry(config, ownerScopeOf(chain));
3890
+ serverEntries.set(key, entry);
3891
+ entry.catch(() => serverEntries.delete(key));
3892
+ }
3893
+ return entry;
3894
+ }
3895
+ async function stopServers(owner) {
3896
+ for (const [key, pending] of Array.from(serverEntries)) {
3897
+ let entry;
3898
+ try {
3899
+ entry = await pending;
3900
+ } catch {
3901
+ serverEntries.delete(key);
3902
+ continue;
3903
+ }
3904
+ if (owner && entry.owner !== owner) continue;
3905
+ serverEntries.delete(key);
3906
+ if (entry.startedByUs) {
3907
+ await entry.server.stop();
3908
+ }
3253
3909
  }
3910
+ }
3911
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
3912
+ async function buildFixtures(chain) {
3913
+ const config = resolveConfig(chain);
3914
+ const entry = await acquireServer(chain);
3915
+ const { server, tokenFactory } = entry;
3916
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
3917
+ const clientConfig = { ...config, publicMode };
3254
3918
  const clientInstance = McpTestClient.create({
3255
- baseUrl: resolveClientBaseUrl(serverInstance),
3256
- entryPath: currentConfig.entryPath,
3257
- transport: currentConfig.transport ?? "streamable-http",
3258
- publicMode: currentConfig.publicMode
3919
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
3920
+ entryPath: clientConfig.entryPath,
3921
+ transport: clientConfig.transport ?? "streamable-http",
3922
+ publicMode
3259
3923
  }).build();
3260
3924
  try {
3261
3925
  await clientInstance.connect();
@@ -3269,17 +3933,24 @@ async function createTestFixtures() {
3269
3933
  throw err;
3270
3934
  }
3271
3935
  }
3272
- const auth = createAuthFixture(tokenFactory);
3273
- const server = createServerFixture(serverInstance);
3274
- return {
3936
+ const context = { extraClients: [] };
3937
+ const fixtures = {
3275
3938
  mcp: clientInstance,
3276
- auth,
3277
- server
3939
+ auth: createAuthFixture(tokenFactory),
3940
+ server: createServerFixture(server, clientConfig, clientInstance, context)
3278
3941
  };
3942
+ fixtureContexts.set(fixtures, context);
3943
+ return fixtures;
3944
+ }
3945
+ async function createTestFixtures() {
3946
+ return buildFixtures(scopes.slice());
3947
+ }
3948
+ async function initializeSharedResources() {
3949
+ await acquireServer(scopes.slice());
3279
3950
  }
3280
3951
  async function cleanupTestFixtures(fixtures, testFailed = false) {
3281
- if (testFailed && serverInstance) {
3282
- const logs = serverInstance.getLogs();
3952
+ if (testFailed) {
3953
+ const logs = fixtures.server.getLogs();
3283
3954
  if (logs.length > 0) {
3284
3955
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
3285
3956
  const recentLogs = logs.slice(-50);
@@ -3290,20 +3961,21 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
3290
3961
  console.error("[TestFixture] === End Server Logs ===\n");
3291
3962
  }
3292
3963
  }
3964
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
3965
+ for (const client of extras) {
3966
+ if (client.isConnected()) {
3967
+ await client.disconnect().catch(() => void 0);
3968
+ }
3969
+ }
3293
3970
  if (fixtures.mcp.isConnected()) {
3294
3971
  await fixtures.mcp.disconnect();
3295
3972
  }
3296
3973
  }
3297
3974
  async function cleanupSharedResources() {
3298
- if (serverInstance && serverStartedByUs) {
3299
- await serverInstance.stop();
3300
- }
3301
- serverInstance = null;
3302
- tokenFactory = null;
3303
- serverStartedByUs = false;
3975
+ await stopServers();
3304
3976
  }
3305
- function resolveClientBaseUrl(server) {
3306
- return currentConfig.baseUrl ?? server.info.baseUrl;
3977
+ function resolveClientBaseUrl(server, config) {
3978
+ return config.baseUrl ?? server.info.baseUrl;
3307
3979
  }
3308
3980
  function createAuthFixture(factory) {
3309
3981
  const users = {
@@ -3349,28 +4021,39 @@ function createAuthFixture(factory) {
3349
4021
  getAudience: () => factory.getAudience()
3350
4022
  };
3351
4023
  }
3352
- function createServerFixture(server) {
4024
+ function createServerFixture(server, config, mcp, context) {
3353
4025
  return {
3354
- info: server.info,
4026
+ // Live view: `pid` is filled in once the server is listening and follows restarts
4027
+ get info() {
4028
+ return server.info;
4029
+ },
3355
4030
  createClient: async (opts) => {
3356
- return McpTestClient.create({
3357
- baseUrl: resolveClientBaseUrl(server),
3358
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
4031
+ const client = await McpTestClient.create({
4032
+ baseUrl: resolveClientBaseUrl(server, config),
4033
+ entryPath: opts?.entryPath ?? config.entryPath,
3359
4034
  transport: opts?.transport ?? "streamable-http",
3360
4035
  auth: opts?.token ? { token: opts.token } : void 0,
3361
4036
  clientInfo: opts?.clientInfo,
3362
- publicMode: currentConfig.publicMode
4037
+ publicMode: config.publicMode
3363
4038
  }).buildAndConnect();
4039
+ context.extraClients.push(client);
4040
+ return client;
3364
4041
  },
3365
4042
  createClientBuilder: () => {
3366
4043
  const builder = new McpTestClientBuilder({
3367
- baseUrl: resolveClientBaseUrl(server),
3368
- entryPath: currentConfig.entryPath,
3369
- publicMode: currentConfig.publicMode
4044
+ baseUrl: resolveClientBaseUrl(server, config),
4045
+ entryPath: config.entryPath,
4046
+ publicMode: config.publicMode
3370
4047
  });
3371
4048
  return builder;
3372
4049
  },
3373
- restart: () => server.restart(),
4050
+ restart: async () => {
4051
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
4052
+ await server.restart();
4053
+ for (const client of reconnect) {
4054
+ await client.reconnect();
4055
+ }
4056
+ },
3374
4057
  getLogs: () => server.getLogs(),
3375
4058
  clearLogs: () => server.clearLogs()
3376
4059
  };
@@ -3381,38 +4064,71 @@ function resolveServerCommand(server) {
3381
4064
  }
3382
4065
  return `npx tsx ${server}`;
3383
4066
  }
3384
- function runWithFixtures(fn) {
3385
- return async () => {
3386
- const fixtures = await createTestFixtures();
3387
- let testFailed = false;
4067
+ function runWithFixtures(fn, chain) {
4068
+ return async (...extra) => {
4069
+ const fixtures = await buildFixtures(chain);
4070
+ let failure;
3388
4071
  try {
3389
- await fn(fixtures);
4072
+ for (const scope of chain) {
4073
+ for (const hook of scope.beforeEachHooks) await hook(fixtures);
4074
+ }
4075
+ await fn(fixtures, ...extra);
3390
4076
  } catch (error) {
3391
- testFailed = true;
3392
- throw error;
3393
- } finally {
3394
- await cleanupTestFixtures(fixtures, testFailed);
4077
+ failure = { error };
4078
+ }
4079
+ try {
4080
+ await runAfterEachHooks(chain, fixtures);
4081
+ } catch (error) {
4082
+ failure ??= { error };
3395
4083
  }
4084
+ await cleanupTestFixtures(fixtures, failure !== void 0);
4085
+ if (failure) throw failure.error;
3396
4086
  };
3397
4087
  }
4088
+ async function runAfterEachHooks(chain, fixtures) {
4089
+ let firstError;
4090
+ let failed = false;
4091
+ for (let i = chain.length - 1; i >= 0; i--) {
4092
+ for (const hook of chain[i].afterEachHooks) {
4093
+ try {
4094
+ await hook(fixtures);
4095
+ } catch (error) {
4096
+ if (!failed) firstError = error;
4097
+ failed = true;
4098
+ }
4099
+ }
4100
+ }
4101
+ if (failed) throw firstError;
4102
+ }
4103
+ function skipTitle(name, scope) {
4104
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
4105
+ }
3398
4106
  function testWithFixtures(name, fn) {
3399
- const scope = currentSkipScope();
4107
+ const chain = scopes.slice();
4108
+ const scope = currentScope();
3400
4109
  if (scope.skipped) {
3401
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
4110
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
3402
4111
  return;
3403
4112
  }
3404
- it(name, runWithFixtures(fn));
4113
+ jestGlobals().it(name, runWithFixtures(fn, chain));
3405
4114
  }
3406
4115
  function use(config) {
3407
- currentConfig = { ...currentConfig, ...config };
3408
- afterAll(async () => {
3409
- await cleanupSharedResources();
3410
- });
4116
+ const scope = currentScope();
4117
+ const { env, ...rest } = config;
4118
+ scope.config = { ...scope.config, ...rest };
4119
+ if (env) scope.envs.push(env);
4120
+ scope.configured = true;
4121
+ if (!scope.cleanupRegistered) {
4122
+ scope.cleanupRegistered = true;
4123
+ jestGlobals().afterAll(async () => {
4124
+ await stopServers(scope === scopes[0] ? void 0 : scope);
4125
+ });
4126
+ }
3411
4127
  }
3412
4128
  function skip(nameOrCondition, fnOrReason) {
3413
4129
  if (typeof nameOrCondition === "boolean") {
3414
4130
  if (nameOrCondition) {
3415
- const scope = currentSkipScope();
4131
+ const scope = currentScope();
3416
4132
  scope.skipped = true;
3417
4133
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
3418
4134
  }
@@ -3423,53 +4139,93 @@ function skip(nameOrCondition, fnOrReason) {
3423
4139
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
3424
4140
  );
3425
4141
  }
3426
- it.skip(nameOrCondition, runWithFixtures(fnOrReason));
4142
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
3427
4143
  }
3428
4144
  function only(name, fn) {
3429
- const scope = currentSkipScope();
4145
+ const scope = currentScope();
3430
4146
  if (scope.skipped) {
3431
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
4147
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
3432
4148
  return;
3433
4149
  }
3434
- it.only(name, runWithFixtures(fn));
4150
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
3435
4151
  }
3436
4152
  function todo(name) {
3437
- it.todo(name);
4153
+ jestGlobals().it.todo(name);
3438
4154
  }
3439
- function withSkipScope(register) {
4155
+ function each(table, ...tagged) {
4156
+ const chain = scopes.slice();
4157
+ const register = jestGlobals().it.each(table, ...tagged);
3440
4158
  return (name, fn) => {
3441
- register(name, () => {
3442
- const parent = currentSkipScope();
3443
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
3444
- try {
3445
- fn();
3446
- } finally {
3447
- skipScopes.pop();
3448
- }
3449
- });
4159
+ const scope = currentScope();
4160
+ if (scope.skipped) {
4161
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
4162
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
4163
+ return;
4164
+ }
4165
+ register(name, runWithFixtures(fn, chain));
3450
4166
  };
3451
4167
  }
3452
- function eachWithSkipScope(each) {
3453
- return ((...eachArgs) => {
3454
- const register = each(...eachArgs);
3455
- return withSkipScope(register);
3456
- });
4168
+ function withScope(fn) {
4169
+ return (...args) => {
4170
+ scopes.push(newScope(currentScope()));
4171
+ try {
4172
+ fn(...args);
4173
+ } finally {
4174
+ scopes.pop();
4175
+ }
4176
+ };
4177
+ }
4178
+ function scopedRegister(pick) {
4179
+ return (name, fn, timeout) => {
4180
+ pick()(name, withScope(fn), timeout);
4181
+ };
4182
+ }
4183
+ function scopedEach(pick) {
4184
+ return (...eachArgs) => (name, fn, timeout) => {
4185
+ const register = pick()(...eachArgs);
4186
+ register(name, withScope(fn), timeout);
4187
+ };
4188
+ }
4189
+ var describeWithScope = Object.assign(
4190
+ scopedRegister(() => jestGlobals().describe),
4191
+ {
4192
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
4193
+ only: Object.assign(
4194
+ scopedRegister(() => jestGlobals().describe.only),
4195
+ {
4196
+ each: scopedEach(() => jestGlobals().describe.only.each)
4197
+ }
4198
+ ),
4199
+ skip: Object.assign(
4200
+ scopedRegister(() => jestGlobals().describe.skip),
4201
+ {
4202
+ each: scopedEach(() => jestGlobals().describe.skip.each)
4203
+ }
4204
+ ),
4205
+ each: scopedEach(() => jestGlobals().describe.each)
4206
+ }
4207
+ );
4208
+ function eachHook(kind) {
4209
+ return (fn, timeout) => {
4210
+ if (typeof fn === "function" && fn.length > 0) {
4211
+ const scope = currentScope();
4212
+ (kind === "beforeEach" ? scope.beforeEachHooks : scope.afterEachHooks).push(fn);
4213
+ return;
4214
+ }
4215
+ jestGlobals()[kind](fn, timeout);
4216
+ };
3457
4217
  }
3458
- var describeWithSkipScope = Object.assign(withSkipScope(describe), describe, {
3459
- only: Object.assign(withSkipScope(describe.only), { each: eachWithSkipScope(describe.only.each) }),
3460
- skip: Object.assign(withSkipScope(describe.skip), { each: eachWithSkipScope(describe.skip.each) }),
3461
- each: eachWithSkipScope(describe.each)
3462
- });
3463
4218
  var test = testWithFixtures;
3464
4219
  test.use = use;
3465
- test.describe = describeWithSkipScope;
3466
- test.beforeAll = beforeAll;
3467
- test.beforeEach = beforeEach;
3468
- test.afterEach = afterEach;
3469
- test.afterAll = afterAll;
4220
+ test.describe = describeWithScope;
4221
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
4222
+ test.beforeEach = eachHook("beforeEach");
4223
+ test.afterEach = eachHook("afterEach");
4224
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
3470
4225
  test.skip = skip;
3471
4226
  test.only = only;
3472
4227
  test.todo = todo;
4228
+ test.each = each;
3473
4229
  export {
3474
4230
  cleanupSharedResources,
3475
4231
  cleanupTestFixtures,