@frontmcp/testing 1.8.6 → 1.8.7
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 +487 -224
- package/esm/index.mjs +644 -347
- package/esm/package.json +6 -6
- package/esm/perf/index.mjs +407 -272
- package/expect.d.ts +12 -11
- package/expect.d.ts.map +1 -1
- package/fixtures/fixture-types.d.ts +22 -4
- package/fixtures/fixture-types.d.ts.map +1 -1
- package/fixtures/index.js +486 -216
- 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 +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +652 -348
- package/package.json +6 -6
- package/perf/baseline-store.d.ts +1 -1
- package/perf/baseline-store.d.ts.map +1 -1
- package/perf/index.js +410 -285
- 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/streamable-http.transport.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -460,6 +460,32 @@ var interceptors = {
|
|
|
460
460
|
}
|
|
461
461
|
};
|
|
462
462
|
|
|
463
|
+
// libs/testing/src/transport/error-utils.ts
|
|
464
|
+
function errorName(err) {
|
|
465
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
466
|
+
const name = err.name;
|
|
467
|
+
return typeof name === "string" ? name : void 0;
|
|
468
|
+
}
|
|
469
|
+
function isAbortError(err) {
|
|
470
|
+
return errorName(err) === "AbortError";
|
|
471
|
+
}
|
|
472
|
+
function errorMessage(err, fallback = "Unknown error") {
|
|
473
|
+
if (typeof err === "string" && err) return err;
|
|
474
|
+
if (typeof err === "object" && err !== null) {
|
|
475
|
+
const message = err.message;
|
|
476
|
+
if (typeof message === "string" && message) return message;
|
|
477
|
+
}
|
|
478
|
+
return fallback;
|
|
479
|
+
}
|
|
480
|
+
var interceptedErrors = /* @__PURE__ */ new WeakSet();
|
|
481
|
+
function markInterceptedError(err) {
|
|
482
|
+
interceptedErrors.add(err);
|
|
483
|
+
return err;
|
|
484
|
+
}
|
|
485
|
+
function isInterceptedError(err) {
|
|
486
|
+
return typeof err === "object" && err !== null && interceptedErrors.has(err);
|
|
487
|
+
}
|
|
488
|
+
|
|
463
489
|
// libs/testing/src/transport/streamable-http.transport.ts
|
|
464
490
|
var DEFAULT_TIMEOUT = 3e4;
|
|
465
491
|
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
@@ -568,7 +594,7 @@ var StreamableHttpTransport = class {
|
|
|
568
594
|
return mockResponse2;
|
|
569
595
|
}
|
|
570
596
|
case "error":
|
|
571
|
-
throw interceptResult.error;
|
|
597
|
+
throw markInterceptedError(interceptResult.error);
|
|
572
598
|
case "continue":
|
|
573
599
|
message = interceptResult.request;
|
|
574
600
|
break;
|
|
@@ -629,7 +655,7 @@ var StreamableHttpTransport = class {
|
|
|
629
655
|
return jsonResponse;
|
|
630
656
|
} catch (error) {
|
|
631
657
|
clearTimeout(timeoutId);
|
|
632
|
-
if (error
|
|
658
|
+
if (isAbortError(error)) {
|
|
633
659
|
return {
|
|
634
660
|
jsonrpc: "2.0",
|
|
635
661
|
id: message.id ?? null,
|
|
@@ -668,7 +694,7 @@ var StreamableHttpTransport = class {
|
|
|
668
694
|
}
|
|
669
695
|
} catch (error) {
|
|
670
696
|
clearTimeout(timeoutId);
|
|
671
|
-
if (error
|
|
697
|
+
if (!isAbortError(error)) {
|
|
672
698
|
throw error;
|
|
673
699
|
}
|
|
674
700
|
}
|
|
@@ -709,7 +735,7 @@ var StreamableHttpTransport = class {
|
|
|
709
735
|
error: {
|
|
710
736
|
code: -32700,
|
|
711
737
|
message: "Parse error",
|
|
712
|
-
data: error
|
|
738
|
+
data: errorMessage(error)
|
|
713
739
|
}
|
|
714
740
|
};
|
|
715
741
|
}
|
|
@@ -1079,7 +1105,7 @@ var StreamableHttpTransport = class {
|
|
|
1079
1105
|
if (this.config.clientInfo) {
|
|
1080
1106
|
headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
|
|
1081
1107
|
}
|
|
1082
|
-
if (this.authToken
|
|
1108
|
+
if (this.authToken) {
|
|
1083
1109
|
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1084
1110
|
}
|
|
1085
1111
|
if (this.sessionId) {
|
|
@@ -1422,6 +1448,15 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
1422
1448
|
version: "0.4.0"
|
|
1423
1449
|
};
|
|
1424
1450
|
var MAX_LIST_PAGES = 1e3;
|
|
1451
|
+
function isAuthFailure(error) {
|
|
1452
|
+
return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
|
|
1453
|
+
}
|
|
1454
|
+
function describeInitializeError(error) {
|
|
1455
|
+
const message = error?.message ?? "Unknown error";
|
|
1456
|
+
const data = error?.data;
|
|
1457
|
+
const detail = typeof data === "string" ? data.trim() : "";
|
|
1458
|
+
return detail ? `${message} - ${detail.slice(0, 500)}` : message;
|
|
1459
|
+
}
|
|
1425
1460
|
function extractEntryPaths(data) {
|
|
1426
1461
|
let payload = data;
|
|
1427
1462
|
if (typeof payload === "string") {
|
|
@@ -1508,19 +1543,22 @@ var McpTestClient = class {
|
|
|
1508
1543
|
async connect() {
|
|
1509
1544
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
1510
1545
|
this.triedDiscoveredEntryPath = false;
|
|
1546
|
+
this.initResult = null;
|
|
1511
1547
|
this.transport = this.createTransport();
|
|
1512
1548
|
await this.transport.connect();
|
|
1513
1549
|
const maxRetries = 3;
|
|
1514
1550
|
const retryDelayMs = 500;
|
|
1515
1551
|
let lastError;
|
|
1516
1552
|
let reportedEntryPaths;
|
|
1553
|
+
let attemptsMade = 0;
|
|
1517
1554
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1555
|
+
attemptsMade = attempt;
|
|
1518
1556
|
const initResponse = await this.initialize();
|
|
1519
1557
|
if (initResponse.success && initResponse.data) {
|
|
1520
1558
|
this.initResult = initResponse.data;
|
|
1521
1559
|
break;
|
|
1522
1560
|
}
|
|
1523
|
-
lastError = initResponse.error
|
|
1561
|
+
lastError = describeInitializeError(initResponse.error);
|
|
1524
1562
|
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
1525
1563
|
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
1526
1564
|
this.triedDiscoveredEntryPath = true;
|
|
@@ -1532,6 +1570,7 @@ var McpTestClient = class {
|
|
|
1532
1570
|
await this.transport.connect();
|
|
1533
1571
|
continue;
|
|
1534
1572
|
}
|
|
1573
|
+
if (isAuthFailure(initResponse.error)) break;
|
|
1535
1574
|
if (attempt < maxRetries) {
|
|
1536
1575
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
1537
1576
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
@@ -1540,7 +1579,7 @@ var McpTestClient = class {
|
|
|
1540
1579
|
if (!this.initResult) {
|
|
1541
1580
|
const served = reportedEntryPaths?.length ? ` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.` : "";
|
|
1542
1581
|
throw new Error(
|
|
1543
|
-
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${
|
|
1582
|
+
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
|
|
1544
1583
|
);
|
|
1545
1584
|
}
|
|
1546
1585
|
this._sessionId = this.transport.getSessionId();
|
|
@@ -1610,18 +1649,47 @@ var McpTestClient = class {
|
|
|
1610
1649
|
return this._authState;
|
|
1611
1650
|
}
|
|
1612
1651
|
/**
|
|
1613
|
-
* Authenticate with a token
|
|
1652
|
+
* Authenticate with a token.
|
|
1653
|
+
*
|
|
1654
|
+
* When connected, this opens a NEW session with the token (the previous session was created
|
|
1655
|
+
* for the previous identity) and rejects if the server refuses it — for example because the
|
|
1656
|
+
* token is expired or badly signed. On rejection the client keeps its previous identity and
|
|
1657
|
+
* session. The token is kept in the client config, so `reconnect()` keeps using it.
|
|
1614
1658
|
*/
|
|
1615
1659
|
async authenticate(token) {
|
|
1660
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
1661
|
+
throw new Error("authenticate() requires a non-empty token");
|
|
1662
|
+
}
|
|
1663
|
+
const previousAuthState = this._authState;
|
|
1664
|
+
const previousConfigAuth = this.config.auth;
|
|
1665
|
+
this.config.auth = { ...previousConfigAuth, token };
|
|
1616
1666
|
this._authState = {
|
|
1617
1667
|
isAnonymous: false,
|
|
1618
1668
|
token,
|
|
1619
1669
|
scopes: this.parseScopesFromToken(token),
|
|
1620
1670
|
user: this.parseUserFromToken(token)
|
|
1621
1671
|
};
|
|
1622
|
-
|
|
1623
|
-
|
|
1672
|
+
const previousTransport = this.transport;
|
|
1673
|
+
if (!previousTransport) {
|
|
1674
|
+
this.log("debug", "Authentication updated (not connected yet)");
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const previousInit = this.initResult;
|
|
1678
|
+
const previousSessionId = this._sessionId;
|
|
1679
|
+
const previousSessionInfo = this._sessionInfo;
|
|
1680
|
+
try {
|
|
1681
|
+
await this.connect();
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
await this.transport?.close().catch(() => void 0);
|
|
1684
|
+
this.transport = previousTransport;
|
|
1685
|
+
this.initResult = previousInit;
|
|
1686
|
+
this._sessionId = previousSessionId;
|
|
1687
|
+
this._sessionInfo = previousSessionInfo;
|
|
1688
|
+
this.config.auth = previousConfigAuth;
|
|
1689
|
+
this._authState = previousAuthState;
|
|
1690
|
+
throw err;
|
|
1624
1691
|
}
|
|
1692
|
+
await previousTransport.close().catch(() => void 0);
|
|
1625
1693
|
this.log("debug", "Authentication updated");
|
|
1626
1694
|
}
|
|
1627
1695
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -2245,10 +2313,11 @@ var McpTestClient = class {
|
|
|
2245
2313
|
requestId: id
|
|
2246
2314
|
};
|
|
2247
2315
|
} catch (err) {
|
|
2316
|
+
if (isInterceptedError(err)) throw err;
|
|
2248
2317
|
const durationMs = Date.now() - start;
|
|
2249
2318
|
const error = {
|
|
2250
2319
|
code: -32603,
|
|
2251
|
-
message: err
|
|
2320
|
+
message: errorMessage(err)
|
|
2252
2321
|
};
|
|
2253
2322
|
return {
|
|
2254
2323
|
success: false,
|
|
@@ -2396,12 +2465,12 @@ var McpTestClient = class {
|
|
|
2396
2465
|
const payload = this.decodeJwtPayload(token);
|
|
2397
2466
|
if (!payload) return [];
|
|
2398
2467
|
const scope = payload["scope"];
|
|
2399
|
-
const
|
|
2468
|
+
const scopes2 = payload["scopes"];
|
|
2400
2469
|
if (typeof scope === "string") {
|
|
2401
2470
|
return scope.split(" ");
|
|
2402
2471
|
}
|
|
2403
|
-
if (Array.isArray(
|
|
2404
|
-
return
|
|
2472
|
+
if (Array.isArray(scopes2)) {
|
|
2473
|
+
return scopes2;
|
|
2405
2474
|
}
|
|
2406
2475
|
return [];
|
|
2407
2476
|
} catch {
|
|
@@ -2533,14 +2602,15 @@ var TestTokenFactory = class {
|
|
|
2533
2602
|
*/
|
|
2534
2603
|
async createTestToken(options) {
|
|
2535
2604
|
await this.ensureKeys();
|
|
2536
|
-
const
|
|
2605
|
+
const nowMs = Date.now();
|
|
2606
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
2537
2607
|
const exp = options.exp ?? 3600;
|
|
2538
2608
|
const payload = {
|
|
2539
2609
|
iss: options.iss ?? this.issuer,
|
|
2540
2610
|
sub: options.sub,
|
|
2541
2611
|
aud: options.aud ?? this.audience,
|
|
2542
|
-
iat
|
|
2543
|
-
exp:
|
|
2612
|
+
iat,
|
|
2613
|
+
exp: Math.ceil(nowMs / 1e3) + exp,
|
|
2544
2614
|
scope: options.scopes?.join(" "),
|
|
2545
2615
|
...options.claims
|
|
2546
2616
|
};
|
|
@@ -2565,10 +2635,10 @@ var TestTokenFactory = class {
|
|
|
2565
2635
|
/**
|
|
2566
2636
|
* Create a regular user token
|
|
2567
2637
|
*/
|
|
2568
|
-
async createUserToken(sub = "user-001",
|
|
2638
|
+
async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
|
|
2569
2639
|
return this.createTestToken({
|
|
2570
2640
|
sub,
|
|
2571
|
-
scopes,
|
|
2641
|
+
scopes: scopes2,
|
|
2572
2642
|
claims: {
|
|
2573
2643
|
email: "user@test.local",
|
|
2574
2644
|
name: "Test User",
|
|
@@ -2579,10 +2649,11 @@ var TestTokenFactory = class {
|
|
|
2579
2649
|
/**
|
|
2580
2650
|
* Create an anonymous user token
|
|
2581
2651
|
*/
|
|
2582
|
-
async createAnonymousToken() {
|
|
2652
|
+
async createAnonymousToken(expiresIn) {
|
|
2583
2653
|
return this.createTestToken({
|
|
2584
2654
|
sub: `anon:${Date.now()}`,
|
|
2585
2655
|
scopes: ["anonymous"],
|
|
2656
|
+
exp: expiresIn,
|
|
2586
2657
|
claims: {
|
|
2587
2658
|
name: "Anonymous",
|
|
2588
2659
|
role: "anonymous"
|
|
@@ -2793,17 +2864,7 @@ function createTestUser(overrides) {
|
|
|
2793
2864
|
|
|
2794
2865
|
// libs/testing/src/auth/mock-oauth-server.ts
|
|
2795
2866
|
var import_http = require("http");
|
|
2796
|
-
var
|
|
2797
|
-
var _sha256Base64url;
|
|
2798
|
-
var _base64urlEncode;
|
|
2799
|
-
async function loadCryptoUtils() {
|
|
2800
|
-
if (!_randomBytes) {
|
|
2801
|
-
const utils = await import("@frontmcp/utils");
|
|
2802
|
-
_randomBytes = utils.randomBytes;
|
|
2803
|
-
_sha256Base64url = utils.sha256Base64url;
|
|
2804
|
-
_base64urlEncode = utils.base64urlEncode;
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2867
|
+
var import_utils = require("@frontmcp/utils");
|
|
2807
2868
|
var MockOAuthServer = class {
|
|
2808
2869
|
tokenFactory;
|
|
2809
2870
|
options;
|
|
@@ -2818,8 +2879,8 @@ var MockOAuthServer = class {
|
|
|
2818
2879
|
accessTokenTtlSeconds;
|
|
2819
2880
|
/** Refresh token TTL in seconds */
|
|
2820
2881
|
refreshTokenTtlSeconds;
|
|
2821
|
-
constructor(
|
|
2822
|
-
this.tokenFactory =
|
|
2882
|
+
constructor(tokenFactory2, options = {}) {
|
|
2883
|
+
this.tokenFactory = tokenFactory2;
|
|
2823
2884
|
this.options = options;
|
|
2824
2885
|
this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
|
|
2825
2886
|
this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
|
|
@@ -3076,14 +3137,14 @@ ${error.stack}` : String(error);
|
|
|
3076
3137
|
return;
|
|
3077
3138
|
}
|
|
3078
3139
|
const code = this.generateCode();
|
|
3079
|
-
const
|
|
3140
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3080
3141
|
this.authCodes.set(code, {
|
|
3081
3142
|
code,
|
|
3082
3143
|
clientId,
|
|
3083
3144
|
redirectUri,
|
|
3084
3145
|
codeChallenge,
|
|
3085
3146
|
codeChallengeMethod,
|
|
3086
|
-
scopes,
|
|
3147
|
+
scopes: scopes2,
|
|
3087
3148
|
user: testUser,
|
|
3088
3149
|
state,
|
|
3089
3150
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3123,7 +3184,7 @@ ${error.stack}` : String(error);
|
|
|
3123
3184
|
}
|
|
3124
3185
|
const grantType = params.get("grant_type");
|
|
3125
3186
|
if (grantType === "anonymous") {
|
|
3126
|
-
const token = await this.tokenFactory.createAnonymousToken();
|
|
3187
|
+
const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
|
|
3127
3188
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3128
3189
|
res.end(
|
|
3129
3190
|
JSON.stringify({
|
|
@@ -3255,6 +3316,8 @@ ${error.stack}` : String(error);
|
|
|
3255
3316
|
codeRecord.used = true;
|
|
3256
3317
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3257
3318
|
sub: codeRecord.user.sub,
|
|
3319
|
+
scopes: codeRecord.scopes,
|
|
3320
|
+
exp: this.accessTokenTtlSeconds,
|
|
3258
3321
|
claims: {
|
|
3259
3322
|
email: codeRecord.user.email,
|
|
3260
3323
|
name: codeRecord.user.name,
|
|
@@ -3263,6 +3326,7 @@ ${error.stack}` : String(error);
|
|
|
3263
3326
|
});
|
|
3264
3327
|
const idToken = await this.tokenFactory.createTestToken({
|
|
3265
3328
|
sub: codeRecord.user.sub,
|
|
3329
|
+
exp: this.accessTokenTtlSeconds,
|
|
3266
3330
|
claims: {
|
|
3267
3331
|
email: codeRecord.user.email,
|
|
3268
3332
|
name: codeRecord.user.name,
|
|
@@ -3340,6 +3404,8 @@ ${error.stack}` : String(error);
|
|
|
3340
3404
|
}
|
|
3341
3405
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3342
3406
|
sub: tokenRecord.user.sub,
|
|
3407
|
+
scopes: tokenRecord.scopes,
|
|
3408
|
+
exp: this.accessTokenTtlSeconds,
|
|
3343
3409
|
claims: {
|
|
3344
3410
|
email: tokenRecord.user.email,
|
|
3345
3411
|
name: tokenRecord.user.name,
|
|
@@ -3444,14 +3510,14 @@ ${error.stack}` : String(error);
|
|
|
3444
3510
|
name
|
|
3445
3511
|
};
|
|
3446
3512
|
const code = this.generateCode();
|
|
3447
|
-
const
|
|
3513
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3448
3514
|
this.authCodes.set(code, {
|
|
3449
3515
|
code,
|
|
3450
3516
|
clientId,
|
|
3451
3517
|
redirectUri,
|
|
3452
3518
|
codeChallenge,
|
|
3453
3519
|
codeChallengeMethod,
|
|
3454
|
-
scopes,
|
|
3520
|
+
scopes: scopes2,
|
|
3455
3521
|
user: testUser,
|
|
3456
3522
|
state,
|
|
3457
3523
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3557,8 +3623,7 @@ ${error.stack}` : String(error);
|
|
|
3557
3623
|
* Generate a random authorization code
|
|
3558
3624
|
*/
|
|
3559
3625
|
async generateCodeAsync() {
|
|
3560
|
-
|
|
3561
|
-
return _base64urlEncode(_randomBytes(32));
|
|
3626
|
+
return (0, import_utils.base64urlEncode)((0, import_utils.randomBytes)(32));
|
|
3562
3627
|
}
|
|
3563
3628
|
/**
|
|
3564
3629
|
* Generate a random authorization code (sync wrapper for compatibility)
|
|
@@ -3574,8 +3639,7 @@ ${error.stack}` : String(error);
|
|
|
3574
3639
|
*/
|
|
3575
3640
|
async computeCodeChallengeAsync(verifier, method) {
|
|
3576
3641
|
if (method === "S256") {
|
|
3577
|
-
|
|
3578
|
-
return _sha256Base64url(verifier);
|
|
3642
|
+
return (0, import_utils.sha256Base64url)(verifier);
|
|
3579
3643
|
}
|
|
3580
3644
|
return verifier;
|
|
3581
3645
|
}
|
|
@@ -4163,7 +4227,7 @@ var MockCimdServer = class {
|
|
|
4163
4227
|
|
|
4164
4228
|
// libs/testing/src/server/test-server.ts
|
|
4165
4229
|
var import_child_process = require("child_process");
|
|
4166
|
-
var
|
|
4230
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
4167
4231
|
|
|
4168
4232
|
// libs/testing/src/errors/index.ts
|
|
4169
4233
|
var TestClientError = class extends Error {
|
|
@@ -4269,6 +4333,8 @@ var E2E_PORT_RANGES = {
|
|
|
4269
4333
|
"demo-e2e-distributed": { start: 50440, size: 20 },
|
|
4270
4334
|
// Protocol revision E2E tests (50460-50479)
|
|
4271
4335
|
"demo-e2e-protocol-20260728": { start: 50460, size: 20 },
|
|
4336
|
+
// Testing library E2E tests (50480-50499)
|
|
4337
|
+
"demo-e2e-testing": { start: 50480, size: 20 },
|
|
4272
4338
|
// Mock servers and utilities (50900-50999)
|
|
4273
4339
|
"mock-oauth": { start: 50900, size: 10 },
|
|
4274
4340
|
"mock-api": { start: 50910, size: 10 },
|
|
@@ -4277,6 +4343,66 @@ var E2E_PORT_RANGES = {
|
|
|
4277
4343
|
default: { start: 51e3, size: 100 }
|
|
4278
4344
|
};
|
|
4279
4345
|
var reservedPorts = /* @__PURE__ */ new Map();
|
|
4346
|
+
var lockedPorts = /* @__PURE__ */ new Set();
|
|
4347
|
+
var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
4348
|
+
function lockDir() {
|
|
4349
|
+
try {
|
|
4350
|
+
const os = require("node:os");
|
|
4351
|
+
const path = require("node:path");
|
|
4352
|
+
return path.join(os.tmpdir(), "frontmcp-testing-ports");
|
|
4353
|
+
} catch {
|
|
4354
|
+
return void 0;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
function pidIsAlive(pid) {
|
|
4358
|
+
try {
|
|
4359
|
+
process.kill(pid, 0);
|
|
4360
|
+
return true;
|
|
4361
|
+
} catch (error) {
|
|
4362
|
+
return error.code === "EPERM";
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
function acquirePortLock(port) {
|
|
4366
|
+
if (lockedPorts.has(port)) return false;
|
|
4367
|
+
let fs;
|
|
4368
|
+
let file;
|
|
4369
|
+
try {
|
|
4370
|
+
fs = require("node:fs");
|
|
4371
|
+
const dir = lockDir();
|
|
4372
|
+
if (!dir) return true;
|
|
4373
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4374
|
+
file = require("node:path").join(dir, `${port}.lock`);
|
|
4375
|
+
} catch {
|
|
4376
|
+
return true;
|
|
4377
|
+
}
|
|
4378
|
+
const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
|
|
4379
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4380
|
+
try {
|
|
4381
|
+
fs.writeFileSync(file, payload, { flag: "wx" });
|
|
4382
|
+
lockedPorts.add(port);
|
|
4383
|
+
return true;
|
|
4384
|
+
} catch (error) {
|
|
4385
|
+
if (error.code !== "EEXIST") return true;
|
|
4386
|
+
try {
|
|
4387
|
+
const held = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
4388
|
+
const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
|
|
4389
|
+
if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
|
|
4390
|
+
fs.unlinkSync(file);
|
|
4391
|
+
} catch {
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
return false;
|
|
4396
|
+
}
|
|
4397
|
+
function releasePortLock(port) {
|
|
4398
|
+
if (!lockedPorts.delete(port)) return;
|
|
4399
|
+
try {
|
|
4400
|
+
const fs = require("node:fs");
|
|
4401
|
+
const dir = lockDir();
|
|
4402
|
+
if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
|
|
4403
|
+
} catch {
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4280
4406
|
var projectPortIndex = /* @__PURE__ */ new Map();
|
|
4281
4407
|
function getPortRange(project) {
|
|
4282
4408
|
const key = project;
|
|
@@ -4287,15 +4413,22 @@ function getPortRange(project) {
|
|
|
4287
4413
|
}
|
|
4288
4414
|
async function reservePort(project, preferredPort) {
|
|
4289
4415
|
const range = getPortRange(project);
|
|
4290
|
-
|
|
4416
|
+
const handle = (port) => ({
|
|
4417
|
+
port,
|
|
4418
|
+
// Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
|
|
4419
|
+
releaseSocket: async () => {
|
|
4420
|
+
await releasePort(port);
|
|
4421
|
+
},
|
|
4422
|
+
release: async () => {
|
|
4423
|
+
await releasePort(port);
|
|
4424
|
+
releasePortLock(port);
|
|
4425
|
+
}
|
|
4426
|
+
});
|
|
4427
|
+
const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
|
|
4428
|
+
if (wantsSpecificPort) {
|
|
4291
4429
|
const reservation = await tryReservePort(preferredPort, project);
|
|
4292
4430
|
if (reservation) {
|
|
4293
|
-
return
|
|
4294
|
-
port: preferredPort,
|
|
4295
|
-
release: async () => {
|
|
4296
|
-
await releasePort(preferredPort);
|
|
4297
|
-
}
|
|
4298
|
-
};
|
|
4431
|
+
return handle(preferredPort);
|
|
4299
4432
|
}
|
|
4300
4433
|
console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
|
|
4301
4434
|
}
|
|
@@ -4304,30 +4437,19 @@ async function reservePort(project, preferredPort) {
|
|
|
4304
4437
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4305
4438
|
const port = range.start + index % range.size;
|
|
4306
4439
|
index = (index + 1) % range.size;
|
|
4307
|
-
if (reservedPorts.has(port)) {
|
|
4440
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) {
|
|
4308
4441
|
continue;
|
|
4309
4442
|
}
|
|
4310
4443
|
const reservation = await tryReservePort(port, project);
|
|
4311
4444
|
if (reservation) {
|
|
4312
4445
|
projectPortIndex.set(project, index);
|
|
4313
|
-
return
|
|
4314
|
-
port,
|
|
4315
|
-
release: async () => {
|
|
4316
|
-
await releasePort(port);
|
|
4317
|
-
}
|
|
4318
|
-
};
|
|
4446
|
+
return handle(port);
|
|
4319
4447
|
}
|
|
4320
4448
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
return {
|
|
4326
|
-
port: dynamicPort,
|
|
4327
|
-
release: async () => {
|
|
4328
|
-
await releasePort(dynamicPort);
|
|
4329
|
-
}
|
|
4330
|
-
};
|
|
4449
|
+
for (let port = 51e3; port < 52e3; port++) {
|
|
4450
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
|
|
4451
|
+
if (await tryReservePort(port, project)) {
|
|
4452
|
+
return handle(port);
|
|
4331
4453
|
}
|
|
4332
4454
|
}
|
|
4333
4455
|
throw new Error(
|
|
@@ -4335,9 +4457,13 @@ async function reservePort(project, preferredPort) {
|
|
|
4335
4457
|
);
|
|
4336
4458
|
}
|
|
4337
4459
|
async function tryReservePort(port, project) {
|
|
4460
|
+
if (!acquirePortLock(port)) {
|
|
4461
|
+
return false;
|
|
4462
|
+
}
|
|
4338
4463
|
return new Promise((resolve) => {
|
|
4339
4464
|
const server = (0, import_net.createServer)();
|
|
4340
4465
|
server.once("error", () => {
|
|
4466
|
+
releasePortLock(port);
|
|
4341
4467
|
resolve(false);
|
|
4342
4468
|
});
|
|
4343
4469
|
server.listen(port, () => {
|
|
@@ -4363,31 +4489,6 @@ async function releasePort(port) {
|
|
|
4363
4489
|
});
|
|
4364
4490
|
});
|
|
4365
4491
|
}
|
|
4366
|
-
async function findAvailablePortInRange(start, end) {
|
|
4367
|
-
for (let port = start; port < end; port++) {
|
|
4368
|
-
if (reservedPorts.has(port)) {
|
|
4369
|
-
continue;
|
|
4370
|
-
}
|
|
4371
|
-
const available = await isPortAvailable(port);
|
|
4372
|
-
if (available) {
|
|
4373
|
-
return port;
|
|
4374
|
-
}
|
|
4375
|
-
}
|
|
4376
|
-
return null;
|
|
4377
|
-
}
|
|
4378
|
-
async function isPortAvailable(port) {
|
|
4379
|
-
return new Promise((resolve) => {
|
|
4380
|
-
const server = (0, import_net.createServer)();
|
|
4381
|
-
server.once("error", () => {
|
|
4382
|
-
resolve(false);
|
|
4383
|
-
});
|
|
4384
|
-
server.listen(port, () => {
|
|
4385
|
-
server.close(() => {
|
|
4386
|
-
resolve(true);
|
|
4387
|
-
});
|
|
4388
|
-
});
|
|
4389
|
-
});
|
|
4390
|
-
}
|
|
4391
4492
|
|
|
4392
4493
|
// libs/testing/src/server/test-server.ts
|
|
4393
4494
|
var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
|
|
@@ -4396,8 +4497,11 @@ var TestServer = class _TestServer {
|
|
|
4396
4497
|
options;
|
|
4397
4498
|
_info;
|
|
4398
4499
|
logs = [];
|
|
4500
|
+
/** Frees the port reservation (socket and cross-process lock); held until stop() */
|
|
4399
4501
|
portRelease = null;
|
|
4400
|
-
|
|
4502
|
+
/** Frees only the reservation socket, just before the server is spawned */
|
|
4503
|
+
portReleaseSocket = null;
|
|
4504
|
+
constructor(options, port, portRelease, portReleaseSocket) {
|
|
4401
4505
|
this.options = {
|
|
4402
4506
|
port,
|
|
4403
4507
|
project: options.project,
|
|
@@ -4409,6 +4513,7 @@ var TestServer = class _TestServer {
|
|
|
4409
4513
|
debug: options.debug ?? DEBUG_SERVER
|
|
4410
4514
|
};
|
|
4411
4515
|
this.portRelease = portRelease ?? null;
|
|
4516
|
+
this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
|
|
4412
4517
|
this._info = {
|
|
4413
4518
|
baseUrl: `http://localhost:${port}`,
|
|
4414
4519
|
port
|
|
@@ -4421,8 +4526,8 @@ var TestServer = class _TestServer {
|
|
|
4421
4526
|
const project = options.project ?? "default";
|
|
4422
4527
|
const maxAttempts = 3;
|
|
4423
4528
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4424
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4425
|
-
const server = new _TestServer(options, port, release);
|
|
4529
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4530
|
+
const server = new _TestServer(options, port, release, releaseSocket);
|
|
4426
4531
|
try {
|
|
4427
4532
|
await server.startProcess();
|
|
4428
4533
|
return server;
|
|
@@ -4462,7 +4567,7 @@ var TestServer = class _TestServer {
|
|
|
4462
4567
|
}
|
|
4463
4568
|
const maxAttempts = 3;
|
|
4464
4569
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4465
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4570
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4466
4571
|
const serverOptions = {
|
|
4467
4572
|
...options,
|
|
4468
4573
|
port,
|
|
@@ -4470,7 +4575,7 @@ var TestServer = class _TestServer {
|
|
|
4470
4575
|
command: `npx nx serve ${project} --port ${port}`,
|
|
4471
4576
|
cwd: options.cwd ?? process.cwd()
|
|
4472
4577
|
};
|
|
4473
|
-
const server = new _TestServer(serverOptions, port, release);
|
|
4578
|
+
const server = new _TestServer(serverOptions, port, release, releaseSocket);
|
|
4474
4579
|
try {
|
|
4475
4580
|
await server.startProcess();
|
|
4476
4581
|
return server;
|
|
@@ -4528,10 +4633,17 @@ var TestServer = class _TestServer {
|
|
|
4528
4633
|
* Stop the test server
|
|
4529
4634
|
*/
|
|
4530
4635
|
async stop() {
|
|
4531
|
-
|
|
4532
|
-
await this.
|
|
4533
|
-
|
|
4636
|
+
try {
|
|
4637
|
+
await this.stopProcess();
|
|
4638
|
+
} finally {
|
|
4639
|
+
if (this.portRelease) {
|
|
4640
|
+
await this.portRelease();
|
|
4641
|
+
this.portRelease = null;
|
|
4642
|
+
}
|
|
4643
|
+
this.portReleaseSocket = null;
|
|
4534
4644
|
}
|
|
4645
|
+
}
|
|
4646
|
+
async stopProcess() {
|
|
4535
4647
|
if (this.process) {
|
|
4536
4648
|
this.log("Stopping server...");
|
|
4537
4649
|
if (this.process.exitCode !== null || this.process.signalCode !== null) {
|
|
@@ -4604,7 +4716,7 @@ var TestServer = class _TestServer {
|
|
|
4604
4716
|
* Restart the server
|
|
4605
4717
|
*/
|
|
4606
4718
|
async restart() {
|
|
4607
|
-
await this.
|
|
4719
|
+
await this.stopProcess();
|
|
4608
4720
|
await this.startProcess();
|
|
4609
4721
|
}
|
|
4610
4722
|
/**
|
|
@@ -4634,9 +4746,9 @@ var TestServer = class _TestServer {
|
|
|
4634
4746
|
PORT: String(this.options.port)
|
|
4635
4747
|
};
|
|
4636
4748
|
const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
|
|
4637
|
-
if (this.
|
|
4638
|
-
await this.
|
|
4639
|
-
this.
|
|
4749
|
+
if (this.portReleaseSocket) {
|
|
4750
|
+
await this.portReleaseSocket();
|
|
4751
|
+
this.portReleaseSocket = null;
|
|
4640
4752
|
await sleep2(300);
|
|
4641
4753
|
}
|
|
4642
4754
|
this.process = (0, import_child_process.spawn)(this.options.command, [], {
|
|
@@ -4646,9 +4758,8 @@ var TestServer = class _TestServer {
|
|
|
4646
4758
|
detached: true,
|
|
4647
4759
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4648
4760
|
});
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
}
|
|
4761
|
+
this._info.pid = this.process.pid;
|
|
4762
|
+
this._info.shellPid = this.process.pid;
|
|
4652
4763
|
let processExited = false;
|
|
4653
4764
|
let exitCode = null;
|
|
4654
4765
|
let exitError = null;
|
|
@@ -4751,6 +4862,8 @@ ${allLogs}`);
|
|
|
4751
4862
|
});
|
|
4752
4863
|
if (response.ok || response.status === 404) {
|
|
4753
4864
|
this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
|
|
4865
|
+
const listeningPid = await findListeningPid(this.options.port);
|
|
4866
|
+
if (listeningPid !== void 0) this._info.pid = listeningPid;
|
|
4754
4867
|
return;
|
|
4755
4868
|
}
|
|
4756
4869
|
lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
|
|
@@ -4795,6 +4908,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
|
|
|
4795
4908
|
function sleep2(ms) {
|
|
4796
4909
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4797
4910
|
}
|
|
4911
|
+
function findListeningPid(port) {
|
|
4912
|
+
if (process.platform === "win32") return Promise.resolve(void 0);
|
|
4913
|
+
return new Promise((resolve) => {
|
|
4914
|
+
(0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
|
|
4915
|
+
if (error) {
|
|
4916
|
+
resolve(void 0);
|
|
4917
|
+
return;
|
|
4918
|
+
}
|
|
4919
|
+
const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
|
|
4920
|
+
resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
|
|
4921
|
+
});
|
|
4922
|
+
});
|
|
4923
|
+
}
|
|
4798
4924
|
function withWorkspaceProtocolFallback(env, cwd) {
|
|
4799
4925
|
if (findInstalledProtocolPackageDir(cwd)) {
|
|
4800
4926
|
return env;
|
|
@@ -4844,7 +4970,7 @@ function withProtocolNodePathAlias(env, cwd, workspacePackageDir) {
|
|
|
4844
4970
|
const fs = require("node:fs");
|
|
4845
4971
|
const os = require("node:os");
|
|
4846
4972
|
const path = require("node:path");
|
|
4847
|
-
const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0,
|
|
4973
|
+
const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0, import_utils2.sha256Hex)(cwd).slice(0, 12));
|
|
4848
4974
|
const scopeDir = path.join(aliasRoot, "@frontmcp");
|
|
4849
4975
|
const aliasPackageDir = path.join(scopeDir, "protocol");
|
|
4850
4976
|
fs.mkdirSync(scopeDir, { recursive: true });
|
|
@@ -5066,94 +5192,162 @@ function hasMimeType(result, mimeType) {
|
|
|
5066
5192
|
return result.hasMimeType(mimeType);
|
|
5067
5193
|
}
|
|
5068
5194
|
|
|
5069
|
-
// libs/testing/src/fixtures/test-fixture.ts
|
|
5070
|
-
var import_globals = require("@jest/globals");
|
|
5071
|
-
|
|
5072
5195
|
// libs/testing/src/fixtures/gateway-token-binding.ts
|
|
5073
|
-
var
|
|
5196
|
+
var import_utils3 = require("@frontmcp/utils");
|
|
5074
5197
|
function gatewayTokenBinding(serverUrl, entryPath) {
|
|
5075
|
-
const base = (0,
|
|
5076
|
-
const prefix = (0,
|
|
5198
|
+
const base = (0, import_utils3.trimTrailing)(serverUrl, "/");
|
|
5199
|
+
const prefix = (0, import_utils3.trimSlashes)(entryPath ?? "");
|
|
5077
5200
|
const mcpUrl = prefix ? `${base}/${prefix}` : base;
|
|
5078
5201
|
return { issuer: mcpUrl, audience: mcpUrl };
|
|
5079
5202
|
}
|
|
5080
5203
|
|
|
5081
5204
|
// libs/testing/src/fixtures/test-fixture.ts
|
|
5082
|
-
var
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5205
|
+
var jestApi;
|
|
5206
|
+
function jestGlobals() {
|
|
5207
|
+
if (jestApi) return jestApi;
|
|
5208
|
+
try {
|
|
5209
|
+
const api = require("@jest/globals");
|
|
5210
|
+
jestApi = {
|
|
5211
|
+
describe: api["describe"],
|
|
5212
|
+
beforeAll: api["beforeAll"],
|
|
5213
|
+
beforeEach: api["beforeEach"],
|
|
5214
|
+
afterEach: api["afterEach"],
|
|
5215
|
+
afterAll: api["afterAll"],
|
|
5216
|
+
it: api["it"]
|
|
5217
|
+
};
|
|
5218
|
+
return jestApi;
|
|
5219
|
+
} catch (error) {
|
|
5220
|
+
throw new Error(
|
|
5221
|
+
`@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
5222
|
+
{ cause: error }
|
|
5223
|
+
);
|
|
5224
|
+
}
|
|
5095
5225
|
}
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5226
|
+
var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
|
|
5227
|
+
function currentScope() {
|
|
5228
|
+
return scopes[scopes.length - 1];
|
|
5229
|
+
}
|
|
5230
|
+
var serverEntries = /* @__PURE__ */ new Map();
|
|
5231
|
+
function resolveConfig(chain) {
|
|
5232
|
+
const merged = {};
|
|
5233
|
+
for (const scope of chain) {
|
|
5234
|
+
const { env, ...rest } = scope.config;
|
|
5235
|
+
Object.assign(merged, rest);
|
|
5236
|
+
if (env) merged.env = { ...merged.env, ...env };
|
|
5237
|
+
}
|
|
5238
|
+
return merged;
|
|
5239
|
+
}
|
|
5240
|
+
function ownerScopeOf(chain) {
|
|
5241
|
+
for (let i = chain.length - 1; i > 0; i--) {
|
|
5242
|
+
if (chain[i].configured) return chain[i];
|
|
5243
|
+
}
|
|
5244
|
+
return chain[0];
|
|
5245
|
+
}
|
|
5246
|
+
function serverKey(config) {
|
|
5247
|
+
const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
5248
|
+
return JSON.stringify({
|
|
5249
|
+
server: config.server,
|
|
5250
|
+
baseUrl: config.baseUrl,
|
|
5251
|
+
project: config.project,
|
|
5252
|
+
port: config.port,
|
|
5253
|
+
entryPath: config.entryPath,
|
|
5254
|
+
startupTimeout: config.startupTimeout,
|
|
5255
|
+
logLevel: config.logLevel,
|
|
5256
|
+
auth: config.auth,
|
|
5257
|
+
env
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
function serverEnv(config) {
|
|
5261
|
+
const extra = {};
|
|
5262
|
+
if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
|
|
5263
|
+
if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
|
|
5264
|
+
if (Object.keys(extra).length === 0 && !config.env) return void 0;
|
|
5265
|
+
return { ...extra, ...config.env };
|
|
5266
|
+
}
|
|
5267
|
+
async function startServerEntry(config, owner) {
|
|
5268
|
+
let server;
|
|
5269
|
+
let startedByUs = false;
|
|
5270
|
+
if (config.baseUrl && !config.server) {
|
|
5271
|
+
server = TestServer.connect(config.baseUrl);
|
|
5272
|
+
} else if (config.server) {
|
|
5273
|
+
const serverCommand = resolveServerCommand(config.server);
|
|
5274
|
+
const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
5275
|
+
if (isDebug) {
|
|
5276
|
+
console.log(`[TestFixture] Starting server: ${serverCommand}`);
|
|
5277
|
+
}
|
|
5278
|
+
try {
|
|
5279
|
+
server = await TestServer.start({
|
|
5280
|
+
project: config.project,
|
|
5281
|
+
port: config.port,
|
|
5282
|
+
command: serverCommand,
|
|
5283
|
+
env: serverEnv(config),
|
|
5284
|
+
startupTimeout: config.startupTimeout ?? 3e4,
|
|
5285
|
+
debug: isDebug
|
|
5286
|
+
});
|
|
5287
|
+
startedByUs = true;
|
|
5104
5288
|
if (isDebug) {
|
|
5105
|
-
console.log(`[TestFixture]
|
|
5289
|
+
console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
|
|
5106
5290
|
}
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
command: serverCommand,
|
|
5112
|
-
env: currentConfig.env,
|
|
5113
|
-
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
5114
|
-
debug: isDebug
|
|
5115
|
-
});
|
|
5116
|
-
serverStartedByUs = true;
|
|
5117
|
-
if (isDebug) {
|
|
5118
|
-
console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
|
|
5119
|
-
}
|
|
5120
|
-
} catch (error) {
|
|
5121
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5122
|
-
throw new Error(
|
|
5123
|
-
`Failed to start test server.
|
|
5291
|
+
} catch (error) {
|
|
5292
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5293
|
+
throw new Error(
|
|
5294
|
+
`Failed to start test server.
|
|
5124
5295
|
|
|
5125
|
-
Server entry: ${
|
|
5126
|
-
Project: ${
|
|
5296
|
+
Server entry: ${config.server}
|
|
5297
|
+
Project: ${config.project ?? "default"}
|
|
5127
5298
|
Command: ${serverCommand}
|
|
5128
5299
|
|
|
5129
|
-
Error: ${errMsg}
|
|
5130
|
-
|
|
5131
|
-
}
|
|
5132
|
-
} else {
|
|
5133
|
-
throw new Error(
|
|
5134
|
-
'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
|
|
5300
|
+
Error: ${errMsg}`,
|
|
5301
|
+
{ cause: error }
|
|
5135
5302
|
);
|
|
5136
5303
|
}
|
|
5304
|
+
} else {
|
|
5305
|
+
throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
|
|
5137
5306
|
}
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
}
|
|
5307
|
+
const sharedSecret = config.env?.["JWT_SECRET"];
|
|
5308
|
+
const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
|
|
5309
|
+
const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
|
|
5310
|
+
return { server, startedByUs, tokenFactory: tokenFactory2, owner };
|
|
5143
5311
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5312
|
+
function acquireServer(chain) {
|
|
5313
|
+
const config = resolveConfig(chain);
|
|
5314
|
+
const key = serverKey(config);
|
|
5315
|
+
let entry = serverEntries.get(key);
|
|
5316
|
+
if (!entry) {
|
|
5317
|
+
entry = startServerEntry(config, ownerScopeOf(chain));
|
|
5318
|
+
serverEntries.set(key, entry);
|
|
5319
|
+
entry.catch(() => serverEntries.delete(key));
|
|
5320
|
+
}
|
|
5321
|
+
return entry;
|
|
5322
|
+
}
|
|
5323
|
+
async function stopServers(owner) {
|
|
5324
|
+
for (const [key, pending] of Array.from(serverEntries)) {
|
|
5325
|
+
let entry;
|
|
5326
|
+
try {
|
|
5327
|
+
entry = await pending;
|
|
5328
|
+
} catch {
|
|
5329
|
+
serverEntries.delete(key);
|
|
5330
|
+
continue;
|
|
5331
|
+
}
|
|
5332
|
+
if (owner && entry.owner !== owner) continue;
|
|
5333
|
+
serverEntries.delete(key);
|
|
5334
|
+
if (entry.startedByUs) {
|
|
5335
|
+
await entry.server.stop();
|
|
5336
|
+
}
|
|
5151
5337
|
}
|
|
5338
|
+
}
|
|
5339
|
+
var fixtureContexts = /* @__PURE__ */ new WeakMap();
|
|
5340
|
+
async function buildFixtures(chain) {
|
|
5341
|
+
const config = resolveConfig(chain);
|
|
5342
|
+
const entry = await acquireServer(chain);
|
|
5343
|
+
const { server, tokenFactory: tokenFactory2 } = entry;
|
|
5344
|
+
const publicMode = config.publicMode ?? config.auth?.mode === "public";
|
|
5345
|
+
const clientConfig = { ...config, publicMode };
|
|
5152
5346
|
const clientInstance = McpTestClient.create({
|
|
5153
|
-
baseUrl: resolveClientBaseUrl(
|
|
5154
|
-
entryPath:
|
|
5155
|
-
transport:
|
|
5156
|
-
publicMode
|
|
5347
|
+
baseUrl: resolveClientBaseUrl(server, clientConfig),
|
|
5348
|
+
entryPath: clientConfig.entryPath,
|
|
5349
|
+
transport: clientConfig.transport ?? "streamable-http",
|
|
5350
|
+
publicMode
|
|
5157
5351
|
}).build();
|
|
5158
5352
|
try {
|
|
5159
5353
|
await clientInstance.connect();
|
|
@@ -5167,17 +5361,18 @@ async function createTestFixtures() {
|
|
|
5167
5361
|
throw err;
|
|
5168
5362
|
}
|
|
5169
5363
|
}
|
|
5170
|
-
const
|
|
5171
|
-
const
|
|
5172
|
-
return {
|
|
5364
|
+
const context = { extraClients: [] };
|
|
5365
|
+
const fixtures = {
|
|
5173
5366
|
mcp: clientInstance,
|
|
5174
|
-
auth,
|
|
5175
|
-
server
|
|
5367
|
+
auth: createAuthFixture(tokenFactory2),
|
|
5368
|
+
server: createServerFixture(server, clientConfig, clientInstance, context)
|
|
5176
5369
|
};
|
|
5370
|
+
fixtureContexts.set(fixtures, context);
|
|
5371
|
+
return fixtures;
|
|
5177
5372
|
}
|
|
5178
5373
|
async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
5179
|
-
if (testFailed
|
|
5180
|
-
const logs =
|
|
5374
|
+
if (testFailed) {
|
|
5375
|
+
const logs = fixtures.server.getLogs();
|
|
5181
5376
|
if (logs.length > 0) {
|
|
5182
5377
|
console.error("\n[TestFixture] === Server Logs (test failed) ===");
|
|
5183
5378
|
const recentLogs = logs.slice(-50);
|
|
@@ -5188,20 +5383,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
|
5188
5383
|
console.error("[TestFixture] === End Server Logs ===\n");
|
|
5189
5384
|
}
|
|
5190
5385
|
}
|
|
5386
|
+
const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
|
|
5387
|
+
for (const client of extras) {
|
|
5388
|
+
if (client.isConnected()) {
|
|
5389
|
+
await client.disconnect().catch(() => void 0);
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5191
5392
|
if (fixtures.mcp.isConnected()) {
|
|
5192
5393
|
await fixtures.mcp.disconnect();
|
|
5193
5394
|
}
|
|
5194
5395
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
await serverInstance.stop();
|
|
5198
|
-
}
|
|
5199
|
-
serverInstance = null;
|
|
5200
|
-
tokenFactory = null;
|
|
5201
|
-
serverStartedByUs = false;
|
|
5202
|
-
}
|
|
5203
|
-
function resolveClientBaseUrl(server) {
|
|
5204
|
-
return currentConfig.baseUrl ?? server.info.baseUrl;
|
|
5396
|
+
function resolveClientBaseUrl(server, config) {
|
|
5397
|
+
return config.baseUrl ?? server.info.baseUrl;
|
|
5205
5398
|
}
|
|
5206
5399
|
function createAuthFixture(factory) {
|
|
5207
5400
|
const users = {
|
|
@@ -5247,28 +5440,39 @@ function createAuthFixture(factory) {
|
|
|
5247
5440
|
getAudience: () => factory.getAudience()
|
|
5248
5441
|
};
|
|
5249
5442
|
}
|
|
5250
|
-
function createServerFixture(server) {
|
|
5443
|
+
function createServerFixture(server, config, mcp, context) {
|
|
5251
5444
|
return {
|
|
5252
|
-
|
|
5445
|
+
// Live view: `pid` is filled in once the server is listening and follows restarts
|
|
5446
|
+
get info() {
|
|
5447
|
+
return server.info;
|
|
5448
|
+
},
|
|
5253
5449
|
createClient: async (opts) => {
|
|
5254
|
-
|
|
5255
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5256
|
-
entryPath: opts?.entryPath ??
|
|
5450
|
+
const client = await McpTestClient.create({
|
|
5451
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5452
|
+
entryPath: opts?.entryPath ?? config.entryPath,
|
|
5257
5453
|
transport: opts?.transport ?? "streamable-http",
|
|
5258
5454
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
5259
5455
|
clientInfo: opts?.clientInfo,
|
|
5260
|
-
publicMode:
|
|
5456
|
+
publicMode: config.publicMode
|
|
5261
5457
|
}).buildAndConnect();
|
|
5458
|
+
context.extraClients.push(client);
|
|
5459
|
+
return client;
|
|
5262
5460
|
},
|
|
5263
5461
|
createClientBuilder: () => {
|
|
5264
5462
|
const builder = new McpTestClientBuilder({
|
|
5265
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5266
|
-
entryPath:
|
|
5267
|
-
publicMode:
|
|
5463
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5464
|
+
entryPath: config.entryPath,
|
|
5465
|
+
publicMode: config.publicMode
|
|
5268
5466
|
});
|
|
5269
5467
|
return builder;
|
|
5270
5468
|
},
|
|
5271
|
-
restart: () =>
|
|
5469
|
+
restart: async () => {
|
|
5470
|
+
const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
|
|
5471
|
+
await server.restart();
|
|
5472
|
+
for (const client of reconnect) {
|
|
5473
|
+
await client.reconnect();
|
|
5474
|
+
}
|
|
5475
|
+
},
|
|
5272
5476
|
getLogs: () => server.getLogs(),
|
|
5273
5477
|
clearLogs: () => server.clearLogs()
|
|
5274
5478
|
};
|
|
@@ -5279,12 +5483,12 @@ function resolveServerCommand(server) {
|
|
|
5279
5483
|
}
|
|
5280
5484
|
return `npx tsx ${server}`;
|
|
5281
5485
|
}
|
|
5282
|
-
function runWithFixtures(fn) {
|
|
5283
|
-
return async () => {
|
|
5284
|
-
const fixtures = await
|
|
5486
|
+
function runWithFixtures(fn, chain) {
|
|
5487
|
+
return async (...extra) => {
|
|
5488
|
+
const fixtures = await buildFixtures(chain);
|
|
5285
5489
|
let testFailed = false;
|
|
5286
5490
|
try {
|
|
5287
|
-
await fn(fixtures);
|
|
5491
|
+
await fn(fixtures, ...extra);
|
|
5288
5492
|
} catch (error) {
|
|
5289
5493
|
testFailed = true;
|
|
5290
5494
|
throw error;
|
|
@@ -5293,24 +5497,37 @@ function runWithFixtures(fn) {
|
|
|
5293
5497
|
}
|
|
5294
5498
|
};
|
|
5295
5499
|
}
|
|
5500
|
+
function skipTitle(name, scope) {
|
|
5501
|
+
return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
|
|
5502
|
+
}
|
|
5296
5503
|
function testWithFixtures(name, fn) {
|
|
5297
|
-
const
|
|
5504
|
+
const chain = scopes.slice();
|
|
5505
|
+
const scope = currentScope();
|
|
5298
5506
|
if (scope.skipped) {
|
|
5299
|
-
|
|
5507
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5300
5508
|
return;
|
|
5301
5509
|
}
|
|
5302
|
-
|
|
5510
|
+
jestGlobals().it(name, runWithFixtures(fn, chain));
|
|
5303
5511
|
}
|
|
5304
5512
|
function use(config) {
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5513
|
+
const scope = currentScope();
|
|
5514
|
+
scope.config = {
|
|
5515
|
+
...scope.config,
|
|
5516
|
+
...config,
|
|
5517
|
+
env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
|
|
5518
|
+
};
|
|
5519
|
+
scope.configured = true;
|
|
5520
|
+
if (!scope.cleanupRegistered) {
|
|
5521
|
+
scope.cleanupRegistered = true;
|
|
5522
|
+
jestGlobals().afterAll(async () => {
|
|
5523
|
+
await stopServers(scope === scopes[0] ? void 0 : scope);
|
|
5524
|
+
});
|
|
5525
|
+
}
|
|
5309
5526
|
}
|
|
5310
5527
|
function skip(nameOrCondition, fnOrReason) {
|
|
5311
5528
|
if (typeof nameOrCondition === "boolean") {
|
|
5312
5529
|
if (nameOrCondition) {
|
|
5313
|
-
const scope =
|
|
5530
|
+
const scope = currentScope();
|
|
5314
5531
|
scope.skipped = true;
|
|
5315
5532
|
scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
|
|
5316
5533
|
}
|
|
@@ -5321,57 +5538,111 @@ function skip(nameOrCondition, fnOrReason) {
|
|
|
5321
5538
|
`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
|
|
5322
5539
|
);
|
|
5323
5540
|
}
|
|
5324
|
-
|
|
5541
|
+
jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
|
|
5325
5542
|
}
|
|
5326
5543
|
function only(name, fn) {
|
|
5327
|
-
const scope =
|
|
5544
|
+
const scope = currentScope();
|
|
5328
5545
|
if (scope.skipped) {
|
|
5329
|
-
|
|
5546
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
|
|
5330
5547
|
return;
|
|
5331
5548
|
}
|
|
5332
|
-
|
|
5549
|
+
jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
|
|
5333
5550
|
}
|
|
5334
5551
|
function todo(name) {
|
|
5335
|
-
|
|
5552
|
+
jestGlobals().it.todo(name);
|
|
5336
5553
|
}
|
|
5337
|
-
function
|
|
5554
|
+
function each(table, ...tagged) {
|
|
5555
|
+
const chain = scopes.slice();
|
|
5556
|
+
const register = jestGlobals().it.each(table, ...tagged);
|
|
5338
5557
|
return (name, fn) => {
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5558
|
+
const scope = currentScope();
|
|
5559
|
+
if (scope.skipped) {
|
|
5560
|
+
const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
|
|
5561
|
+
skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5562
|
+
return;
|
|
5563
|
+
}
|
|
5564
|
+
register(name, runWithFixtures(fn, chain));
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5567
|
+
function withScope(fn) {
|
|
5568
|
+
return (...args) => {
|
|
5569
|
+
const parent = currentScope();
|
|
5570
|
+
scopes.push({
|
|
5571
|
+
skipped: parent.skipped,
|
|
5572
|
+
reason: parent.reason,
|
|
5573
|
+
config: {},
|
|
5574
|
+
configured: false,
|
|
5575
|
+
cleanupRegistered: false
|
|
5347
5576
|
});
|
|
5577
|
+
try {
|
|
5578
|
+
fn(...args);
|
|
5579
|
+
} finally {
|
|
5580
|
+
scopes.pop();
|
|
5581
|
+
}
|
|
5348
5582
|
};
|
|
5349
5583
|
}
|
|
5350
|
-
function
|
|
5351
|
-
return (
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
});
|
|
5584
|
+
function scopedRegister(pick) {
|
|
5585
|
+
return (name, fn, timeout) => {
|
|
5586
|
+
pick()(name, withScope(fn), timeout);
|
|
5587
|
+
};
|
|
5355
5588
|
}
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
}
|
|
5589
|
+
function scopedEach(pick) {
|
|
5590
|
+
return (...eachArgs) => (name, fn, timeout) => {
|
|
5591
|
+
const register = pick()(...eachArgs);
|
|
5592
|
+
register(name, withScope(fn), timeout);
|
|
5593
|
+
};
|
|
5594
|
+
}
|
|
5595
|
+
var describeWithScope = Object.assign(
|
|
5596
|
+
scopedRegister(() => jestGlobals().describe),
|
|
5597
|
+
{
|
|
5598
|
+
// `describe.skip` still evaluates its callback during collection, so it needs a scope too
|
|
5599
|
+
only: Object.assign(
|
|
5600
|
+
scopedRegister(() => jestGlobals().describe.only),
|
|
5601
|
+
{
|
|
5602
|
+
each: scopedEach(() => jestGlobals().describe.only.each)
|
|
5603
|
+
}
|
|
5604
|
+
),
|
|
5605
|
+
skip: Object.assign(
|
|
5606
|
+
scopedRegister(() => jestGlobals().describe.skip),
|
|
5607
|
+
{
|
|
5608
|
+
each: scopedEach(() => jestGlobals().describe.skip.each)
|
|
5609
|
+
}
|
|
5610
|
+
),
|
|
5611
|
+
each: scopedEach(() => jestGlobals().describe.each)
|
|
5612
|
+
}
|
|
5613
|
+
);
|
|
5361
5614
|
var test = testWithFixtures;
|
|
5362
5615
|
test.use = use;
|
|
5363
|
-
test.describe =
|
|
5364
|
-
test.beforeAll =
|
|
5365
|
-
test.beforeEach =
|
|
5366
|
-
test.afterEach =
|
|
5367
|
-
test.afterAll =
|
|
5616
|
+
test.describe = describeWithScope;
|
|
5617
|
+
test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
|
|
5618
|
+
test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
|
|
5619
|
+
test.afterEach = ((...args) => jestGlobals().afterEach(...args));
|
|
5620
|
+
test.afterAll = ((...args) => jestGlobals().afterAll(...args));
|
|
5368
5621
|
test.skip = skip;
|
|
5369
5622
|
test.only = only;
|
|
5370
5623
|
test.todo = todo;
|
|
5624
|
+
test.each = each;
|
|
5371
5625
|
|
|
5372
5626
|
// libs/testing/src/expect.ts
|
|
5373
|
-
var
|
|
5374
|
-
|
|
5627
|
+
var resolvedExpect;
|
|
5628
|
+
function resolveJestExpect() {
|
|
5629
|
+
if (resolvedExpect) return resolvedExpect;
|
|
5630
|
+
try {
|
|
5631
|
+
resolvedExpect = require("@jest/globals").expect;
|
|
5632
|
+
return resolvedExpect;
|
|
5633
|
+
} catch (error) {
|
|
5634
|
+
throw new Error(
|
|
5635
|
+
`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
5636
|
+
{ cause: error }
|
|
5637
|
+
);
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5640
|
+
var expect = new Proxy(function frontmcpExpect() {
|
|
5641
|
+
}, {
|
|
5642
|
+
apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
|
|
5643
|
+
get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
|
|
5644
|
+
has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
|
|
5645
|
+
});
|
|
5375
5646
|
|
|
5376
5647
|
// libs/testing/src/platform/platform-types.ts
|
|
5377
5648
|
function getPlatformMetaNamespace(platform) {
|
|
@@ -6034,34 +6305,44 @@ var HttpInterceptorImpl = class {
|
|
|
6034
6305
|
}
|
|
6035
6306
|
};
|
|
6036
6307
|
}
|
|
6037
|
-
get(url, response) {
|
|
6308
|
+
get(url, response, options) {
|
|
6038
6309
|
return this.mock({
|
|
6039
|
-
match: { url, method: "GET" },
|
|
6040
|
-
response: normalizeResponse(response)
|
|
6310
|
+
match: { url, method: "GET", ...options?.match },
|
|
6311
|
+
response: normalizeResponse(response),
|
|
6312
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6313
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6041
6314
|
});
|
|
6042
6315
|
}
|
|
6043
|
-
post(url, response) {
|
|
6316
|
+
post(url, response, options) {
|
|
6044
6317
|
return this.mock({
|
|
6045
|
-
match: { url, method: "POST" },
|
|
6046
|
-
response: normalizeResponse(response)
|
|
6318
|
+
match: { url, method: "POST", ...options?.match },
|
|
6319
|
+
response: normalizeResponse(response),
|
|
6320
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6321
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6047
6322
|
});
|
|
6048
6323
|
}
|
|
6049
|
-
put(url, response) {
|
|
6324
|
+
put(url, response, options) {
|
|
6050
6325
|
return this.mock({
|
|
6051
|
-
match: { url, method: "PUT" },
|
|
6052
|
-
response: normalizeResponse(response)
|
|
6326
|
+
match: { url, method: "PUT", ...options?.match },
|
|
6327
|
+
response: normalizeResponse(response),
|
|
6328
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6329
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6053
6330
|
});
|
|
6054
6331
|
}
|
|
6055
|
-
delete(url, response) {
|
|
6332
|
+
delete(url, response, options) {
|
|
6056
6333
|
return this.mock({
|
|
6057
|
-
match: { url, method: "DELETE" },
|
|
6058
|
-
response: normalizeResponse(response)
|
|
6334
|
+
match: { url, method: "DELETE", ...options?.match },
|
|
6335
|
+
response: normalizeResponse(response),
|
|
6336
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6337
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6059
6338
|
});
|
|
6060
6339
|
}
|
|
6061
|
-
any(url, response) {
|
|
6340
|
+
any(url, response, options) {
|
|
6062
6341
|
return this.mock({
|
|
6063
|
-
match: { url },
|
|
6064
|
-
response: normalizeResponse(response)
|
|
6342
|
+
match: { url, ...options?.match },
|
|
6343
|
+
response: normalizeResponse(response),
|
|
6344
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6345
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6065
6346
|
});
|
|
6066
6347
|
}
|
|
6067
6348
|
clear() {
|
|
@@ -6093,6 +6374,9 @@ ${descriptions.join("\n")}`);
|
|
|
6093
6374
|
if (index !== -1) {
|
|
6094
6375
|
activeInterceptors.splice(index, 1);
|
|
6095
6376
|
}
|
|
6377
|
+
if (activeInterceptors.length === 0) {
|
|
6378
|
+
httpMock.disable();
|
|
6379
|
+
}
|
|
6096
6380
|
}
|
|
6097
6381
|
// Internal methods
|
|
6098
6382
|
isActive() {
|
|
@@ -6192,43 +6476,49 @@ ${descriptions.join("\n")}`);
|
|
|
6192
6476
|
}
|
|
6193
6477
|
};
|
|
6194
6478
|
async function interceptedFetch(input, init) {
|
|
6195
|
-
const
|
|
6196
|
-
const
|
|
6479
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
6480
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
|
|
6481
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
6197
6482
|
const headers = {};
|
|
6198
|
-
|
|
6199
|
-
if (
|
|
6200
|
-
|
|
6483
|
+
const collectHeaders = (source) => {
|
|
6484
|
+
if (!source) return;
|
|
6485
|
+
if (source instanceof Headers) {
|
|
6486
|
+
source.forEach((value, key) => {
|
|
6201
6487
|
headers[key.toLowerCase()] = value;
|
|
6202
6488
|
});
|
|
6203
|
-
} else if (Array.isArray(
|
|
6204
|
-
for (const [key, value] of
|
|
6489
|
+
} else if (Array.isArray(source)) {
|
|
6490
|
+
for (const [key, value] of source) {
|
|
6205
6491
|
headers[key.toLowerCase()] = value;
|
|
6206
6492
|
}
|
|
6207
6493
|
} else {
|
|
6208
|
-
for (const [key, value] of Object.entries(
|
|
6494
|
+
for (const [key, value] of Object.entries(source)) {
|
|
6209
6495
|
headers[key.toLowerCase()] = value;
|
|
6210
6496
|
}
|
|
6211
6497
|
}
|
|
6212
|
-
}
|
|
6498
|
+
};
|
|
6499
|
+
collectHeaders(init?.headers ?? request?.headers);
|
|
6213
6500
|
let body;
|
|
6214
6501
|
let rawBody;
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6502
|
+
let bodySource = init?.body;
|
|
6503
|
+
if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
|
|
6504
|
+
bodySource = await request.clone().text();
|
|
6505
|
+
}
|
|
6506
|
+
if (bodySource) {
|
|
6507
|
+
if (typeof bodySource === "string") {
|
|
6508
|
+
rawBody = bodySource;
|
|
6509
|
+
} else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
|
|
6510
|
+
rawBody = new TextDecoder().decode(bodySource);
|
|
6511
|
+
} else if (bodySource instanceof URLSearchParams) {
|
|
6512
|
+
rawBody = bodySource.toString();
|
|
6513
|
+
}
|
|
6514
|
+
if (rawBody !== void 0) {
|
|
6225
6515
|
try {
|
|
6226
6516
|
body = JSON.parse(rawBody);
|
|
6227
6517
|
} catch {
|
|
6228
6518
|
body = rawBody;
|
|
6229
6519
|
}
|
|
6230
6520
|
} else {
|
|
6231
|
-
body =
|
|
6521
|
+
body = bodySource;
|
|
6232
6522
|
}
|
|
6233
6523
|
}
|
|
6234
6524
|
const requestInfo = {
|
|
@@ -6258,8 +6548,20 @@ async function interceptedFetch(input, init) {
|
|
|
6258
6548
|
Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
|
|
6259
6549
|
);
|
|
6260
6550
|
}
|
|
6551
|
+
var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
|
|
6552
|
+
function isResponseDescriptor(value) {
|
|
6553
|
+
const keys = Object.keys(value);
|
|
6554
|
+
if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
|
|
6555
|
+
if ("status" in value) {
|
|
6556
|
+
const status = value["status"];
|
|
6557
|
+
if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
|
|
6558
|
+
}
|
|
6559
|
+
return true;
|
|
6560
|
+
}
|
|
6261
6561
|
function normalizeResponse(response) {
|
|
6262
|
-
|
|
6562
|
+
const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
|
|
6563
|
+
if (isNetworkError) return response;
|
|
6564
|
+
if (Array.isArray(response) || !isResponseDescriptor(response)) {
|
|
6263
6565
|
return { body: response };
|
|
6264
6566
|
}
|
|
6265
6567
|
return response;
|
|
@@ -7593,47 +7895,48 @@ function clearGlobalMeasurements() {
|
|
|
7593
7895
|
}
|
|
7594
7896
|
|
|
7595
7897
|
// libs/testing/src/perf/perf-test.ts
|
|
7596
|
-
var
|
|
7597
|
-
var
|
|
7598
|
-
var
|
|
7599
|
-
var
|
|
7600
|
-
async function
|
|
7601
|
-
if (!
|
|
7602
|
-
|
|
7603
|
-
}
|
|
7604
|
-
if (!
|
|
7605
|
-
if (
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
} else if (
|
|
7609
|
-
const serverCommand = resolveServerCommand2(
|
|
7898
|
+
var currentConfig = {};
|
|
7899
|
+
var serverInstance = null;
|
|
7900
|
+
var tokenFactory = null;
|
|
7901
|
+
var serverStartedByUs = false;
|
|
7902
|
+
async function initializeSharedResources() {
|
|
7903
|
+
if (!tokenFactory) {
|
|
7904
|
+
tokenFactory = new TestTokenFactory();
|
|
7905
|
+
}
|
|
7906
|
+
if (!serverInstance) {
|
|
7907
|
+
if (currentConfig.baseUrl) {
|
|
7908
|
+
serverInstance = TestServer.connect(currentConfig.baseUrl);
|
|
7909
|
+
serverStartedByUs = false;
|
|
7910
|
+
} else if (currentConfig.server) {
|
|
7911
|
+
const serverCommand = resolveServerCommand2(currentConfig.server);
|
|
7610
7912
|
const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
7611
7913
|
if (isDebug) {
|
|
7612
7914
|
console.log(`[PerfTest] Starting server: ${serverCommand}`);
|
|
7613
7915
|
}
|
|
7614
7916
|
try {
|
|
7615
|
-
|
|
7616
|
-
project:
|
|
7617
|
-
port:
|
|
7917
|
+
serverInstance = await TestServer.start({
|
|
7918
|
+
project: currentConfig.project,
|
|
7919
|
+
port: currentConfig.port,
|
|
7618
7920
|
command: serverCommand,
|
|
7619
|
-
env:
|
|
7620
|
-
startupTimeout:
|
|
7921
|
+
env: currentConfig.env,
|
|
7922
|
+
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
7621
7923
|
debug: isDebug
|
|
7622
7924
|
});
|
|
7623
|
-
|
|
7925
|
+
serverStartedByUs = true;
|
|
7624
7926
|
if (isDebug) {
|
|
7625
|
-
console.log(`[PerfTest] Server started at ${
|
|
7927
|
+
console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
|
|
7626
7928
|
}
|
|
7627
7929
|
} catch (error) {
|
|
7628
7930
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
7629
7931
|
throw new Error(
|
|
7630
7932
|
`Failed to start test server.
|
|
7631
7933
|
|
|
7632
|
-
Server entry: ${
|
|
7633
|
-
Project: ${
|
|
7934
|
+
Server entry: ${currentConfig.server}
|
|
7935
|
+
Project: ${currentConfig.project ?? "default"}
|
|
7634
7936
|
Command: ${serverCommand}
|
|
7635
7937
|
|
|
7636
|
-
Error: ${errMsg}
|
|
7938
|
+
Error: ${errMsg}`,
|
|
7939
|
+
{ cause: error }
|
|
7637
7940
|
);
|
|
7638
7941
|
}
|
|
7639
7942
|
} else {
|
|
@@ -7641,23 +7944,23 @@ Error: ${errMsg}`
|
|
|
7641
7944
|
}
|
|
7642
7945
|
}
|
|
7643
7946
|
}
|
|
7644
|
-
async function
|
|
7645
|
-
await
|
|
7646
|
-
if (!
|
|
7947
|
+
async function createTestFixtures(testName) {
|
|
7948
|
+
await initializeSharedResources();
|
|
7949
|
+
if (!serverInstance) {
|
|
7647
7950
|
throw new Error("Server instance not initialized");
|
|
7648
7951
|
}
|
|
7649
|
-
if (!
|
|
7952
|
+
if (!tokenFactory) {
|
|
7650
7953
|
throw new Error("Token factory not initialized");
|
|
7651
7954
|
}
|
|
7652
7955
|
const clientInstance = await McpTestClient.create({
|
|
7653
|
-
baseUrl:
|
|
7654
|
-
transport:
|
|
7655
|
-
publicMode:
|
|
7956
|
+
baseUrl: serverInstance.info.baseUrl,
|
|
7957
|
+
transport: currentConfig.transport ?? "streamable-http",
|
|
7958
|
+
publicMode: currentConfig.publicMode
|
|
7656
7959
|
}).buildAndConnect();
|
|
7657
|
-
const auth = createAuthFixture2(
|
|
7658
|
-
const server = createServerFixture2(
|
|
7659
|
-
const perfImpl = createPerfFixtures(testName,
|
|
7660
|
-
if (
|
|
7960
|
+
const auth = createAuthFixture2(tokenFactory);
|
|
7961
|
+
const server = createServerFixture2(serverInstance);
|
|
7962
|
+
const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
|
|
7963
|
+
if (currentConfig.forceGcOnBaseline !== false) {
|
|
7661
7964
|
await perfImpl.baseline();
|
|
7662
7965
|
}
|
|
7663
7966
|
return {
|
|
@@ -7673,8 +7976,8 @@ async function createTestFixtures2(testName) {
|
|
|
7673
7976
|
async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
7674
7977
|
const measurement = perfImpl.buildMeasurement();
|
|
7675
7978
|
addGlobalMeasurement(measurement);
|
|
7676
|
-
if (testFailed &&
|
|
7677
|
-
const logs =
|
|
7979
|
+
if (testFailed && serverInstance) {
|
|
7980
|
+
const logs = serverInstance.getLogs();
|
|
7678
7981
|
if (logs.length > 0) {
|
|
7679
7982
|
console.error("\n[PerfTest] === Server Logs (test failed) ===");
|
|
7680
7983
|
const recentLogs = logs.slice(-50);
|
|
@@ -7689,13 +7992,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
|
7689
7992
|
await fixtures.mcp.disconnect();
|
|
7690
7993
|
}
|
|
7691
7994
|
}
|
|
7692
|
-
async function
|
|
7693
|
-
if (
|
|
7694
|
-
await
|
|
7995
|
+
async function cleanupSharedResources() {
|
|
7996
|
+
if (serverInstance && serverStartedByUs) {
|
|
7997
|
+
await serverInstance.stop();
|
|
7695
7998
|
}
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7999
|
+
serverInstance = null;
|
|
8000
|
+
tokenFactory = null;
|
|
8001
|
+
serverStartedByUs = false;
|
|
7699
8002
|
}
|
|
7700
8003
|
function createAuthFixture2(factory) {
|
|
7701
8004
|
const users = {
|
|
@@ -7750,13 +8053,13 @@ function createServerFixture2(server) {
|
|
|
7750
8053
|
transport: opts?.transport ?? "streamable-http",
|
|
7751
8054
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
7752
8055
|
clientInfo: opts?.clientInfo,
|
|
7753
|
-
publicMode:
|
|
8056
|
+
publicMode: currentConfig.publicMode
|
|
7754
8057
|
}).buildAndConnect();
|
|
7755
8058
|
},
|
|
7756
8059
|
createClientBuilder: () => {
|
|
7757
8060
|
return new McpTestClientBuilder({
|
|
7758
8061
|
baseUrl: server.info.baseUrl,
|
|
7759
|
-
publicMode:
|
|
8062
|
+
publicMode: currentConfig.publicMode
|
|
7760
8063
|
});
|
|
7761
8064
|
},
|
|
7762
8065
|
restart: () => server.restart(),
|
|
@@ -7772,7 +8075,7 @@ function resolveServerCommand2(server) {
|
|
|
7772
8075
|
}
|
|
7773
8076
|
function perfTestWithFixtures(name, fn) {
|
|
7774
8077
|
it(name, async () => {
|
|
7775
|
-
const { fixtures, perfImpl } = await
|
|
8078
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7776
8079
|
let testFailed = false;
|
|
7777
8080
|
try {
|
|
7778
8081
|
await fn(fixtures);
|
|
@@ -7785,14 +8088,14 @@ function perfTestWithFixtures(name, fn) {
|
|
|
7785
8088
|
});
|
|
7786
8089
|
}
|
|
7787
8090
|
function use2(config) {
|
|
7788
|
-
|
|
8091
|
+
currentConfig = { ...currentConfig, ...config };
|
|
7789
8092
|
afterAll(async () => {
|
|
7790
|
-
await
|
|
8093
|
+
await cleanupSharedResources();
|
|
7791
8094
|
});
|
|
7792
8095
|
}
|
|
7793
8096
|
function skip2(name, fn) {
|
|
7794
8097
|
it.skip(name, async () => {
|
|
7795
|
-
const { fixtures, perfImpl } = await
|
|
8098
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7796
8099
|
let testFailed = false;
|
|
7797
8100
|
try {
|
|
7798
8101
|
await fn(fixtures);
|
|
@@ -7806,7 +8109,7 @@ function skip2(name, fn) {
|
|
|
7806
8109
|
}
|
|
7807
8110
|
function only2(name, fn) {
|
|
7808
8111
|
it.only(name, async () => {
|
|
7809
|
-
const { fixtures, perfImpl } = await
|
|
8112
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7810
8113
|
let testFailed = false;
|
|
7811
8114
|
try {
|
|
7812
8115
|
await fn(fixtures);
|
|
@@ -7823,16 +8126,19 @@ function todo2(name) {
|
|
|
7823
8126
|
}
|
|
7824
8127
|
var perfTest = perfTestWithFixtures;
|
|
7825
8128
|
perfTest.use = use2;
|
|
7826
|
-
|
|
7827
|
-
perfTest
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
8129
|
+
for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
|
|
8130
|
+
Object.defineProperty(perfTest, hook, {
|
|
8131
|
+
get: () => globalThis[hook],
|
|
8132
|
+
enumerable: true,
|
|
8133
|
+
configurable: true
|
|
8134
|
+
});
|
|
8135
|
+
}
|
|
7831
8136
|
perfTest.skip = skip2;
|
|
7832
8137
|
perfTest.only = only2;
|
|
7833
8138
|
perfTest.todo = todo2;
|
|
7834
8139
|
|
|
7835
8140
|
// libs/testing/src/perf/baseline-store.ts
|
|
8141
|
+
var import_utils4 = require("@frontmcp/utils");
|
|
7836
8142
|
var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
|
|
7837
8143
|
var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
|
|
7838
8144
|
var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
|
|
@@ -7847,11 +8153,10 @@ var BaselineStore = class {
|
|
|
7847
8153
|
*/
|
|
7848
8154
|
async load() {
|
|
7849
8155
|
try {
|
|
7850
|
-
|
|
7851
|
-
if (!await fileExists(this.baselinePath)) {
|
|
8156
|
+
if (!await (0, import_utils4.fileExists)(this.baselinePath)) {
|
|
7852
8157
|
return null;
|
|
7853
8158
|
}
|
|
7854
|
-
const content = await readFile(this.baselinePath);
|
|
8159
|
+
const content = await (0, import_utils4.readFile)(this.baselinePath);
|
|
7855
8160
|
this.baseline = JSON.parse(content);
|
|
7856
8161
|
return this.baseline;
|
|
7857
8162
|
} catch {
|
|
@@ -7862,10 +8167,9 @@ var BaselineStore = class {
|
|
|
7862
8167
|
* Save baseline to local file.
|
|
7863
8168
|
*/
|
|
7864
8169
|
async save(baseline) {
|
|
7865
|
-
const { writeFile, ensureDir } = await import("@frontmcp/utils");
|
|
7866
8170
|
const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
|
|
7867
|
-
await ensureDir(dir);
|
|
7868
|
-
await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
8171
|
+
await (0, import_utils4.ensureDir)(dir);
|
|
8172
|
+
await (0, import_utils4.writeFile)(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
7869
8173
|
this.baseline = baseline;
|
|
7870
8174
|
}
|
|
7871
8175
|
/**
|
|
@@ -8118,6 +8422,7 @@ function getRegressionDetector(config) {
|
|
|
8118
8422
|
}
|
|
8119
8423
|
|
|
8120
8424
|
// libs/testing/src/perf/report-generator.ts
|
|
8425
|
+
var import_utils5 = require("@frontmcp/utils");
|
|
8121
8426
|
var ReportGenerator = class {
|
|
8122
8427
|
detector;
|
|
8123
8428
|
constructor() {
|
|
@@ -8373,14 +8678,13 @@ var ReportGenerator = class {
|
|
|
8373
8678
|
}
|
|
8374
8679
|
};
|
|
8375
8680
|
async function saveReports(measurements, outputDir, baseline, gitInfo) {
|
|
8376
|
-
|
|
8377
|
-
await ensureDir(outputDir);
|
|
8681
|
+
await (0, import_utils5.ensureDir)(outputDir);
|
|
8378
8682
|
const generator = new ReportGenerator();
|
|
8379
8683
|
const report = generator.generateReport(measurements, baseline, gitInfo);
|
|
8380
8684
|
const jsonPath = `${outputDir}/report.json`;
|
|
8381
8685
|
const markdownPath = `${outputDir}/report.md`;
|
|
8382
|
-
await writeFile(jsonPath, generator.generateJsonReport(report));
|
|
8383
|
-
await writeFile(markdownPath, generator.generateMarkdownReport(report));
|
|
8686
|
+
await (0, import_utils5.writeFile)(jsonPath, generator.generateJsonReport(report));
|
|
8687
|
+
await (0, import_utils5.writeFile)(markdownPath, generator.generateMarkdownReport(report));
|
|
8384
8688
|
return { jsonPath, markdownPath };
|
|
8385
8689
|
}
|
|
8386
8690
|
function createReportGenerator() {
|