@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/client/mcp-test-client.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +524 -31
- package/esm/index.mjs +525 -31
- package/esm/matchers/index.mjs +3 -0
- package/esm/package.json +8 -6
- package/esm/perf/index.mjs +466 -7
- package/esm/perf/perf-setup.mjs +3 -0
- package/esm/playwright/index.mjs +3 -0
- package/esm/setup.mjs +3 -0
- package/fixtures/fixture-types.d.ts +19 -6
- package/fixtures/fixture-types.d.ts.map +1 -1
- package/fixtures/index.d.ts +1 -1
- package/fixtures/index.d.ts.map +1 -1
- package/fixtures/index.js +521 -31
- package/fixtures/test-fixture.d.ts.map +1 -1
- package/index.d.ts +2 -1
- package/index.d.ts.map +1 -1
- package/index.js +523 -31
- package/package.json +8 -6
- package/perf/index.js +463 -7
- package/transport/index.d.ts +1 -0
- package/transport/index.d.ts.map +1 -1
- package/transport/sse.transport.d.ts +80 -0
- package/transport/sse.transport.d.ts.map +1 -0
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/
|
|
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 ??
|
|
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 } =
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3360
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
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
|
-
|
|
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 = (
|
|
3753
|
-
test.afterEach = (
|
|
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;
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAMH,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,YAAY,EACV,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,WAAW,EACX,SAAS,GACV,MAAM,gCAAgC,CAAC;AAMxC,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACrG,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAMH,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,YAAY,EACV,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,WAAW,EACX,SAAS,GACV,MAAM,gCAAgC,CAAC;AAMxC,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACrG,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAMzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACjE,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC1G,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,YAAY,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAMhH,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAM9E,OAAO,EACL,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,OAAO,EACP,cAAc,EACd,WAAW,GACZ,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EACL,eAAe,EACf,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,GACf,MAAM,gBAAgB,CAAC;AAMxB,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,2BAA2B,EAC3B,iBAAiB,EACjB,eAAe,EACf,IAAI,EACJ,QAAQ,EACR,gBAAgB,EAChB,MAAM,EAEN,cAAc,EACd,eAAe,GAChB,MAAM,gCAAgC,CAAC;AAMxC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,YAAY,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,aAAa,EACb,MAAM,EACN,eAAe,EACf,gBAAgB,EAChB,QAAQ,GACT,MAAM,YAAY,CAAC;AAQpB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAMlC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEzG,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,yBAAyB,EACzB,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,gBAAgB,GACjB,MAAM,eAAe,CAAC;AAMvB,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAErD,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,aAAa,CAAC;AAMrB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAMhD,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE1F,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,YAAY,CAAC;AAMpB,OAAO,EAEL,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EAEtB,yBAAyB,EACzB,wBAAwB,EAExB,oCAAoC,EACpC,mCAAmC,EACnC,qCAAqC,EACrC,oCAAoC,EACpC,qCAAqC,EACrC,oCAAoC,GACrC,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EAEL,QAAQ,EAGR,gBAAgB,EAChB,aAAa,EACb,OAAO,EACP,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,cAAc,EAGd,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAGlB,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,uBAAuB,EAGvB,aAAa,EACb,wBAAwB,EACxB,uBAAuB,EACvB,gBAAgB,EAGhB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EAGrB,eAAe,EACf,WAAW,EACX,qBAAqB,GACtB,MAAM,QAAQ,CAAC;AAMhB,OAAO,EAAE,SAAS,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEtF,YAAY,EAEV,aAAa,EACb,UAAU,EACV,YAAY,EAGZ,eAAe,EACf,SAAS,EACT,cAAc,EAGd,oBAAoB,EACpB,mBAAmB,EAGnB,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAGhB,eAAe,EACf,cAAc,EACd,UAAU,EAGV,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,oBAAoB,GACrB,MAAM,QAAQ,CAAC"}
|