@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/esm/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
1
4
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
5
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
6
|
}) : x)(function(x) {
|
|
@@ -341,8 +344,479 @@ var interceptors = {
|
|
|
341
344
|
}
|
|
342
345
|
};
|
|
343
346
|
|
|
344
|
-
// libs/testing/src/transport/
|
|
347
|
+
// libs/testing/src/transport/error-utils.ts
|
|
348
|
+
function errorName(err) {
|
|
349
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
350
|
+
const name = err.name;
|
|
351
|
+
return typeof name === "string" ? name : void 0;
|
|
352
|
+
}
|
|
353
|
+
function isAbortError(err) {
|
|
354
|
+
return errorName(err) === "AbortError";
|
|
355
|
+
}
|
|
356
|
+
function errorMessage(err, fallback = "Unknown error") {
|
|
357
|
+
if (typeof err === "string" && err) return err;
|
|
358
|
+
if (typeof err === "object" && err !== null) {
|
|
359
|
+
const message = err.message;
|
|
360
|
+
if (typeof message === "string" && message) return message;
|
|
361
|
+
}
|
|
362
|
+
return fallback;
|
|
363
|
+
}
|
|
364
|
+
var interceptedErrors = /* @__PURE__ */ new WeakSet();
|
|
365
|
+
function markInterceptedError(err) {
|
|
366
|
+
interceptedErrors.add(err);
|
|
367
|
+
return err;
|
|
368
|
+
}
|
|
369
|
+
function isInterceptedError(err) {
|
|
370
|
+
return typeof err === "object" && err !== null && interceptedErrors.has(err);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// libs/testing/src/transport/sse.transport.ts
|
|
345
374
|
var DEFAULT_TIMEOUT = 3e4;
|
|
375
|
+
var SseTransport = class {
|
|
376
|
+
baseUrl;
|
|
377
|
+
entryPath;
|
|
378
|
+
timeout;
|
|
379
|
+
authHeaders;
|
|
380
|
+
publicMode;
|
|
381
|
+
debug;
|
|
382
|
+
clientInfo;
|
|
383
|
+
notificationHandler;
|
|
384
|
+
interceptors;
|
|
385
|
+
elicitationHandler;
|
|
386
|
+
state = "disconnected";
|
|
387
|
+
authToken;
|
|
388
|
+
sessionId;
|
|
389
|
+
messageEndpoint;
|
|
390
|
+
stream;
|
|
391
|
+
pending = /* @__PURE__ */ new Map();
|
|
392
|
+
connectionCount = 0;
|
|
393
|
+
reconnectCount = 0;
|
|
394
|
+
lastRequestHeaders = {};
|
|
395
|
+
constructor(config) {
|
|
396
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
397
|
+
this.entryPath = config.entryPath;
|
|
398
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
399
|
+
this.authHeaders = config.auth?.headers ?? {};
|
|
400
|
+
this.authToken = config.auth?.token;
|
|
401
|
+
this.publicMode = config.publicMode ?? false;
|
|
402
|
+
this.debug = config.debug ?? false;
|
|
403
|
+
this.clientInfo = config.clientInfo;
|
|
404
|
+
this.notificationHandler = config.notificationHandler;
|
|
405
|
+
this.interceptors = config.interceptors;
|
|
406
|
+
this.elicitationHandler = config.elicitationHandler;
|
|
407
|
+
}
|
|
408
|
+
async connect() {
|
|
409
|
+
this.state = "connecting";
|
|
410
|
+
this.connectionCount++;
|
|
411
|
+
try {
|
|
412
|
+
if (!this.publicMode && !this.authToken) {
|
|
413
|
+
await this.requestAnonymousToken();
|
|
414
|
+
}
|
|
415
|
+
await this.openStream();
|
|
416
|
+
this.state = "connected";
|
|
417
|
+
this.log(`Connected; messages go to ${this.messageEndpoint}`);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
this.state = "error";
|
|
420
|
+
this.stream?.abort();
|
|
421
|
+
this.stream = void 0;
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async request(message) {
|
|
426
|
+
this.ensureConnected();
|
|
427
|
+
const startTime = Date.now();
|
|
428
|
+
if (this.interceptors) {
|
|
429
|
+
const intercepted = await this.interceptors.processRequest(message, {
|
|
430
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
431
|
+
transport: "sse",
|
|
432
|
+
sessionId: this.sessionId
|
|
433
|
+
});
|
|
434
|
+
switch (intercepted.type) {
|
|
435
|
+
case "mock":
|
|
436
|
+
return await this.interceptors.processResponse(
|
|
437
|
+
message,
|
|
438
|
+
intercepted.response,
|
|
439
|
+
Date.now() - startTime
|
|
440
|
+
);
|
|
441
|
+
case "error":
|
|
442
|
+
throw markInterceptedError(intercepted.error);
|
|
443
|
+
case "continue":
|
|
444
|
+
message = intercepted.request;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
let response;
|
|
449
|
+
if (message.id === void 0) {
|
|
450
|
+
await this.post(JSON.stringify(message));
|
|
451
|
+
response = { jsonrpc: "2.0", id: null, result: void 0 };
|
|
452
|
+
} else {
|
|
453
|
+
response = await this.postAndAwait(message.id, JSON.stringify(message));
|
|
454
|
+
}
|
|
455
|
+
if (this.interceptors) {
|
|
456
|
+
response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
|
|
457
|
+
}
|
|
458
|
+
return response;
|
|
459
|
+
}
|
|
460
|
+
async notify(message) {
|
|
461
|
+
this.ensureConnected();
|
|
462
|
+
const result = await this.post(JSON.stringify(message));
|
|
463
|
+
if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
|
|
464
|
+
}
|
|
465
|
+
async sendRaw(data) {
|
|
466
|
+
this.ensureConnected();
|
|
467
|
+
let id;
|
|
468
|
+
try {
|
|
469
|
+
const parsed = JSON.parse(data);
|
|
470
|
+
if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
if (id !== void 0) return this.postAndAwait(id, data);
|
|
474
|
+
const result = await this.post(data);
|
|
475
|
+
if (!result.ok) {
|
|
476
|
+
return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
|
|
477
|
+
}
|
|
478
|
+
return { jsonrpc: "2.0", id: null, result: void 0 };
|
|
479
|
+
}
|
|
480
|
+
async close() {
|
|
481
|
+
this.closeStream("Transport closed");
|
|
482
|
+
this.state = "disconnected";
|
|
483
|
+
this.sessionId = void 0;
|
|
484
|
+
this.messageEndpoint = void 0;
|
|
485
|
+
this.log("SSE transport closed");
|
|
486
|
+
}
|
|
487
|
+
isConnected() {
|
|
488
|
+
return this.state === "connected";
|
|
489
|
+
}
|
|
490
|
+
getState() {
|
|
491
|
+
return this.state;
|
|
492
|
+
}
|
|
493
|
+
getSessionId() {
|
|
494
|
+
return this.sessionId;
|
|
495
|
+
}
|
|
496
|
+
setAuthToken(token) {
|
|
497
|
+
this.authToken = token;
|
|
498
|
+
}
|
|
499
|
+
setTimeout(ms) {
|
|
500
|
+
this.timeout = ms;
|
|
501
|
+
}
|
|
502
|
+
getMessageEndpoint() {
|
|
503
|
+
return this.messageEndpoint;
|
|
504
|
+
}
|
|
505
|
+
getConnectionCount() {
|
|
506
|
+
return this.connectionCount;
|
|
507
|
+
}
|
|
508
|
+
getReconnectCount() {
|
|
509
|
+
return this.reconnectCount;
|
|
510
|
+
}
|
|
511
|
+
getLastRequestHeaders() {
|
|
512
|
+
return { ...this.lastRequestHeaders };
|
|
513
|
+
}
|
|
514
|
+
setInterceptors(interceptors2) {
|
|
515
|
+
this.interceptors = interceptors2;
|
|
516
|
+
}
|
|
517
|
+
getInterceptors() {
|
|
518
|
+
return this.interceptors;
|
|
519
|
+
}
|
|
520
|
+
setElicitationHandler(handler) {
|
|
521
|
+
this.elicitationHandler = handler;
|
|
522
|
+
}
|
|
523
|
+
async simulateDisconnect() {
|
|
524
|
+
this.closeStream("Simulated disconnect");
|
|
525
|
+
this.state = "disconnected";
|
|
526
|
+
this.sessionId = void 0;
|
|
527
|
+
this.messageEndpoint = void 0;
|
|
528
|
+
}
|
|
529
|
+
async waitForReconnect(timeoutMs) {
|
|
530
|
+
this.reconnectCount++;
|
|
531
|
+
let timer;
|
|
532
|
+
try {
|
|
533
|
+
await Promise.race([
|
|
534
|
+
this.connect(),
|
|
535
|
+
new Promise((_, reject) => {
|
|
536
|
+
timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
|
|
537
|
+
})
|
|
538
|
+
]);
|
|
539
|
+
} finally {
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
544
|
+
// STREAM
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
546
|
+
/** Open `GET <entry>/sse` and wait for the `endpoint` event. */
|
|
547
|
+
async openStream() {
|
|
548
|
+
const controller = new AbortController();
|
|
549
|
+
this.stream = controller;
|
|
550
|
+
const url = this.sseUrl();
|
|
551
|
+
const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
|
|
552
|
+
this.log(`GET ${url}`);
|
|
553
|
+
const connectTimer = setTimeout(() => controller.abort(), this.timeout);
|
|
554
|
+
let response;
|
|
555
|
+
try {
|
|
556
|
+
response = await fetch(url, {
|
|
557
|
+
method: "GET",
|
|
558
|
+
headers: { ...headers, Accept: "text/event-stream" },
|
|
559
|
+
signal: controller.signal
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
clearTimeout(connectTimer);
|
|
563
|
+
throw new Error(
|
|
564
|
+
isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
|
|
565
|
+
{ cause: error }
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
569
|
+
if (!response.ok || !response.body || !isEventStream) {
|
|
570
|
+
clearTimeout(connectTimer);
|
|
571
|
+
const body = await response.text().catch(() => "");
|
|
572
|
+
throw new Error(
|
|
573
|
+
`SSE connection to ${url} failed: HTTP ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}. The legacy HTTP+SSE transport needs a server that serves it (@FrontMcp({ transport: { protocol: { legacy: true } } })).`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const endpoint = new Promise((resolve, reject) => {
|
|
577
|
+
const reader = response.body?.getReader();
|
|
578
|
+
if (!reader) {
|
|
579
|
+
reject(new Error(`SSE connection to ${url} has no body`));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
void this.readStream(controller, reader, resolve, reject);
|
|
583
|
+
});
|
|
584
|
+
try {
|
|
585
|
+
const data = await endpoint;
|
|
586
|
+
const messageUrl = new URL(data, url);
|
|
587
|
+
this.messageEndpoint = messageUrl.toString();
|
|
588
|
+
this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
|
|
589
|
+
} finally {
|
|
590
|
+
clearTimeout(connectTimer);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async readStream(controller, reader, onEndpoint, onEndpointError) {
|
|
594
|
+
const decoder = new TextDecoder();
|
|
595
|
+
let buffer = "";
|
|
596
|
+
let endpointSeen = false;
|
|
597
|
+
try {
|
|
598
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
599
|
+
const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
600
|
+
buffer = remainder;
|
|
601
|
+
for (const text of completeEvents) {
|
|
602
|
+
const event = parseSSEEvent(text);
|
|
603
|
+
if (!event) continue;
|
|
604
|
+
if (event.type === "endpoint") {
|
|
605
|
+
if (!endpointSeen) {
|
|
606
|
+
endpointSeen = true;
|
|
607
|
+
onEndpoint(event.data);
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
await this.handleMessage(event.data);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (!controller.signal.aborted) this.log("SSE stream failed:", error);
|
|
616
|
+
} finally {
|
|
617
|
+
reader.releaseLock();
|
|
618
|
+
}
|
|
619
|
+
if (!endpointSeen) {
|
|
620
|
+
onEndpointError(
|
|
621
|
+
new Error(
|
|
622
|
+
controller.signal.aborted ? `SSE stream at ${this.sseUrl()} sent no endpoint event within ${this.timeout}ms` : `SSE stream at ${this.sseUrl()} ended before the server sent its endpoint`
|
|
623
|
+
)
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (this.stream === controller) {
|
|
627
|
+
this.stream = void 0;
|
|
628
|
+
this.rejectPending("SSE stream closed by the server");
|
|
629
|
+
if (this.state === "connected") this.state = "disconnected";
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async handleMessage(data) {
|
|
633
|
+
let message;
|
|
634
|
+
try {
|
|
635
|
+
message = JSON.parse(data);
|
|
636
|
+
} catch {
|
|
637
|
+
this.log("Ignoring SSE event that is not JSON:", data);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (!message || typeof message !== "object") return;
|
|
641
|
+
if (typeof message.method === "string") {
|
|
642
|
+
const request = message;
|
|
643
|
+
if (request.id === void 0 || request.id === null) {
|
|
644
|
+
this.notificationHandler?.(request);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
await this.answerServerRequest(request);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (message.id !== void 0 && message.id !== null) {
|
|
651
|
+
const pending = this.pending.get(message.id);
|
|
652
|
+
if (pending) {
|
|
653
|
+
clearTimeout(pending.timer);
|
|
654
|
+
this.pending.delete(message.id);
|
|
655
|
+
pending.resolve(message);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
|
|
660
|
+
async answerServerRequest(request) {
|
|
661
|
+
const id = request.id;
|
|
662
|
+
let reply;
|
|
663
|
+
if (request.method === "elicitation/create") {
|
|
664
|
+
reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
|
|
665
|
+
} else if (request.method === "ping") {
|
|
666
|
+
reply = { jsonrpc: "2.0", id, result: {} };
|
|
667
|
+
} else {
|
|
668
|
+
reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
|
|
669
|
+
}
|
|
670
|
+
const result = await this.post(JSON.stringify(reply));
|
|
671
|
+
if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
|
|
672
|
+
}
|
|
673
|
+
async elicit(params) {
|
|
674
|
+
if (!this.elicitationHandler) return { action: "decline" };
|
|
675
|
+
try {
|
|
676
|
+
return await this.elicitationHandler(params);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
this.log("Elicitation handler error:", error);
|
|
679
|
+
return { action: "cancel" };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
closeStream(reason) {
|
|
683
|
+
const controller = this.stream;
|
|
684
|
+
this.stream = void 0;
|
|
685
|
+
controller?.abort();
|
|
686
|
+
this.rejectPending(reason);
|
|
687
|
+
}
|
|
688
|
+
rejectPending(reason) {
|
|
689
|
+
for (const [id, pending] of this.pending) {
|
|
690
|
+
clearTimeout(pending.timer);
|
|
691
|
+
pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
|
|
692
|
+
}
|
|
693
|
+
this.pending.clear();
|
|
694
|
+
}
|
|
695
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
696
|
+
// POSTING
|
|
697
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
698
|
+
/** POST a message and resolve with the response the stream carries for `id`. */
|
|
699
|
+
async postAndAwait(id, body) {
|
|
700
|
+
const answer = new Promise((resolve) => {
|
|
701
|
+
const timer = setTimeout(() => {
|
|
702
|
+
this.pending.delete(id);
|
|
703
|
+
resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
|
|
704
|
+
}, this.timeout);
|
|
705
|
+
this.pending.set(id, { resolve, timer });
|
|
706
|
+
});
|
|
707
|
+
const result = await this.post(body);
|
|
708
|
+
if (!result.ok) {
|
|
709
|
+
const pending = this.pending.get(id);
|
|
710
|
+
if (pending) {
|
|
711
|
+
clearTimeout(pending.timer);
|
|
712
|
+
this.pending.delete(id);
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
jsonrpc: "2.0",
|
|
716
|
+
id,
|
|
717
|
+
error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return answer;
|
|
721
|
+
}
|
|
722
|
+
async post(body) {
|
|
723
|
+
const endpoint = this.messageEndpoint;
|
|
724
|
+
if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
|
|
725
|
+
const headers = this.buildHeaders();
|
|
726
|
+
this.lastRequestHeaders = headers;
|
|
727
|
+
this.log(`POST ${endpoint}`, body);
|
|
728
|
+
const controller = new AbortController();
|
|
729
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
730
|
+
try {
|
|
731
|
+
const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
|
|
732
|
+
const text = await response.text();
|
|
733
|
+
return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (isAbortError(error)) {
|
|
736
|
+
return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
|
|
737
|
+
}
|
|
738
|
+
throw error;
|
|
739
|
+
} finally {
|
|
740
|
+
clearTimeout(timer);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
744
|
+
// HELPERS
|
|
745
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
746
|
+
async requestAnonymousToken() {
|
|
747
|
+
const url = new URL(this.baseUrl);
|
|
748
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
|
|
749
|
+
url.search = "";
|
|
750
|
+
const tokenUrl = url.toString();
|
|
751
|
+
try {
|
|
752
|
+
const response = await fetch(tokenUrl, {
|
|
753
|
+
method: "POST",
|
|
754
|
+
headers: { "Content-Type": "application/json" },
|
|
755
|
+
body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
|
|
756
|
+
});
|
|
757
|
+
if (!response.ok) {
|
|
758
|
+
this.log(`No anonymous token (HTTP ${response.status})`);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const token = await response.json();
|
|
762
|
+
if (token.access_token) this.authToken = token.access_token;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
this.log("Error requesting anonymous token:", error);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
|
|
768
|
+
sseUrl() {
|
|
769
|
+
const url = new URL(this.baseUrl);
|
|
770
|
+
const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
|
|
771
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
772
|
+
url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
|
|
773
|
+
return url.toString();
|
|
774
|
+
}
|
|
775
|
+
buildHeaders() {
|
|
776
|
+
const headers = {
|
|
777
|
+
"Content-Type": "application/json",
|
|
778
|
+
Accept: "application/json, text/event-stream"
|
|
779
|
+
};
|
|
780
|
+
if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
|
|
781
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
782
|
+
if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
|
|
783
|
+
Object.assign(headers, this.authHeaders);
|
|
784
|
+
return headers;
|
|
785
|
+
}
|
|
786
|
+
ensureConnected() {
|
|
787
|
+
if (this.state !== "connected") {
|
|
788
|
+
throw new Error("Transport not connected. Call connect() first.");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
log(message, data) {
|
|
792
|
+
if (this.debug) {
|
|
793
|
+
console.log(`[SSE] ${message}`, data ?? "");
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
function splitSSEEvents(buffer) {
|
|
798
|
+
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
799
|
+
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
800
|
+
const completeEvents = normalized.split("\n\n");
|
|
801
|
+
const remainder = completeEvents.pop() ?? "";
|
|
802
|
+
return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
|
|
803
|
+
}
|
|
804
|
+
function parseSSEEvent(text) {
|
|
805
|
+
let type = "message";
|
|
806
|
+
const data = [];
|
|
807
|
+
for (const line of text.split("\n")) {
|
|
808
|
+
if (line.startsWith(":")) continue;
|
|
809
|
+
const colon = line.indexOf(":");
|
|
810
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
811
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
812
|
+
if (field === "event") type = value;
|
|
813
|
+
else if (field === "data") data.push(value);
|
|
814
|
+
}
|
|
815
|
+
return data.length > 0 ? { type, data: data.join("\n") } : void 0;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// libs/testing/src/transport/streamable-http.transport.ts
|
|
819
|
+
var DEFAULT_TIMEOUT2 = 3e4;
|
|
346
820
|
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
347
821
|
var StreamableHttpTransport = class {
|
|
348
822
|
config;
|
|
@@ -364,7 +838,7 @@ var StreamableHttpTransport = class {
|
|
|
364
838
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
365
839
|
// Remove trailing slash
|
|
366
840
|
entryPath: config.entryPath,
|
|
367
|
-
timeout: config.timeout ??
|
|
841
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT2,
|
|
368
842
|
auth: config.auth ?? {},
|
|
369
843
|
publicMode: config.publicMode ?? false,
|
|
370
844
|
debug: config.debug ?? false,
|
|
@@ -449,7 +923,7 @@ var StreamableHttpTransport = class {
|
|
|
449
923
|
return mockResponse2;
|
|
450
924
|
}
|
|
451
925
|
case "error":
|
|
452
|
-
throw interceptResult.error;
|
|
926
|
+
throw markInterceptedError(interceptResult.error);
|
|
453
927
|
case "continue":
|
|
454
928
|
message = interceptResult.request;
|
|
455
929
|
break;
|
|
@@ -510,7 +984,7 @@ var StreamableHttpTransport = class {
|
|
|
510
984
|
return jsonResponse;
|
|
511
985
|
} catch (error) {
|
|
512
986
|
clearTimeout(timeoutId);
|
|
513
|
-
if (error
|
|
987
|
+
if (isAbortError(error)) {
|
|
514
988
|
return {
|
|
515
989
|
jsonrpc: "2.0",
|
|
516
990
|
id: message.id ?? null,
|
|
@@ -549,7 +1023,7 @@ var StreamableHttpTransport = class {
|
|
|
549
1023
|
}
|
|
550
1024
|
} catch (error) {
|
|
551
1025
|
clearTimeout(timeoutId);
|
|
552
|
-
if (error
|
|
1026
|
+
if (!isAbortError(error)) {
|
|
553
1027
|
throw error;
|
|
554
1028
|
}
|
|
555
1029
|
}
|
|
@@ -590,7 +1064,7 @@ var StreamableHttpTransport = class {
|
|
|
590
1064
|
error: {
|
|
591
1065
|
code: -32700,
|
|
592
1066
|
message: "Parse error",
|
|
593
|
-
data: error
|
|
1067
|
+
data: errorMessage(error)
|
|
594
1068
|
}
|
|
595
1069
|
};
|
|
596
1070
|
}
|
|
@@ -704,7 +1178,7 @@ var StreamableHttpTransport = class {
|
|
|
704
1178
|
let buffer = "";
|
|
705
1179
|
try {
|
|
706
1180
|
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
707
|
-
const { completeEvents, remainder } =
|
|
1181
|
+
const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
708
1182
|
buffer = remainder;
|
|
709
1183
|
for (const eventText of completeEvents) {
|
|
710
1184
|
for (const event of this.parseSSEEvents(eventText, void 0).events) {
|
|
@@ -960,7 +1434,7 @@ var StreamableHttpTransport = class {
|
|
|
960
1434
|
if (this.config.clientInfo) {
|
|
961
1435
|
headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
|
|
962
1436
|
}
|
|
963
|
-
if (this.authToken
|
|
1437
|
+
if (this.authToken) {
|
|
964
1438
|
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
965
1439
|
}
|
|
966
1440
|
if (this.sessionId) {
|
|
@@ -1038,7 +1512,7 @@ var StreamableHttpTransport = class {
|
|
|
1038
1512
|
};
|
|
1039
1513
|
}
|
|
1040
1514
|
};
|
|
1041
|
-
function
|
|
1515
|
+
function splitSSEEvents2(buffer) {
|
|
1042
1516
|
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
1043
1517
|
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
1044
1518
|
const completeEvents = normalized.split("\n\n");
|
|
@@ -1295,7 +1769,7 @@ var McpTestClientBuilder = class {
|
|
|
1295
1769
|
};
|
|
1296
1770
|
|
|
1297
1771
|
// libs/testing/src/client/mcp-test-client.ts
|
|
1298
|
-
var
|
|
1772
|
+
var DEFAULT_TIMEOUT3 = 3e4;
|
|
1299
1773
|
var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
1300
1774
|
var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
|
|
1301
1775
|
var DEFAULT_CLIENT_INFO = {
|
|
@@ -1303,6 +1777,15 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
1303
1777
|
version: "0.4.0"
|
|
1304
1778
|
};
|
|
1305
1779
|
var MAX_LIST_PAGES = 1e3;
|
|
1780
|
+
function isAuthFailure(error) {
|
|
1781
|
+
return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
|
|
1782
|
+
}
|
|
1783
|
+
function describeInitializeError(error) {
|
|
1784
|
+
const message = error?.message ?? "Unknown error";
|
|
1785
|
+
const data = error?.data;
|
|
1786
|
+
const detail = typeof data === "string" ? data.trim() : "";
|
|
1787
|
+
return detail ? `${message} - ${detail.slice(0, 500)}` : message;
|
|
1788
|
+
}
|
|
1306
1789
|
function extractEntryPaths(data) {
|
|
1307
1790
|
let payload = data;
|
|
1308
1791
|
if (typeof payload === "string") {
|
|
@@ -1356,7 +1839,7 @@ var McpTestClient = class {
|
|
|
1356
1839
|
transport: config.transport ?? "streamable-http",
|
|
1357
1840
|
auth: config.auth ?? {},
|
|
1358
1841
|
publicMode: config.publicMode ?? false,
|
|
1359
|
-
timeout: config.timeout ??
|
|
1842
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT3,
|
|
1360
1843
|
debug: config.debug ?? false,
|
|
1361
1844
|
protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
|
|
1362
1845
|
clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
|
|
@@ -1389,19 +1872,22 @@ var McpTestClient = class {
|
|
|
1389
1872
|
async connect() {
|
|
1390
1873
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
1391
1874
|
this.triedDiscoveredEntryPath = false;
|
|
1875
|
+
this.initResult = null;
|
|
1392
1876
|
this.transport = this.createTransport();
|
|
1393
1877
|
await this.transport.connect();
|
|
1394
1878
|
const maxRetries = 3;
|
|
1395
1879
|
const retryDelayMs = 500;
|
|
1396
1880
|
let lastError;
|
|
1397
1881
|
let reportedEntryPaths;
|
|
1882
|
+
let attemptsMade = 0;
|
|
1398
1883
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1884
|
+
attemptsMade = attempt;
|
|
1399
1885
|
const initResponse = await this.initialize();
|
|
1400
1886
|
if (initResponse.success && initResponse.data) {
|
|
1401
1887
|
this.initResult = initResponse.data;
|
|
1402
1888
|
break;
|
|
1403
1889
|
}
|
|
1404
|
-
lastError = initResponse.error
|
|
1890
|
+
lastError = describeInitializeError(initResponse.error);
|
|
1405
1891
|
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
1406
1892
|
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
1407
1893
|
this.triedDiscoveredEntryPath = true;
|
|
@@ -1413,6 +1899,7 @@ var McpTestClient = class {
|
|
|
1413
1899
|
await this.transport.connect();
|
|
1414
1900
|
continue;
|
|
1415
1901
|
}
|
|
1902
|
+
if (isAuthFailure(initResponse.error)) break;
|
|
1416
1903
|
if (attempt < maxRetries) {
|
|
1417
1904
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
1418
1905
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
@@ -1421,7 +1908,7 @@ var McpTestClient = class {
|
|
|
1421
1908
|
if (!this.initResult) {
|
|
1422
1909
|
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.` : "";
|
|
1423
1910
|
throw new Error(
|
|
1424
|
-
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${
|
|
1911
|
+
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
|
|
1425
1912
|
);
|
|
1426
1913
|
}
|
|
1427
1914
|
this._sessionId = this.transport.getSessionId();
|
|
@@ -1491,18 +1978,47 @@ var McpTestClient = class {
|
|
|
1491
1978
|
return this._authState;
|
|
1492
1979
|
}
|
|
1493
1980
|
/**
|
|
1494
|
-
* Authenticate with a token
|
|
1981
|
+
* Authenticate with a token.
|
|
1982
|
+
*
|
|
1983
|
+
* When connected, this opens a NEW session with the token (the previous session was created
|
|
1984
|
+
* for the previous identity) and rejects if the server refuses it — for example because the
|
|
1985
|
+
* token is expired or badly signed. On rejection the client keeps its previous identity and
|
|
1986
|
+
* session. The token is kept in the client config, so `reconnect()` keeps using it.
|
|
1495
1987
|
*/
|
|
1496
1988
|
async authenticate(token) {
|
|
1989
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
1990
|
+
throw new Error("authenticate() requires a non-empty token");
|
|
1991
|
+
}
|
|
1992
|
+
const previousAuthState = this._authState;
|
|
1993
|
+
const previousConfigAuth = this.config.auth;
|
|
1994
|
+
this.config.auth = { ...previousConfigAuth, token };
|
|
1497
1995
|
this._authState = {
|
|
1498
1996
|
isAnonymous: false,
|
|
1499
1997
|
token,
|
|
1500
1998
|
scopes: this.parseScopesFromToken(token),
|
|
1501
1999
|
user: this.parseUserFromToken(token)
|
|
1502
2000
|
};
|
|
1503
|
-
|
|
1504
|
-
|
|
2001
|
+
const previousTransport = this.transport;
|
|
2002
|
+
if (!previousTransport) {
|
|
2003
|
+
this.log("debug", "Authentication updated (not connected yet)");
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
const previousInit = this.initResult;
|
|
2007
|
+
const previousSessionId = this._sessionId;
|
|
2008
|
+
const previousSessionInfo = this._sessionInfo;
|
|
2009
|
+
try {
|
|
2010
|
+
await this.connect();
|
|
2011
|
+
} catch (err) {
|
|
2012
|
+
await this.transport?.close().catch(() => void 0);
|
|
2013
|
+
this.transport = previousTransport;
|
|
2014
|
+
this.initResult = previousInit;
|
|
2015
|
+
this._sessionId = previousSessionId;
|
|
2016
|
+
this._sessionInfo = previousSessionInfo;
|
|
2017
|
+
this.config.auth = previousConfigAuth;
|
|
2018
|
+
this._authState = previousAuthState;
|
|
2019
|
+
throw err;
|
|
1505
2020
|
}
|
|
2021
|
+
await previousTransport.close().catch(() => void 0);
|
|
1506
2022
|
this.log("debug", "Authentication updated");
|
|
1507
2023
|
}
|
|
1508
2024
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -2089,7 +2605,18 @@ var McpTestClient = class {
|
|
|
2089
2605
|
notificationHandler: this.recordNotification
|
|
2090
2606
|
});
|
|
2091
2607
|
case "sse":
|
|
2092
|
-
|
|
2608
|
+
return new SseTransport({
|
|
2609
|
+
baseUrl,
|
|
2610
|
+
entryPath: this.config.entryPath,
|
|
2611
|
+
timeout: this.config.timeout,
|
|
2612
|
+
auth: this.config.auth,
|
|
2613
|
+
publicMode: this.config.publicMode,
|
|
2614
|
+
debug: this.config.debug,
|
|
2615
|
+
interceptors: this._interceptors,
|
|
2616
|
+
clientInfo: this.config.clientInfo,
|
|
2617
|
+
elicitationHandler: this._elicitationHandler,
|
|
2618
|
+
notificationHandler: this.recordNotification
|
|
2619
|
+
});
|
|
2093
2620
|
default:
|
|
2094
2621
|
throw new Error(`Unknown transport type: ${this.config.transport}`);
|
|
2095
2622
|
}
|
|
@@ -2126,10 +2653,11 @@ var McpTestClient = class {
|
|
|
2126
2653
|
requestId: id
|
|
2127
2654
|
};
|
|
2128
2655
|
} catch (err) {
|
|
2656
|
+
if (isInterceptedError(err)) throw err;
|
|
2129
2657
|
const durationMs = Date.now() - start;
|
|
2130
2658
|
const error = {
|
|
2131
2659
|
code: -32603,
|
|
2132
|
-
message: err
|
|
2660
|
+
message: errorMessage(err)
|
|
2133
2661
|
};
|
|
2134
2662
|
return {
|
|
2135
2663
|
success: false,
|
|
@@ -2277,12 +2805,12 @@ var McpTestClient = class {
|
|
|
2277
2805
|
const payload = this.decodeJwtPayload(token);
|
|
2278
2806
|
if (!payload) return [];
|
|
2279
2807
|
const scope = payload["scope"];
|
|
2280
|
-
const
|
|
2808
|
+
const scopes2 = payload["scopes"];
|
|
2281
2809
|
if (typeof scope === "string") {
|
|
2282
2810
|
return scope.split(" ");
|
|
2283
2811
|
}
|
|
2284
|
-
if (Array.isArray(
|
|
2285
|
-
return
|
|
2812
|
+
if (Array.isArray(scopes2)) {
|
|
2813
|
+
return scopes2;
|
|
2286
2814
|
}
|
|
2287
2815
|
return [];
|
|
2288
2816
|
} catch {
|
|
@@ -2414,14 +2942,15 @@ var TestTokenFactory = class {
|
|
|
2414
2942
|
*/
|
|
2415
2943
|
async createTestToken(options) {
|
|
2416
2944
|
await this.ensureKeys();
|
|
2417
|
-
const
|
|
2945
|
+
const nowMs = Date.now();
|
|
2946
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
2418
2947
|
const exp = options.exp ?? 3600;
|
|
2419
2948
|
const payload = {
|
|
2420
2949
|
iss: options.iss ?? this.issuer,
|
|
2421
2950
|
sub: options.sub,
|
|
2422
2951
|
aud: options.aud ?? this.audience,
|
|
2423
|
-
iat
|
|
2424
|
-
exp:
|
|
2952
|
+
iat,
|
|
2953
|
+
exp: Math.ceil(nowMs / 1e3) + exp,
|
|
2425
2954
|
scope: options.scopes?.join(" "),
|
|
2426
2955
|
...options.claims
|
|
2427
2956
|
};
|
|
@@ -2446,10 +2975,10 @@ var TestTokenFactory = class {
|
|
|
2446
2975
|
/**
|
|
2447
2976
|
* Create a regular user token
|
|
2448
2977
|
*/
|
|
2449
|
-
async createUserToken(sub = "user-001",
|
|
2978
|
+
async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
|
|
2450
2979
|
return this.createTestToken({
|
|
2451
2980
|
sub,
|
|
2452
|
-
scopes,
|
|
2981
|
+
scopes: scopes2,
|
|
2453
2982
|
claims: {
|
|
2454
2983
|
email: "user@test.local",
|
|
2455
2984
|
name: "Test User",
|
|
@@ -2460,10 +2989,11 @@ var TestTokenFactory = class {
|
|
|
2460
2989
|
/**
|
|
2461
2990
|
* Create an anonymous user token
|
|
2462
2991
|
*/
|
|
2463
|
-
async createAnonymousToken() {
|
|
2992
|
+
async createAnonymousToken(expiresIn) {
|
|
2464
2993
|
return this.createTestToken({
|
|
2465
2994
|
sub: `anon:${Date.now()}`,
|
|
2466
2995
|
scopes: ["anonymous"],
|
|
2996
|
+
exp: expiresIn,
|
|
2467
2997
|
claims: {
|
|
2468
2998
|
name: "Anonymous",
|
|
2469
2999
|
role: "anonymous"
|
|
@@ -2674,17 +3204,7 @@ function createTestUser(overrides) {
|
|
|
2674
3204
|
|
|
2675
3205
|
// libs/testing/src/auth/mock-oauth-server.ts
|
|
2676
3206
|
import { createServer } from "http";
|
|
2677
|
-
|
|
2678
|
-
var _sha256Base64url;
|
|
2679
|
-
var _base64urlEncode;
|
|
2680
|
-
async function loadCryptoUtils() {
|
|
2681
|
-
if (!_randomBytes) {
|
|
2682
|
-
const utils = await import("@frontmcp/utils");
|
|
2683
|
-
_randomBytes = utils.randomBytes;
|
|
2684
|
-
_sha256Base64url = utils.sha256Base64url;
|
|
2685
|
-
_base64urlEncode = utils.base64urlEncode;
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
3207
|
+
import { base64urlEncode, randomBytes, sha256Base64url } from "@frontmcp/utils";
|
|
2688
3208
|
var MockOAuthServer = class {
|
|
2689
3209
|
tokenFactory;
|
|
2690
3210
|
options;
|
|
@@ -2699,8 +3219,8 @@ var MockOAuthServer = class {
|
|
|
2699
3219
|
accessTokenTtlSeconds;
|
|
2700
3220
|
/** Refresh token TTL in seconds */
|
|
2701
3221
|
refreshTokenTtlSeconds;
|
|
2702
|
-
constructor(
|
|
2703
|
-
this.tokenFactory =
|
|
3222
|
+
constructor(tokenFactory2, options = {}) {
|
|
3223
|
+
this.tokenFactory = tokenFactory2;
|
|
2704
3224
|
this.options = options;
|
|
2705
3225
|
this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
|
|
2706
3226
|
this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
|
|
@@ -2957,14 +3477,14 @@ ${error.stack}` : String(error);
|
|
|
2957
3477
|
return;
|
|
2958
3478
|
}
|
|
2959
3479
|
const code = this.generateCode();
|
|
2960
|
-
const
|
|
3480
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
2961
3481
|
this.authCodes.set(code, {
|
|
2962
3482
|
code,
|
|
2963
3483
|
clientId,
|
|
2964
3484
|
redirectUri,
|
|
2965
3485
|
codeChallenge,
|
|
2966
3486
|
codeChallengeMethod,
|
|
2967
|
-
scopes,
|
|
3487
|
+
scopes: scopes2,
|
|
2968
3488
|
user: testUser,
|
|
2969
3489
|
state,
|
|
2970
3490
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3004,7 +3524,7 @@ ${error.stack}` : String(error);
|
|
|
3004
3524
|
}
|
|
3005
3525
|
const grantType = params.get("grant_type");
|
|
3006
3526
|
if (grantType === "anonymous") {
|
|
3007
|
-
const token = await this.tokenFactory.createAnonymousToken();
|
|
3527
|
+
const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
|
|
3008
3528
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3009
3529
|
res.end(
|
|
3010
3530
|
JSON.stringify({
|
|
@@ -3136,6 +3656,8 @@ ${error.stack}` : String(error);
|
|
|
3136
3656
|
codeRecord.used = true;
|
|
3137
3657
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3138
3658
|
sub: codeRecord.user.sub,
|
|
3659
|
+
scopes: codeRecord.scopes,
|
|
3660
|
+
exp: this.accessTokenTtlSeconds,
|
|
3139
3661
|
claims: {
|
|
3140
3662
|
email: codeRecord.user.email,
|
|
3141
3663
|
name: codeRecord.user.name,
|
|
@@ -3144,6 +3666,7 @@ ${error.stack}` : String(error);
|
|
|
3144
3666
|
});
|
|
3145
3667
|
const idToken = await this.tokenFactory.createTestToken({
|
|
3146
3668
|
sub: codeRecord.user.sub,
|
|
3669
|
+
exp: this.accessTokenTtlSeconds,
|
|
3147
3670
|
claims: {
|
|
3148
3671
|
email: codeRecord.user.email,
|
|
3149
3672
|
name: codeRecord.user.name,
|
|
@@ -3221,6 +3744,8 @@ ${error.stack}` : String(error);
|
|
|
3221
3744
|
}
|
|
3222
3745
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3223
3746
|
sub: tokenRecord.user.sub,
|
|
3747
|
+
scopes: tokenRecord.scopes,
|
|
3748
|
+
exp: this.accessTokenTtlSeconds,
|
|
3224
3749
|
claims: {
|
|
3225
3750
|
email: tokenRecord.user.email,
|
|
3226
3751
|
name: tokenRecord.user.name,
|
|
@@ -3325,14 +3850,14 @@ ${error.stack}` : String(error);
|
|
|
3325
3850
|
name
|
|
3326
3851
|
};
|
|
3327
3852
|
const code = this.generateCode();
|
|
3328
|
-
const
|
|
3853
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3329
3854
|
this.authCodes.set(code, {
|
|
3330
3855
|
code,
|
|
3331
3856
|
clientId,
|
|
3332
3857
|
redirectUri,
|
|
3333
3858
|
codeChallenge,
|
|
3334
3859
|
codeChallengeMethod,
|
|
3335
|
-
scopes,
|
|
3860
|
+
scopes: scopes2,
|
|
3336
3861
|
user: testUser,
|
|
3337
3862
|
state,
|
|
3338
3863
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3438,8 +3963,7 @@ ${error.stack}` : String(error);
|
|
|
3438
3963
|
* Generate a random authorization code
|
|
3439
3964
|
*/
|
|
3440
3965
|
async generateCodeAsync() {
|
|
3441
|
-
|
|
3442
|
-
return _base64urlEncode(_randomBytes(32));
|
|
3966
|
+
return base64urlEncode(randomBytes(32));
|
|
3443
3967
|
}
|
|
3444
3968
|
/**
|
|
3445
3969
|
* Generate a random authorization code (sync wrapper for compatibility)
|
|
@@ -3455,8 +3979,7 @@ ${error.stack}` : String(error);
|
|
|
3455
3979
|
*/
|
|
3456
3980
|
async computeCodeChallengeAsync(verifier, method) {
|
|
3457
3981
|
if (method === "S256") {
|
|
3458
|
-
|
|
3459
|
-
return _sha256Base64url(verifier);
|
|
3982
|
+
return sha256Base64url(verifier);
|
|
3460
3983
|
}
|
|
3461
3984
|
return verifier;
|
|
3462
3985
|
}
|
|
@@ -4043,7 +4566,7 @@ var MockCimdServer = class {
|
|
|
4043
4566
|
};
|
|
4044
4567
|
|
|
4045
4568
|
// libs/testing/src/server/test-server.ts
|
|
4046
|
-
import { spawn } from "child_process";
|
|
4569
|
+
import { execFile, spawn } from "child_process";
|
|
4047
4570
|
import { sha256Hex } from "@frontmcp/utils";
|
|
4048
4571
|
|
|
4049
4572
|
// libs/testing/src/errors/index.ts
|
|
@@ -4150,6 +4673,8 @@ var E2E_PORT_RANGES = {
|
|
|
4150
4673
|
"demo-e2e-distributed": { start: 50440, size: 20 },
|
|
4151
4674
|
// Protocol revision E2E tests (50460-50479)
|
|
4152
4675
|
"demo-e2e-protocol-20260728": { start: 50460, size: 20 },
|
|
4676
|
+
// Testing library E2E tests (50480-50499)
|
|
4677
|
+
"demo-e2e-testing": { start: 50480, size: 20 },
|
|
4153
4678
|
// Mock servers and utilities (50900-50999)
|
|
4154
4679
|
"mock-oauth": { start: 50900, size: 10 },
|
|
4155
4680
|
"mock-api": { start: 50910, size: 10 },
|
|
@@ -4158,6 +4683,66 @@ var E2E_PORT_RANGES = {
|
|
|
4158
4683
|
default: { start: 51e3, size: 100 }
|
|
4159
4684
|
};
|
|
4160
4685
|
var reservedPorts = /* @__PURE__ */ new Map();
|
|
4686
|
+
var lockedPorts = /* @__PURE__ */ new Set();
|
|
4687
|
+
var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
4688
|
+
function lockDir() {
|
|
4689
|
+
try {
|
|
4690
|
+
const os = __require("node:os");
|
|
4691
|
+
const path = __require("node:path");
|
|
4692
|
+
return path.join(os.tmpdir(), "frontmcp-testing-ports");
|
|
4693
|
+
} catch {
|
|
4694
|
+
return void 0;
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
function pidIsAlive(pid) {
|
|
4698
|
+
try {
|
|
4699
|
+
process.kill(pid, 0);
|
|
4700
|
+
return true;
|
|
4701
|
+
} catch (error) {
|
|
4702
|
+
return error.code === "EPERM";
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
function acquirePortLock(port) {
|
|
4706
|
+
if (lockedPorts.has(port)) return false;
|
|
4707
|
+
let fs;
|
|
4708
|
+
let file;
|
|
4709
|
+
try {
|
|
4710
|
+
fs = __require("node:fs");
|
|
4711
|
+
const dir = lockDir();
|
|
4712
|
+
if (!dir) return true;
|
|
4713
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4714
|
+
file = __require("node:path").join(dir, `${port}.lock`);
|
|
4715
|
+
} catch {
|
|
4716
|
+
return true;
|
|
4717
|
+
}
|
|
4718
|
+
const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
|
|
4719
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4720
|
+
try {
|
|
4721
|
+
fs.writeFileSync(file, payload, { flag: "wx" });
|
|
4722
|
+
lockedPorts.add(port);
|
|
4723
|
+
return true;
|
|
4724
|
+
} catch (error) {
|
|
4725
|
+
if (error.code !== "EEXIST") return true;
|
|
4726
|
+
try {
|
|
4727
|
+
const held = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
4728
|
+
const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
|
|
4729
|
+
if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
|
|
4730
|
+
fs.unlinkSync(file);
|
|
4731
|
+
} catch {
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
return false;
|
|
4736
|
+
}
|
|
4737
|
+
function releasePortLock(port) {
|
|
4738
|
+
if (!lockedPorts.delete(port)) return;
|
|
4739
|
+
try {
|
|
4740
|
+
const fs = __require("node:fs");
|
|
4741
|
+
const dir = lockDir();
|
|
4742
|
+
if (dir) fs.unlinkSync(__require("node:path").join(dir, `${port}.lock`));
|
|
4743
|
+
} catch {
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4161
4746
|
var projectPortIndex = /* @__PURE__ */ new Map();
|
|
4162
4747
|
function getPortRange(project) {
|
|
4163
4748
|
const key = project;
|
|
@@ -4168,15 +4753,22 @@ function getPortRange(project) {
|
|
|
4168
4753
|
}
|
|
4169
4754
|
async function reservePort(project, preferredPort) {
|
|
4170
4755
|
const range = getPortRange(project);
|
|
4171
|
-
|
|
4756
|
+
const handle = (port) => ({
|
|
4757
|
+
port,
|
|
4758
|
+
// Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
|
|
4759
|
+
releaseSocket: async () => {
|
|
4760
|
+
await releasePort(port);
|
|
4761
|
+
},
|
|
4762
|
+
release: async () => {
|
|
4763
|
+
await releasePort(port);
|
|
4764
|
+
releasePortLock(port);
|
|
4765
|
+
}
|
|
4766
|
+
});
|
|
4767
|
+
const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
|
|
4768
|
+
if (wantsSpecificPort) {
|
|
4172
4769
|
const reservation = await tryReservePort(preferredPort, project);
|
|
4173
4770
|
if (reservation) {
|
|
4174
|
-
return
|
|
4175
|
-
port: preferredPort,
|
|
4176
|
-
release: async () => {
|
|
4177
|
-
await releasePort(preferredPort);
|
|
4178
|
-
}
|
|
4179
|
-
};
|
|
4771
|
+
return handle(preferredPort);
|
|
4180
4772
|
}
|
|
4181
4773
|
console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
|
|
4182
4774
|
}
|
|
@@ -4185,30 +4777,19 @@ async function reservePort(project, preferredPort) {
|
|
|
4185
4777
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4186
4778
|
const port = range.start + index % range.size;
|
|
4187
4779
|
index = (index + 1) % range.size;
|
|
4188
|
-
if (reservedPorts.has(port)) {
|
|
4780
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) {
|
|
4189
4781
|
continue;
|
|
4190
4782
|
}
|
|
4191
4783
|
const reservation = await tryReservePort(port, project);
|
|
4192
4784
|
if (reservation) {
|
|
4193
4785
|
projectPortIndex.set(project, index);
|
|
4194
|
-
return
|
|
4195
|
-
port,
|
|
4196
|
-
release: async () => {
|
|
4197
|
-
await releasePort(port);
|
|
4198
|
-
}
|
|
4199
|
-
};
|
|
4786
|
+
return handle(port);
|
|
4200
4787
|
}
|
|
4201
4788
|
}
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
return {
|
|
4207
|
-
port: dynamicPort,
|
|
4208
|
-
release: async () => {
|
|
4209
|
-
await releasePort(dynamicPort);
|
|
4210
|
-
}
|
|
4211
|
-
};
|
|
4789
|
+
for (let port = 51e3; port < 52e3; port++) {
|
|
4790
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
|
|
4791
|
+
if (await tryReservePort(port, project)) {
|
|
4792
|
+
return handle(port);
|
|
4212
4793
|
}
|
|
4213
4794
|
}
|
|
4214
4795
|
throw new Error(
|
|
@@ -4216,9 +4797,13 @@ async function reservePort(project, preferredPort) {
|
|
|
4216
4797
|
);
|
|
4217
4798
|
}
|
|
4218
4799
|
async function tryReservePort(port, project) {
|
|
4800
|
+
if (!acquirePortLock(port)) {
|
|
4801
|
+
return false;
|
|
4802
|
+
}
|
|
4219
4803
|
return new Promise((resolve) => {
|
|
4220
4804
|
const server = createServer4();
|
|
4221
4805
|
server.once("error", () => {
|
|
4806
|
+
releasePortLock(port);
|
|
4222
4807
|
resolve(false);
|
|
4223
4808
|
});
|
|
4224
4809
|
server.listen(port, () => {
|
|
@@ -4244,31 +4829,6 @@ async function releasePort(port) {
|
|
|
4244
4829
|
});
|
|
4245
4830
|
});
|
|
4246
4831
|
}
|
|
4247
|
-
async function findAvailablePortInRange(start, end) {
|
|
4248
|
-
for (let port = start; port < end; port++) {
|
|
4249
|
-
if (reservedPorts.has(port)) {
|
|
4250
|
-
continue;
|
|
4251
|
-
}
|
|
4252
|
-
const available = await isPortAvailable(port);
|
|
4253
|
-
if (available) {
|
|
4254
|
-
return port;
|
|
4255
|
-
}
|
|
4256
|
-
}
|
|
4257
|
-
return null;
|
|
4258
|
-
}
|
|
4259
|
-
async function isPortAvailable(port) {
|
|
4260
|
-
return new Promise((resolve) => {
|
|
4261
|
-
const server = createServer4();
|
|
4262
|
-
server.once("error", () => {
|
|
4263
|
-
resolve(false);
|
|
4264
|
-
});
|
|
4265
|
-
server.listen(port, () => {
|
|
4266
|
-
server.close(() => {
|
|
4267
|
-
resolve(true);
|
|
4268
|
-
});
|
|
4269
|
-
});
|
|
4270
|
-
});
|
|
4271
|
-
}
|
|
4272
4832
|
|
|
4273
4833
|
// libs/testing/src/server/test-server.ts
|
|
4274
4834
|
var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
|
|
@@ -4277,8 +4837,11 @@ var TestServer = class _TestServer {
|
|
|
4277
4837
|
options;
|
|
4278
4838
|
_info;
|
|
4279
4839
|
logs = [];
|
|
4840
|
+
/** Frees the port reservation (socket and cross-process lock); held until stop() */
|
|
4280
4841
|
portRelease = null;
|
|
4281
|
-
|
|
4842
|
+
/** Frees only the reservation socket, just before the server is spawned */
|
|
4843
|
+
portReleaseSocket = null;
|
|
4844
|
+
constructor(options, port, portRelease, portReleaseSocket) {
|
|
4282
4845
|
this.options = {
|
|
4283
4846
|
port,
|
|
4284
4847
|
project: options.project,
|
|
@@ -4290,6 +4853,7 @@ var TestServer = class _TestServer {
|
|
|
4290
4853
|
debug: options.debug ?? DEBUG_SERVER
|
|
4291
4854
|
};
|
|
4292
4855
|
this.portRelease = portRelease ?? null;
|
|
4856
|
+
this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
|
|
4293
4857
|
this._info = {
|
|
4294
4858
|
baseUrl: `http://localhost:${port}`,
|
|
4295
4859
|
port
|
|
@@ -4302,8 +4866,8 @@ var TestServer = class _TestServer {
|
|
|
4302
4866
|
const project = options.project ?? "default";
|
|
4303
4867
|
const maxAttempts = 3;
|
|
4304
4868
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4305
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4306
|
-
const server = new _TestServer(options, port, release);
|
|
4869
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4870
|
+
const server = new _TestServer(options, port, release, releaseSocket);
|
|
4307
4871
|
try {
|
|
4308
4872
|
await server.startProcess();
|
|
4309
4873
|
return server;
|
|
@@ -4343,7 +4907,7 @@ var TestServer = class _TestServer {
|
|
|
4343
4907
|
}
|
|
4344
4908
|
const maxAttempts = 3;
|
|
4345
4909
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4346
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4910
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4347
4911
|
const serverOptions = {
|
|
4348
4912
|
...options,
|
|
4349
4913
|
port,
|
|
@@ -4351,7 +4915,7 @@ var TestServer = class _TestServer {
|
|
|
4351
4915
|
command: `npx nx serve ${project} --port ${port}`,
|
|
4352
4916
|
cwd: options.cwd ?? process.cwd()
|
|
4353
4917
|
};
|
|
4354
|
-
const server = new _TestServer(serverOptions, port, release);
|
|
4918
|
+
const server = new _TestServer(serverOptions, port, release, releaseSocket);
|
|
4355
4919
|
try {
|
|
4356
4920
|
await server.startProcess();
|
|
4357
4921
|
return server;
|
|
@@ -4409,10 +4973,17 @@ var TestServer = class _TestServer {
|
|
|
4409
4973
|
* Stop the test server
|
|
4410
4974
|
*/
|
|
4411
4975
|
async stop() {
|
|
4412
|
-
|
|
4413
|
-
await this.
|
|
4414
|
-
|
|
4976
|
+
try {
|
|
4977
|
+
await this.stopProcess();
|
|
4978
|
+
} finally {
|
|
4979
|
+
if (this.portRelease) {
|
|
4980
|
+
await this.portRelease();
|
|
4981
|
+
this.portRelease = null;
|
|
4982
|
+
}
|
|
4983
|
+
this.portReleaseSocket = null;
|
|
4415
4984
|
}
|
|
4985
|
+
}
|
|
4986
|
+
async stopProcess() {
|
|
4416
4987
|
if (this.process) {
|
|
4417
4988
|
this.log("Stopping server...");
|
|
4418
4989
|
if (this.process.exitCode !== null || this.process.signalCode !== null) {
|
|
@@ -4485,7 +5056,7 @@ var TestServer = class _TestServer {
|
|
|
4485
5056
|
* Restart the server
|
|
4486
5057
|
*/
|
|
4487
5058
|
async restart() {
|
|
4488
|
-
await this.
|
|
5059
|
+
await this.stopProcess();
|
|
4489
5060
|
await this.startProcess();
|
|
4490
5061
|
}
|
|
4491
5062
|
/**
|
|
@@ -4515,9 +5086,9 @@ var TestServer = class _TestServer {
|
|
|
4515
5086
|
PORT: String(this.options.port)
|
|
4516
5087
|
};
|
|
4517
5088
|
const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
|
|
4518
|
-
if (this.
|
|
4519
|
-
await this.
|
|
4520
|
-
this.
|
|
5089
|
+
if (this.portReleaseSocket) {
|
|
5090
|
+
await this.portReleaseSocket();
|
|
5091
|
+
this.portReleaseSocket = null;
|
|
4521
5092
|
await sleep2(300);
|
|
4522
5093
|
}
|
|
4523
5094
|
this.process = spawn(this.options.command, [], {
|
|
@@ -4527,9 +5098,8 @@ var TestServer = class _TestServer {
|
|
|
4527
5098
|
detached: true,
|
|
4528
5099
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4529
5100
|
});
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
}
|
|
5101
|
+
this._info.pid = this.process.pid;
|
|
5102
|
+
this._info.shellPid = this.process.pid;
|
|
4533
5103
|
let processExited = false;
|
|
4534
5104
|
let exitCode = null;
|
|
4535
5105
|
let exitError = null;
|
|
@@ -4632,6 +5202,8 @@ ${allLogs}`);
|
|
|
4632
5202
|
});
|
|
4633
5203
|
if (response.ok || response.status === 404) {
|
|
4634
5204
|
this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
|
|
5205
|
+
const listeningPid = await findListeningPid(this.options.port);
|
|
5206
|
+
if (listeningPid !== void 0) this._info.pid = listeningPid;
|
|
4635
5207
|
return;
|
|
4636
5208
|
}
|
|
4637
5209
|
lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
|
|
@@ -4676,6 +5248,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
|
|
|
4676
5248
|
function sleep2(ms) {
|
|
4677
5249
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4678
5250
|
}
|
|
5251
|
+
function findListeningPid(port) {
|
|
5252
|
+
if (process.platform === "win32") return Promise.resolve(void 0);
|
|
5253
|
+
return new Promise((resolve) => {
|
|
5254
|
+
execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
|
|
5255
|
+
if (error) {
|
|
5256
|
+
resolve(void 0);
|
|
5257
|
+
return;
|
|
5258
|
+
}
|
|
5259
|
+
const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
|
|
5260
|
+
resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
|
|
5261
|
+
});
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
4679
5264
|
function withWorkspaceProtocolFallback(env, cwd) {
|
|
4680
5265
|
if (findInstalledProtocolPackageDir(cwd)) {
|
|
4681
5266
|
return env;
|
|
@@ -4947,16 +5532,6 @@ function hasMimeType(result, mimeType) {
|
|
|
4947
5532
|
return result.hasMimeType(mimeType);
|
|
4948
5533
|
}
|
|
4949
5534
|
|
|
4950
|
-
// libs/testing/src/fixtures/test-fixture.ts
|
|
4951
|
-
import {
|
|
4952
|
-
afterAll as _afterAll,
|
|
4953
|
-
afterEach as _afterEach,
|
|
4954
|
-
beforeAll as _beforeAll,
|
|
4955
|
-
beforeEach as _beforeEach,
|
|
4956
|
-
describe as _describe,
|
|
4957
|
-
it as _it
|
|
4958
|
-
} from "@jest/globals";
|
|
4959
|
-
|
|
4960
5535
|
// libs/testing/src/fixtures/gateway-token-binding.ts
|
|
4961
5536
|
import { trimSlashes, trimTrailing } from "@frontmcp/utils";
|
|
4962
5537
|
function gatewayTokenBinding(serverUrl, entryPath) {
|
|
@@ -4967,81 +5542,163 @@ function gatewayTokenBinding(serverUrl, entryPath) {
|
|
|
4967
5542
|
}
|
|
4968
5543
|
|
|
4969
5544
|
// libs/testing/src/fixtures/test-fixture.ts
|
|
4970
|
-
var
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5545
|
+
var jestApi;
|
|
5546
|
+
function jestGlobals() {
|
|
5547
|
+
if (jestApi) return jestApi;
|
|
5548
|
+
try {
|
|
5549
|
+
const api = __require("@jest/globals");
|
|
5550
|
+
jestApi = {
|
|
5551
|
+
describe: api["describe"],
|
|
5552
|
+
beforeAll: api["beforeAll"],
|
|
5553
|
+
beforeEach: api["beforeEach"],
|
|
5554
|
+
afterEach: api["afterEach"],
|
|
5555
|
+
afterAll: api["afterAll"],
|
|
5556
|
+
it: api["it"]
|
|
5557
|
+
};
|
|
5558
|
+
return jestApi;
|
|
5559
|
+
} catch (error) {
|
|
5560
|
+
throw new Error(
|
|
5561
|
+
`@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
5562
|
+
{ cause: error }
|
|
5563
|
+
);
|
|
5564
|
+
}
|
|
4983
5565
|
}
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
5566
|
+
function newScope(parent) {
|
|
5567
|
+
return {
|
|
5568
|
+
skipped: parent?.skipped ?? false,
|
|
5569
|
+
reason: parent?.reason,
|
|
5570
|
+
config: {},
|
|
5571
|
+
envs: [],
|
|
5572
|
+
configured: false,
|
|
5573
|
+
cleanupRegistered: false,
|
|
5574
|
+
beforeEachHooks: [],
|
|
5575
|
+
afterEachHooks: []
|
|
5576
|
+
};
|
|
5577
|
+
}
|
|
5578
|
+
var scopes = [newScope()];
|
|
5579
|
+
function currentScope() {
|
|
5580
|
+
return scopes[scopes.length - 1];
|
|
5581
|
+
}
|
|
5582
|
+
var serverEntries = /* @__PURE__ */ new Map();
|
|
5583
|
+
function resolveConfig(chain) {
|
|
5584
|
+
const merged = {};
|
|
5585
|
+
for (const scope of chain) {
|
|
5586
|
+
Object.assign(merged, scope.config);
|
|
5587
|
+
for (const env of scope.envs) merged.env = { ...merged.env, ...env };
|
|
5588
|
+
}
|
|
5589
|
+
return merged;
|
|
5590
|
+
}
|
|
5591
|
+
function ownerScopeOf(chain) {
|
|
5592
|
+
for (let i = chain.length - 1; i > 0; i--) {
|
|
5593
|
+
if (chain[i].configured) return chain[i];
|
|
5594
|
+
}
|
|
5595
|
+
return chain[0];
|
|
5596
|
+
}
|
|
5597
|
+
function serverKey(config) {
|
|
5598
|
+
const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
5599
|
+
return JSON.stringify({
|
|
5600
|
+
server: config.server,
|
|
5601
|
+
baseUrl: config.baseUrl,
|
|
5602
|
+
project: config.project,
|
|
5603
|
+
port: config.port,
|
|
5604
|
+
entryPath: config.entryPath,
|
|
5605
|
+
startupTimeout: config.startupTimeout,
|
|
5606
|
+
logLevel: config.logLevel,
|
|
5607
|
+
auth: config.auth,
|
|
5608
|
+
env
|
|
5609
|
+
});
|
|
5610
|
+
}
|
|
5611
|
+
function serverEnv(config) {
|
|
5612
|
+
const extra = {};
|
|
5613
|
+
if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
|
|
5614
|
+
if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
|
|
5615
|
+
if (Object.keys(extra).length === 0 && !config.env) return void 0;
|
|
5616
|
+
return { ...extra, ...config.env };
|
|
5617
|
+
}
|
|
5618
|
+
async function startServerEntry(config, owner) {
|
|
5619
|
+
let server;
|
|
5620
|
+
let startedByUs = false;
|
|
5621
|
+
if (config.baseUrl && !config.server) {
|
|
5622
|
+
server = TestServer.connect(config.baseUrl);
|
|
5623
|
+
} else if (config.server) {
|
|
5624
|
+
const serverCommand = resolveServerCommand(config.server);
|
|
5625
|
+
const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
5626
|
+
if (isDebug) {
|
|
5627
|
+
console.log(`[TestFixture] Starting server: ${serverCommand}`);
|
|
5628
|
+
}
|
|
5629
|
+
try {
|
|
5630
|
+
server = await TestServer.start({
|
|
5631
|
+
project: config.project,
|
|
5632
|
+
port: config.port,
|
|
5633
|
+
command: serverCommand,
|
|
5634
|
+
env: serverEnv(config),
|
|
5635
|
+
startupTimeout: config.startupTimeout ?? 3e4,
|
|
5636
|
+
debug: isDebug
|
|
5637
|
+
});
|
|
5638
|
+
startedByUs = true;
|
|
4992
5639
|
if (isDebug) {
|
|
4993
|
-
console.log(`[TestFixture]
|
|
5640
|
+
console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
|
|
4994
5641
|
}
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
command: serverCommand,
|
|
5000
|
-
env: currentConfig.env,
|
|
5001
|
-
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
5002
|
-
debug: isDebug
|
|
5003
|
-
});
|
|
5004
|
-
serverStartedByUs = true;
|
|
5005
|
-
if (isDebug) {
|
|
5006
|
-
console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
|
|
5007
|
-
}
|
|
5008
|
-
} catch (error) {
|
|
5009
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5010
|
-
throw new Error(
|
|
5011
|
-
`Failed to start test server.
|
|
5642
|
+
} catch (error) {
|
|
5643
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5644
|
+
throw new Error(
|
|
5645
|
+
`Failed to start test server.
|
|
5012
5646
|
|
|
5013
|
-
Server entry: ${
|
|
5014
|
-
Project: ${
|
|
5647
|
+
Server entry: ${config.server}
|
|
5648
|
+
Project: ${config.project ?? "default"}
|
|
5015
5649
|
Command: ${serverCommand}
|
|
5016
5650
|
|
|
5017
|
-
Error: ${errMsg}
|
|
5018
|
-
|
|
5019
|
-
}
|
|
5020
|
-
} else {
|
|
5021
|
-
throw new Error(
|
|
5022
|
-
'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
|
|
5651
|
+
Error: ${errMsg}`,
|
|
5652
|
+
{ cause: error }
|
|
5023
5653
|
);
|
|
5024
5654
|
}
|
|
5655
|
+
} else {
|
|
5656
|
+
throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
|
|
5025
5657
|
}
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
}
|
|
5658
|
+
const sharedSecret = config.env?.["JWT_SECRET"];
|
|
5659
|
+
const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
|
|
5660
|
+
const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
|
|
5661
|
+
return { server, startedByUs, tokenFactory: tokenFactory2, owner };
|
|
5031
5662
|
}
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5663
|
+
function acquireServer(chain) {
|
|
5664
|
+
const config = resolveConfig(chain);
|
|
5665
|
+
const key = serverKey(config);
|
|
5666
|
+
let entry = serverEntries.get(key);
|
|
5667
|
+
if (!entry) {
|
|
5668
|
+
entry = startServerEntry(config, ownerScopeOf(chain));
|
|
5669
|
+
serverEntries.set(key, entry);
|
|
5670
|
+
entry.catch(() => serverEntries.delete(key));
|
|
5671
|
+
}
|
|
5672
|
+
return entry;
|
|
5673
|
+
}
|
|
5674
|
+
async function stopServers(owner) {
|
|
5675
|
+
for (const [key, pending] of Array.from(serverEntries)) {
|
|
5676
|
+
let entry;
|
|
5677
|
+
try {
|
|
5678
|
+
entry = await pending;
|
|
5679
|
+
} catch {
|
|
5680
|
+
serverEntries.delete(key);
|
|
5681
|
+
continue;
|
|
5682
|
+
}
|
|
5683
|
+
if (owner && entry.owner !== owner) continue;
|
|
5684
|
+
serverEntries.delete(key);
|
|
5685
|
+
if (entry.startedByUs) {
|
|
5686
|
+
await entry.server.stop();
|
|
5687
|
+
}
|
|
5039
5688
|
}
|
|
5689
|
+
}
|
|
5690
|
+
var fixtureContexts = /* @__PURE__ */ new WeakMap();
|
|
5691
|
+
async function buildFixtures(chain) {
|
|
5692
|
+
const config = resolveConfig(chain);
|
|
5693
|
+
const entry = await acquireServer(chain);
|
|
5694
|
+
const { server, tokenFactory: tokenFactory2 } = entry;
|
|
5695
|
+
const publicMode = config.publicMode ?? config.auth?.mode === "public";
|
|
5696
|
+
const clientConfig = { ...config, publicMode };
|
|
5040
5697
|
const clientInstance = McpTestClient.create({
|
|
5041
|
-
baseUrl: resolveClientBaseUrl(
|
|
5042
|
-
entryPath:
|
|
5043
|
-
transport:
|
|
5044
|
-
publicMode
|
|
5698
|
+
baseUrl: resolveClientBaseUrl(server, clientConfig),
|
|
5699
|
+
entryPath: clientConfig.entryPath,
|
|
5700
|
+
transport: clientConfig.transport ?? "streamable-http",
|
|
5701
|
+
publicMode
|
|
5045
5702
|
}).build();
|
|
5046
5703
|
try {
|
|
5047
5704
|
await clientInstance.connect();
|
|
@@ -5055,17 +5712,18 @@ async function createTestFixtures() {
|
|
|
5055
5712
|
throw err;
|
|
5056
5713
|
}
|
|
5057
5714
|
}
|
|
5058
|
-
const
|
|
5059
|
-
const
|
|
5060
|
-
return {
|
|
5715
|
+
const context = { extraClients: [] };
|
|
5716
|
+
const fixtures = {
|
|
5061
5717
|
mcp: clientInstance,
|
|
5062
|
-
auth,
|
|
5063
|
-
server
|
|
5718
|
+
auth: createAuthFixture(tokenFactory2),
|
|
5719
|
+
server: createServerFixture(server, clientConfig, clientInstance, context)
|
|
5064
5720
|
};
|
|
5721
|
+
fixtureContexts.set(fixtures, context);
|
|
5722
|
+
return fixtures;
|
|
5065
5723
|
}
|
|
5066
5724
|
async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
5067
|
-
if (testFailed
|
|
5068
|
-
const logs =
|
|
5725
|
+
if (testFailed) {
|
|
5726
|
+
const logs = fixtures.server.getLogs();
|
|
5069
5727
|
if (logs.length > 0) {
|
|
5070
5728
|
console.error("\n[TestFixture] === Server Logs (test failed) ===");
|
|
5071
5729
|
const recentLogs = logs.slice(-50);
|
|
@@ -5076,20 +5734,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
|
5076
5734
|
console.error("[TestFixture] === End Server Logs ===\n");
|
|
5077
5735
|
}
|
|
5078
5736
|
}
|
|
5737
|
+
const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
|
|
5738
|
+
for (const client of extras) {
|
|
5739
|
+
if (client.isConnected()) {
|
|
5740
|
+
await client.disconnect().catch(() => void 0);
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5079
5743
|
if (fixtures.mcp.isConnected()) {
|
|
5080
5744
|
await fixtures.mcp.disconnect();
|
|
5081
5745
|
}
|
|
5082
5746
|
}
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
await serverInstance.stop();
|
|
5086
|
-
}
|
|
5087
|
-
serverInstance = null;
|
|
5088
|
-
tokenFactory = null;
|
|
5089
|
-
serverStartedByUs = false;
|
|
5090
|
-
}
|
|
5091
|
-
function resolveClientBaseUrl(server) {
|
|
5092
|
-
return currentConfig.baseUrl ?? server.info.baseUrl;
|
|
5747
|
+
function resolveClientBaseUrl(server, config) {
|
|
5748
|
+
return config.baseUrl ?? server.info.baseUrl;
|
|
5093
5749
|
}
|
|
5094
5750
|
function createAuthFixture(factory) {
|
|
5095
5751
|
const users = {
|
|
@@ -5135,28 +5791,39 @@ function createAuthFixture(factory) {
|
|
|
5135
5791
|
getAudience: () => factory.getAudience()
|
|
5136
5792
|
};
|
|
5137
5793
|
}
|
|
5138
|
-
function createServerFixture(server) {
|
|
5794
|
+
function createServerFixture(server, config, mcp, context) {
|
|
5139
5795
|
return {
|
|
5140
|
-
|
|
5796
|
+
// Live view: `pid` is filled in once the server is listening and follows restarts
|
|
5797
|
+
get info() {
|
|
5798
|
+
return server.info;
|
|
5799
|
+
},
|
|
5141
5800
|
createClient: async (opts) => {
|
|
5142
|
-
|
|
5143
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5144
|
-
entryPath: opts?.entryPath ??
|
|
5801
|
+
const client = await McpTestClient.create({
|
|
5802
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5803
|
+
entryPath: opts?.entryPath ?? config.entryPath,
|
|
5145
5804
|
transport: opts?.transport ?? "streamable-http",
|
|
5146
5805
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
5147
5806
|
clientInfo: opts?.clientInfo,
|
|
5148
|
-
publicMode:
|
|
5807
|
+
publicMode: config.publicMode
|
|
5149
5808
|
}).buildAndConnect();
|
|
5809
|
+
context.extraClients.push(client);
|
|
5810
|
+
return client;
|
|
5150
5811
|
},
|
|
5151
5812
|
createClientBuilder: () => {
|
|
5152
5813
|
const builder = new McpTestClientBuilder({
|
|
5153
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5154
|
-
entryPath:
|
|
5155
|
-
publicMode:
|
|
5814
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5815
|
+
entryPath: config.entryPath,
|
|
5816
|
+
publicMode: config.publicMode
|
|
5156
5817
|
});
|
|
5157
5818
|
return builder;
|
|
5158
5819
|
},
|
|
5159
|
-
restart: () =>
|
|
5820
|
+
restart: async () => {
|
|
5821
|
+
const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
|
|
5822
|
+
await server.restart();
|
|
5823
|
+
for (const client of reconnect) {
|
|
5824
|
+
await client.reconnect();
|
|
5825
|
+
}
|
|
5826
|
+
},
|
|
5160
5827
|
getLogs: () => server.getLogs(),
|
|
5161
5828
|
clearLogs: () => server.clearLogs()
|
|
5162
5829
|
};
|
|
@@ -5167,38 +5834,71 @@ function resolveServerCommand(server) {
|
|
|
5167
5834
|
}
|
|
5168
5835
|
return `npx tsx ${server}`;
|
|
5169
5836
|
}
|
|
5170
|
-
function runWithFixtures(fn) {
|
|
5171
|
-
return async () => {
|
|
5172
|
-
const fixtures = await
|
|
5173
|
-
let
|
|
5837
|
+
function runWithFixtures(fn, chain) {
|
|
5838
|
+
return async (...extra) => {
|
|
5839
|
+
const fixtures = await buildFixtures(chain);
|
|
5840
|
+
let failure;
|
|
5174
5841
|
try {
|
|
5175
|
-
|
|
5842
|
+
for (const scope of chain) {
|
|
5843
|
+
for (const hook of scope.beforeEachHooks) await hook(fixtures);
|
|
5844
|
+
}
|
|
5845
|
+
await fn(fixtures, ...extra);
|
|
5176
5846
|
} catch (error) {
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
await
|
|
5847
|
+
failure = { error };
|
|
5848
|
+
}
|
|
5849
|
+
try {
|
|
5850
|
+
await runAfterEachHooks(chain, fixtures);
|
|
5851
|
+
} catch (error) {
|
|
5852
|
+
failure ??= { error };
|
|
5181
5853
|
}
|
|
5854
|
+
await cleanupTestFixtures(fixtures, failure !== void 0);
|
|
5855
|
+
if (failure) throw failure.error;
|
|
5182
5856
|
};
|
|
5183
5857
|
}
|
|
5858
|
+
async function runAfterEachHooks(chain, fixtures) {
|
|
5859
|
+
let firstError;
|
|
5860
|
+
let failed = false;
|
|
5861
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
5862
|
+
for (const hook of chain[i].afterEachHooks) {
|
|
5863
|
+
try {
|
|
5864
|
+
await hook(fixtures);
|
|
5865
|
+
} catch (error) {
|
|
5866
|
+
if (!failed) firstError = error;
|
|
5867
|
+
failed = true;
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
if (failed) throw firstError;
|
|
5872
|
+
}
|
|
5873
|
+
function skipTitle(name, scope) {
|
|
5874
|
+
return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
|
|
5875
|
+
}
|
|
5184
5876
|
function testWithFixtures(name, fn) {
|
|
5185
|
-
const
|
|
5877
|
+
const chain = scopes.slice();
|
|
5878
|
+
const scope = currentScope();
|
|
5186
5879
|
if (scope.skipped) {
|
|
5187
|
-
|
|
5880
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5188
5881
|
return;
|
|
5189
5882
|
}
|
|
5190
|
-
|
|
5883
|
+
jestGlobals().it(name, runWithFixtures(fn, chain));
|
|
5191
5884
|
}
|
|
5192
5885
|
function use(config) {
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5886
|
+
const scope = currentScope();
|
|
5887
|
+
const { env, ...rest } = config;
|
|
5888
|
+
scope.config = { ...scope.config, ...rest };
|
|
5889
|
+
if (env) scope.envs.push(env);
|
|
5890
|
+
scope.configured = true;
|
|
5891
|
+
if (!scope.cleanupRegistered) {
|
|
5892
|
+
scope.cleanupRegistered = true;
|
|
5893
|
+
jestGlobals().afterAll(async () => {
|
|
5894
|
+
await stopServers(scope === scopes[0] ? void 0 : scope);
|
|
5895
|
+
});
|
|
5896
|
+
}
|
|
5197
5897
|
}
|
|
5198
5898
|
function skip(nameOrCondition, fnOrReason) {
|
|
5199
5899
|
if (typeof nameOrCondition === "boolean") {
|
|
5200
5900
|
if (nameOrCondition) {
|
|
5201
|
-
const scope =
|
|
5901
|
+
const scope = currentScope();
|
|
5202
5902
|
scope.skipped = true;
|
|
5203
5903
|
scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
|
|
5204
5904
|
}
|
|
@@ -5209,57 +5909,114 @@ function skip(nameOrCondition, fnOrReason) {
|
|
|
5209
5909
|
`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
|
|
5210
5910
|
);
|
|
5211
5911
|
}
|
|
5212
|
-
|
|
5912
|
+
jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
|
|
5213
5913
|
}
|
|
5214
5914
|
function only(name, fn) {
|
|
5215
|
-
const scope =
|
|
5915
|
+
const scope = currentScope();
|
|
5216
5916
|
if (scope.skipped) {
|
|
5217
|
-
|
|
5917
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
|
|
5218
5918
|
return;
|
|
5219
5919
|
}
|
|
5220
|
-
|
|
5920
|
+
jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
|
|
5221
5921
|
}
|
|
5222
5922
|
function todo(name) {
|
|
5223
|
-
|
|
5923
|
+
jestGlobals().it.todo(name);
|
|
5224
5924
|
}
|
|
5225
|
-
function
|
|
5925
|
+
function each(table, ...tagged) {
|
|
5926
|
+
const chain = scopes.slice();
|
|
5927
|
+
const register = jestGlobals().it.each(table, ...tagged);
|
|
5226
5928
|
return (name, fn) => {
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
}
|
|
5235
|
-
});
|
|
5929
|
+
const scope = currentScope();
|
|
5930
|
+
if (scope.skipped) {
|
|
5931
|
+
const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
|
|
5932
|
+
skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5933
|
+
return;
|
|
5934
|
+
}
|
|
5935
|
+
register(name, runWithFixtures(fn, chain));
|
|
5236
5936
|
};
|
|
5237
5937
|
}
|
|
5238
|
-
function
|
|
5239
|
-
return (
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5938
|
+
function withScope(fn) {
|
|
5939
|
+
return (...args) => {
|
|
5940
|
+
scopes.push(newScope(currentScope()));
|
|
5941
|
+
try {
|
|
5942
|
+
fn(...args);
|
|
5943
|
+
} finally {
|
|
5944
|
+
scopes.pop();
|
|
5945
|
+
}
|
|
5946
|
+
};
|
|
5947
|
+
}
|
|
5948
|
+
function scopedRegister(pick) {
|
|
5949
|
+
return (name, fn, timeout) => {
|
|
5950
|
+
pick()(name, withScope(fn), timeout);
|
|
5951
|
+
};
|
|
5952
|
+
}
|
|
5953
|
+
function scopedEach(pick) {
|
|
5954
|
+
return (...eachArgs) => (name, fn, timeout) => {
|
|
5955
|
+
const register = pick()(...eachArgs);
|
|
5956
|
+
register(name, withScope(fn), timeout);
|
|
5957
|
+
};
|
|
5958
|
+
}
|
|
5959
|
+
var describeWithScope = Object.assign(
|
|
5960
|
+
scopedRegister(() => jestGlobals().describe),
|
|
5961
|
+
{
|
|
5962
|
+
// `describe.skip` still evaluates its callback during collection, so it needs a scope too
|
|
5963
|
+
only: Object.assign(
|
|
5964
|
+
scopedRegister(() => jestGlobals().describe.only),
|
|
5965
|
+
{
|
|
5966
|
+
each: scopedEach(() => jestGlobals().describe.only.each)
|
|
5967
|
+
}
|
|
5968
|
+
),
|
|
5969
|
+
skip: Object.assign(
|
|
5970
|
+
scopedRegister(() => jestGlobals().describe.skip),
|
|
5971
|
+
{
|
|
5972
|
+
each: scopedEach(() => jestGlobals().describe.skip.each)
|
|
5973
|
+
}
|
|
5974
|
+
),
|
|
5975
|
+
each: scopedEach(() => jestGlobals().describe.each)
|
|
5976
|
+
}
|
|
5977
|
+
);
|
|
5978
|
+
function eachHook(kind) {
|
|
5979
|
+
return (fn, timeout) => {
|
|
5980
|
+
if (typeof fn === "function" && fn.length > 0) {
|
|
5981
|
+
const scope = currentScope();
|
|
5982
|
+
(kind === "beforeEach" ? scope.beforeEachHooks : scope.afterEachHooks).push(fn);
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
jestGlobals()[kind](fn, timeout);
|
|
5986
|
+
};
|
|
5243
5987
|
}
|
|
5244
|
-
var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
|
|
5245
|
-
only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
|
|
5246
|
-
skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
|
|
5247
|
-
each: eachWithSkipScope(describe2.each)
|
|
5248
|
-
});
|
|
5249
5988
|
var test = testWithFixtures;
|
|
5250
5989
|
test.use = use;
|
|
5251
|
-
test.describe =
|
|
5252
|
-
test.beforeAll =
|
|
5253
|
-
test.beforeEach =
|
|
5254
|
-
test.afterEach =
|
|
5255
|
-
test.afterAll =
|
|
5990
|
+
test.describe = describeWithScope;
|
|
5991
|
+
test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
|
|
5992
|
+
test.beforeEach = eachHook("beforeEach");
|
|
5993
|
+
test.afterEach = eachHook("afterEach");
|
|
5994
|
+
test.afterAll = ((...args) => jestGlobals().afterAll(...args));
|
|
5256
5995
|
test.skip = skip;
|
|
5257
5996
|
test.only = only;
|
|
5258
5997
|
test.todo = todo;
|
|
5998
|
+
test.each = each;
|
|
5259
5999
|
|
|
5260
6000
|
// libs/testing/src/expect.ts
|
|
5261
|
-
|
|
5262
|
-
|
|
6001
|
+
var resolvedExpect;
|
|
6002
|
+
function resolveJestExpect() {
|
|
6003
|
+
if (resolvedExpect) return resolvedExpect;
|
|
6004
|
+
try {
|
|
6005
|
+
resolvedExpect = __require("@jest/globals").expect;
|
|
6006
|
+
return resolvedExpect;
|
|
6007
|
+
} catch (error) {
|
|
6008
|
+
throw new Error(
|
|
6009
|
+
`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
6010
|
+
{ cause: error }
|
|
6011
|
+
);
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
var expect = new Proxy(function frontmcpExpect() {
|
|
6015
|
+
}, {
|
|
6016
|
+
apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
|
|
6017
|
+
get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
|
|
6018
|
+
has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
|
|
6019
|
+
});
|
|
5263
6020
|
|
|
5264
6021
|
// libs/testing/src/platform/platform-types.ts
|
|
5265
6022
|
function getPlatformMetaNamespace(platform) {
|
|
@@ -5922,34 +6679,44 @@ var HttpInterceptorImpl = class {
|
|
|
5922
6679
|
}
|
|
5923
6680
|
};
|
|
5924
6681
|
}
|
|
5925
|
-
get(url, response) {
|
|
6682
|
+
get(url, response, options) {
|
|
5926
6683
|
return this.mock({
|
|
5927
|
-
match: { url, method: "GET" },
|
|
5928
|
-
response: normalizeResponse(response)
|
|
6684
|
+
match: { url, method: "GET", ...options?.match },
|
|
6685
|
+
response: normalizeResponse(response),
|
|
6686
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6687
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5929
6688
|
});
|
|
5930
6689
|
}
|
|
5931
|
-
post(url, response) {
|
|
6690
|
+
post(url, response, options) {
|
|
5932
6691
|
return this.mock({
|
|
5933
|
-
match: { url, method: "POST" },
|
|
5934
|
-
response: normalizeResponse(response)
|
|
6692
|
+
match: { url, method: "POST", ...options?.match },
|
|
6693
|
+
response: normalizeResponse(response),
|
|
6694
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6695
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5935
6696
|
});
|
|
5936
6697
|
}
|
|
5937
|
-
put(url, response) {
|
|
6698
|
+
put(url, response, options) {
|
|
5938
6699
|
return this.mock({
|
|
5939
|
-
match: { url, method: "PUT" },
|
|
5940
|
-
response: normalizeResponse(response)
|
|
6700
|
+
match: { url, method: "PUT", ...options?.match },
|
|
6701
|
+
response: normalizeResponse(response),
|
|
6702
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6703
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5941
6704
|
});
|
|
5942
6705
|
}
|
|
5943
|
-
delete(url, response) {
|
|
6706
|
+
delete(url, response, options) {
|
|
5944
6707
|
return this.mock({
|
|
5945
|
-
match: { url, method: "DELETE" },
|
|
5946
|
-
response: normalizeResponse(response)
|
|
6708
|
+
match: { url, method: "DELETE", ...options?.match },
|
|
6709
|
+
response: normalizeResponse(response),
|
|
6710
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6711
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5947
6712
|
});
|
|
5948
6713
|
}
|
|
5949
|
-
any(url, response) {
|
|
6714
|
+
any(url, response, options) {
|
|
5950
6715
|
return this.mock({
|
|
5951
|
-
match: { url },
|
|
5952
|
-
response: normalizeResponse(response)
|
|
6716
|
+
match: { url, ...options?.match },
|
|
6717
|
+
response: normalizeResponse(response),
|
|
6718
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6719
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5953
6720
|
});
|
|
5954
6721
|
}
|
|
5955
6722
|
clear() {
|
|
@@ -5981,6 +6748,9 @@ ${descriptions.join("\n")}`);
|
|
|
5981
6748
|
if (index !== -1) {
|
|
5982
6749
|
activeInterceptors.splice(index, 1);
|
|
5983
6750
|
}
|
|
6751
|
+
if (activeInterceptors.length === 0) {
|
|
6752
|
+
httpMock.disable();
|
|
6753
|
+
}
|
|
5984
6754
|
}
|
|
5985
6755
|
// Internal methods
|
|
5986
6756
|
isActive() {
|
|
@@ -6080,43 +6850,49 @@ ${descriptions.join("\n")}`);
|
|
|
6080
6850
|
}
|
|
6081
6851
|
};
|
|
6082
6852
|
async function interceptedFetch(input, init) {
|
|
6083
|
-
const
|
|
6084
|
-
const
|
|
6853
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
6854
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
|
|
6855
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
6085
6856
|
const headers = {};
|
|
6086
|
-
|
|
6087
|
-
if (
|
|
6088
|
-
|
|
6857
|
+
const collectHeaders = (source) => {
|
|
6858
|
+
if (!source) return;
|
|
6859
|
+
if (source instanceof Headers) {
|
|
6860
|
+
source.forEach((value, key) => {
|
|
6089
6861
|
headers[key.toLowerCase()] = value;
|
|
6090
6862
|
});
|
|
6091
|
-
} else if (Array.isArray(
|
|
6092
|
-
for (const [key, value] of
|
|
6863
|
+
} else if (Array.isArray(source)) {
|
|
6864
|
+
for (const [key, value] of source) {
|
|
6093
6865
|
headers[key.toLowerCase()] = value;
|
|
6094
6866
|
}
|
|
6095
6867
|
} else {
|
|
6096
|
-
for (const [key, value] of Object.entries(
|
|
6868
|
+
for (const [key, value] of Object.entries(source)) {
|
|
6097
6869
|
headers[key.toLowerCase()] = value;
|
|
6098
6870
|
}
|
|
6099
6871
|
}
|
|
6100
|
-
}
|
|
6872
|
+
};
|
|
6873
|
+
collectHeaders(init?.headers ?? request?.headers);
|
|
6101
6874
|
let body;
|
|
6102
6875
|
let rawBody;
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6876
|
+
let bodySource = init?.body;
|
|
6877
|
+
if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
|
|
6878
|
+
bodySource = await request.clone().text();
|
|
6879
|
+
}
|
|
6880
|
+
if (bodySource) {
|
|
6881
|
+
if (typeof bodySource === "string") {
|
|
6882
|
+
rawBody = bodySource;
|
|
6883
|
+
} else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
|
|
6884
|
+
rawBody = new TextDecoder().decode(bodySource);
|
|
6885
|
+
} else if (bodySource instanceof URLSearchParams) {
|
|
6886
|
+
rawBody = bodySource.toString();
|
|
6887
|
+
}
|
|
6888
|
+
if (rawBody !== void 0) {
|
|
6113
6889
|
try {
|
|
6114
6890
|
body = JSON.parse(rawBody);
|
|
6115
6891
|
} catch {
|
|
6116
6892
|
body = rawBody;
|
|
6117
6893
|
}
|
|
6118
6894
|
} else {
|
|
6119
|
-
body =
|
|
6895
|
+
body = bodySource;
|
|
6120
6896
|
}
|
|
6121
6897
|
}
|
|
6122
6898
|
const requestInfo = {
|
|
@@ -6146,8 +6922,20 @@ async function interceptedFetch(input, init) {
|
|
|
6146
6922
|
Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
|
|
6147
6923
|
);
|
|
6148
6924
|
}
|
|
6925
|
+
var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
|
|
6926
|
+
function isResponseDescriptor(value) {
|
|
6927
|
+
const keys = Object.keys(value);
|
|
6928
|
+
if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
|
|
6929
|
+
if ("status" in value) {
|
|
6930
|
+
const status = value["status"];
|
|
6931
|
+
if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
|
|
6932
|
+
}
|
|
6933
|
+
return true;
|
|
6934
|
+
}
|
|
6149
6935
|
function normalizeResponse(response) {
|
|
6150
|
-
|
|
6936
|
+
const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
|
|
6937
|
+
if (isNetworkError) return response;
|
|
6938
|
+
if (Array.isArray(response) || !isResponseDescriptor(response)) {
|
|
6151
6939
|
return { body: response };
|
|
6152
6940
|
}
|
|
6153
6941
|
return response;
|
|
@@ -7481,47 +8269,48 @@ function clearGlobalMeasurements() {
|
|
|
7481
8269
|
}
|
|
7482
8270
|
|
|
7483
8271
|
// libs/testing/src/perf/perf-test.ts
|
|
7484
|
-
var
|
|
7485
|
-
var
|
|
7486
|
-
var
|
|
7487
|
-
var
|
|
7488
|
-
async function
|
|
7489
|
-
if (!
|
|
7490
|
-
|
|
7491
|
-
}
|
|
7492
|
-
if (!
|
|
7493
|
-
if (
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
} else if (
|
|
7497
|
-
const serverCommand = resolveServerCommand2(
|
|
8272
|
+
var currentConfig = {};
|
|
8273
|
+
var serverInstance = null;
|
|
8274
|
+
var tokenFactory = null;
|
|
8275
|
+
var serverStartedByUs = false;
|
|
8276
|
+
async function initializeSharedResources() {
|
|
8277
|
+
if (!tokenFactory) {
|
|
8278
|
+
tokenFactory = new TestTokenFactory();
|
|
8279
|
+
}
|
|
8280
|
+
if (!serverInstance) {
|
|
8281
|
+
if (currentConfig.baseUrl) {
|
|
8282
|
+
serverInstance = TestServer.connect(currentConfig.baseUrl);
|
|
8283
|
+
serverStartedByUs = false;
|
|
8284
|
+
} else if (currentConfig.server) {
|
|
8285
|
+
const serverCommand = resolveServerCommand2(currentConfig.server);
|
|
7498
8286
|
const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
7499
8287
|
if (isDebug) {
|
|
7500
8288
|
console.log(`[PerfTest] Starting server: ${serverCommand}`);
|
|
7501
8289
|
}
|
|
7502
8290
|
try {
|
|
7503
|
-
|
|
7504
|
-
project:
|
|
7505
|
-
port:
|
|
8291
|
+
serverInstance = await TestServer.start({
|
|
8292
|
+
project: currentConfig.project,
|
|
8293
|
+
port: currentConfig.port,
|
|
7506
8294
|
command: serverCommand,
|
|
7507
|
-
env:
|
|
7508
|
-
startupTimeout:
|
|
8295
|
+
env: currentConfig.env,
|
|
8296
|
+
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
7509
8297
|
debug: isDebug
|
|
7510
8298
|
});
|
|
7511
|
-
|
|
8299
|
+
serverStartedByUs = true;
|
|
7512
8300
|
if (isDebug) {
|
|
7513
|
-
console.log(`[PerfTest] Server started at ${
|
|
8301
|
+
console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
|
|
7514
8302
|
}
|
|
7515
8303
|
} catch (error) {
|
|
7516
8304
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
7517
8305
|
throw new Error(
|
|
7518
8306
|
`Failed to start test server.
|
|
7519
8307
|
|
|
7520
|
-
Server entry: ${
|
|
7521
|
-
Project: ${
|
|
8308
|
+
Server entry: ${currentConfig.server}
|
|
8309
|
+
Project: ${currentConfig.project ?? "default"}
|
|
7522
8310
|
Command: ${serverCommand}
|
|
7523
8311
|
|
|
7524
|
-
Error: ${errMsg}
|
|
8312
|
+
Error: ${errMsg}`,
|
|
8313
|
+
{ cause: error }
|
|
7525
8314
|
);
|
|
7526
8315
|
}
|
|
7527
8316
|
} else {
|
|
@@ -7529,23 +8318,23 @@ Error: ${errMsg}`
|
|
|
7529
8318
|
}
|
|
7530
8319
|
}
|
|
7531
8320
|
}
|
|
7532
|
-
async function
|
|
7533
|
-
await
|
|
7534
|
-
if (!
|
|
8321
|
+
async function createTestFixtures(testName) {
|
|
8322
|
+
await initializeSharedResources();
|
|
8323
|
+
if (!serverInstance) {
|
|
7535
8324
|
throw new Error("Server instance not initialized");
|
|
7536
8325
|
}
|
|
7537
|
-
if (!
|
|
8326
|
+
if (!tokenFactory) {
|
|
7538
8327
|
throw new Error("Token factory not initialized");
|
|
7539
8328
|
}
|
|
7540
8329
|
const clientInstance = await McpTestClient.create({
|
|
7541
|
-
baseUrl:
|
|
7542
|
-
transport:
|
|
7543
|
-
publicMode:
|
|
8330
|
+
baseUrl: serverInstance.info.baseUrl,
|
|
8331
|
+
transport: currentConfig.transport ?? "streamable-http",
|
|
8332
|
+
publicMode: currentConfig.publicMode
|
|
7544
8333
|
}).buildAndConnect();
|
|
7545
|
-
const auth = createAuthFixture2(
|
|
7546
|
-
const server = createServerFixture2(
|
|
7547
|
-
const perfImpl = createPerfFixtures(testName,
|
|
7548
|
-
if (
|
|
8334
|
+
const auth = createAuthFixture2(tokenFactory);
|
|
8335
|
+
const server = createServerFixture2(serverInstance);
|
|
8336
|
+
const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
|
|
8337
|
+
if (currentConfig.forceGcOnBaseline !== false) {
|
|
7549
8338
|
await perfImpl.baseline();
|
|
7550
8339
|
}
|
|
7551
8340
|
return {
|
|
@@ -7561,8 +8350,8 @@ async function createTestFixtures2(testName) {
|
|
|
7561
8350
|
async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
7562
8351
|
const measurement = perfImpl.buildMeasurement();
|
|
7563
8352
|
addGlobalMeasurement(measurement);
|
|
7564
|
-
if (testFailed &&
|
|
7565
|
-
const logs =
|
|
8353
|
+
if (testFailed && serverInstance) {
|
|
8354
|
+
const logs = serverInstance.getLogs();
|
|
7566
8355
|
if (logs.length > 0) {
|
|
7567
8356
|
console.error("\n[PerfTest] === Server Logs (test failed) ===");
|
|
7568
8357
|
const recentLogs = logs.slice(-50);
|
|
@@ -7577,13 +8366,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
|
7577
8366
|
await fixtures.mcp.disconnect();
|
|
7578
8367
|
}
|
|
7579
8368
|
}
|
|
7580
|
-
async function
|
|
7581
|
-
if (
|
|
7582
|
-
await
|
|
8369
|
+
async function cleanupSharedResources() {
|
|
8370
|
+
if (serverInstance && serverStartedByUs) {
|
|
8371
|
+
await serverInstance.stop();
|
|
7583
8372
|
}
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
8373
|
+
serverInstance = null;
|
|
8374
|
+
tokenFactory = null;
|
|
8375
|
+
serverStartedByUs = false;
|
|
7587
8376
|
}
|
|
7588
8377
|
function createAuthFixture2(factory) {
|
|
7589
8378
|
const users = {
|
|
@@ -7638,13 +8427,13 @@ function createServerFixture2(server) {
|
|
|
7638
8427
|
transport: opts?.transport ?? "streamable-http",
|
|
7639
8428
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
7640
8429
|
clientInfo: opts?.clientInfo,
|
|
7641
|
-
publicMode:
|
|
8430
|
+
publicMode: currentConfig.publicMode
|
|
7642
8431
|
}).buildAndConnect();
|
|
7643
8432
|
},
|
|
7644
8433
|
createClientBuilder: () => {
|
|
7645
8434
|
return new McpTestClientBuilder({
|
|
7646
8435
|
baseUrl: server.info.baseUrl,
|
|
7647
|
-
publicMode:
|
|
8436
|
+
publicMode: currentConfig.publicMode
|
|
7648
8437
|
});
|
|
7649
8438
|
},
|
|
7650
8439
|
restart: () => server.restart(),
|
|
@@ -7660,7 +8449,7 @@ function resolveServerCommand2(server) {
|
|
|
7660
8449
|
}
|
|
7661
8450
|
function perfTestWithFixtures(name, fn) {
|
|
7662
8451
|
it(name, async () => {
|
|
7663
|
-
const { fixtures, perfImpl } = await
|
|
8452
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7664
8453
|
let testFailed = false;
|
|
7665
8454
|
try {
|
|
7666
8455
|
await fn(fixtures);
|
|
@@ -7673,14 +8462,14 @@ function perfTestWithFixtures(name, fn) {
|
|
|
7673
8462
|
});
|
|
7674
8463
|
}
|
|
7675
8464
|
function use2(config) {
|
|
7676
|
-
|
|
8465
|
+
currentConfig = { ...currentConfig, ...config };
|
|
7677
8466
|
afterAll(async () => {
|
|
7678
|
-
await
|
|
8467
|
+
await cleanupSharedResources();
|
|
7679
8468
|
});
|
|
7680
8469
|
}
|
|
7681
8470
|
function skip2(name, fn) {
|
|
7682
8471
|
it.skip(name, async () => {
|
|
7683
|
-
const { fixtures, perfImpl } = await
|
|
8472
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7684
8473
|
let testFailed = false;
|
|
7685
8474
|
try {
|
|
7686
8475
|
await fn(fixtures);
|
|
@@ -7694,7 +8483,7 @@ function skip2(name, fn) {
|
|
|
7694
8483
|
}
|
|
7695
8484
|
function only2(name, fn) {
|
|
7696
8485
|
it.only(name, async () => {
|
|
7697
|
-
const { fixtures, perfImpl } = await
|
|
8486
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7698
8487
|
let testFailed = false;
|
|
7699
8488
|
try {
|
|
7700
8489
|
await fn(fixtures);
|
|
@@ -7711,16 +8500,19 @@ function todo2(name) {
|
|
|
7711
8500
|
}
|
|
7712
8501
|
var perfTest = perfTestWithFixtures;
|
|
7713
8502
|
perfTest.use = use2;
|
|
7714
|
-
|
|
7715
|
-
perfTest
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
8503
|
+
for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
|
|
8504
|
+
Object.defineProperty(perfTest, hook, {
|
|
8505
|
+
get: () => globalThis[hook],
|
|
8506
|
+
enumerable: true,
|
|
8507
|
+
configurable: true
|
|
8508
|
+
});
|
|
8509
|
+
}
|
|
7719
8510
|
perfTest.skip = skip2;
|
|
7720
8511
|
perfTest.only = only2;
|
|
7721
8512
|
perfTest.todo = todo2;
|
|
7722
8513
|
|
|
7723
8514
|
// libs/testing/src/perf/baseline-store.ts
|
|
8515
|
+
import { ensureDir, fileExists, readFile, writeFile } from "@frontmcp/utils";
|
|
7724
8516
|
var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
|
|
7725
8517
|
var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
|
|
7726
8518
|
var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
|
|
@@ -7735,7 +8527,6 @@ var BaselineStore = class {
|
|
|
7735
8527
|
*/
|
|
7736
8528
|
async load() {
|
|
7737
8529
|
try {
|
|
7738
|
-
const { readFile, fileExists } = await import("@frontmcp/utils");
|
|
7739
8530
|
if (!await fileExists(this.baselinePath)) {
|
|
7740
8531
|
return null;
|
|
7741
8532
|
}
|
|
@@ -7750,7 +8541,6 @@ var BaselineStore = class {
|
|
|
7750
8541
|
* Save baseline to local file.
|
|
7751
8542
|
*/
|
|
7752
8543
|
async save(baseline) {
|
|
7753
|
-
const { writeFile, ensureDir } = await import("@frontmcp/utils");
|
|
7754
8544
|
const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
|
|
7755
8545
|
await ensureDir(dir);
|
|
7756
8546
|
await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
@@ -8006,6 +8796,7 @@ function getRegressionDetector(config) {
|
|
|
8006
8796
|
}
|
|
8007
8797
|
|
|
8008
8798
|
// libs/testing/src/perf/report-generator.ts
|
|
8799
|
+
import { ensureDir as ensureDir2, writeFile as writeFile2 } from "@frontmcp/utils";
|
|
8009
8800
|
var ReportGenerator = class {
|
|
8010
8801
|
detector;
|
|
8011
8802
|
constructor() {
|
|
@@ -8261,14 +9052,13 @@ var ReportGenerator = class {
|
|
|
8261
9052
|
}
|
|
8262
9053
|
};
|
|
8263
9054
|
async function saveReports(measurements, outputDir, baseline, gitInfo) {
|
|
8264
|
-
|
|
8265
|
-
await ensureDir(outputDir);
|
|
9055
|
+
await ensureDir2(outputDir);
|
|
8266
9056
|
const generator = new ReportGenerator();
|
|
8267
9057
|
const report = generator.generateReport(measurements, baseline, gitInfo);
|
|
8268
9058
|
const jsonPath = `${outputDir}/report.json`;
|
|
8269
9059
|
const markdownPath = `${outputDir}/report.md`;
|
|
8270
|
-
await
|
|
8271
|
-
await
|
|
9060
|
+
await writeFile2(jsonPath, generator.generateJsonReport(report));
|
|
9061
|
+
await writeFile2(markdownPath, generator.generateMarkdownReport(report));
|
|
8272
9062
|
return { jsonPath, markdownPath };
|
|
8273
9063
|
}
|
|
8274
9064
|
function createReportGenerator() {
|
|
@@ -8329,6 +9119,7 @@ export {
|
|
|
8329
9119
|
RegressionDetector,
|
|
8330
9120
|
ReportGenerator,
|
|
8331
9121
|
ServerStartError,
|
|
9122
|
+
SseTransport,
|
|
8332
9123
|
StreamableHttpTransport,
|
|
8333
9124
|
TestClientError,
|
|
8334
9125
|
TestServer,
|