@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.
- package/auth/mock-oauth-server.d.ts.map +1 -1
- package/auth/token-factory.d.ts +2 -2
- package/auth/token-factory.d.ts.map +1 -1
- package/client/mcp-test-client.d.ts +6 -1
- package/client/mcp-test-client.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +993 -237
- package/esm/index.mjs +1151 -360
- package/esm/matchers/index.mjs +3 -0
- package/esm/package.json +8 -6
- package/esm/perf/index.mjs +874 -280
- package/esm/perf/perf-setup.mjs +3 -0
- package/esm/playwright/index.mjs +3 -0
- package/esm/setup.mjs +3 -0
- package/expect.d.ts +12 -11
- package/expect.d.ts.map +1 -1
- package/fixtures/fixture-types.d.ts +41 -10
- 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 +989 -229
- package/fixtures/test-fixture.d.ts +6 -5
- package/fixtures/test-fixture.d.ts.map +1 -1
- package/http-mock/http-mock.d.ts +5 -3
- package/http-mock/http-mock.d.ts.map +1 -1
- package/http-mock/http-mock.types.d.ts +17 -6
- package/http-mock/http-mock.types.d.ts.map +1 -1
- package/http-mock/index.d.ts +1 -1
- package/http-mock/index.d.ts.map +1 -1
- package/index.d.ts +3 -2
- package/index.d.ts.map +1 -1
- package/index.js +1157 -361
- package/package.json +8 -6
- package/perf/baseline-store.d.ts +1 -1
- package/perf/baseline-store.d.ts.map +1 -1
- package/perf/index.js +874 -293
- package/perf/perf-test.d.ts.map +1 -1
- package/perf/report-generator.d.ts +1 -1
- package/perf/report-generator.d.ts.map +1 -1
- package/server/port-registry.d.ts +7 -1
- package/server/port-registry.d.ts.map +1 -1
- package/server/test-server.d.ts +10 -1
- package/server/test-server.d.ts.map +1 -1
- package/transport/error-utils.d.ts +15 -0
- package/transport/error-utils.d.ts.map +1 -0
- 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/transport/streamable-http.transport.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -60,6 +60,7 @@ __export(index_exports, {
|
|
|
60
60
|
RegressionDetector: () => RegressionDetector,
|
|
61
61
|
ReportGenerator: () => ReportGenerator,
|
|
62
62
|
ServerStartError: () => ServerStartError,
|
|
63
|
+
SseTransport: () => SseTransport,
|
|
63
64
|
StreamableHttpTransport: () => StreamableHttpTransport,
|
|
64
65
|
TestClientError: () => TestClientError,
|
|
65
66
|
TestServer: () => TestServer,
|
|
@@ -460,8 +461,479 @@ var interceptors = {
|
|
|
460
461
|
}
|
|
461
462
|
};
|
|
462
463
|
|
|
463
|
-
// libs/testing/src/transport/
|
|
464
|
+
// libs/testing/src/transport/error-utils.ts
|
|
465
|
+
function errorName(err) {
|
|
466
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
467
|
+
const name = err.name;
|
|
468
|
+
return typeof name === "string" ? name : void 0;
|
|
469
|
+
}
|
|
470
|
+
function isAbortError(err) {
|
|
471
|
+
return errorName(err) === "AbortError";
|
|
472
|
+
}
|
|
473
|
+
function errorMessage(err, fallback = "Unknown error") {
|
|
474
|
+
if (typeof err === "string" && err) return err;
|
|
475
|
+
if (typeof err === "object" && err !== null) {
|
|
476
|
+
const message = err.message;
|
|
477
|
+
if (typeof message === "string" && message) return message;
|
|
478
|
+
}
|
|
479
|
+
return fallback;
|
|
480
|
+
}
|
|
481
|
+
var interceptedErrors = /* @__PURE__ */ new WeakSet();
|
|
482
|
+
function markInterceptedError(err) {
|
|
483
|
+
interceptedErrors.add(err);
|
|
484
|
+
return err;
|
|
485
|
+
}
|
|
486
|
+
function isInterceptedError(err) {
|
|
487
|
+
return typeof err === "object" && err !== null && interceptedErrors.has(err);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// libs/testing/src/transport/sse.transport.ts
|
|
464
491
|
var DEFAULT_TIMEOUT = 3e4;
|
|
492
|
+
var SseTransport = class {
|
|
493
|
+
baseUrl;
|
|
494
|
+
entryPath;
|
|
495
|
+
timeout;
|
|
496
|
+
authHeaders;
|
|
497
|
+
publicMode;
|
|
498
|
+
debug;
|
|
499
|
+
clientInfo;
|
|
500
|
+
notificationHandler;
|
|
501
|
+
interceptors;
|
|
502
|
+
elicitationHandler;
|
|
503
|
+
state = "disconnected";
|
|
504
|
+
authToken;
|
|
505
|
+
sessionId;
|
|
506
|
+
messageEndpoint;
|
|
507
|
+
stream;
|
|
508
|
+
pending = /* @__PURE__ */ new Map();
|
|
509
|
+
connectionCount = 0;
|
|
510
|
+
reconnectCount = 0;
|
|
511
|
+
lastRequestHeaders = {};
|
|
512
|
+
constructor(config) {
|
|
513
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
514
|
+
this.entryPath = config.entryPath;
|
|
515
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
516
|
+
this.authHeaders = config.auth?.headers ?? {};
|
|
517
|
+
this.authToken = config.auth?.token;
|
|
518
|
+
this.publicMode = config.publicMode ?? false;
|
|
519
|
+
this.debug = config.debug ?? false;
|
|
520
|
+
this.clientInfo = config.clientInfo;
|
|
521
|
+
this.notificationHandler = config.notificationHandler;
|
|
522
|
+
this.interceptors = config.interceptors;
|
|
523
|
+
this.elicitationHandler = config.elicitationHandler;
|
|
524
|
+
}
|
|
525
|
+
async connect() {
|
|
526
|
+
this.state = "connecting";
|
|
527
|
+
this.connectionCount++;
|
|
528
|
+
try {
|
|
529
|
+
if (!this.publicMode && !this.authToken) {
|
|
530
|
+
await this.requestAnonymousToken();
|
|
531
|
+
}
|
|
532
|
+
await this.openStream();
|
|
533
|
+
this.state = "connected";
|
|
534
|
+
this.log(`Connected; messages go to ${this.messageEndpoint}`);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
this.state = "error";
|
|
537
|
+
this.stream?.abort();
|
|
538
|
+
this.stream = void 0;
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async request(message) {
|
|
543
|
+
this.ensureConnected();
|
|
544
|
+
const startTime = Date.now();
|
|
545
|
+
if (this.interceptors) {
|
|
546
|
+
const intercepted = await this.interceptors.processRequest(message, {
|
|
547
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
548
|
+
transport: "sse",
|
|
549
|
+
sessionId: this.sessionId
|
|
550
|
+
});
|
|
551
|
+
switch (intercepted.type) {
|
|
552
|
+
case "mock":
|
|
553
|
+
return await this.interceptors.processResponse(
|
|
554
|
+
message,
|
|
555
|
+
intercepted.response,
|
|
556
|
+
Date.now() - startTime
|
|
557
|
+
);
|
|
558
|
+
case "error":
|
|
559
|
+
throw markInterceptedError(intercepted.error);
|
|
560
|
+
case "continue":
|
|
561
|
+
message = intercepted.request;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
let response;
|
|
566
|
+
if (message.id === void 0) {
|
|
567
|
+
await this.post(JSON.stringify(message));
|
|
568
|
+
response = { jsonrpc: "2.0", id: null, result: void 0 };
|
|
569
|
+
} else {
|
|
570
|
+
response = await this.postAndAwait(message.id, JSON.stringify(message));
|
|
571
|
+
}
|
|
572
|
+
if (this.interceptors) {
|
|
573
|
+
response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
|
|
574
|
+
}
|
|
575
|
+
return response;
|
|
576
|
+
}
|
|
577
|
+
async notify(message) {
|
|
578
|
+
this.ensureConnected();
|
|
579
|
+
const result = await this.post(JSON.stringify(message));
|
|
580
|
+
if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
|
|
581
|
+
}
|
|
582
|
+
async sendRaw(data) {
|
|
583
|
+
this.ensureConnected();
|
|
584
|
+
let id;
|
|
585
|
+
try {
|
|
586
|
+
const parsed = JSON.parse(data);
|
|
587
|
+
if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
590
|
+
if (id !== void 0) return this.postAndAwait(id, data);
|
|
591
|
+
const result = await this.post(data);
|
|
592
|
+
if (!result.ok) {
|
|
593
|
+
return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
|
|
594
|
+
}
|
|
595
|
+
return { jsonrpc: "2.0", id: null, result: void 0 };
|
|
596
|
+
}
|
|
597
|
+
async close() {
|
|
598
|
+
this.closeStream("Transport closed");
|
|
599
|
+
this.state = "disconnected";
|
|
600
|
+
this.sessionId = void 0;
|
|
601
|
+
this.messageEndpoint = void 0;
|
|
602
|
+
this.log("SSE transport closed");
|
|
603
|
+
}
|
|
604
|
+
isConnected() {
|
|
605
|
+
return this.state === "connected";
|
|
606
|
+
}
|
|
607
|
+
getState() {
|
|
608
|
+
return this.state;
|
|
609
|
+
}
|
|
610
|
+
getSessionId() {
|
|
611
|
+
return this.sessionId;
|
|
612
|
+
}
|
|
613
|
+
setAuthToken(token) {
|
|
614
|
+
this.authToken = token;
|
|
615
|
+
}
|
|
616
|
+
setTimeout(ms) {
|
|
617
|
+
this.timeout = ms;
|
|
618
|
+
}
|
|
619
|
+
getMessageEndpoint() {
|
|
620
|
+
return this.messageEndpoint;
|
|
621
|
+
}
|
|
622
|
+
getConnectionCount() {
|
|
623
|
+
return this.connectionCount;
|
|
624
|
+
}
|
|
625
|
+
getReconnectCount() {
|
|
626
|
+
return this.reconnectCount;
|
|
627
|
+
}
|
|
628
|
+
getLastRequestHeaders() {
|
|
629
|
+
return { ...this.lastRequestHeaders };
|
|
630
|
+
}
|
|
631
|
+
setInterceptors(interceptors2) {
|
|
632
|
+
this.interceptors = interceptors2;
|
|
633
|
+
}
|
|
634
|
+
getInterceptors() {
|
|
635
|
+
return this.interceptors;
|
|
636
|
+
}
|
|
637
|
+
setElicitationHandler(handler) {
|
|
638
|
+
this.elicitationHandler = handler;
|
|
639
|
+
}
|
|
640
|
+
async simulateDisconnect() {
|
|
641
|
+
this.closeStream("Simulated disconnect");
|
|
642
|
+
this.state = "disconnected";
|
|
643
|
+
this.sessionId = void 0;
|
|
644
|
+
this.messageEndpoint = void 0;
|
|
645
|
+
}
|
|
646
|
+
async waitForReconnect(timeoutMs) {
|
|
647
|
+
this.reconnectCount++;
|
|
648
|
+
let timer;
|
|
649
|
+
try {
|
|
650
|
+
await Promise.race([
|
|
651
|
+
this.connect(),
|
|
652
|
+
new Promise((_, reject) => {
|
|
653
|
+
timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
|
|
654
|
+
})
|
|
655
|
+
]);
|
|
656
|
+
} finally {
|
|
657
|
+
clearTimeout(timer);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
661
|
+
// STREAM
|
|
662
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
663
|
+
/** Open `GET <entry>/sse` and wait for the `endpoint` event. */
|
|
664
|
+
async openStream() {
|
|
665
|
+
const controller = new AbortController();
|
|
666
|
+
this.stream = controller;
|
|
667
|
+
const url = this.sseUrl();
|
|
668
|
+
const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
|
|
669
|
+
this.log(`GET ${url}`);
|
|
670
|
+
const connectTimer = setTimeout(() => controller.abort(), this.timeout);
|
|
671
|
+
let response;
|
|
672
|
+
try {
|
|
673
|
+
response = await fetch(url, {
|
|
674
|
+
method: "GET",
|
|
675
|
+
headers: { ...headers, Accept: "text/event-stream" },
|
|
676
|
+
signal: controller.signal
|
|
677
|
+
});
|
|
678
|
+
} catch (error) {
|
|
679
|
+
clearTimeout(connectTimer);
|
|
680
|
+
throw new Error(
|
|
681
|
+
isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
|
|
682
|
+
{ cause: error }
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
686
|
+
if (!response.ok || !response.body || !isEventStream) {
|
|
687
|
+
clearTimeout(connectTimer);
|
|
688
|
+
const body = await response.text().catch(() => "");
|
|
689
|
+
throw new Error(
|
|
690
|
+
`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 } } })).`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
const endpoint = new Promise((resolve, reject) => {
|
|
694
|
+
const reader = response.body?.getReader();
|
|
695
|
+
if (!reader) {
|
|
696
|
+
reject(new Error(`SSE connection to ${url} has no body`));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
void this.readStream(controller, reader, resolve, reject);
|
|
700
|
+
});
|
|
701
|
+
try {
|
|
702
|
+
const data = await endpoint;
|
|
703
|
+
const messageUrl = new URL(data, url);
|
|
704
|
+
this.messageEndpoint = messageUrl.toString();
|
|
705
|
+
this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
|
|
706
|
+
} finally {
|
|
707
|
+
clearTimeout(connectTimer);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async readStream(controller, reader, onEndpoint, onEndpointError) {
|
|
711
|
+
const decoder = new TextDecoder();
|
|
712
|
+
let buffer = "";
|
|
713
|
+
let endpointSeen = false;
|
|
714
|
+
try {
|
|
715
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
716
|
+
const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
717
|
+
buffer = remainder;
|
|
718
|
+
for (const text of completeEvents) {
|
|
719
|
+
const event = parseSSEEvent(text);
|
|
720
|
+
if (!event) continue;
|
|
721
|
+
if (event.type === "endpoint") {
|
|
722
|
+
if (!endpointSeen) {
|
|
723
|
+
endpointSeen = true;
|
|
724
|
+
onEndpoint(event.data);
|
|
725
|
+
}
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
await this.handleMessage(event.data);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (!controller.signal.aborted) this.log("SSE stream failed:", error);
|
|
733
|
+
} finally {
|
|
734
|
+
reader.releaseLock();
|
|
735
|
+
}
|
|
736
|
+
if (!endpointSeen) {
|
|
737
|
+
onEndpointError(
|
|
738
|
+
new Error(
|
|
739
|
+
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`
|
|
740
|
+
)
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (this.stream === controller) {
|
|
744
|
+
this.stream = void 0;
|
|
745
|
+
this.rejectPending("SSE stream closed by the server");
|
|
746
|
+
if (this.state === "connected") this.state = "disconnected";
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
async handleMessage(data) {
|
|
750
|
+
let message;
|
|
751
|
+
try {
|
|
752
|
+
message = JSON.parse(data);
|
|
753
|
+
} catch {
|
|
754
|
+
this.log("Ignoring SSE event that is not JSON:", data);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (!message || typeof message !== "object") return;
|
|
758
|
+
if (typeof message.method === "string") {
|
|
759
|
+
const request = message;
|
|
760
|
+
if (request.id === void 0 || request.id === null) {
|
|
761
|
+
this.notificationHandler?.(request);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
await this.answerServerRequest(request);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (message.id !== void 0 && message.id !== null) {
|
|
768
|
+
const pending = this.pending.get(message.id);
|
|
769
|
+
if (pending) {
|
|
770
|
+
clearTimeout(pending.timer);
|
|
771
|
+
this.pending.delete(message.id);
|
|
772
|
+
pending.resolve(message);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
|
|
777
|
+
async answerServerRequest(request) {
|
|
778
|
+
const id = request.id;
|
|
779
|
+
let reply;
|
|
780
|
+
if (request.method === "elicitation/create") {
|
|
781
|
+
reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
|
|
782
|
+
} else if (request.method === "ping") {
|
|
783
|
+
reply = { jsonrpc: "2.0", id, result: {} };
|
|
784
|
+
} else {
|
|
785
|
+
reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
|
|
786
|
+
}
|
|
787
|
+
const result = await this.post(JSON.stringify(reply));
|
|
788
|
+
if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
|
|
789
|
+
}
|
|
790
|
+
async elicit(params) {
|
|
791
|
+
if (!this.elicitationHandler) return { action: "decline" };
|
|
792
|
+
try {
|
|
793
|
+
return await this.elicitationHandler(params);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
this.log("Elicitation handler error:", error);
|
|
796
|
+
return { action: "cancel" };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
closeStream(reason) {
|
|
800
|
+
const controller = this.stream;
|
|
801
|
+
this.stream = void 0;
|
|
802
|
+
controller?.abort();
|
|
803
|
+
this.rejectPending(reason);
|
|
804
|
+
}
|
|
805
|
+
rejectPending(reason) {
|
|
806
|
+
for (const [id, pending] of this.pending) {
|
|
807
|
+
clearTimeout(pending.timer);
|
|
808
|
+
pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
|
|
809
|
+
}
|
|
810
|
+
this.pending.clear();
|
|
811
|
+
}
|
|
812
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
813
|
+
// POSTING
|
|
814
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
815
|
+
/** POST a message and resolve with the response the stream carries for `id`. */
|
|
816
|
+
async postAndAwait(id, body) {
|
|
817
|
+
const answer = new Promise((resolve) => {
|
|
818
|
+
const timer = setTimeout(() => {
|
|
819
|
+
this.pending.delete(id);
|
|
820
|
+
resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
|
|
821
|
+
}, this.timeout);
|
|
822
|
+
this.pending.set(id, { resolve, timer });
|
|
823
|
+
});
|
|
824
|
+
const result = await this.post(body);
|
|
825
|
+
if (!result.ok) {
|
|
826
|
+
const pending = this.pending.get(id);
|
|
827
|
+
if (pending) {
|
|
828
|
+
clearTimeout(pending.timer);
|
|
829
|
+
this.pending.delete(id);
|
|
830
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
jsonrpc: "2.0",
|
|
833
|
+
id,
|
|
834
|
+
error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
return answer;
|
|
838
|
+
}
|
|
839
|
+
async post(body) {
|
|
840
|
+
const endpoint = this.messageEndpoint;
|
|
841
|
+
if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
|
|
842
|
+
const headers = this.buildHeaders();
|
|
843
|
+
this.lastRequestHeaders = headers;
|
|
844
|
+
this.log(`POST ${endpoint}`, body);
|
|
845
|
+
const controller = new AbortController();
|
|
846
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
847
|
+
try {
|
|
848
|
+
const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
|
|
849
|
+
const text = await response.text();
|
|
850
|
+
return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
|
|
851
|
+
} catch (error) {
|
|
852
|
+
if (isAbortError(error)) {
|
|
853
|
+
return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
|
|
854
|
+
}
|
|
855
|
+
throw error;
|
|
856
|
+
} finally {
|
|
857
|
+
clearTimeout(timer);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
861
|
+
// HELPERS
|
|
862
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
863
|
+
async requestAnonymousToken() {
|
|
864
|
+
const url = new URL(this.baseUrl);
|
|
865
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
|
|
866
|
+
url.search = "";
|
|
867
|
+
const tokenUrl = url.toString();
|
|
868
|
+
try {
|
|
869
|
+
const response = await fetch(tokenUrl, {
|
|
870
|
+
method: "POST",
|
|
871
|
+
headers: { "Content-Type": "application/json" },
|
|
872
|
+
body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
|
|
873
|
+
});
|
|
874
|
+
if (!response.ok) {
|
|
875
|
+
this.log(`No anonymous token (HTTP ${response.status})`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const token = await response.json();
|
|
879
|
+
if (token.access_token) this.authToken = token.access_token;
|
|
880
|
+
} catch (error) {
|
|
881
|
+
this.log("Error requesting anonymous token:", error);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
|
|
885
|
+
sseUrl() {
|
|
886
|
+
const url = new URL(this.baseUrl);
|
|
887
|
+
const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
|
|
888
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
889
|
+
url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
|
|
890
|
+
return url.toString();
|
|
891
|
+
}
|
|
892
|
+
buildHeaders() {
|
|
893
|
+
const headers = {
|
|
894
|
+
"Content-Type": "application/json",
|
|
895
|
+
Accept: "application/json, text/event-stream"
|
|
896
|
+
};
|
|
897
|
+
if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
|
|
898
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
899
|
+
if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
|
|
900
|
+
Object.assign(headers, this.authHeaders);
|
|
901
|
+
return headers;
|
|
902
|
+
}
|
|
903
|
+
ensureConnected() {
|
|
904
|
+
if (this.state !== "connected") {
|
|
905
|
+
throw new Error("Transport not connected. Call connect() first.");
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
log(message, data) {
|
|
909
|
+
if (this.debug) {
|
|
910
|
+
console.log(`[SSE] ${message}`, data ?? "");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
function splitSSEEvents(buffer) {
|
|
915
|
+
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
916
|
+
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
917
|
+
const completeEvents = normalized.split("\n\n");
|
|
918
|
+
const remainder = completeEvents.pop() ?? "";
|
|
919
|
+
return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
|
|
920
|
+
}
|
|
921
|
+
function parseSSEEvent(text) {
|
|
922
|
+
let type = "message";
|
|
923
|
+
const data = [];
|
|
924
|
+
for (const line of text.split("\n")) {
|
|
925
|
+
if (line.startsWith(":")) continue;
|
|
926
|
+
const colon = line.indexOf(":");
|
|
927
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
928
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
929
|
+
if (field === "event") type = value;
|
|
930
|
+
else if (field === "data") data.push(value);
|
|
931
|
+
}
|
|
932
|
+
return data.length > 0 ? { type, data: data.join("\n") } : void 0;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// libs/testing/src/transport/streamable-http.transport.ts
|
|
936
|
+
var DEFAULT_TIMEOUT2 = 3e4;
|
|
465
937
|
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
466
938
|
var StreamableHttpTransport = class {
|
|
467
939
|
config;
|
|
@@ -483,7 +955,7 @@ var StreamableHttpTransport = class {
|
|
|
483
955
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
484
956
|
// Remove trailing slash
|
|
485
957
|
entryPath: config.entryPath,
|
|
486
|
-
timeout: config.timeout ??
|
|
958
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT2,
|
|
487
959
|
auth: config.auth ?? {},
|
|
488
960
|
publicMode: config.publicMode ?? false,
|
|
489
961
|
debug: config.debug ?? false,
|
|
@@ -568,7 +1040,7 @@ var StreamableHttpTransport = class {
|
|
|
568
1040
|
return mockResponse2;
|
|
569
1041
|
}
|
|
570
1042
|
case "error":
|
|
571
|
-
throw interceptResult.error;
|
|
1043
|
+
throw markInterceptedError(interceptResult.error);
|
|
572
1044
|
case "continue":
|
|
573
1045
|
message = interceptResult.request;
|
|
574
1046
|
break;
|
|
@@ -629,7 +1101,7 @@ var StreamableHttpTransport = class {
|
|
|
629
1101
|
return jsonResponse;
|
|
630
1102
|
} catch (error) {
|
|
631
1103
|
clearTimeout(timeoutId);
|
|
632
|
-
if (error
|
|
1104
|
+
if (isAbortError(error)) {
|
|
633
1105
|
return {
|
|
634
1106
|
jsonrpc: "2.0",
|
|
635
1107
|
id: message.id ?? null,
|
|
@@ -668,7 +1140,7 @@ var StreamableHttpTransport = class {
|
|
|
668
1140
|
}
|
|
669
1141
|
} catch (error) {
|
|
670
1142
|
clearTimeout(timeoutId);
|
|
671
|
-
if (error
|
|
1143
|
+
if (!isAbortError(error)) {
|
|
672
1144
|
throw error;
|
|
673
1145
|
}
|
|
674
1146
|
}
|
|
@@ -709,7 +1181,7 @@ var StreamableHttpTransport = class {
|
|
|
709
1181
|
error: {
|
|
710
1182
|
code: -32700,
|
|
711
1183
|
message: "Parse error",
|
|
712
|
-
data: error
|
|
1184
|
+
data: errorMessage(error)
|
|
713
1185
|
}
|
|
714
1186
|
};
|
|
715
1187
|
}
|
|
@@ -823,7 +1295,7 @@ var StreamableHttpTransport = class {
|
|
|
823
1295
|
let buffer = "";
|
|
824
1296
|
try {
|
|
825
1297
|
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
826
|
-
const { completeEvents, remainder } =
|
|
1298
|
+
const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
827
1299
|
buffer = remainder;
|
|
828
1300
|
for (const eventText of completeEvents) {
|
|
829
1301
|
for (const event of this.parseSSEEvents(eventText, void 0).events) {
|
|
@@ -1079,7 +1551,7 @@ var StreamableHttpTransport = class {
|
|
|
1079
1551
|
if (this.config.clientInfo) {
|
|
1080
1552
|
headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
|
|
1081
1553
|
}
|
|
1082
|
-
if (this.authToken
|
|
1554
|
+
if (this.authToken) {
|
|
1083
1555
|
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1084
1556
|
}
|
|
1085
1557
|
if (this.sessionId) {
|
|
@@ -1157,7 +1629,7 @@ var StreamableHttpTransport = class {
|
|
|
1157
1629
|
};
|
|
1158
1630
|
}
|
|
1159
1631
|
};
|
|
1160
|
-
function
|
|
1632
|
+
function splitSSEEvents2(buffer) {
|
|
1161
1633
|
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
1162
1634
|
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
1163
1635
|
const completeEvents = normalized.split("\n\n");
|
|
@@ -1414,7 +1886,7 @@ var McpTestClientBuilder = class {
|
|
|
1414
1886
|
};
|
|
1415
1887
|
|
|
1416
1888
|
// libs/testing/src/client/mcp-test-client.ts
|
|
1417
|
-
var
|
|
1889
|
+
var DEFAULT_TIMEOUT3 = 3e4;
|
|
1418
1890
|
var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
1419
1891
|
var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
|
|
1420
1892
|
var DEFAULT_CLIENT_INFO = {
|
|
@@ -1422,6 +1894,15 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
1422
1894
|
version: "0.4.0"
|
|
1423
1895
|
};
|
|
1424
1896
|
var MAX_LIST_PAGES = 1e3;
|
|
1897
|
+
function isAuthFailure(error) {
|
|
1898
|
+
return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
|
|
1899
|
+
}
|
|
1900
|
+
function describeInitializeError(error) {
|
|
1901
|
+
const message = error?.message ?? "Unknown error";
|
|
1902
|
+
const data = error?.data;
|
|
1903
|
+
const detail = typeof data === "string" ? data.trim() : "";
|
|
1904
|
+
return detail ? `${message} - ${detail.slice(0, 500)}` : message;
|
|
1905
|
+
}
|
|
1425
1906
|
function extractEntryPaths(data) {
|
|
1426
1907
|
let payload = data;
|
|
1427
1908
|
if (typeof payload === "string") {
|
|
@@ -1475,7 +1956,7 @@ var McpTestClient = class {
|
|
|
1475
1956
|
transport: config.transport ?? "streamable-http",
|
|
1476
1957
|
auth: config.auth ?? {},
|
|
1477
1958
|
publicMode: config.publicMode ?? false,
|
|
1478
|
-
timeout: config.timeout ??
|
|
1959
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT3,
|
|
1479
1960
|
debug: config.debug ?? false,
|
|
1480
1961
|
protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
|
|
1481
1962
|
clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
|
|
@@ -1508,19 +1989,22 @@ var McpTestClient = class {
|
|
|
1508
1989
|
async connect() {
|
|
1509
1990
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
1510
1991
|
this.triedDiscoveredEntryPath = false;
|
|
1992
|
+
this.initResult = null;
|
|
1511
1993
|
this.transport = this.createTransport();
|
|
1512
1994
|
await this.transport.connect();
|
|
1513
1995
|
const maxRetries = 3;
|
|
1514
1996
|
const retryDelayMs = 500;
|
|
1515
1997
|
let lastError;
|
|
1516
1998
|
let reportedEntryPaths;
|
|
1999
|
+
let attemptsMade = 0;
|
|
1517
2000
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2001
|
+
attemptsMade = attempt;
|
|
1518
2002
|
const initResponse = await this.initialize();
|
|
1519
2003
|
if (initResponse.success && initResponse.data) {
|
|
1520
2004
|
this.initResult = initResponse.data;
|
|
1521
2005
|
break;
|
|
1522
2006
|
}
|
|
1523
|
-
lastError = initResponse.error
|
|
2007
|
+
lastError = describeInitializeError(initResponse.error);
|
|
1524
2008
|
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
1525
2009
|
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
1526
2010
|
this.triedDiscoveredEntryPath = true;
|
|
@@ -1532,6 +2016,7 @@ var McpTestClient = class {
|
|
|
1532
2016
|
await this.transport.connect();
|
|
1533
2017
|
continue;
|
|
1534
2018
|
}
|
|
2019
|
+
if (isAuthFailure(initResponse.error)) break;
|
|
1535
2020
|
if (attempt < maxRetries) {
|
|
1536
2021
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
1537
2022
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
@@ -1540,7 +2025,7 @@ var McpTestClient = class {
|
|
|
1540
2025
|
if (!this.initResult) {
|
|
1541
2026
|
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.` : "";
|
|
1542
2027
|
throw new Error(
|
|
1543
|
-
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${
|
|
2028
|
+
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
|
|
1544
2029
|
);
|
|
1545
2030
|
}
|
|
1546
2031
|
this._sessionId = this.transport.getSessionId();
|
|
@@ -1610,18 +2095,47 @@ var McpTestClient = class {
|
|
|
1610
2095
|
return this._authState;
|
|
1611
2096
|
}
|
|
1612
2097
|
/**
|
|
1613
|
-
* Authenticate with a token
|
|
2098
|
+
* Authenticate with a token.
|
|
2099
|
+
*
|
|
2100
|
+
* When connected, this opens a NEW session with the token (the previous session was created
|
|
2101
|
+
* for the previous identity) and rejects if the server refuses it — for example because the
|
|
2102
|
+
* token is expired or badly signed. On rejection the client keeps its previous identity and
|
|
2103
|
+
* session. The token is kept in the client config, so `reconnect()` keeps using it.
|
|
1614
2104
|
*/
|
|
1615
2105
|
async authenticate(token) {
|
|
2106
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
2107
|
+
throw new Error("authenticate() requires a non-empty token");
|
|
2108
|
+
}
|
|
2109
|
+
const previousAuthState = this._authState;
|
|
2110
|
+
const previousConfigAuth = this.config.auth;
|
|
2111
|
+
this.config.auth = { ...previousConfigAuth, token };
|
|
1616
2112
|
this._authState = {
|
|
1617
2113
|
isAnonymous: false,
|
|
1618
2114
|
token,
|
|
1619
2115
|
scopes: this.parseScopesFromToken(token),
|
|
1620
2116
|
user: this.parseUserFromToken(token)
|
|
1621
2117
|
};
|
|
1622
|
-
|
|
1623
|
-
|
|
2118
|
+
const previousTransport = this.transport;
|
|
2119
|
+
if (!previousTransport) {
|
|
2120
|
+
this.log("debug", "Authentication updated (not connected yet)");
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
const previousInit = this.initResult;
|
|
2124
|
+
const previousSessionId = this._sessionId;
|
|
2125
|
+
const previousSessionInfo = this._sessionInfo;
|
|
2126
|
+
try {
|
|
2127
|
+
await this.connect();
|
|
2128
|
+
} catch (err) {
|
|
2129
|
+
await this.transport?.close().catch(() => void 0);
|
|
2130
|
+
this.transport = previousTransport;
|
|
2131
|
+
this.initResult = previousInit;
|
|
2132
|
+
this._sessionId = previousSessionId;
|
|
2133
|
+
this._sessionInfo = previousSessionInfo;
|
|
2134
|
+
this.config.auth = previousConfigAuth;
|
|
2135
|
+
this._authState = previousAuthState;
|
|
2136
|
+
throw err;
|
|
1624
2137
|
}
|
|
2138
|
+
await previousTransport.close().catch(() => void 0);
|
|
1625
2139
|
this.log("debug", "Authentication updated");
|
|
1626
2140
|
}
|
|
1627
2141
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -2208,7 +2722,18 @@ var McpTestClient = class {
|
|
|
2208
2722
|
notificationHandler: this.recordNotification
|
|
2209
2723
|
});
|
|
2210
2724
|
case "sse":
|
|
2211
|
-
|
|
2725
|
+
return new SseTransport({
|
|
2726
|
+
baseUrl,
|
|
2727
|
+
entryPath: this.config.entryPath,
|
|
2728
|
+
timeout: this.config.timeout,
|
|
2729
|
+
auth: this.config.auth,
|
|
2730
|
+
publicMode: this.config.publicMode,
|
|
2731
|
+
debug: this.config.debug,
|
|
2732
|
+
interceptors: this._interceptors,
|
|
2733
|
+
clientInfo: this.config.clientInfo,
|
|
2734
|
+
elicitationHandler: this._elicitationHandler,
|
|
2735
|
+
notificationHandler: this.recordNotification
|
|
2736
|
+
});
|
|
2212
2737
|
default:
|
|
2213
2738
|
throw new Error(`Unknown transport type: ${this.config.transport}`);
|
|
2214
2739
|
}
|
|
@@ -2245,10 +2770,11 @@ var McpTestClient = class {
|
|
|
2245
2770
|
requestId: id
|
|
2246
2771
|
};
|
|
2247
2772
|
} catch (err) {
|
|
2773
|
+
if (isInterceptedError(err)) throw err;
|
|
2248
2774
|
const durationMs = Date.now() - start;
|
|
2249
2775
|
const error = {
|
|
2250
2776
|
code: -32603,
|
|
2251
|
-
message: err
|
|
2777
|
+
message: errorMessage(err)
|
|
2252
2778
|
};
|
|
2253
2779
|
return {
|
|
2254
2780
|
success: false,
|
|
@@ -2396,12 +2922,12 @@ var McpTestClient = class {
|
|
|
2396
2922
|
const payload = this.decodeJwtPayload(token);
|
|
2397
2923
|
if (!payload) return [];
|
|
2398
2924
|
const scope = payload["scope"];
|
|
2399
|
-
const
|
|
2925
|
+
const scopes2 = payload["scopes"];
|
|
2400
2926
|
if (typeof scope === "string") {
|
|
2401
2927
|
return scope.split(" ");
|
|
2402
2928
|
}
|
|
2403
|
-
if (Array.isArray(
|
|
2404
|
-
return
|
|
2929
|
+
if (Array.isArray(scopes2)) {
|
|
2930
|
+
return scopes2;
|
|
2405
2931
|
}
|
|
2406
2932
|
return [];
|
|
2407
2933
|
} catch {
|
|
@@ -2533,14 +3059,15 @@ var TestTokenFactory = class {
|
|
|
2533
3059
|
*/
|
|
2534
3060
|
async createTestToken(options) {
|
|
2535
3061
|
await this.ensureKeys();
|
|
2536
|
-
const
|
|
3062
|
+
const nowMs = Date.now();
|
|
3063
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
2537
3064
|
const exp = options.exp ?? 3600;
|
|
2538
3065
|
const payload = {
|
|
2539
3066
|
iss: options.iss ?? this.issuer,
|
|
2540
3067
|
sub: options.sub,
|
|
2541
3068
|
aud: options.aud ?? this.audience,
|
|
2542
|
-
iat
|
|
2543
|
-
exp:
|
|
3069
|
+
iat,
|
|
3070
|
+
exp: Math.ceil(nowMs / 1e3) + exp,
|
|
2544
3071
|
scope: options.scopes?.join(" "),
|
|
2545
3072
|
...options.claims
|
|
2546
3073
|
};
|
|
@@ -2565,10 +3092,10 @@ var TestTokenFactory = class {
|
|
|
2565
3092
|
/**
|
|
2566
3093
|
* Create a regular user token
|
|
2567
3094
|
*/
|
|
2568
|
-
async createUserToken(sub = "user-001",
|
|
3095
|
+
async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
|
|
2569
3096
|
return this.createTestToken({
|
|
2570
3097
|
sub,
|
|
2571
|
-
scopes,
|
|
3098
|
+
scopes: scopes2,
|
|
2572
3099
|
claims: {
|
|
2573
3100
|
email: "user@test.local",
|
|
2574
3101
|
name: "Test User",
|
|
@@ -2579,10 +3106,11 @@ var TestTokenFactory = class {
|
|
|
2579
3106
|
/**
|
|
2580
3107
|
* Create an anonymous user token
|
|
2581
3108
|
*/
|
|
2582
|
-
async createAnonymousToken() {
|
|
3109
|
+
async createAnonymousToken(expiresIn) {
|
|
2583
3110
|
return this.createTestToken({
|
|
2584
3111
|
sub: `anon:${Date.now()}`,
|
|
2585
3112
|
scopes: ["anonymous"],
|
|
3113
|
+
exp: expiresIn,
|
|
2586
3114
|
claims: {
|
|
2587
3115
|
name: "Anonymous",
|
|
2588
3116
|
role: "anonymous"
|
|
@@ -2793,17 +3321,7 @@ function createTestUser(overrides) {
|
|
|
2793
3321
|
|
|
2794
3322
|
// libs/testing/src/auth/mock-oauth-server.ts
|
|
2795
3323
|
var import_http = require("http");
|
|
2796
|
-
var
|
|
2797
|
-
var _sha256Base64url;
|
|
2798
|
-
var _base64urlEncode;
|
|
2799
|
-
async function loadCryptoUtils() {
|
|
2800
|
-
if (!_randomBytes) {
|
|
2801
|
-
const utils = await import("@frontmcp/utils");
|
|
2802
|
-
_randomBytes = utils.randomBytes;
|
|
2803
|
-
_sha256Base64url = utils.sha256Base64url;
|
|
2804
|
-
_base64urlEncode = utils.base64urlEncode;
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
3324
|
+
var import_utils = require("@frontmcp/utils");
|
|
2807
3325
|
var MockOAuthServer = class {
|
|
2808
3326
|
tokenFactory;
|
|
2809
3327
|
options;
|
|
@@ -2818,8 +3336,8 @@ var MockOAuthServer = class {
|
|
|
2818
3336
|
accessTokenTtlSeconds;
|
|
2819
3337
|
/** Refresh token TTL in seconds */
|
|
2820
3338
|
refreshTokenTtlSeconds;
|
|
2821
|
-
constructor(
|
|
2822
|
-
this.tokenFactory =
|
|
3339
|
+
constructor(tokenFactory2, options = {}) {
|
|
3340
|
+
this.tokenFactory = tokenFactory2;
|
|
2823
3341
|
this.options = options;
|
|
2824
3342
|
this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
|
|
2825
3343
|
this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
|
|
@@ -3076,14 +3594,14 @@ ${error.stack}` : String(error);
|
|
|
3076
3594
|
return;
|
|
3077
3595
|
}
|
|
3078
3596
|
const code = this.generateCode();
|
|
3079
|
-
const
|
|
3597
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3080
3598
|
this.authCodes.set(code, {
|
|
3081
3599
|
code,
|
|
3082
3600
|
clientId,
|
|
3083
3601
|
redirectUri,
|
|
3084
3602
|
codeChallenge,
|
|
3085
3603
|
codeChallengeMethod,
|
|
3086
|
-
scopes,
|
|
3604
|
+
scopes: scopes2,
|
|
3087
3605
|
user: testUser,
|
|
3088
3606
|
state,
|
|
3089
3607
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3123,7 +3641,7 @@ ${error.stack}` : String(error);
|
|
|
3123
3641
|
}
|
|
3124
3642
|
const grantType = params.get("grant_type");
|
|
3125
3643
|
if (grantType === "anonymous") {
|
|
3126
|
-
const token = await this.tokenFactory.createAnonymousToken();
|
|
3644
|
+
const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
|
|
3127
3645
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3128
3646
|
res.end(
|
|
3129
3647
|
JSON.stringify({
|
|
@@ -3255,6 +3773,8 @@ ${error.stack}` : String(error);
|
|
|
3255
3773
|
codeRecord.used = true;
|
|
3256
3774
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3257
3775
|
sub: codeRecord.user.sub,
|
|
3776
|
+
scopes: codeRecord.scopes,
|
|
3777
|
+
exp: this.accessTokenTtlSeconds,
|
|
3258
3778
|
claims: {
|
|
3259
3779
|
email: codeRecord.user.email,
|
|
3260
3780
|
name: codeRecord.user.name,
|
|
@@ -3263,6 +3783,7 @@ ${error.stack}` : String(error);
|
|
|
3263
3783
|
});
|
|
3264
3784
|
const idToken = await this.tokenFactory.createTestToken({
|
|
3265
3785
|
sub: codeRecord.user.sub,
|
|
3786
|
+
exp: this.accessTokenTtlSeconds,
|
|
3266
3787
|
claims: {
|
|
3267
3788
|
email: codeRecord.user.email,
|
|
3268
3789
|
name: codeRecord.user.name,
|
|
@@ -3340,6 +3861,8 @@ ${error.stack}` : String(error);
|
|
|
3340
3861
|
}
|
|
3341
3862
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3342
3863
|
sub: tokenRecord.user.sub,
|
|
3864
|
+
scopes: tokenRecord.scopes,
|
|
3865
|
+
exp: this.accessTokenTtlSeconds,
|
|
3343
3866
|
claims: {
|
|
3344
3867
|
email: tokenRecord.user.email,
|
|
3345
3868
|
name: tokenRecord.user.name,
|
|
@@ -3444,14 +3967,14 @@ ${error.stack}` : String(error);
|
|
|
3444
3967
|
name
|
|
3445
3968
|
};
|
|
3446
3969
|
const code = this.generateCode();
|
|
3447
|
-
const
|
|
3970
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3448
3971
|
this.authCodes.set(code, {
|
|
3449
3972
|
code,
|
|
3450
3973
|
clientId,
|
|
3451
3974
|
redirectUri,
|
|
3452
3975
|
codeChallenge,
|
|
3453
3976
|
codeChallengeMethod,
|
|
3454
|
-
scopes,
|
|
3977
|
+
scopes: scopes2,
|
|
3455
3978
|
user: testUser,
|
|
3456
3979
|
state,
|
|
3457
3980
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3557,8 +4080,7 @@ ${error.stack}` : String(error);
|
|
|
3557
4080
|
* Generate a random authorization code
|
|
3558
4081
|
*/
|
|
3559
4082
|
async generateCodeAsync() {
|
|
3560
|
-
|
|
3561
|
-
return _base64urlEncode(_randomBytes(32));
|
|
4083
|
+
return (0, import_utils.base64urlEncode)((0, import_utils.randomBytes)(32));
|
|
3562
4084
|
}
|
|
3563
4085
|
/**
|
|
3564
4086
|
* Generate a random authorization code (sync wrapper for compatibility)
|
|
@@ -3574,8 +4096,7 @@ ${error.stack}` : String(error);
|
|
|
3574
4096
|
*/
|
|
3575
4097
|
async computeCodeChallengeAsync(verifier, method) {
|
|
3576
4098
|
if (method === "S256") {
|
|
3577
|
-
|
|
3578
|
-
return _sha256Base64url(verifier);
|
|
4099
|
+
return (0, import_utils.sha256Base64url)(verifier);
|
|
3579
4100
|
}
|
|
3580
4101
|
return verifier;
|
|
3581
4102
|
}
|
|
@@ -4163,7 +4684,7 @@ var MockCimdServer = class {
|
|
|
4163
4684
|
|
|
4164
4685
|
// libs/testing/src/server/test-server.ts
|
|
4165
4686
|
var import_child_process = require("child_process");
|
|
4166
|
-
var
|
|
4687
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
4167
4688
|
|
|
4168
4689
|
// libs/testing/src/errors/index.ts
|
|
4169
4690
|
var TestClientError = class extends Error {
|
|
@@ -4269,6 +4790,8 @@ var E2E_PORT_RANGES = {
|
|
|
4269
4790
|
"demo-e2e-distributed": { start: 50440, size: 20 },
|
|
4270
4791
|
// Protocol revision E2E tests (50460-50479)
|
|
4271
4792
|
"demo-e2e-protocol-20260728": { start: 50460, size: 20 },
|
|
4793
|
+
// Testing library E2E tests (50480-50499)
|
|
4794
|
+
"demo-e2e-testing": { start: 50480, size: 20 },
|
|
4272
4795
|
// Mock servers and utilities (50900-50999)
|
|
4273
4796
|
"mock-oauth": { start: 50900, size: 10 },
|
|
4274
4797
|
"mock-api": { start: 50910, size: 10 },
|
|
@@ -4277,6 +4800,66 @@ var E2E_PORT_RANGES = {
|
|
|
4277
4800
|
default: { start: 51e3, size: 100 }
|
|
4278
4801
|
};
|
|
4279
4802
|
var reservedPorts = /* @__PURE__ */ new Map();
|
|
4803
|
+
var lockedPorts = /* @__PURE__ */ new Set();
|
|
4804
|
+
var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
4805
|
+
function lockDir() {
|
|
4806
|
+
try {
|
|
4807
|
+
const os = require("node:os");
|
|
4808
|
+
const path = require("node:path");
|
|
4809
|
+
return path.join(os.tmpdir(), "frontmcp-testing-ports");
|
|
4810
|
+
} catch {
|
|
4811
|
+
return void 0;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
function pidIsAlive(pid) {
|
|
4815
|
+
try {
|
|
4816
|
+
process.kill(pid, 0);
|
|
4817
|
+
return true;
|
|
4818
|
+
} catch (error) {
|
|
4819
|
+
return error.code === "EPERM";
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
function acquirePortLock(port) {
|
|
4823
|
+
if (lockedPorts.has(port)) return false;
|
|
4824
|
+
let fs;
|
|
4825
|
+
let file;
|
|
4826
|
+
try {
|
|
4827
|
+
fs = require("node:fs");
|
|
4828
|
+
const dir = lockDir();
|
|
4829
|
+
if (!dir) return true;
|
|
4830
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4831
|
+
file = require("node:path").join(dir, `${port}.lock`);
|
|
4832
|
+
} catch {
|
|
4833
|
+
return true;
|
|
4834
|
+
}
|
|
4835
|
+
const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
|
|
4836
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4837
|
+
try {
|
|
4838
|
+
fs.writeFileSync(file, payload, { flag: "wx" });
|
|
4839
|
+
lockedPorts.add(port);
|
|
4840
|
+
return true;
|
|
4841
|
+
} catch (error) {
|
|
4842
|
+
if (error.code !== "EEXIST") return true;
|
|
4843
|
+
try {
|
|
4844
|
+
const held = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
4845
|
+
const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
|
|
4846
|
+
if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
|
|
4847
|
+
fs.unlinkSync(file);
|
|
4848
|
+
} catch {
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
return false;
|
|
4853
|
+
}
|
|
4854
|
+
function releasePortLock(port) {
|
|
4855
|
+
if (!lockedPorts.delete(port)) return;
|
|
4856
|
+
try {
|
|
4857
|
+
const fs = require("node:fs");
|
|
4858
|
+
const dir = lockDir();
|
|
4859
|
+
if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
|
|
4860
|
+
} catch {
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4280
4863
|
var projectPortIndex = /* @__PURE__ */ new Map();
|
|
4281
4864
|
function getPortRange(project) {
|
|
4282
4865
|
const key = project;
|
|
@@ -4287,15 +4870,22 @@ function getPortRange(project) {
|
|
|
4287
4870
|
}
|
|
4288
4871
|
async function reservePort(project, preferredPort) {
|
|
4289
4872
|
const range = getPortRange(project);
|
|
4290
|
-
|
|
4873
|
+
const handle = (port) => ({
|
|
4874
|
+
port,
|
|
4875
|
+
// Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
|
|
4876
|
+
releaseSocket: async () => {
|
|
4877
|
+
await releasePort(port);
|
|
4878
|
+
},
|
|
4879
|
+
release: async () => {
|
|
4880
|
+
await releasePort(port);
|
|
4881
|
+
releasePortLock(port);
|
|
4882
|
+
}
|
|
4883
|
+
});
|
|
4884
|
+
const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
|
|
4885
|
+
if (wantsSpecificPort) {
|
|
4291
4886
|
const reservation = await tryReservePort(preferredPort, project);
|
|
4292
4887
|
if (reservation) {
|
|
4293
|
-
return
|
|
4294
|
-
port: preferredPort,
|
|
4295
|
-
release: async () => {
|
|
4296
|
-
await releasePort(preferredPort);
|
|
4297
|
-
}
|
|
4298
|
-
};
|
|
4888
|
+
return handle(preferredPort);
|
|
4299
4889
|
}
|
|
4300
4890
|
console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
|
|
4301
4891
|
}
|
|
@@ -4304,30 +4894,19 @@ async function reservePort(project, preferredPort) {
|
|
|
4304
4894
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4305
4895
|
const port = range.start + index % range.size;
|
|
4306
4896
|
index = (index + 1) % range.size;
|
|
4307
|
-
if (reservedPorts.has(port)) {
|
|
4897
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) {
|
|
4308
4898
|
continue;
|
|
4309
4899
|
}
|
|
4310
4900
|
const reservation = await tryReservePort(port, project);
|
|
4311
4901
|
if (reservation) {
|
|
4312
4902
|
projectPortIndex.set(project, index);
|
|
4313
|
-
return
|
|
4314
|
-
port,
|
|
4315
|
-
release: async () => {
|
|
4316
|
-
await releasePort(port);
|
|
4317
|
-
}
|
|
4318
|
-
};
|
|
4903
|
+
return handle(port);
|
|
4319
4904
|
}
|
|
4320
4905
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
return {
|
|
4326
|
-
port: dynamicPort,
|
|
4327
|
-
release: async () => {
|
|
4328
|
-
await releasePort(dynamicPort);
|
|
4329
|
-
}
|
|
4330
|
-
};
|
|
4906
|
+
for (let port = 51e3; port < 52e3; port++) {
|
|
4907
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
|
|
4908
|
+
if (await tryReservePort(port, project)) {
|
|
4909
|
+
return handle(port);
|
|
4331
4910
|
}
|
|
4332
4911
|
}
|
|
4333
4912
|
throw new Error(
|
|
@@ -4335,9 +4914,13 @@ async function reservePort(project, preferredPort) {
|
|
|
4335
4914
|
);
|
|
4336
4915
|
}
|
|
4337
4916
|
async function tryReservePort(port, project) {
|
|
4917
|
+
if (!acquirePortLock(port)) {
|
|
4918
|
+
return false;
|
|
4919
|
+
}
|
|
4338
4920
|
return new Promise((resolve) => {
|
|
4339
4921
|
const server = (0, import_net.createServer)();
|
|
4340
4922
|
server.once("error", () => {
|
|
4923
|
+
releasePortLock(port);
|
|
4341
4924
|
resolve(false);
|
|
4342
4925
|
});
|
|
4343
4926
|
server.listen(port, () => {
|
|
@@ -4363,31 +4946,6 @@ async function releasePort(port) {
|
|
|
4363
4946
|
});
|
|
4364
4947
|
});
|
|
4365
4948
|
}
|
|
4366
|
-
async function findAvailablePortInRange(start, end) {
|
|
4367
|
-
for (let port = start; port < end; port++) {
|
|
4368
|
-
if (reservedPorts.has(port)) {
|
|
4369
|
-
continue;
|
|
4370
|
-
}
|
|
4371
|
-
const available = await isPortAvailable(port);
|
|
4372
|
-
if (available) {
|
|
4373
|
-
return port;
|
|
4374
|
-
}
|
|
4375
|
-
}
|
|
4376
|
-
return null;
|
|
4377
|
-
}
|
|
4378
|
-
async function isPortAvailable(port) {
|
|
4379
|
-
return new Promise((resolve) => {
|
|
4380
|
-
const server = (0, import_net.createServer)();
|
|
4381
|
-
server.once("error", () => {
|
|
4382
|
-
resolve(false);
|
|
4383
|
-
});
|
|
4384
|
-
server.listen(port, () => {
|
|
4385
|
-
server.close(() => {
|
|
4386
|
-
resolve(true);
|
|
4387
|
-
});
|
|
4388
|
-
});
|
|
4389
|
-
});
|
|
4390
|
-
}
|
|
4391
4949
|
|
|
4392
4950
|
// libs/testing/src/server/test-server.ts
|
|
4393
4951
|
var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
|
|
@@ -4396,8 +4954,11 @@ var TestServer = class _TestServer {
|
|
|
4396
4954
|
options;
|
|
4397
4955
|
_info;
|
|
4398
4956
|
logs = [];
|
|
4957
|
+
/** Frees the port reservation (socket and cross-process lock); held until stop() */
|
|
4399
4958
|
portRelease = null;
|
|
4400
|
-
|
|
4959
|
+
/** Frees only the reservation socket, just before the server is spawned */
|
|
4960
|
+
portReleaseSocket = null;
|
|
4961
|
+
constructor(options, port, portRelease, portReleaseSocket) {
|
|
4401
4962
|
this.options = {
|
|
4402
4963
|
port,
|
|
4403
4964
|
project: options.project,
|
|
@@ -4409,6 +4970,7 @@ var TestServer = class _TestServer {
|
|
|
4409
4970
|
debug: options.debug ?? DEBUG_SERVER
|
|
4410
4971
|
};
|
|
4411
4972
|
this.portRelease = portRelease ?? null;
|
|
4973
|
+
this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
|
|
4412
4974
|
this._info = {
|
|
4413
4975
|
baseUrl: `http://localhost:${port}`,
|
|
4414
4976
|
port
|
|
@@ -4421,8 +4983,8 @@ var TestServer = class _TestServer {
|
|
|
4421
4983
|
const project = options.project ?? "default";
|
|
4422
4984
|
const maxAttempts = 3;
|
|
4423
4985
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4424
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4425
|
-
const server = new _TestServer(options, port, release);
|
|
4986
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4987
|
+
const server = new _TestServer(options, port, release, releaseSocket);
|
|
4426
4988
|
try {
|
|
4427
4989
|
await server.startProcess();
|
|
4428
4990
|
return server;
|
|
@@ -4462,7 +5024,7 @@ var TestServer = class _TestServer {
|
|
|
4462
5024
|
}
|
|
4463
5025
|
const maxAttempts = 3;
|
|
4464
5026
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4465
|
-
const { port, release } = await reservePort(project, options.port);
|
|
5027
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4466
5028
|
const serverOptions = {
|
|
4467
5029
|
...options,
|
|
4468
5030
|
port,
|
|
@@ -4470,7 +5032,7 @@ var TestServer = class _TestServer {
|
|
|
4470
5032
|
command: `npx nx serve ${project} --port ${port}`,
|
|
4471
5033
|
cwd: options.cwd ?? process.cwd()
|
|
4472
5034
|
};
|
|
4473
|
-
const server = new _TestServer(serverOptions, port, release);
|
|
5035
|
+
const server = new _TestServer(serverOptions, port, release, releaseSocket);
|
|
4474
5036
|
try {
|
|
4475
5037
|
await server.startProcess();
|
|
4476
5038
|
return server;
|
|
@@ -4528,10 +5090,17 @@ var TestServer = class _TestServer {
|
|
|
4528
5090
|
* Stop the test server
|
|
4529
5091
|
*/
|
|
4530
5092
|
async stop() {
|
|
4531
|
-
|
|
4532
|
-
await this.
|
|
4533
|
-
|
|
5093
|
+
try {
|
|
5094
|
+
await this.stopProcess();
|
|
5095
|
+
} finally {
|
|
5096
|
+
if (this.portRelease) {
|
|
5097
|
+
await this.portRelease();
|
|
5098
|
+
this.portRelease = null;
|
|
5099
|
+
}
|
|
5100
|
+
this.portReleaseSocket = null;
|
|
4534
5101
|
}
|
|
5102
|
+
}
|
|
5103
|
+
async stopProcess() {
|
|
4535
5104
|
if (this.process) {
|
|
4536
5105
|
this.log("Stopping server...");
|
|
4537
5106
|
if (this.process.exitCode !== null || this.process.signalCode !== null) {
|
|
@@ -4604,7 +5173,7 @@ var TestServer = class _TestServer {
|
|
|
4604
5173
|
* Restart the server
|
|
4605
5174
|
*/
|
|
4606
5175
|
async restart() {
|
|
4607
|
-
await this.
|
|
5176
|
+
await this.stopProcess();
|
|
4608
5177
|
await this.startProcess();
|
|
4609
5178
|
}
|
|
4610
5179
|
/**
|
|
@@ -4634,9 +5203,9 @@ var TestServer = class _TestServer {
|
|
|
4634
5203
|
PORT: String(this.options.port)
|
|
4635
5204
|
};
|
|
4636
5205
|
const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
|
|
4637
|
-
if (this.
|
|
4638
|
-
await this.
|
|
4639
|
-
this.
|
|
5206
|
+
if (this.portReleaseSocket) {
|
|
5207
|
+
await this.portReleaseSocket();
|
|
5208
|
+
this.portReleaseSocket = null;
|
|
4640
5209
|
await sleep2(300);
|
|
4641
5210
|
}
|
|
4642
5211
|
this.process = (0, import_child_process.spawn)(this.options.command, [], {
|
|
@@ -4646,9 +5215,8 @@ var TestServer = class _TestServer {
|
|
|
4646
5215
|
detached: true,
|
|
4647
5216
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4648
5217
|
});
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
}
|
|
5218
|
+
this._info.pid = this.process.pid;
|
|
5219
|
+
this._info.shellPid = this.process.pid;
|
|
4652
5220
|
let processExited = false;
|
|
4653
5221
|
let exitCode = null;
|
|
4654
5222
|
let exitError = null;
|
|
@@ -4751,6 +5319,8 @@ ${allLogs}`);
|
|
|
4751
5319
|
});
|
|
4752
5320
|
if (response.ok || response.status === 404) {
|
|
4753
5321
|
this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
|
|
5322
|
+
const listeningPid = await findListeningPid(this.options.port);
|
|
5323
|
+
if (listeningPid !== void 0) this._info.pid = listeningPid;
|
|
4754
5324
|
return;
|
|
4755
5325
|
}
|
|
4756
5326
|
lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
|
|
@@ -4795,6 +5365,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
|
|
|
4795
5365
|
function sleep2(ms) {
|
|
4796
5366
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4797
5367
|
}
|
|
5368
|
+
function findListeningPid(port) {
|
|
5369
|
+
if (process.platform === "win32") return Promise.resolve(void 0);
|
|
5370
|
+
return new Promise((resolve) => {
|
|
5371
|
+
(0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
|
|
5372
|
+
if (error) {
|
|
5373
|
+
resolve(void 0);
|
|
5374
|
+
return;
|
|
5375
|
+
}
|
|
5376
|
+
const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
|
|
5377
|
+
resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
|
|
5378
|
+
});
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
4798
5381
|
function withWorkspaceProtocolFallback(env, cwd) {
|
|
4799
5382
|
if (findInstalledProtocolPackageDir(cwd)) {
|
|
4800
5383
|
return env;
|
|
@@ -4844,7 +5427,7 @@ function withProtocolNodePathAlias(env, cwd, workspacePackageDir) {
|
|
|
4844
5427
|
const fs = require("node:fs");
|
|
4845
5428
|
const os = require("node:os");
|
|
4846
5429
|
const path = require("node:path");
|
|
4847
|
-
const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0,
|
|
5430
|
+
const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0, import_utils2.sha256Hex)(cwd).slice(0, 12));
|
|
4848
5431
|
const scopeDir = path.join(aliasRoot, "@frontmcp");
|
|
4849
5432
|
const aliasPackageDir = path.join(scopeDir, "protocol");
|
|
4850
5433
|
fs.mkdirSync(scopeDir, { recursive: true });
|
|
@@ -5066,94 +5649,173 @@ function hasMimeType(result, mimeType) {
|
|
|
5066
5649
|
return result.hasMimeType(mimeType);
|
|
5067
5650
|
}
|
|
5068
5651
|
|
|
5069
|
-
// libs/testing/src/fixtures/test-fixture.ts
|
|
5070
|
-
var import_globals = require("@jest/globals");
|
|
5071
|
-
|
|
5072
5652
|
// libs/testing/src/fixtures/gateway-token-binding.ts
|
|
5073
|
-
var
|
|
5653
|
+
var import_utils3 = require("@frontmcp/utils");
|
|
5074
5654
|
function gatewayTokenBinding(serverUrl, entryPath) {
|
|
5075
|
-
const base = (0,
|
|
5076
|
-
const prefix = (0,
|
|
5655
|
+
const base = (0, import_utils3.trimTrailing)(serverUrl, "/");
|
|
5656
|
+
const prefix = (0, import_utils3.trimSlashes)(entryPath ?? "");
|
|
5077
5657
|
const mcpUrl = prefix ? `${base}/${prefix}` : base;
|
|
5078
5658
|
return { issuer: mcpUrl, audience: mcpUrl };
|
|
5079
5659
|
}
|
|
5080
5660
|
|
|
5081
5661
|
// libs/testing/src/fixtures/test-fixture.ts
|
|
5082
|
-
var
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5662
|
+
var jestApi;
|
|
5663
|
+
function jestGlobals() {
|
|
5664
|
+
if (jestApi) return jestApi;
|
|
5665
|
+
try {
|
|
5666
|
+
const api = require("@jest/globals");
|
|
5667
|
+
jestApi = {
|
|
5668
|
+
describe: api["describe"],
|
|
5669
|
+
beforeAll: api["beforeAll"],
|
|
5670
|
+
beforeEach: api["beforeEach"],
|
|
5671
|
+
afterEach: api["afterEach"],
|
|
5672
|
+
afterAll: api["afterAll"],
|
|
5673
|
+
it: api["it"]
|
|
5674
|
+
};
|
|
5675
|
+
return jestApi;
|
|
5676
|
+
} catch (error) {
|
|
5677
|
+
throw new Error(
|
|
5678
|
+
`@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
5679
|
+
{ cause: error }
|
|
5680
|
+
);
|
|
5681
|
+
}
|
|
5095
5682
|
}
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5683
|
+
function newScope(parent) {
|
|
5684
|
+
return {
|
|
5685
|
+
skipped: parent?.skipped ?? false,
|
|
5686
|
+
reason: parent?.reason,
|
|
5687
|
+
config: {},
|
|
5688
|
+
envs: [],
|
|
5689
|
+
configured: false,
|
|
5690
|
+
cleanupRegistered: false,
|
|
5691
|
+
beforeEachHooks: [],
|
|
5692
|
+
afterEachHooks: []
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
var scopes = [newScope()];
|
|
5696
|
+
function currentScope() {
|
|
5697
|
+
return scopes[scopes.length - 1];
|
|
5698
|
+
}
|
|
5699
|
+
var serverEntries = /* @__PURE__ */ new Map();
|
|
5700
|
+
function resolveConfig(chain) {
|
|
5701
|
+
const merged = {};
|
|
5702
|
+
for (const scope of chain) {
|
|
5703
|
+
Object.assign(merged, scope.config);
|
|
5704
|
+
for (const env of scope.envs) merged.env = { ...merged.env, ...env };
|
|
5705
|
+
}
|
|
5706
|
+
return merged;
|
|
5707
|
+
}
|
|
5708
|
+
function ownerScopeOf(chain) {
|
|
5709
|
+
for (let i = chain.length - 1; i > 0; i--) {
|
|
5710
|
+
if (chain[i].configured) return chain[i];
|
|
5711
|
+
}
|
|
5712
|
+
return chain[0];
|
|
5713
|
+
}
|
|
5714
|
+
function serverKey(config) {
|
|
5715
|
+
const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
5716
|
+
return JSON.stringify({
|
|
5717
|
+
server: config.server,
|
|
5718
|
+
baseUrl: config.baseUrl,
|
|
5719
|
+
project: config.project,
|
|
5720
|
+
port: config.port,
|
|
5721
|
+
entryPath: config.entryPath,
|
|
5722
|
+
startupTimeout: config.startupTimeout,
|
|
5723
|
+
logLevel: config.logLevel,
|
|
5724
|
+
auth: config.auth,
|
|
5725
|
+
env
|
|
5726
|
+
});
|
|
5727
|
+
}
|
|
5728
|
+
function serverEnv(config) {
|
|
5729
|
+
const extra = {};
|
|
5730
|
+
if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
|
|
5731
|
+
if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
|
|
5732
|
+
if (Object.keys(extra).length === 0 && !config.env) return void 0;
|
|
5733
|
+
return { ...extra, ...config.env };
|
|
5734
|
+
}
|
|
5735
|
+
async function startServerEntry(config, owner) {
|
|
5736
|
+
let server;
|
|
5737
|
+
let startedByUs = false;
|
|
5738
|
+
if (config.baseUrl && !config.server) {
|
|
5739
|
+
server = TestServer.connect(config.baseUrl);
|
|
5740
|
+
} else if (config.server) {
|
|
5741
|
+
const serverCommand = resolveServerCommand(config.server);
|
|
5742
|
+
const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
5743
|
+
if (isDebug) {
|
|
5744
|
+
console.log(`[TestFixture] Starting server: ${serverCommand}`);
|
|
5745
|
+
}
|
|
5746
|
+
try {
|
|
5747
|
+
server = await TestServer.start({
|
|
5748
|
+
project: config.project,
|
|
5749
|
+
port: config.port,
|
|
5750
|
+
command: serverCommand,
|
|
5751
|
+
env: serverEnv(config),
|
|
5752
|
+
startupTimeout: config.startupTimeout ?? 3e4,
|
|
5753
|
+
debug: isDebug
|
|
5754
|
+
});
|
|
5755
|
+
startedByUs = true;
|
|
5104
5756
|
if (isDebug) {
|
|
5105
|
-
console.log(`[TestFixture]
|
|
5757
|
+
console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
|
|
5106
5758
|
}
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
command: serverCommand,
|
|
5112
|
-
env: currentConfig.env,
|
|
5113
|
-
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
5114
|
-
debug: isDebug
|
|
5115
|
-
});
|
|
5116
|
-
serverStartedByUs = true;
|
|
5117
|
-
if (isDebug) {
|
|
5118
|
-
console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
|
|
5119
|
-
}
|
|
5120
|
-
} catch (error) {
|
|
5121
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5122
|
-
throw new Error(
|
|
5123
|
-
`Failed to start test server.
|
|
5759
|
+
} catch (error) {
|
|
5760
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5761
|
+
throw new Error(
|
|
5762
|
+
`Failed to start test server.
|
|
5124
5763
|
|
|
5125
|
-
Server entry: ${
|
|
5126
|
-
Project: ${
|
|
5764
|
+
Server entry: ${config.server}
|
|
5765
|
+
Project: ${config.project ?? "default"}
|
|
5127
5766
|
Command: ${serverCommand}
|
|
5128
5767
|
|
|
5129
|
-
Error: ${errMsg}
|
|
5130
|
-
|
|
5131
|
-
}
|
|
5132
|
-
} else {
|
|
5133
|
-
throw new Error(
|
|
5134
|
-
'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
|
|
5768
|
+
Error: ${errMsg}`,
|
|
5769
|
+
{ cause: error }
|
|
5135
5770
|
);
|
|
5136
5771
|
}
|
|
5772
|
+
} else {
|
|
5773
|
+
throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
|
|
5137
5774
|
}
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
}
|
|
5775
|
+
const sharedSecret = config.env?.["JWT_SECRET"];
|
|
5776
|
+
const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
|
|
5777
|
+
const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
|
|
5778
|
+
return { server, startedByUs, tokenFactory: tokenFactory2, owner };
|
|
5143
5779
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5780
|
+
function acquireServer(chain) {
|
|
5781
|
+
const config = resolveConfig(chain);
|
|
5782
|
+
const key = serverKey(config);
|
|
5783
|
+
let entry = serverEntries.get(key);
|
|
5784
|
+
if (!entry) {
|
|
5785
|
+
entry = startServerEntry(config, ownerScopeOf(chain));
|
|
5786
|
+
serverEntries.set(key, entry);
|
|
5787
|
+
entry.catch(() => serverEntries.delete(key));
|
|
5788
|
+
}
|
|
5789
|
+
return entry;
|
|
5790
|
+
}
|
|
5791
|
+
async function stopServers(owner) {
|
|
5792
|
+
for (const [key, pending] of Array.from(serverEntries)) {
|
|
5793
|
+
let entry;
|
|
5794
|
+
try {
|
|
5795
|
+
entry = await pending;
|
|
5796
|
+
} catch {
|
|
5797
|
+
serverEntries.delete(key);
|
|
5798
|
+
continue;
|
|
5799
|
+
}
|
|
5800
|
+
if (owner && entry.owner !== owner) continue;
|
|
5801
|
+
serverEntries.delete(key);
|
|
5802
|
+
if (entry.startedByUs) {
|
|
5803
|
+
await entry.server.stop();
|
|
5804
|
+
}
|
|
5151
5805
|
}
|
|
5806
|
+
}
|
|
5807
|
+
var fixtureContexts = /* @__PURE__ */ new WeakMap();
|
|
5808
|
+
async function buildFixtures(chain) {
|
|
5809
|
+
const config = resolveConfig(chain);
|
|
5810
|
+
const entry = await acquireServer(chain);
|
|
5811
|
+
const { server, tokenFactory: tokenFactory2 } = entry;
|
|
5812
|
+
const publicMode = config.publicMode ?? config.auth?.mode === "public";
|
|
5813
|
+
const clientConfig = { ...config, publicMode };
|
|
5152
5814
|
const clientInstance = McpTestClient.create({
|
|
5153
|
-
baseUrl: resolveClientBaseUrl(
|
|
5154
|
-
entryPath:
|
|
5155
|
-
transport:
|
|
5156
|
-
publicMode
|
|
5815
|
+
baseUrl: resolveClientBaseUrl(server, clientConfig),
|
|
5816
|
+
entryPath: clientConfig.entryPath,
|
|
5817
|
+
transport: clientConfig.transport ?? "streamable-http",
|
|
5818
|
+
publicMode
|
|
5157
5819
|
}).build();
|
|
5158
5820
|
try {
|
|
5159
5821
|
await clientInstance.connect();
|
|
@@ -5167,17 +5829,18 @@ async function createTestFixtures() {
|
|
|
5167
5829
|
throw err;
|
|
5168
5830
|
}
|
|
5169
5831
|
}
|
|
5170
|
-
const
|
|
5171
|
-
const
|
|
5172
|
-
return {
|
|
5832
|
+
const context = { extraClients: [] };
|
|
5833
|
+
const fixtures = {
|
|
5173
5834
|
mcp: clientInstance,
|
|
5174
|
-
auth,
|
|
5175
|
-
server
|
|
5835
|
+
auth: createAuthFixture(tokenFactory2),
|
|
5836
|
+
server: createServerFixture(server, clientConfig, clientInstance, context)
|
|
5176
5837
|
};
|
|
5838
|
+
fixtureContexts.set(fixtures, context);
|
|
5839
|
+
return fixtures;
|
|
5177
5840
|
}
|
|
5178
5841
|
async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
5179
|
-
if (testFailed
|
|
5180
|
-
const logs =
|
|
5842
|
+
if (testFailed) {
|
|
5843
|
+
const logs = fixtures.server.getLogs();
|
|
5181
5844
|
if (logs.length > 0) {
|
|
5182
5845
|
console.error("\n[TestFixture] === Server Logs (test failed) ===");
|
|
5183
5846
|
const recentLogs = logs.slice(-50);
|
|
@@ -5188,20 +5851,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
|
5188
5851
|
console.error("[TestFixture] === End Server Logs ===\n");
|
|
5189
5852
|
}
|
|
5190
5853
|
}
|
|
5854
|
+
const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
|
|
5855
|
+
for (const client of extras) {
|
|
5856
|
+
if (client.isConnected()) {
|
|
5857
|
+
await client.disconnect().catch(() => void 0);
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5191
5860
|
if (fixtures.mcp.isConnected()) {
|
|
5192
5861
|
await fixtures.mcp.disconnect();
|
|
5193
5862
|
}
|
|
5194
5863
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
await serverInstance.stop();
|
|
5198
|
-
}
|
|
5199
|
-
serverInstance = null;
|
|
5200
|
-
tokenFactory = null;
|
|
5201
|
-
serverStartedByUs = false;
|
|
5202
|
-
}
|
|
5203
|
-
function resolveClientBaseUrl(server) {
|
|
5204
|
-
return currentConfig.baseUrl ?? server.info.baseUrl;
|
|
5864
|
+
function resolveClientBaseUrl(server, config) {
|
|
5865
|
+
return config.baseUrl ?? server.info.baseUrl;
|
|
5205
5866
|
}
|
|
5206
5867
|
function createAuthFixture(factory) {
|
|
5207
5868
|
const users = {
|
|
@@ -5247,28 +5908,39 @@ function createAuthFixture(factory) {
|
|
|
5247
5908
|
getAudience: () => factory.getAudience()
|
|
5248
5909
|
};
|
|
5249
5910
|
}
|
|
5250
|
-
function createServerFixture(server) {
|
|
5911
|
+
function createServerFixture(server, config, mcp, context) {
|
|
5251
5912
|
return {
|
|
5252
|
-
|
|
5913
|
+
// Live view: `pid` is filled in once the server is listening and follows restarts
|
|
5914
|
+
get info() {
|
|
5915
|
+
return server.info;
|
|
5916
|
+
},
|
|
5253
5917
|
createClient: async (opts) => {
|
|
5254
|
-
|
|
5255
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5256
|
-
entryPath: opts?.entryPath ??
|
|
5918
|
+
const client = await McpTestClient.create({
|
|
5919
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5920
|
+
entryPath: opts?.entryPath ?? config.entryPath,
|
|
5257
5921
|
transport: opts?.transport ?? "streamable-http",
|
|
5258
5922
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
5259
5923
|
clientInfo: opts?.clientInfo,
|
|
5260
|
-
publicMode:
|
|
5924
|
+
publicMode: config.publicMode
|
|
5261
5925
|
}).buildAndConnect();
|
|
5926
|
+
context.extraClients.push(client);
|
|
5927
|
+
return client;
|
|
5262
5928
|
},
|
|
5263
5929
|
createClientBuilder: () => {
|
|
5264
5930
|
const builder = new McpTestClientBuilder({
|
|
5265
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5266
|
-
entryPath:
|
|
5267
|
-
publicMode:
|
|
5931
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5932
|
+
entryPath: config.entryPath,
|
|
5933
|
+
publicMode: config.publicMode
|
|
5268
5934
|
});
|
|
5269
5935
|
return builder;
|
|
5270
5936
|
},
|
|
5271
|
-
restart: () =>
|
|
5937
|
+
restart: async () => {
|
|
5938
|
+
const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
|
|
5939
|
+
await server.restart();
|
|
5940
|
+
for (const client of reconnect) {
|
|
5941
|
+
await client.reconnect();
|
|
5942
|
+
}
|
|
5943
|
+
},
|
|
5272
5944
|
getLogs: () => server.getLogs(),
|
|
5273
5945
|
clearLogs: () => server.clearLogs()
|
|
5274
5946
|
};
|
|
@@ -5279,38 +5951,71 @@ function resolveServerCommand(server) {
|
|
|
5279
5951
|
}
|
|
5280
5952
|
return `npx tsx ${server}`;
|
|
5281
5953
|
}
|
|
5282
|
-
function runWithFixtures(fn) {
|
|
5283
|
-
return async () => {
|
|
5284
|
-
const fixtures = await
|
|
5285
|
-
let
|
|
5954
|
+
function runWithFixtures(fn, chain) {
|
|
5955
|
+
return async (...extra) => {
|
|
5956
|
+
const fixtures = await buildFixtures(chain);
|
|
5957
|
+
let failure;
|
|
5286
5958
|
try {
|
|
5287
|
-
|
|
5959
|
+
for (const scope of chain) {
|
|
5960
|
+
for (const hook of scope.beforeEachHooks) await hook(fixtures);
|
|
5961
|
+
}
|
|
5962
|
+
await fn(fixtures, ...extra);
|
|
5288
5963
|
} catch (error) {
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
await
|
|
5964
|
+
failure = { error };
|
|
5965
|
+
}
|
|
5966
|
+
try {
|
|
5967
|
+
await runAfterEachHooks(chain, fixtures);
|
|
5968
|
+
} catch (error) {
|
|
5969
|
+
failure ??= { error };
|
|
5293
5970
|
}
|
|
5971
|
+
await cleanupTestFixtures(fixtures, failure !== void 0);
|
|
5972
|
+
if (failure) throw failure.error;
|
|
5294
5973
|
};
|
|
5295
5974
|
}
|
|
5975
|
+
async function runAfterEachHooks(chain, fixtures) {
|
|
5976
|
+
let firstError;
|
|
5977
|
+
let failed = false;
|
|
5978
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
5979
|
+
for (const hook of chain[i].afterEachHooks) {
|
|
5980
|
+
try {
|
|
5981
|
+
await hook(fixtures);
|
|
5982
|
+
} catch (error) {
|
|
5983
|
+
if (!failed) firstError = error;
|
|
5984
|
+
failed = true;
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5987
|
+
}
|
|
5988
|
+
if (failed) throw firstError;
|
|
5989
|
+
}
|
|
5990
|
+
function skipTitle(name, scope) {
|
|
5991
|
+
return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
|
|
5992
|
+
}
|
|
5296
5993
|
function testWithFixtures(name, fn) {
|
|
5297
|
-
const
|
|
5994
|
+
const chain = scopes.slice();
|
|
5995
|
+
const scope = currentScope();
|
|
5298
5996
|
if (scope.skipped) {
|
|
5299
|
-
|
|
5997
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5300
5998
|
return;
|
|
5301
5999
|
}
|
|
5302
|
-
|
|
6000
|
+
jestGlobals().it(name, runWithFixtures(fn, chain));
|
|
5303
6001
|
}
|
|
5304
6002
|
function use(config) {
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
6003
|
+
const scope = currentScope();
|
|
6004
|
+
const { env, ...rest } = config;
|
|
6005
|
+
scope.config = { ...scope.config, ...rest };
|
|
6006
|
+
if (env) scope.envs.push(env);
|
|
6007
|
+
scope.configured = true;
|
|
6008
|
+
if (!scope.cleanupRegistered) {
|
|
6009
|
+
scope.cleanupRegistered = true;
|
|
6010
|
+
jestGlobals().afterAll(async () => {
|
|
6011
|
+
await stopServers(scope === scopes[0] ? void 0 : scope);
|
|
6012
|
+
});
|
|
6013
|
+
}
|
|
5309
6014
|
}
|
|
5310
6015
|
function skip(nameOrCondition, fnOrReason) {
|
|
5311
6016
|
if (typeof nameOrCondition === "boolean") {
|
|
5312
6017
|
if (nameOrCondition) {
|
|
5313
|
-
const scope =
|
|
6018
|
+
const scope = currentScope();
|
|
5314
6019
|
scope.skipped = true;
|
|
5315
6020
|
scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
|
|
5316
6021
|
}
|
|
@@ -5321,57 +6026,114 @@ function skip(nameOrCondition, fnOrReason) {
|
|
|
5321
6026
|
`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
|
|
5322
6027
|
);
|
|
5323
6028
|
}
|
|
5324
|
-
|
|
6029
|
+
jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
|
|
5325
6030
|
}
|
|
5326
6031
|
function only(name, fn) {
|
|
5327
|
-
const scope =
|
|
6032
|
+
const scope = currentScope();
|
|
5328
6033
|
if (scope.skipped) {
|
|
5329
|
-
|
|
6034
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
|
|
5330
6035
|
return;
|
|
5331
6036
|
}
|
|
5332
|
-
|
|
6037
|
+
jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
|
|
5333
6038
|
}
|
|
5334
6039
|
function todo(name) {
|
|
5335
|
-
|
|
6040
|
+
jestGlobals().it.todo(name);
|
|
5336
6041
|
}
|
|
5337
|
-
function
|
|
6042
|
+
function each(table, ...tagged) {
|
|
6043
|
+
const chain = scopes.slice();
|
|
6044
|
+
const register = jestGlobals().it.each(table, ...tagged);
|
|
5338
6045
|
return (name, fn) => {
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
}
|
|
5347
|
-
});
|
|
6046
|
+
const scope = currentScope();
|
|
6047
|
+
if (scope.skipped) {
|
|
6048
|
+
const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
|
|
6049
|
+
skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
register(name, runWithFixtures(fn, chain));
|
|
5348
6053
|
};
|
|
5349
6054
|
}
|
|
5350
|
-
function
|
|
5351
|
-
return (
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
6055
|
+
function withScope(fn) {
|
|
6056
|
+
return (...args) => {
|
|
6057
|
+
scopes.push(newScope(currentScope()));
|
|
6058
|
+
try {
|
|
6059
|
+
fn(...args);
|
|
6060
|
+
} finally {
|
|
6061
|
+
scopes.pop();
|
|
6062
|
+
}
|
|
6063
|
+
};
|
|
6064
|
+
}
|
|
6065
|
+
function scopedRegister(pick) {
|
|
6066
|
+
return (name, fn, timeout) => {
|
|
6067
|
+
pick()(name, withScope(fn), timeout);
|
|
6068
|
+
};
|
|
6069
|
+
}
|
|
6070
|
+
function scopedEach(pick) {
|
|
6071
|
+
return (...eachArgs) => (name, fn, timeout) => {
|
|
6072
|
+
const register = pick()(...eachArgs);
|
|
6073
|
+
register(name, withScope(fn), timeout);
|
|
6074
|
+
};
|
|
6075
|
+
}
|
|
6076
|
+
var describeWithScope = Object.assign(
|
|
6077
|
+
scopedRegister(() => jestGlobals().describe),
|
|
6078
|
+
{
|
|
6079
|
+
// `describe.skip` still evaluates its callback during collection, so it needs a scope too
|
|
6080
|
+
only: Object.assign(
|
|
6081
|
+
scopedRegister(() => jestGlobals().describe.only),
|
|
6082
|
+
{
|
|
6083
|
+
each: scopedEach(() => jestGlobals().describe.only.each)
|
|
6084
|
+
}
|
|
6085
|
+
),
|
|
6086
|
+
skip: Object.assign(
|
|
6087
|
+
scopedRegister(() => jestGlobals().describe.skip),
|
|
6088
|
+
{
|
|
6089
|
+
each: scopedEach(() => jestGlobals().describe.skip.each)
|
|
6090
|
+
}
|
|
6091
|
+
),
|
|
6092
|
+
each: scopedEach(() => jestGlobals().describe.each)
|
|
6093
|
+
}
|
|
6094
|
+
);
|
|
6095
|
+
function eachHook(kind) {
|
|
6096
|
+
return (fn, timeout) => {
|
|
6097
|
+
if (typeof fn === "function" && fn.length > 0) {
|
|
6098
|
+
const scope = currentScope();
|
|
6099
|
+
(kind === "beforeEach" ? scope.beforeEachHooks : scope.afterEachHooks).push(fn);
|
|
6100
|
+
return;
|
|
6101
|
+
}
|
|
6102
|
+
jestGlobals()[kind](fn, timeout);
|
|
6103
|
+
};
|
|
5355
6104
|
}
|
|
5356
|
-
var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
|
|
5357
|
-
only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
|
|
5358
|
-
skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
|
|
5359
|
-
each: eachWithSkipScope(describe2.each)
|
|
5360
|
-
});
|
|
5361
6105
|
var test = testWithFixtures;
|
|
5362
6106
|
test.use = use;
|
|
5363
|
-
test.describe =
|
|
5364
|
-
test.beforeAll =
|
|
5365
|
-
test.beforeEach =
|
|
5366
|
-
test.afterEach =
|
|
5367
|
-
test.afterAll =
|
|
6107
|
+
test.describe = describeWithScope;
|
|
6108
|
+
test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
|
|
6109
|
+
test.beforeEach = eachHook("beforeEach");
|
|
6110
|
+
test.afterEach = eachHook("afterEach");
|
|
6111
|
+
test.afterAll = ((...args) => jestGlobals().afterAll(...args));
|
|
5368
6112
|
test.skip = skip;
|
|
5369
6113
|
test.only = only;
|
|
5370
6114
|
test.todo = todo;
|
|
6115
|
+
test.each = each;
|
|
5371
6116
|
|
|
5372
6117
|
// libs/testing/src/expect.ts
|
|
5373
|
-
var
|
|
5374
|
-
|
|
6118
|
+
var resolvedExpect;
|
|
6119
|
+
function resolveJestExpect() {
|
|
6120
|
+
if (resolvedExpect) return resolvedExpect;
|
|
6121
|
+
try {
|
|
6122
|
+
resolvedExpect = require("@jest/globals").expect;
|
|
6123
|
+
return resolvedExpect;
|
|
6124
|
+
} catch (error) {
|
|
6125
|
+
throw new Error(
|
|
6126
|
+
`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
6127
|
+
{ cause: error }
|
|
6128
|
+
);
|
|
6129
|
+
}
|
|
6130
|
+
}
|
|
6131
|
+
var expect = new Proxy(function frontmcpExpect() {
|
|
6132
|
+
}, {
|
|
6133
|
+
apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
|
|
6134
|
+
get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
|
|
6135
|
+
has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
|
|
6136
|
+
});
|
|
5375
6137
|
|
|
5376
6138
|
// libs/testing/src/platform/platform-types.ts
|
|
5377
6139
|
function getPlatformMetaNamespace(platform) {
|
|
@@ -6034,34 +6796,44 @@ var HttpInterceptorImpl = class {
|
|
|
6034
6796
|
}
|
|
6035
6797
|
};
|
|
6036
6798
|
}
|
|
6037
|
-
get(url, response) {
|
|
6799
|
+
get(url, response, options) {
|
|
6038
6800
|
return this.mock({
|
|
6039
|
-
match: { url, method: "GET" },
|
|
6040
|
-
response: normalizeResponse(response)
|
|
6801
|
+
match: { url, method: "GET", ...options?.match },
|
|
6802
|
+
response: normalizeResponse(response),
|
|
6803
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6804
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6041
6805
|
});
|
|
6042
6806
|
}
|
|
6043
|
-
post(url, response) {
|
|
6807
|
+
post(url, response, options) {
|
|
6044
6808
|
return this.mock({
|
|
6045
|
-
match: { url, method: "POST" },
|
|
6046
|
-
response: normalizeResponse(response)
|
|
6809
|
+
match: { url, method: "POST", ...options?.match },
|
|
6810
|
+
response: normalizeResponse(response),
|
|
6811
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6812
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6047
6813
|
});
|
|
6048
6814
|
}
|
|
6049
|
-
put(url, response) {
|
|
6815
|
+
put(url, response, options) {
|
|
6050
6816
|
return this.mock({
|
|
6051
|
-
match: { url, method: "PUT" },
|
|
6052
|
-
response: normalizeResponse(response)
|
|
6817
|
+
match: { url, method: "PUT", ...options?.match },
|
|
6818
|
+
response: normalizeResponse(response),
|
|
6819
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6820
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6053
6821
|
});
|
|
6054
6822
|
}
|
|
6055
|
-
delete(url, response) {
|
|
6823
|
+
delete(url, response, options) {
|
|
6056
6824
|
return this.mock({
|
|
6057
|
-
match: { url, method: "DELETE" },
|
|
6058
|
-
response: normalizeResponse(response)
|
|
6825
|
+
match: { url, method: "DELETE", ...options?.match },
|
|
6826
|
+
response: normalizeResponse(response),
|
|
6827
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6828
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6059
6829
|
});
|
|
6060
6830
|
}
|
|
6061
|
-
any(url, response) {
|
|
6831
|
+
any(url, response, options) {
|
|
6062
6832
|
return this.mock({
|
|
6063
|
-
match: { url },
|
|
6064
|
-
response: normalizeResponse(response)
|
|
6833
|
+
match: { url, ...options?.match },
|
|
6834
|
+
response: normalizeResponse(response),
|
|
6835
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6836
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6065
6837
|
});
|
|
6066
6838
|
}
|
|
6067
6839
|
clear() {
|
|
@@ -6093,6 +6865,9 @@ ${descriptions.join("\n")}`);
|
|
|
6093
6865
|
if (index !== -1) {
|
|
6094
6866
|
activeInterceptors.splice(index, 1);
|
|
6095
6867
|
}
|
|
6868
|
+
if (activeInterceptors.length === 0) {
|
|
6869
|
+
httpMock.disable();
|
|
6870
|
+
}
|
|
6096
6871
|
}
|
|
6097
6872
|
// Internal methods
|
|
6098
6873
|
isActive() {
|
|
@@ -6192,43 +6967,49 @@ ${descriptions.join("\n")}`);
|
|
|
6192
6967
|
}
|
|
6193
6968
|
};
|
|
6194
6969
|
async function interceptedFetch(input, init) {
|
|
6195
|
-
const
|
|
6196
|
-
const
|
|
6970
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
6971
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
|
|
6972
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
6197
6973
|
const headers = {};
|
|
6198
|
-
|
|
6199
|
-
if (
|
|
6200
|
-
|
|
6974
|
+
const collectHeaders = (source) => {
|
|
6975
|
+
if (!source) return;
|
|
6976
|
+
if (source instanceof Headers) {
|
|
6977
|
+
source.forEach((value, key) => {
|
|
6201
6978
|
headers[key.toLowerCase()] = value;
|
|
6202
6979
|
});
|
|
6203
|
-
} else if (Array.isArray(
|
|
6204
|
-
for (const [key, value] of
|
|
6980
|
+
} else if (Array.isArray(source)) {
|
|
6981
|
+
for (const [key, value] of source) {
|
|
6205
6982
|
headers[key.toLowerCase()] = value;
|
|
6206
6983
|
}
|
|
6207
6984
|
} else {
|
|
6208
|
-
for (const [key, value] of Object.entries(
|
|
6985
|
+
for (const [key, value] of Object.entries(source)) {
|
|
6209
6986
|
headers[key.toLowerCase()] = value;
|
|
6210
6987
|
}
|
|
6211
6988
|
}
|
|
6212
|
-
}
|
|
6989
|
+
};
|
|
6990
|
+
collectHeaders(init?.headers ?? request?.headers);
|
|
6213
6991
|
let body;
|
|
6214
6992
|
let rawBody;
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6993
|
+
let bodySource = init?.body;
|
|
6994
|
+
if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
|
|
6995
|
+
bodySource = await request.clone().text();
|
|
6996
|
+
}
|
|
6997
|
+
if (bodySource) {
|
|
6998
|
+
if (typeof bodySource === "string") {
|
|
6999
|
+
rawBody = bodySource;
|
|
7000
|
+
} else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
|
|
7001
|
+
rawBody = new TextDecoder().decode(bodySource);
|
|
7002
|
+
} else if (bodySource instanceof URLSearchParams) {
|
|
7003
|
+
rawBody = bodySource.toString();
|
|
7004
|
+
}
|
|
7005
|
+
if (rawBody !== void 0) {
|
|
6225
7006
|
try {
|
|
6226
7007
|
body = JSON.parse(rawBody);
|
|
6227
7008
|
} catch {
|
|
6228
7009
|
body = rawBody;
|
|
6229
7010
|
}
|
|
6230
7011
|
} else {
|
|
6231
|
-
body =
|
|
7012
|
+
body = bodySource;
|
|
6232
7013
|
}
|
|
6233
7014
|
}
|
|
6234
7015
|
const requestInfo = {
|
|
@@ -6258,8 +7039,20 @@ async function interceptedFetch(input, init) {
|
|
|
6258
7039
|
Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
|
|
6259
7040
|
);
|
|
6260
7041
|
}
|
|
7042
|
+
var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
|
|
7043
|
+
function isResponseDescriptor(value) {
|
|
7044
|
+
const keys = Object.keys(value);
|
|
7045
|
+
if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
|
|
7046
|
+
if ("status" in value) {
|
|
7047
|
+
const status = value["status"];
|
|
7048
|
+
if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
|
|
7049
|
+
}
|
|
7050
|
+
return true;
|
|
7051
|
+
}
|
|
6261
7052
|
function normalizeResponse(response) {
|
|
6262
|
-
|
|
7053
|
+
const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
|
|
7054
|
+
if (isNetworkError) return response;
|
|
7055
|
+
if (Array.isArray(response) || !isResponseDescriptor(response)) {
|
|
6263
7056
|
return { body: response };
|
|
6264
7057
|
}
|
|
6265
7058
|
return response;
|
|
@@ -7593,47 +8386,48 @@ function clearGlobalMeasurements() {
|
|
|
7593
8386
|
}
|
|
7594
8387
|
|
|
7595
8388
|
// libs/testing/src/perf/perf-test.ts
|
|
7596
|
-
var
|
|
7597
|
-
var
|
|
7598
|
-
var
|
|
7599
|
-
var
|
|
7600
|
-
async function
|
|
7601
|
-
if (!
|
|
7602
|
-
|
|
7603
|
-
}
|
|
7604
|
-
if (!
|
|
7605
|
-
if (
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
} else if (
|
|
7609
|
-
const serverCommand = resolveServerCommand2(
|
|
8389
|
+
var currentConfig = {};
|
|
8390
|
+
var serverInstance = null;
|
|
8391
|
+
var tokenFactory = null;
|
|
8392
|
+
var serverStartedByUs = false;
|
|
8393
|
+
async function initializeSharedResources() {
|
|
8394
|
+
if (!tokenFactory) {
|
|
8395
|
+
tokenFactory = new TestTokenFactory();
|
|
8396
|
+
}
|
|
8397
|
+
if (!serverInstance) {
|
|
8398
|
+
if (currentConfig.baseUrl) {
|
|
8399
|
+
serverInstance = TestServer.connect(currentConfig.baseUrl);
|
|
8400
|
+
serverStartedByUs = false;
|
|
8401
|
+
} else if (currentConfig.server) {
|
|
8402
|
+
const serverCommand = resolveServerCommand2(currentConfig.server);
|
|
7610
8403
|
const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
7611
8404
|
if (isDebug) {
|
|
7612
8405
|
console.log(`[PerfTest] Starting server: ${serverCommand}`);
|
|
7613
8406
|
}
|
|
7614
8407
|
try {
|
|
7615
|
-
|
|
7616
|
-
project:
|
|
7617
|
-
port:
|
|
8408
|
+
serverInstance = await TestServer.start({
|
|
8409
|
+
project: currentConfig.project,
|
|
8410
|
+
port: currentConfig.port,
|
|
7618
8411
|
command: serverCommand,
|
|
7619
|
-
env:
|
|
7620
|
-
startupTimeout:
|
|
8412
|
+
env: currentConfig.env,
|
|
8413
|
+
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
7621
8414
|
debug: isDebug
|
|
7622
8415
|
});
|
|
7623
|
-
|
|
8416
|
+
serverStartedByUs = true;
|
|
7624
8417
|
if (isDebug) {
|
|
7625
|
-
console.log(`[PerfTest] Server started at ${
|
|
8418
|
+
console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
|
|
7626
8419
|
}
|
|
7627
8420
|
} catch (error) {
|
|
7628
8421
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
7629
8422
|
throw new Error(
|
|
7630
8423
|
`Failed to start test server.
|
|
7631
8424
|
|
|
7632
|
-
Server entry: ${
|
|
7633
|
-
Project: ${
|
|
8425
|
+
Server entry: ${currentConfig.server}
|
|
8426
|
+
Project: ${currentConfig.project ?? "default"}
|
|
7634
8427
|
Command: ${serverCommand}
|
|
7635
8428
|
|
|
7636
|
-
Error: ${errMsg}
|
|
8429
|
+
Error: ${errMsg}`,
|
|
8430
|
+
{ cause: error }
|
|
7637
8431
|
);
|
|
7638
8432
|
}
|
|
7639
8433
|
} else {
|
|
@@ -7641,23 +8435,23 @@ Error: ${errMsg}`
|
|
|
7641
8435
|
}
|
|
7642
8436
|
}
|
|
7643
8437
|
}
|
|
7644
|
-
async function
|
|
7645
|
-
await
|
|
7646
|
-
if (!
|
|
8438
|
+
async function createTestFixtures(testName) {
|
|
8439
|
+
await initializeSharedResources();
|
|
8440
|
+
if (!serverInstance) {
|
|
7647
8441
|
throw new Error("Server instance not initialized");
|
|
7648
8442
|
}
|
|
7649
|
-
if (!
|
|
8443
|
+
if (!tokenFactory) {
|
|
7650
8444
|
throw new Error("Token factory not initialized");
|
|
7651
8445
|
}
|
|
7652
8446
|
const clientInstance = await McpTestClient.create({
|
|
7653
|
-
baseUrl:
|
|
7654
|
-
transport:
|
|
7655
|
-
publicMode:
|
|
8447
|
+
baseUrl: serverInstance.info.baseUrl,
|
|
8448
|
+
transport: currentConfig.transport ?? "streamable-http",
|
|
8449
|
+
publicMode: currentConfig.publicMode
|
|
7656
8450
|
}).buildAndConnect();
|
|
7657
|
-
const auth = createAuthFixture2(
|
|
7658
|
-
const server = createServerFixture2(
|
|
7659
|
-
const perfImpl = createPerfFixtures(testName,
|
|
7660
|
-
if (
|
|
8451
|
+
const auth = createAuthFixture2(tokenFactory);
|
|
8452
|
+
const server = createServerFixture2(serverInstance);
|
|
8453
|
+
const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
|
|
8454
|
+
if (currentConfig.forceGcOnBaseline !== false) {
|
|
7661
8455
|
await perfImpl.baseline();
|
|
7662
8456
|
}
|
|
7663
8457
|
return {
|
|
@@ -7673,8 +8467,8 @@ async function createTestFixtures2(testName) {
|
|
|
7673
8467
|
async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
7674
8468
|
const measurement = perfImpl.buildMeasurement();
|
|
7675
8469
|
addGlobalMeasurement(measurement);
|
|
7676
|
-
if (testFailed &&
|
|
7677
|
-
const logs =
|
|
8470
|
+
if (testFailed && serverInstance) {
|
|
8471
|
+
const logs = serverInstance.getLogs();
|
|
7678
8472
|
if (logs.length > 0) {
|
|
7679
8473
|
console.error("\n[PerfTest] === Server Logs (test failed) ===");
|
|
7680
8474
|
const recentLogs = logs.slice(-50);
|
|
@@ -7689,13 +8483,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
|
7689
8483
|
await fixtures.mcp.disconnect();
|
|
7690
8484
|
}
|
|
7691
8485
|
}
|
|
7692
|
-
async function
|
|
7693
|
-
if (
|
|
7694
|
-
await
|
|
8486
|
+
async function cleanupSharedResources() {
|
|
8487
|
+
if (serverInstance && serverStartedByUs) {
|
|
8488
|
+
await serverInstance.stop();
|
|
7695
8489
|
}
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
8490
|
+
serverInstance = null;
|
|
8491
|
+
tokenFactory = null;
|
|
8492
|
+
serverStartedByUs = false;
|
|
7699
8493
|
}
|
|
7700
8494
|
function createAuthFixture2(factory) {
|
|
7701
8495
|
const users = {
|
|
@@ -7750,13 +8544,13 @@ function createServerFixture2(server) {
|
|
|
7750
8544
|
transport: opts?.transport ?? "streamable-http",
|
|
7751
8545
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
7752
8546
|
clientInfo: opts?.clientInfo,
|
|
7753
|
-
publicMode:
|
|
8547
|
+
publicMode: currentConfig.publicMode
|
|
7754
8548
|
}).buildAndConnect();
|
|
7755
8549
|
},
|
|
7756
8550
|
createClientBuilder: () => {
|
|
7757
8551
|
return new McpTestClientBuilder({
|
|
7758
8552
|
baseUrl: server.info.baseUrl,
|
|
7759
|
-
publicMode:
|
|
8553
|
+
publicMode: currentConfig.publicMode
|
|
7760
8554
|
});
|
|
7761
8555
|
},
|
|
7762
8556
|
restart: () => server.restart(),
|
|
@@ -7772,7 +8566,7 @@ function resolveServerCommand2(server) {
|
|
|
7772
8566
|
}
|
|
7773
8567
|
function perfTestWithFixtures(name, fn) {
|
|
7774
8568
|
it(name, async () => {
|
|
7775
|
-
const { fixtures, perfImpl } = await
|
|
8569
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7776
8570
|
let testFailed = false;
|
|
7777
8571
|
try {
|
|
7778
8572
|
await fn(fixtures);
|
|
@@ -7785,14 +8579,14 @@ function perfTestWithFixtures(name, fn) {
|
|
|
7785
8579
|
});
|
|
7786
8580
|
}
|
|
7787
8581
|
function use2(config) {
|
|
7788
|
-
|
|
8582
|
+
currentConfig = { ...currentConfig, ...config };
|
|
7789
8583
|
afterAll(async () => {
|
|
7790
|
-
await
|
|
8584
|
+
await cleanupSharedResources();
|
|
7791
8585
|
});
|
|
7792
8586
|
}
|
|
7793
8587
|
function skip2(name, fn) {
|
|
7794
8588
|
it.skip(name, async () => {
|
|
7795
|
-
const { fixtures, perfImpl } = await
|
|
8589
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7796
8590
|
let testFailed = false;
|
|
7797
8591
|
try {
|
|
7798
8592
|
await fn(fixtures);
|
|
@@ -7806,7 +8600,7 @@ function skip2(name, fn) {
|
|
|
7806
8600
|
}
|
|
7807
8601
|
function only2(name, fn) {
|
|
7808
8602
|
it.only(name, async () => {
|
|
7809
|
-
const { fixtures, perfImpl } = await
|
|
8603
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7810
8604
|
let testFailed = false;
|
|
7811
8605
|
try {
|
|
7812
8606
|
await fn(fixtures);
|
|
@@ -7823,16 +8617,19 @@ function todo2(name) {
|
|
|
7823
8617
|
}
|
|
7824
8618
|
var perfTest = perfTestWithFixtures;
|
|
7825
8619
|
perfTest.use = use2;
|
|
7826
|
-
|
|
7827
|
-
perfTest
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
8620
|
+
for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
|
|
8621
|
+
Object.defineProperty(perfTest, hook, {
|
|
8622
|
+
get: () => globalThis[hook],
|
|
8623
|
+
enumerable: true,
|
|
8624
|
+
configurable: true
|
|
8625
|
+
});
|
|
8626
|
+
}
|
|
7831
8627
|
perfTest.skip = skip2;
|
|
7832
8628
|
perfTest.only = only2;
|
|
7833
8629
|
perfTest.todo = todo2;
|
|
7834
8630
|
|
|
7835
8631
|
// libs/testing/src/perf/baseline-store.ts
|
|
8632
|
+
var import_utils4 = require("@frontmcp/utils");
|
|
7836
8633
|
var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
|
|
7837
8634
|
var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
|
|
7838
8635
|
var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
|
|
@@ -7847,11 +8644,10 @@ var BaselineStore = class {
|
|
|
7847
8644
|
*/
|
|
7848
8645
|
async load() {
|
|
7849
8646
|
try {
|
|
7850
|
-
|
|
7851
|
-
if (!await fileExists(this.baselinePath)) {
|
|
8647
|
+
if (!await (0, import_utils4.fileExists)(this.baselinePath)) {
|
|
7852
8648
|
return null;
|
|
7853
8649
|
}
|
|
7854
|
-
const content = await readFile(this.baselinePath);
|
|
8650
|
+
const content = await (0, import_utils4.readFile)(this.baselinePath);
|
|
7855
8651
|
this.baseline = JSON.parse(content);
|
|
7856
8652
|
return this.baseline;
|
|
7857
8653
|
} catch {
|
|
@@ -7862,10 +8658,9 @@ var BaselineStore = class {
|
|
|
7862
8658
|
* Save baseline to local file.
|
|
7863
8659
|
*/
|
|
7864
8660
|
async save(baseline) {
|
|
7865
|
-
const { writeFile, ensureDir } = await import("@frontmcp/utils");
|
|
7866
8661
|
const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
|
|
7867
|
-
await ensureDir(dir);
|
|
7868
|
-
await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
8662
|
+
await (0, import_utils4.ensureDir)(dir);
|
|
8663
|
+
await (0, import_utils4.writeFile)(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
7869
8664
|
this.baseline = baseline;
|
|
7870
8665
|
}
|
|
7871
8666
|
/**
|
|
@@ -8118,6 +8913,7 @@ function getRegressionDetector(config) {
|
|
|
8118
8913
|
}
|
|
8119
8914
|
|
|
8120
8915
|
// libs/testing/src/perf/report-generator.ts
|
|
8916
|
+
var import_utils5 = require("@frontmcp/utils");
|
|
8121
8917
|
var ReportGenerator = class {
|
|
8122
8918
|
detector;
|
|
8123
8919
|
constructor() {
|
|
@@ -8373,14 +9169,13 @@ var ReportGenerator = class {
|
|
|
8373
9169
|
}
|
|
8374
9170
|
};
|
|
8375
9171
|
async function saveReports(measurements, outputDir, baseline, gitInfo) {
|
|
8376
|
-
|
|
8377
|
-
await ensureDir(outputDir);
|
|
9172
|
+
await (0, import_utils5.ensureDir)(outputDir);
|
|
8378
9173
|
const generator = new ReportGenerator();
|
|
8379
9174
|
const report = generator.generateReport(measurements, baseline, gitInfo);
|
|
8380
9175
|
const jsonPath = `${outputDir}/report.json`;
|
|
8381
9176
|
const markdownPath = `${outputDir}/report.md`;
|
|
8382
|
-
await writeFile(jsonPath, generator.generateJsonReport(report));
|
|
8383
|
-
await writeFile(markdownPath, generator.generateMarkdownReport(report));
|
|
9177
|
+
await (0, import_utils5.writeFile)(jsonPath, generator.generateJsonReport(report));
|
|
9178
|
+
await (0, import_utils5.writeFile)(markdownPath, generator.generateMarkdownReport(report));
|
|
8384
9179
|
return { jsonPath, markdownPath };
|
|
8385
9180
|
}
|
|
8386
9181
|
function createReportGenerator() {
|
|
@@ -8442,6 +9237,7 @@ function isTcpPortListening(port, host = "127.0.0.1", timeoutMs = 1500) {
|
|
|
8442
9237
|
RegressionDetector,
|
|
8443
9238
|
ReportGenerator,
|
|
8444
9239
|
ServerStartError,
|
|
9240
|
+
SseTransport,
|
|
8445
9241
|
StreamableHttpTransport,
|
|
8446
9242
|
TestClientError,
|
|
8447
9243
|
TestServer,
|