@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/esm/index.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
5
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
6
  }) : x)(function(x) {
@@ -367,8 +370,453 @@ function isInterceptedError(err) {
367
370
  return typeof err === "object" && err !== null && interceptedErrors.has(err);
368
371
  }
369
372
 
370
- // libs/testing/src/transport/streamable-http.transport.ts
373
+ // libs/testing/src/transport/sse.transport.ts
371
374
  var DEFAULT_TIMEOUT = 3e4;
375
+ var SseTransport = class {
376
+ baseUrl;
377
+ entryPath;
378
+ timeout;
379
+ authHeaders;
380
+ publicMode;
381
+ debug;
382
+ clientInfo;
383
+ notificationHandler;
384
+ interceptors;
385
+ elicitationHandler;
386
+ state = "disconnected";
387
+ authToken;
388
+ sessionId;
389
+ messageEndpoint;
390
+ stream;
391
+ pending = /* @__PURE__ */ new Map();
392
+ connectionCount = 0;
393
+ reconnectCount = 0;
394
+ lastRequestHeaders = {};
395
+ constructor(config) {
396
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
397
+ this.entryPath = config.entryPath;
398
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
399
+ this.authHeaders = config.auth?.headers ?? {};
400
+ this.authToken = config.auth?.token;
401
+ this.publicMode = config.publicMode ?? false;
402
+ this.debug = config.debug ?? false;
403
+ this.clientInfo = config.clientInfo;
404
+ this.notificationHandler = config.notificationHandler;
405
+ this.interceptors = config.interceptors;
406
+ this.elicitationHandler = config.elicitationHandler;
407
+ }
408
+ async connect() {
409
+ this.state = "connecting";
410
+ this.connectionCount++;
411
+ try {
412
+ if (!this.publicMode && !this.authToken) {
413
+ await this.requestAnonymousToken();
414
+ }
415
+ await this.openStream();
416
+ this.state = "connected";
417
+ this.log(`Connected; messages go to ${this.messageEndpoint}`);
418
+ } catch (error) {
419
+ this.state = "error";
420
+ this.stream?.abort();
421
+ this.stream = void 0;
422
+ throw error;
423
+ }
424
+ }
425
+ async request(message) {
426
+ this.ensureConnected();
427
+ const startTime = Date.now();
428
+ if (this.interceptors) {
429
+ const intercepted = await this.interceptors.processRequest(message, {
430
+ timestamp: /* @__PURE__ */ new Date(),
431
+ transport: "sse",
432
+ sessionId: this.sessionId
433
+ });
434
+ switch (intercepted.type) {
435
+ case "mock":
436
+ return await this.interceptors.processResponse(
437
+ message,
438
+ intercepted.response,
439
+ Date.now() - startTime
440
+ );
441
+ case "error":
442
+ throw markInterceptedError(intercepted.error);
443
+ case "continue":
444
+ message = intercepted.request;
445
+ break;
446
+ }
447
+ }
448
+ let response;
449
+ if (message.id === void 0) {
450
+ await this.post(JSON.stringify(message));
451
+ response = { jsonrpc: "2.0", id: null, result: void 0 };
452
+ } else {
453
+ response = await this.postAndAwait(message.id, JSON.stringify(message));
454
+ }
455
+ if (this.interceptors) {
456
+ response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
457
+ }
458
+ return response;
459
+ }
460
+ async notify(message) {
461
+ this.ensureConnected();
462
+ const result = await this.post(JSON.stringify(message));
463
+ if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
464
+ }
465
+ async sendRaw(data) {
466
+ this.ensureConnected();
467
+ let id;
468
+ try {
469
+ const parsed = JSON.parse(data);
470
+ if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
471
+ } catch {
472
+ }
473
+ if (id !== void 0) return this.postAndAwait(id, data);
474
+ const result = await this.post(data);
475
+ if (!result.ok) {
476
+ return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
477
+ }
478
+ return { jsonrpc: "2.0", id: null, result: void 0 };
479
+ }
480
+ async close() {
481
+ this.closeStream("Transport closed");
482
+ this.state = "disconnected";
483
+ this.sessionId = void 0;
484
+ this.messageEndpoint = void 0;
485
+ this.log("SSE transport closed");
486
+ }
487
+ isConnected() {
488
+ return this.state === "connected";
489
+ }
490
+ getState() {
491
+ return this.state;
492
+ }
493
+ getSessionId() {
494
+ return this.sessionId;
495
+ }
496
+ setAuthToken(token) {
497
+ this.authToken = token;
498
+ }
499
+ setTimeout(ms) {
500
+ this.timeout = ms;
501
+ }
502
+ getMessageEndpoint() {
503
+ return this.messageEndpoint;
504
+ }
505
+ getConnectionCount() {
506
+ return this.connectionCount;
507
+ }
508
+ getReconnectCount() {
509
+ return this.reconnectCount;
510
+ }
511
+ getLastRequestHeaders() {
512
+ return { ...this.lastRequestHeaders };
513
+ }
514
+ setInterceptors(interceptors2) {
515
+ this.interceptors = interceptors2;
516
+ }
517
+ getInterceptors() {
518
+ return this.interceptors;
519
+ }
520
+ setElicitationHandler(handler) {
521
+ this.elicitationHandler = handler;
522
+ }
523
+ async simulateDisconnect() {
524
+ this.closeStream("Simulated disconnect");
525
+ this.state = "disconnected";
526
+ this.sessionId = void 0;
527
+ this.messageEndpoint = void 0;
528
+ }
529
+ async waitForReconnect(timeoutMs) {
530
+ this.reconnectCount++;
531
+ let timer;
532
+ try {
533
+ await Promise.race([
534
+ this.connect(),
535
+ new Promise((_, reject) => {
536
+ timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
537
+ })
538
+ ]);
539
+ } finally {
540
+ clearTimeout(timer);
541
+ }
542
+ }
543
+ // ═══════════════════════════════════════════════════════════════════
544
+ // STREAM
545
+ // ═══════════════════════════════════════════════════════════════════
546
+ /** Open `GET <entry>/sse` and wait for the `endpoint` event. */
547
+ async openStream() {
548
+ const controller = new AbortController();
549
+ this.stream = controller;
550
+ const url = this.sseUrl();
551
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
552
+ this.log(`GET ${url}`);
553
+ const connectTimer = setTimeout(() => controller.abort(), this.timeout);
554
+ let response;
555
+ try {
556
+ response = await fetch(url, {
557
+ method: "GET",
558
+ headers: { ...headers, Accept: "text/event-stream" },
559
+ signal: controller.signal
560
+ });
561
+ } catch (error) {
562
+ clearTimeout(connectTimer);
563
+ throw new Error(
564
+ isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
565
+ { cause: error }
566
+ );
567
+ }
568
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
569
+ if (!response.ok || !response.body || !isEventStream) {
570
+ clearTimeout(connectTimer);
571
+ const body = await response.text().catch(() => "");
572
+ throw new Error(
573
+ `SSE connection to ${url} failed: HTTP ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}. The legacy HTTP+SSE transport needs a server that serves it (@FrontMcp({ transport: { protocol: { legacy: true } } })).`
574
+ );
575
+ }
576
+ const endpoint = new Promise((resolve, reject) => {
577
+ const reader = response.body?.getReader();
578
+ if (!reader) {
579
+ reject(new Error(`SSE connection to ${url} has no body`));
580
+ return;
581
+ }
582
+ void this.readStream(controller, reader, resolve, reject);
583
+ });
584
+ try {
585
+ const data = await endpoint;
586
+ const messageUrl = new URL(data, url);
587
+ this.messageEndpoint = messageUrl.toString();
588
+ this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
589
+ } finally {
590
+ clearTimeout(connectTimer);
591
+ }
592
+ }
593
+ async readStream(controller, reader, onEndpoint, onEndpointError) {
594
+ const decoder = new TextDecoder();
595
+ let buffer = "";
596
+ let endpointSeen = false;
597
+ try {
598
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
599
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
600
+ buffer = remainder;
601
+ for (const text of completeEvents) {
602
+ const event = parseSSEEvent(text);
603
+ if (!event) continue;
604
+ if (event.type === "endpoint") {
605
+ if (!endpointSeen) {
606
+ endpointSeen = true;
607
+ onEndpoint(event.data);
608
+ }
609
+ continue;
610
+ }
611
+ await this.handleMessage(event.data);
612
+ }
613
+ }
614
+ } catch (error) {
615
+ if (!controller.signal.aborted) this.log("SSE stream failed:", error);
616
+ } finally {
617
+ reader.releaseLock();
618
+ }
619
+ if (!endpointSeen) {
620
+ onEndpointError(
621
+ new Error(
622
+ controller.signal.aborted ? `SSE stream at ${this.sseUrl()} sent no endpoint event within ${this.timeout}ms` : `SSE stream at ${this.sseUrl()} ended before the server sent its endpoint`
623
+ )
624
+ );
625
+ }
626
+ if (this.stream === controller) {
627
+ this.stream = void 0;
628
+ this.rejectPending("SSE stream closed by the server");
629
+ if (this.state === "connected") this.state = "disconnected";
630
+ }
631
+ }
632
+ async handleMessage(data) {
633
+ let message;
634
+ try {
635
+ message = JSON.parse(data);
636
+ } catch {
637
+ this.log("Ignoring SSE event that is not JSON:", data);
638
+ return;
639
+ }
640
+ if (!message || typeof message !== "object") return;
641
+ if (typeof message.method === "string") {
642
+ const request = message;
643
+ if (request.id === void 0 || request.id === null) {
644
+ this.notificationHandler?.(request);
645
+ return;
646
+ }
647
+ await this.answerServerRequest(request);
648
+ return;
649
+ }
650
+ if (message.id !== void 0 && message.id !== null) {
651
+ const pending = this.pending.get(message.id);
652
+ if (pending) {
653
+ clearTimeout(pending.timer);
654
+ this.pending.delete(message.id);
655
+ pending.resolve(message);
656
+ }
657
+ }
658
+ }
659
+ /** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
660
+ async answerServerRequest(request) {
661
+ const id = request.id;
662
+ let reply;
663
+ if (request.method === "elicitation/create") {
664
+ reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
665
+ } else if (request.method === "ping") {
666
+ reply = { jsonrpc: "2.0", id, result: {} };
667
+ } else {
668
+ reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
669
+ }
670
+ const result = await this.post(JSON.stringify(reply));
671
+ if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
672
+ }
673
+ async elicit(params) {
674
+ if (!this.elicitationHandler) return { action: "decline" };
675
+ try {
676
+ return await this.elicitationHandler(params);
677
+ } catch (error) {
678
+ this.log("Elicitation handler error:", error);
679
+ return { action: "cancel" };
680
+ }
681
+ }
682
+ closeStream(reason) {
683
+ const controller = this.stream;
684
+ this.stream = void 0;
685
+ controller?.abort();
686
+ this.rejectPending(reason);
687
+ }
688
+ rejectPending(reason) {
689
+ for (const [id, pending] of this.pending) {
690
+ clearTimeout(pending.timer);
691
+ pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
692
+ }
693
+ this.pending.clear();
694
+ }
695
+ // ═══════════════════════════════════════════════════════════════════
696
+ // POSTING
697
+ // ═══════════════════════════════════════════════════════════════════
698
+ /** POST a message and resolve with the response the stream carries for `id`. */
699
+ async postAndAwait(id, body) {
700
+ const answer = new Promise((resolve) => {
701
+ const timer = setTimeout(() => {
702
+ this.pending.delete(id);
703
+ resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
704
+ }, this.timeout);
705
+ this.pending.set(id, { resolve, timer });
706
+ });
707
+ const result = await this.post(body);
708
+ if (!result.ok) {
709
+ const pending = this.pending.get(id);
710
+ if (pending) {
711
+ clearTimeout(pending.timer);
712
+ this.pending.delete(id);
713
+ }
714
+ return {
715
+ jsonrpc: "2.0",
716
+ id,
717
+ error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
718
+ };
719
+ }
720
+ return answer;
721
+ }
722
+ async post(body) {
723
+ const endpoint = this.messageEndpoint;
724
+ if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
725
+ const headers = this.buildHeaders();
726
+ this.lastRequestHeaders = headers;
727
+ this.log(`POST ${endpoint}`, body);
728
+ const controller = new AbortController();
729
+ const timer = setTimeout(() => controller.abort(), this.timeout);
730
+ try {
731
+ const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
732
+ const text = await response.text();
733
+ return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
734
+ } catch (error) {
735
+ if (isAbortError(error)) {
736
+ return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
737
+ }
738
+ throw error;
739
+ } finally {
740
+ clearTimeout(timer);
741
+ }
742
+ }
743
+ // ═══════════════════════════════════════════════════════════════════
744
+ // HELPERS
745
+ // ═══════════════════════════════════════════════════════════════════
746
+ async requestAnonymousToken() {
747
+ const url = new URL(this.baseUrl);
748
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
749
+ url.search = "";
750
+ const tokenUrl = url.toString();
751
+ try {
752
+ const response = await fetch(tokenUrl, {
753
+ method: "POST",
754
+ headers: { "Content-Type": "application/json" },
755
+ body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
756
+ });
757
+ if (!response.ok) {
758
+ this.log(`No anonymous token (HTTP ${response.status})`);
759
+ return;
760
+ }
761
+ const token = await response.json();
762
+ if (token.access_token) this.authToken = token.access_token;
763
+ } catch (error) {
764
+ this.log("Error requesting anonymous token:", error);
765
+ }
766
+ }
767
+ /** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
768
+ sseUrl() {
769
+ const url = new URL(this.baseUrl);
770
+ const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
771
+ const basePath = url.pathname.replace(/\/+$/, "");
772
+ url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
773
+ return url.toString();
774
+ }
775
+ buildHeaders() {
776
+ const headers = {
777
+ "Content-Type": "application/json",
778
+ Accept: "application/json, text/event-stream"
779
+ };
780
+ if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
781
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
782
+ if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
783
+ Object.assign(headers, this.authHeaders);
784
+ return headers;
785
+ }
786
+ ensureConnected() {
787
+ if (this.state !== "connected") {
788
+ throw new Error("Transport not connected. Call connect() first.");
789
+ }
790
+ }
791
+ log(message, data) {
792
+ if (this.debug) {
793
+ console.log(`[SSE] ${message}`, data ?? "");
794
+ }
795
+ }
796
+ };
797
+ function splitSSEEvents(buffer) {
798
+ const endsWithCarriageReturn = buffer.endsWith("\r");
799
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
800
+ const completeEvents = normalized.split("\n\n");
801
+ const remainder = completeEvents.pop() ?? "";
802
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
803
+ }
804
+ function parseSSEEvent(text) {
805
+ let type = "message";
806
+ const data = [];
807
+ for (const line of text.split("\n")) {
808
+ if (line.startsWith(":")) continue;
809
+ const colon = line.indexOf(":");
810
+ const field = colon === -1 ? line : line.slice(0, colon);
811
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
812
+ if (field === "event") type = value;
813
+ else if (field === "data") data.push(value);
814
+ }
815
+ return data.length > 0 ? { type, data: data.join("\n") } : void 0;
816
+ }
817
+
818
+ // libs/testing/src/transport/streamable-http.transport.ts
819
+ var DEFAULT_TIMEOUT2 = 3e4;
372
820
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
373
821
  var StreamableHttpTransport = class {
374
822
  config;
@@ -390,7 +838,7 @@ var StreamableHttpTransport = class {
390
838
  baseUrl: config.baseUrl.replace(/\/$/, ""),
391
839
  // Remove trailing slash
392
840
  entryPath: config.entryPath,
393
- timeout: config.timeout ?? DEFAULT_TIMEOUT,
841
+ timeout: config.timeout ?? DEFAULT_TIMEOUT2,
394
842
  auth: config.auth ?? {},
395
843
  publicMode: config.publicMode ?? false,
396
844
  debug: config.debug ?? false,
@@ -730,7 +1178,7 @@ var StreamableHttpTransport = class {
730
1178
  let buffer = "";
731
1179
  try {
732
1180
  for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
733
- const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1181
+ const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
734
1182
  buffer = remainder;
735
1183
  for (const eventText of completeEvents) {
736
1184
  for (const event of this.parseSSEEvents(eventText, void 0).events) {
@@ -1064,7 +1512,7 @@ var StreamableHttpTransport = class {
1064
1512
  };
1065
1513
  }
1066
1514
  };
1067
- function splitSSEEvents(buffer) {
1515
+ function splitSSEEvents2(buffer) {
1068
1516
  const endsWithCarriageReturn = buffer.endsWith("\r");
1069
1517
  const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1070
1518
  const completeEvents = normalized.split("\n\n");
@@ -1321,7 +1769,7 @@ var McpTestClientBuilder = class {
1321
1769
  };
1322
1770
 
1323
1771
  // libs/testing/src/client/mcp-test-client.ts
1324
- var DEFAULT_TIMEOUT2 = 3e4;
1772
+ var DEFAULT_TIMEOUT3 = 3e4;
1325
1773
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1326
1774
  var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1327
1775
  var DEFAULT_CLIENT_INFO = {
@@ -1391,7 +1839,7 @@ var McpTestClient = class {
1391
1839
  transport: config.transport ?? "streamable-http",
1392
1840
  auth: config.auth ?? {},
1393
1841
  publicMode: config.publicMode ?? false,
1394
- timeout: config.timeout ?? DEFAULT_TIMEOUT2,
1842
+ timeout: config.timeout ?? DEFAULT_TIMEOUT3,
1395
1843
  debug: config.debug ?? false,
1396
1844
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
1397
1845
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
@@ -2157,7 +2605,18 @@ var McpTestClient = class {
2157
2605
  notificationHandler: this.recordNotification
2158
2606
  });
2159
2607
  case "sse":
2160
- throw new Error("SSE transport not yet implemented");
2608
+ return new SseTransport({
2609
+ baseUrl,
2610
+ entryPath: this.config.entryPath,
2611
+ timeout: this.config.timeout,
2612
+ auth: this.config.auth,
2613
+ publicMode: this.config.publicMode,
2614
+ debug: this.config.debug,
2615
+ interceptors: this._interceptors,
2616
+ clientInfo: this.config.clientInfo,
2617
+ elicitationHandler: this._elicitationHandler,
2618
+ notificationHandler: this.recordNotification
2619
+ });
2161
2620
  default:
2162
2621
  throw new Error(`Unknown transport type: ${this.config.transport}`);
2163
2622
  }
@@ -5104,7 +5563,19 @@ function jestGlobals() {
5104
5563
  );
5105
5564
  }
5106
5565
  }
5107
- var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
5566
+ function newScope(parent) {
5567
+ return {
5568
+ skipped: parent?.skipped ?? false,
5569
+ reason: parent?.reason,
5570
+ config: {},
5571
+ envs: [],
5572
+ configured: false,
5573
+ cleanupRegistered: false,
5574
+ beforeEachHooks: [],
5575
+ afterEachHooks: []
5576
+ };
5577
+ }
5578
+ var scopes = [newScope()];
5108
5579
  function currentScope() {
5109
5580
  return scopes[scopes.length - 1];
5110
5581
  }
@@ -5112,9 +5583,8 @@ var serverEntries = /* @__PURE__ */ new Map();
5112
5583
  function resolveConfig(chain) {
5113
5584
  const merged = {};
5114
5585
  for (const scope of chain) {
5115
- const { env, ...rest } = scope.config;
5116
- Object.assign(merged, rest);
5117
- if (env) merged.env = { ...merged.env, ...env };
5586
+ Object.assign(merged, scope.config);
5587
+ for (const env of scope.envs) merged.env = { ...merged.env, ...env };
5118
5588
  }
5119
5589
  return merged;
5120
5590
  }
@@ -5367,17 +5837,39 @@ function resolveServerCommand(server) {
5367
5837
  function runWithFixtures(fn, chain) {
5368
5838
  return async (...extra) => {
5369
5839
  const fixtures = await buildFixtures(chain);
5370
- let testFailed = false;
5840
+ let failure;
5371
5841
  try {
5842
+ for (const scope of chain) {
5843
+ for (const hook of scope.beforeEachHooks) await hook(fixtures);
5844
+ }
5372
5845
  await fn(fixtures, ...extra);
5373
5846
  } catch (error) {
5374
- testFailed = true;
5375
- throw error;
5376
- } finally {
5377
- await cleanupTestFixtures(fixtures, testFailed);
5847
+ failure = { error };
5378
5848
  }
5849
+ try {
5850
+ await runAfterEachHooks(chain, fixtures);
5851
+ } catch (error) {
5852
+ failure ??= { error };
5853
+ }
5854
+ await cleanupTestFixtures(fixtures, failure !== void 0);
5855
+ if (failure) throw failure.error;
5379
5856
  };
5380
5857
  }
5858
+ async function runAfterEachHooks(chain, fixtures) {
5859
+ let firstError;
5860
+ let failed = false;
5861
+ for (let i = chain.length - 1; i >= 0; i--) {
5862
+ for (const hook of chain[i].afterEachHooks) {
5863
+ try {
5864
+ await hook(fixtures);
5865
+ } catch (error) {
5866
+ if (!failed) firstError = error;
5867
+ failed = true;
5868
+ }
5869
+ }
5870
+ }
5871
+ if (failed) throw firstError;
5872
+ }
5381
5873
  function skipTitle(name, scope) {
5382
5874
  return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
5383
5875
  }
@@ -5392,11 +5884,9 @@ function testWithFixtures(name, fn) {
5392
5884
  }
5393
5885
  function use(config) {
5394
5886
  const scope = currentScope();
5395
- scope.config = {
5396
- ...scope.config,
5397
- ...config,
5398
- env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
5399
- };
5887
+ const { env, ...rest } = config;
5888
+ scope.config = { ...scope.config, ...rest };
5889
+ if (env) scope.envs.push(env);
5400
5890
  scope.configured = true;
5401
5891
  if (!scope.cleanupRegistered) {
5402
5892
  scope.cleanupRegistered = true;
@@ -5447,14 +5937,7 @@ function each(table, ...tagged) {
5447
5937
  }
5448
5938
  function withScope(fn) {
5449
5939
  return (...args) => {
5450
- const parent = currentScope();
5451
- scopes.push({
5452
- skipped: parent.skipped,
5453
- reason: parent.reason,
5454
- config: {},
5455
- configured: false,
5456
- cleanupRegistered: false
5457
- });
5940
+ scopes.push(newScope(currentScope()));
5458
5941
  try {
5459
5942
  fn(...args);
5460
5943
  } finally {
@@ -5492,12 +5975,22 @@ var describeWithScope = Object.assign(
5492
5975
  each: scopedEach(() => jestGlobals().describe.each)
5493
5976
  }
5494
5977
  );
5978
+ function eachHook(kind) {
5979
+ return (fn, timeout) => {
5980
+ if (typeof fn === "function" && fn.length > 0) {
5981
+ const scope = currentScope();
5982
+ (kind === "beforeEach" ? scope.beforeEachHooks : scope.afterEachHooks).push(fn);
5983
+ return;
5984
+ }
5985
+ jestGlobals()[kind](fn, timeout);
5986
+ };
5987
+ }
5495
5988
  var test = testWithFixtures;
5496
5989
  test.use = use;
5497
5990
  test.describe = describeWithScope;
5498
5991
  test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
5499
- test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
5500
- test.afterEach = ((...args) => jestGlobals().afterEach(...args));
5992
+ test.beforeEach = eachHook("beforeEach");
5993
+ test.afterEach = eachHook("afterEach");
5501
5994
  test.afterAll = ((...args) => jestGlobals().afterAll(...args));
5502
5995
  test.skip = skip;
5503
5996
  test.only = only;
@@ -8626,6 +9119,7 @@ export {
8626
9119
  RegressionDetector,
8627
9120
  ReportGenerator,
8628
9121
  ServerStartError,
9122
+ SseTransport,
8629
9123
  StreamableHttpTransport,
8630
9124
  TestClientError,
8631
9125
  TestServer,
@@ -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
  // libs/testing/src/platform/platform-types.ts
2
5
  function getPlatformMimeType(platform) {
3
6
  return "text/html;profile=mcp-app";