@frontmcp/testing 1.8.7 → 1.9.1-rc.1

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.
package/fixtures/index.js CHANGED
@@ -512,8 +512,453 @@ function isInterceptedError(err) {
512
512
  return typeof err === "object" && err !== null && interceptedErrors.has(err);
513
513
  }
514
514
 
515
- // libs/testing/src/transport/streamable-http.transport.ts
515
+ // libs/testing/src/transport/sse.transport.ts
516
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;
517
962
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
518
963
  var StreamableHttpTransport = class {
519
964
  config;
@@ -535,7 +980,7 @@ var StreamableHttpTransport = class {
535
980
  baseUrl: config.baseUrl.replace(/\/$/, ""),
536
981
  // Remove trailing slash
537
982
  entryPath: config.entryPath,
538
- timeout: config.timeout ?? DEFAULT_TIMEOUT,
983
+ timeout: config.timeout ?? DEFAULT_TIMEOUT2,
539
984
  auth: config.auth ?? {},
540
985
  publicMode: config.publicMode ?? false,
541
986
  debug: config.debug ?? false,
@@ -875,7 +1320,7 @@ var StreamableHttpTransport = class {
875
1320
  let buffer = "";
876
1321
  try {
877
1322
  for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
878
- const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1323
+ const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
879
1324
  buffer = remainder;
880
1325
  for (const eventText of completeEvents) {
881
1326
  for (const event of this.parseSSEEvents(eventText, void 0).events) {
@@ -1209,7 +1654,7 @@ var StreamableHttpTransport = class {
1209
1654
  };
1210
1655
  }
1211
1656
  };
1212
- function splitSSEEvents(buffer) {
1657
+ function splitSSEEvents2(buffer) {
1213
1658
  const endsWithCarriageReturn = buffer.endsWith("\r");
1214
1659
  const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1215
1660
  const completeEvents = normalized.split("\n\n");
@@ -1447,7 +1892,7 @@ var McpTestClientBuilder = class {
1447
1892
  };
1448
1893
 
1449
1894
  // libs/testing/src/client/mcp-test-client.ts
1450
- var DEFAULT_TIMEOUT2 = 3e4;
1895
+ var DEFAULT_TIMEOUT3 = 3e4;
1451
1896
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1452
1897
  var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1453
1898
  var DEFAULT_CLIENT_INFO = {
@@ -1517,7 +1962,7 @@ var McpTestClient = class {
1517
1962
  transport: config.transport ?? "streamable-http",
1518
1963
  auth: config.auth ?? {},
1519
1964
  publicMode: config.publicMode ?? false,
1520
- timeout: config.timeout ?? DEFAULT_TIMEOUT2,
1965
+ timeout: config.timeout ?? DEFAULT_TIMEOUT3,
1521
1966
  debug: config.debug ?? false,
1522
1967
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
1523
1968
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
@@ -2283,7 +2728,18 @@ var McpTestClient = class {
2283
2728
  notificationHandler: this.recordNotification
2284
2729
  });
2285
2730
  case "sse":
2286
- 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
+ });
2287
2743
  default:
2288
2744
  throw new Error(`Unknown transport type: ${this.config.transport}`);
2289
2745
  }
@@ -3348,7 +3804,19 @@ function jestGlobals() {
3348
3804
  );
3349
3805
  }
3350
3806
  }
3351
- var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
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()];
3352
3820
  function currentScope() {
3353
3821
  return scopes[scopes.length - 1];
3354
3822
  }
@@ -3356,9 +3824,8 @@ var serverEntries = /* @__PURE__ */ new Map();
3356
3824
  function resolveConfig(chain) {
3357
3825
  const merged = {};
3358
3826
  for (const scope of chain) {
3359
- const { env, ...rest } = scope.config;
3360
- Object.assign(merged, rest);
3361
- if (env) merged.env = { ...merged.env, ...env };
3827
+ Object.assign(merged, scope.config);
3828
+ for (const env of scope.envs) merged.env = { ...merged.env, ...env };
3362
3829
  }
3363
3830
  return merged;
3364
3831
  }
@@ -3620,17 +4087,39 @@ function resolveServerCommand(server) {
3620
4087
  function runWithFixtures(fn, chain) {
3621
4088
  return async (...extra) => {
3622
4089
  const fixtures = await buildFixtures(chain);
3623
- let testFailed = false;
4090
+ let failure;
3624
4091
  try {
4092
+ for (const scope of chain) {
4093
+ for (const hook of scope.beforeEachHooks) await hook(fixtures);
4094
+ }
3625
4095
  await fn(fixtures, ...extra);
3626
4096
  } catch (error) {
3627
- testFailed = true;
3628
- throw error;
3629
- } finally {
3630
- await cleanupTestFixtures(fixtures, testFailed);
4097
+ failure = { error };
3631
4098
  }
4099
+ try {
4100
+ await runAfterEachHooks(chain, fixtures);
4101
+ } catch (error) {
4102
+ failure ??= { error };
4103
+ }
4104
+ await cleanupTestFixtures(fixtures, failure !== void 0);
4105
+ if (failure) throw failure.error;
3632
4106
  };
3633
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
+ }
3634
4123
  function skipTitle(name, scope) {
3635
4124
  return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
3636
4125
  }
@@ -3645,11 +4134,9 @@ function testWithFixtures(name, fn) {
3645
4134
  }
3646
4135
  function use(config) {
3647
4136
  const scope = currentScope();
3648
- scope.config = {
3649
- ...scope.config,
3650
- ...config,
3651
- env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
3652
- };
4137
+ const { env, ...rest } = config;
4138
+ scope.config = { ...scope.config, ...rest };
4139
+ if (env) scope.envs.push(env);
3653
4140
  scope.configured = true;
3654
4141
  if (!scope.cleanupRegistered) {
3655
4142
  scope.cleanupRegistered = true;
@@ -3700,14 +4187,7 @@ function each(table, ...tagged) {
3700
4187
  }
3701
4188
  function withScope(fn) {
3702
4189
  return (...args) => {
3703
- const parent = currentScope();
3704
- scopes.push({
3705
- skipped: parent.skipped,
3706
- reason: parent.reason,
3707
- config: {},
3708
- configured: false,
3709
- cleanupRegistered: false
3710
- });
4190
+ scopes.push(newScope(currentScope()));
3711
4191
  try {
3712
4192
  fn(...args);
3713
4193
  } finally {
@@ -3745,12 +4225,22 @@ var describeWithScope = Object.assign(
3745
4225
  each: scopedEach(() => jestGlobals().describe.each)
3746
4226
  }
3747
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
+ };
4237
+ }
3748
4238
  var test = testWithFixtures;
3749
4239
  test.use = use;
3750
4240
  test.describe = describeWithScope;
3751
4241
  test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
3752
- test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
3753
- test.afterEach = ((...args) => jestGlobals().afterEach(...args));
4242
+ test.beforeEach = eachHook("beforeEach");
4243
+ test.afterEach = eachHook("afterEach");
3754
4244
  test.afterAll = ((...args) => jestGlobals().afterAll(...args));
3755
4245
  test.skip = skip;
3756
4246
  test.only = only;
@@ -1 +1 @@
1
- {"version":3,"file":"test-fixture.d.ts","sourceRoot":"","sources":["../../src/fixtures/test-fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,EAIV,YAAY,EAGZ,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AA4RzB;;;GAGG;AACH,iBAAe,kBAAkB,IAAI,OAAO,CAAC,YAAY,CAAC,CAEzD;AAED;;GAEG;AACH,iBAAe,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAExD;AAED;;;;GAIG;AACH,iBAAe,mBAAmB,CAAC,QAAQ,EAAE,YAAY,EAAE,UAAU,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B5F;AAED;;GAEG;AACH,iBAAe,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAErD;AAqWD,QAAA,MAAM,IAAI,EAAuB,gBAAgB,CAAC;AAsBlD,OAAO,EAAE,IAAI,EAAE,CAAC;AAGhB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"test-fixture.d.ts","sourceRoot":"","sources":["../../src/fixtures/test-fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,EAIV,YAAY,EAGZ,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAqTzB;;;GAGG;AACH,iBAAe,kBAAkB,IAAI,OAAO,CAAC,YAAY,CAAC,CAEzD;AAED;;GAEG;AACH,iBAAe,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAExD;AAED;;;;GAIG;AACH,iBAAe,mBAAmB,CAAC,QAAQ,EAAE,YAAY,EAAE,UAAU,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B5F;AAED;;GAEG;AACH,iBAAe,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAErD;AA2YD,QAAA,MAAM,IAAI,EAAuB,gBAAgB,CAAC;AAsBlD,OAAO,EAAE,IAAI,EAAE,CAAC;AAGhB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,CAAC"}
package/index.d.ts CHANGED
@@ -43,6 +43,7 @@ export { McpTestClientBuilder } from './client/mcp-test-client.builder';
43
43
  export type { McpTestClientConfig, McpResponse, McpErrorInfo, TestTransportType, TestAuthConfig, ToolResultWrapper, ResourceContentWrapper, PromptResultWrapper, LogEntry, LogLevel, RequestTrace, NotificationEntry, ProgressUpdate, ToolCallOptions, SessionInfo, AuthState, } from './client/mcp-test-client.types';
44
44
  export type { McpTransport, TransportConfig, TransportState } from './transport/transport.interface';
45
45
  export { StreamableHttpTransport } from './transport/streamable-http.transport';
46
+ export { SseTransport } from './transport/sse.transport';
46
47
  export { TestTokenFactory } from './auth/token-factory';
47
48
  export type { CreateTokenOptions, TokenFactoryOptions } from './auth/token-factory';
48
49
  export { AuthHeaders } from './auth/auth-headers';
@@ -60,7 +61,7 @@ export { McpAssertions, containsTool, containsResource, containsResourceTemplate
60
61
  export { TestClientError, ConnectionError, TimeoutError, McpProtocolError, ServerStartError, AssertionError, } from './errors/index';
61
62
  export type { InitializeResult, ListToolsResult, CallToolResult, ListResourcesResult, ReadResourceResult, ListResourceTemplatesResult, ListPromptsResult, GetPromptResult, Tool, Resource, ResourceTemplate, Prompt, JSONRPCRequest, JSONRPCResponse, } from './client/mcp-test-client.types';
62
63
  export { test } from './fixtures';
63
- export type { TestConfig, TestFixtures, AuthFixture, ServerFixture, TestFn, TestWithFixtures, TestUser, } from './fixtures';
64
+ export type { TestConfig, TestFixtures, AuthFixture, ServerFixture, TestFn, FixtureEachHook, TestWithFixtures, TestUser, } from './fixtures';
64
65
  export { expect } from './expect';
65
66
  export { mcpMatchers } from './matchers';
66
67
  export type { McpMatchers } from './matchers';
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}