@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/esm/index.mjs
CHANGED
|
@@ -341,6 +341,32 @@ var interceptors = {
|
|
|
341
341
|
}
|
|
342
342
|
};
|
|
343
343
|
|
|
344
|
+
// libs/testing/src/transport/error-utils.ts
|
|
345
|
+
function errorName(err) {
|
|
346
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
347
|
+
const name = err.name;
|
|
348
|
+
return typeof name === "string" ? name : void 0;
|
|
349
|
+
}
|
|
350
|
+
function isAbortError(err) {
|
|
351
|
+
return errorName(err) === "AbortError";
|
|
352
|
+
}
|
|
353
|
+
function errorMessage(err, fallback = "Unknown error") {
|
|
354
|
+
if (typeof err === "string" && err) return err;
|
|
355
|
+
if (typeof err === "object" && err !== null) {
|
|
356
|
+
const message = err.message;
|
|
357
|
+
if (typeof message === "string" && message) return message;
|
|
358
|
+
}
|
|
359
|
+
return fallback;
|
|
360
|
+
}
|
|
361
|
+
var interceptedErrors = /* @__PURE__ */ new WeakSet();
|
|
362
|
+
function markInterceptedError(err) {
|
|
363
|
+
interceptedErrors.add(err);
|
|
364
|
+
return err;
|
|
365
|
+
}
|
|
366
|
+
function isInterceptedError(err) {
|
|
367
|
+
return typeof err === "object" && err !== null && interceptedErrors.has(err);
|
|
368
|
+
}
|
|
369
|
+
|
|
344
370
|
// libs/testing/src/transport/streamable-http.transport.ts
|
|
345
371
|
var DEFAULT_TIMEOUT = 3e4;
|
|
346
372
|
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
@@ -449,7 +475,7 @@ var StreamableHttpTransport = class {
|
|
|
449
475
|
return mockResponse2;
|
|
450
476
|
}
|
|
451
477
|
case "error":
|
|
452
|
-
throw interceptResult.error;
|
|
478
|
+
throw markInterceptedError(interceptResult.error);
|
|
453
479
|
case "continue":
|
|
454
480
|
message = interceptResult.request;
|
|
455
481
|
break;
|
|
@@ -510,7 +536,7 @@ var StreamableHttpTransport = class {
|
|
|
510
536
|
return jsonResponse;
|
|
511
537
|
} catch (error) {
|
|
512
538
|
clearTimeout(timeoutId);
|
|
513
|
-
if (error
|
|
539
|
+
if (isAbortError(error)) {
|
|
514
540
|
return {
|
|
515
541
|
jsonrpc: "2.0",
|
|
516
542
|
id: message.id ?? null,
|
|
@@ -549,7 +575,7 @@ var StreamableHttpTransport = class {
|
|
|
549
575
|
}
|
|
550
576
|
} catch (error) {
|
|
551
577
|
clearTimeout(timeoutId);
|
|
552
|
-
if (error
|
|
578
|
+
if (!isAbortError(error)) {
|
|
553
579
|
throw error;
|
|
554
580
|
}
|
|
555
581
|
}
|
|
@@ -590,7 +616,7 @@ var StreamableHttpTransport = class {
|
|
|
590
616
|
error: {
|
|
591
617
|
code: -32700,
|
|
592
618
|
message: "Parse error",
|
|
593
|
-
data: error
|
|
619
|
+
data: errorMessage(error)
|
|
594
620
|
}
|
|
595
621
|
};
|
|
596
622
|
}
|
|
@@ -960,7 +986,7 @@ var StreamableHttpTransport = class {
|
|
|
960
986
|
if (this.config.clientInfo) {
|
|
961
987
|
headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
|
|
962
988
|
}
|
|
963
|
-
if (this.authToken
|
|
989
|
+
if (this.authToken) {
|
|
964
990
|
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
965
991
|
}
|
|
966
992
|
if (this.sessionId) {
|
|
@@ -1303,6 +1329,15 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
1303
1329
|
version: "0.4.0"
|
|
1304
1330
|
};
|
|
1305
1331
|
var MAX_LIST_PAGES = 1e3;
|
|
1332
|
+
function isAuthFailure(error) {
|
|
1333
|
+
return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
|
|
1334
|
+
}
|
|
1335
|
+
function describeInitializeError(error) {
|
|
1336
|
+
const message = error?.message ?? "Unknown error";
|
|
1337
|
+
const data = error?.data;
|
|
1338
|
+
const detail = typeof data === "string" ? data.trim() : "";
|
|
1339
|
+
return detail ? `${message} - ${detail.slice(0, 500)}` : message;
|
|
1340
|
+
}
|
|
1306
1341
|
function extractEntryPaths(data) {
|
|
1307
1342
|
let payload = data;
|
|
1308
1343
|
if (typeof payload === "string") {
|
|
@@ -1389,19 +1424,22 @@ var McpTestClient = class {
|
|
|
1389
1424
|
async connect() {
|
|
1390
1425
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
1391
1426
|
this.triedDiscoveredEntryPath = false;
|
|
1427
|
+
this.initResult = null;
|
|
1392
1428
|
this.transport = this.createTransport();
|
|
1393
1429
|
await this.transport.connect();
|
|
1394
1430
|
const maxRetries = 3;
|
|
1395
1431
|
const retryDelayMs = 500;
|
|
1396
1432
|
let lastError;
|
|
1397
1433
|
let reportedEntryPaths;
|
|
1434
|
+
let attemptsMade = 0;
|
|
1398
1435
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1436
|
+
attemptsMade = attempt;
|
|
1399
1437
|
const initResponse = await this.initialize();
|
|
1400
1438
|
if (initResponse.success && initResponse.data) {
|
|
1401
1439
|
this.initResult = initResponse.data;
|
|
1402
1440
|
break;
|
|
1403
1441
|
}
|
|
1404
|
-
lastError = initResponse.error
|
|
1442
|
+
lastError = describeInitializeError(initResponse.error);
|
|
1405
1443
|
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
1406
1444
|
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
1407
1445
|
this.triedDiscoveredEntryPath = true;
|
|
@@ -1413,6 +1451,7 @@ var McpTestClient = class {
|
|
|
1413
1451
|
await this.transport.connect();
|
|
1414
1452
|
continue;
|
|
1415
1453
|
}
|
|
1454
|
+
if (isAuthFailure(initResponse.error)) break;
|
|
1416
1455
|
if (attempt < maxRetries) {
|
|
1417
1456
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
1418
1457
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
@@ -1421,7 +1460,7 @@ var McpTestClient = class {
|
|
|
1421
1460
|
if (!this.initResult) {
|
|
1422
1461
|
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
1462
|
throw new Error(
|
|
1424
|
-
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${
|
|
1463
|
+
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
|
|
1425
1464
|
);
|
|
1426
1465
|
}
|
|
1427
1466
|
this._sessionId = this.transport.getSessionId();
|
|
@@ -1491,18 +1530,47 @@ var McpTestClient = class {
|
|
|
1491
1530
|
return this._authState;
|
|
1492
1531
|
}
|
|
1493
1532
|
/**
|
|
1494
|
-
* Authenticate with a token
|
|
1533
|
+
* Authenticate with a token.
|
|
1534
|
+
*
|
|
1535
|
+
* When connected, this opens a NEW session with the token (the previous session was created
|
|
1536
|
+
* for the previous identity) and rejects if the server refuses it — for example because the
|
|
1537
|
+
* token is expired or badly signed. On rejection the client keeps its previous identity and
|
|
1538
|
+
* session. The token is kept in the client config, so `reconnect()` keeps using it.
|
|
1495
1539
|
*/
|
|
1496
1540
|
async authenticate(token) {
|
|
1541
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
1542
|
+
throw new Error("authenticate() requires a non-empty token");
|
|
1543
|
+
}
|
|
1544
|
+
const previousAuthState = this._authState;
|
|
1545
|
+
const previousConfigAuth = this.config.auth;
|
|
1546
|
+
this.config.auth = { ...previousConfigAuth, token };
|
|
1497
1547
|
this._authState = {
|
|
1498
1548
|
isAnonymous: false,
|
|
1499
1549
|
token,
|
|
1500
1550
|
scopes: this.parseScopesFromToken(token),
|
|
1501
1551
|
user: this.parseUserFromToken(token)
|
|
1502
1552
|
};
|
|
1503
|
-
|
|
1504
|
-
|
|
1553
|
+
const previousTransport = this.transport;
|
|
1554
|
+
if (!previousTransport) {
|
|
1555
|
+
this.log("debug", "Authentication updated (not connected yet)");
|
|
1556
|
+
return;
|
|
1505
1557
|
}
|
|
1558
|
+
const previousInit = this.initResult;
|
|
1559
|
+
const previousSessionId = this._sessionId;
|
|
1560
|
+
const previousSessionInfo = this._sessionInfo;
|
|
1561
|
+
try {
|
|
1562
|
+
await this.connect();
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
await this.transport?.close().catch(() => void 0);
|
|
1565
|
+
this.transport = previousTransport;
|
|
1566
|
+
this.initResult = previousInit;
|
|
1567
|
+
this._sessionId = previousSessionId;
|
|
1568
|
+
this._sessionInfo = previousSessionInfo;
|
|
1569
|
+
this.config.auth = previousConfigAuth;
|
|
1570
|
+
this._authState = previousAuthState;
|
|
1571
|
+
throw err;
|
|
1572
|
+
}
|
|
1573
|
+
await previousTransport.close().catch(() => void 0);
|
|
1506
1574
|
this.log("debug", "Authentication updated");
|
|
1507
1575
|
}
|
|
1508
1576
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -2126,10 +2194,11 @@ var McpTestClient = class {
|
|
|
2126
2194
|
requestId: id
|
|
2127
2195
|
};
|
|
2128
2196
|
} catch (err) {
|
|
2197
|
+
if (isInterceptedError(err)) throw err;
|
|
2129
2198
|
const durationMs = Date.now() - start;
|
|
2130
2199
|
const error = {
|
|
2131
2200
|
code: -32603,
|
|
2132
|
-
message: err
|
|
2201
|
+
message: errorMessage(err)
|
|
2133
2202
|
};
|
|
2134
2203
|
return {
|
|
2135
2204
|
success: false,
|
|
@@ -2277,12 +2346,12 @@ var McpTestClient = class {
|
|
|
2277
2346
|
const payload = this.decodeJwtPayload(token);
|
|
2278
2347
|
if (!payload) return [];
|
|
2279
2348
|
const scope = payload["scope"];
|
|
2280
|
-
const
|
|
2349
|
+
const scopes2 = payload["scopes"];
|
|
2281
2350
|
if (typeof scope === "string") {
|
|
2282
2351
|
return scope.split(" ");
|
|
2283
2352
|
}
|
|
2284
|
-
if (Array.isArray(
|
|
2285
|
-
return
|
|
2353
|
+
if (Array.isArray(scopes2)) {
|
|
2354
|
+
return scopes2;
|
|
2286
2355
|
}
|
|
2287
2356
|
return [];
|
|
2288
2357
|
} catch {
|
|
@@ -2414,14 +2483,15 @@ var TestTokenFactory = class {
|
|
|
2414
2483
|
*/
|
|
2415
2484
|
async createTestToken(options) {
|
|
2416
2485
|
await this.ensureKeys();
|
|
2417
|
-
const
|
|
2486
|
+
const nowMs = Date.now();
|
|
2487
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
2418
2488
|
const exp = options.exp ?? 3600;
|
|
2419
2489
|
const payload = {
|
|
2420
2490
|
iss: options.iss ?? this.issuer,
|
|
2421
2491
|
sub: options.sub,
|
|
2422
2492
|
aud: options.aud ?? this.audience,
|
|
2423
|
-
iat
|
|
2424
|
-
exp:
|
|
2493
|
+
iat,
|
|
2494
|
+
exp: Math.ceil(nowMs / 1e3) + exp,
|
|
2425
2495
|
scope: options.scopes?.join(" "),
|
|
2426
2496
|
...options.claims
|
|
2427
2497
|
};
|
|
@@ -2446,10 +2516,10 @@ var TestTokenFactory = class {
|
|
|
2446
2516
|
/**
|
|
2447
2517
|
* Create a regular user token
|
|
2448
2518
|
*/
|
|
2449
|
-
async createUserToken(sub = "user-001",
|
|
2519
|
+
async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
|
|
2450
2520
|
return this.createTestToken({
|
|
2451
2521
|
sub,
|
|
2452
|
-
scopes,
|
|
2522
|
+
scopes: scopes2,
|
|
2453
2523
|
claims: {
|
|
2454
2524
|
email: "user@test.local",
|
|
2455
2525
|
name: "Test User",
|
|
@@ -2460,10 +2530,11 @@ var TestTokenFactory = class {
|
|
|
2460
2530
|
/**
|
|
2461
2531
|
* Create an anonymous user token
|
|
2462
2532
|
*/
|
|
2463
|
-
async createAnonymousToken() {
|
|
2533
|
+
async createAnonymousToken(expiresIn) {
|
|
2464
2534
|
return this.createTestToken({
|
|
2465
2535
|
sub: `anon:${Date.now()}`,
|
|
2466
2536
|
scopes: ["anonymous"],
|
|
2537
|
+
exp: expiresIn,
|
|
2467
2538
|
claims: {
|
|
2468
2539
|
name: "Anonymous",
|
|
2469
2540
|
role: "anonymous"
|
|
@@ -2674,17 +2745,7 @@ function createTestUser(overrides) {
|
|
|
2674
2745
|
|
|
2675
2746
|
// libs/testing/src/auth/mock-oauth-server.ts
|
|
2676
2747
|
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
|
-
}
|
|
2748
|
+
import { base64urlEncode, randomBytes, sha256Base64url } from "@frontmcp/utils";
|
|
2688
2749
|
var MockOAuthServer = class {
|
|
2689
2750
|
tokenFactory;
|
|
2690
2751
|
options;
|
|
@@ -2699,8 +2760,8 @@ var MockOAuthServer = class {
|
|
|
2699
2760
|
accessTokenTtlSeconds;
|
|
2700
2761
|
/** Refresh token TTL in seconds */
|
|
2701
2762
|
refreshTokenTtlSeconds;
|
|
2702
|
-
constructor(
|
|
2703
|
-
this.tokenFactory =
|
|
2763
|
+
constructor(tokenFactory2, options = {}) {
|
|
2764
|
+
this.tokenFactory = tokenFactory2;
|
|
2704
2765
|
this.options = options;
|
|
2705
2766
|
this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
|
|
2706
2767
|
this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
|
|
@@ -2957,14 +3018,14 @@ ${error.stack}` : String(error);
|
|
|
2957
3018
|
return;
|
|
2958
3019
|
}
|
|
2959
3020
|
const code = this.generateCode();
|
|
2960
|
-
const
|
|
3021
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
2961
3022
|
this.authCodes.set(code, {
|
|
2962
3023
|
code,
|
|
2963
3024
|
clientId,
|
|
2964
3025
|
redirectUri,
|
|
2965
3026
|
codeChallenge,
|
|
2966
3027
|
codeChallengeMethod,
|
|
2967
|
-
scopes,
|
|
3028
|
+
scopes: scopes2,
|
|
2968
3029
|
user: testUser,
|
|
2969
3030
|
state,
|
|
2970
3031
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3004,7 +3065,7 @@ ${error.stack}` : String(error);
|
|
|
3004
3065
|
}
|
|
3005
3066
|
const grantType = params.get("grant_type");
|
|
3006
3067
|
if (grantType === "anonymous") {
|
|
3007
|
-
const token = await this.tokenFactory.createAnonymousToken();
|
|
3068
|
+
const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
|
|
3008
3069
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3009
3070
|
res.end(
|
|
3010
3071
|
JSON.stringify({
|
|
@@ -3136,6 +3197,8 @@ ${error.stack}` : String(error);
|
|
|
3136
3197
|
codeRecord.used = true;
|
|
3137
3198
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3138
3199
|
sub: codeRecord.user.sub,
|
|
3200
|
+
scopes: codeRecord.scopes,
|
|
3201
|
+
exp: this.accessTokenTtlSeconds,
|
|
3139
3202
|
claims: {
|
|
3140
3203
|
email: codeRecord.user.email,
|
|
3141
3204
|
name: codeRecord.user.name,
|
|
@@ -3144,6 +3207,7 @@ ${error.stack}` : String(error);
|
|
|
3144
3207
|
});
|
|
3145
3208
|
const idToken = await this.tokenFactory.createTestToken({
|
|
3146
3209
|
sub: codeRecord.user.sub,
|
|
3210
|
+
exp: this.accessTokenTtlSeconds,
|
|
3147
3211
|
claims: {
|
|
3148
3212
|
email: codeRecord.user.email,
|
|
3149
3213
|
name: codeRecord.user.name,
|
|
@@ -3221,6 +3285,8 @@ ${error.stack}` : String(error);
|
|
|
3221
3285
|
}
|
|
3222
3286
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3223
3287
|
sub: tokenRecord.user.sub,
|
|
3288
|
+
scopes: tokenRecord.scopes,
|
|
3289
|
+
exp: this.accessTokenTtlSeconds,
|
|
3224
3290
|
claims: {
|
|
3225
3291
|
email: tokenRecord.user.email,
|
|
3226
3292
|
name: tokenRecord.user.name,
|
|
@@ -3325,14 +3391,14 @@ ${error.stack}` : String(error);
|
|
|
3325
3391
|
name
|
|
3326
3392
|
};
|
|
3327
3393
|
const code = this.generateCode();
|
|
3328
|
-
const
|
|
3394
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3329
3395
|
this.authCodes.set(code, {
|
|
3330
3396
|
code,
|
|
3331
3397
|
clientId,
|
|
3332
3398
|
redirectUri,
|
|
3333
3399
|
codeChallenge,
|
|
3334
3400
|
codeChallengeMethod,
|
|
3335
|
-
scopes,
|
|
3401
|
+
scopes: scopes2,
|
|
3336
3402
|
user: testUser,
|
|
3337
3403
|
state,
|
|
3338
3404
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3438,8 +3504,7 @@ ${error.stack}` : String(error);
|
|
|
3438
3504
|
* Generate a random authorization code
|
|
3439
3505
|
*/
|
|
3440
3506
|
async generateCodeAsync() {
|
|
3441
|
-
|
|
3442
|
-
return _base64urlEncode(_randomBytes(32));
|
|
3507
|
+
return base64urlEncode(randomBytes(32));
|
|
3443
3508
|
}
|
|
3444
3509
|
/**
|
|
3445
3510
|
* Generate a random authorization code (sync wrapper for compatibility)
|
|
@@ -3455,8 +3520,7 @@ ${error.stack}` : String(error);
|
|
|
3455
3520
|
*/
|
|
3456
3521
|
async computeCodeChallengeAsync(verifier, method) {
|
|
3457
3522
|
if (method === "S256") {
|
|
3458
|
-
|
|
3459
|
-
return _sha256Base64url(verifier);
|
|
3523
|
+
return sha256Base64url(verifier);
|
|
3460
3524
|
}
|
|
3461
3525
|
return verifier;
|
|
3462
3526
|
}
|
|
@@ -4043,7 +4107,7 @@ var MockCimdServer = class {
|
|
|
4043
4107
|
};
|
|
4044
4108
|
|
|
4045
4109
|
// libs/testing/src/server/test-server.ts
|
|
4046
|
-
import { spawn } from "child_process";
|
|
4110
|
+
import { execFile, spawn } from "child_process";
|
|
4047
4111
|
import { sha256Hex } from "@frontmcp/utils";
|
|
4048
4112
|
|
|
4049
4113
|
// libs/testing/src/errors/index.ts
|
|
@@ -4150,6 +4214,8 @@ var E2E_PORT_RANGES = {
|
|
|
4150
4214
|
"demo-e2e-distributed": { start: 50440, size: 20 },
|
|
4151
4215
|
// Protocol revision E2E tests (50460-50479)
|
|
4152
4216
|
"demo-e2e-protocol-20260728": { start: 50460, size: 20 },
|
|
4217
|
+
// Testing library E2E tests (50480-50499)
|
|
4218
|
+
"demo-e2e-testing": { start: 50480, size: 20 },
|
|
4153
4219
|
// Mock servers and utilities (50900-50999)
|
|
4154
4220
|
"mock-oauth": { start: 50900, size: 10 },
|
|
4155
4221
|
"mock-api": { start: 50910, size: 10 },
|
|
@@ -4158,6 +4224,66 @@ var E2E_PORT_RANGES = {
|
|
|
4158
4224
|
default: { start: 51e3, size: 100 }
|
|
4159
4225
|
};
|
|
4160
4226
|
var reservedPorts = /* @__PURE__ */ new Map();
|
|
4227
|
+
var lockedPorts = /* @__PURE__ */ new Set();
|
|
4228
|
+
var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
4229
|
+
function lockDir() {
|
|
4230
|
+
try {
|
|
4231
|
+
const os = __require("node:os");
|
|
4232
|
+
const path = __require("node:path");
|
|
4233
|
+
return path.join(os.tmpdir(), "frontmcp-testing-ports");
|
|
4234
|
+
} catch {
|
|
4235
|
+
return void 0;
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
function pidIsAlive(pid) {
|
|
4239
|
+
try {
|
|
4240
|
+
process.kill(pid, 0);
|
|
4241
|
+
return true;
|
|
4242
|
+
} catch (error) {
|
|
4243
|
+
return error.code === "EPERM";
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
function acquirePortLock(port) {
|
|
4247
|
+
if (lockedPorts.has(port)) return false;
|
|
4248
|
+
let fs;
|
|
4249
|
+
let file;
|
|
4250
|
+
try {
|
|
4251
|
+
fs = __require("node:fs");
|
|
4252
|
+
const dir = lockDir();
|
|
4253
|
+
if (!dir) return true;
|
|
4254
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4255
|
+
file = __require("node:path").join(dir, `${port}.lock`);
|
|
4256
|
+
} catch {
|
|
4257
|
+
return true;
|
|
4258
|
+
}
|
|
4259
|
+
const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
|
|
4260
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4261
|
+
try {
|
|
4262
|
+
fs.writeFileSync(file, payload, { flag: "wx" });
|
|
4263
|
+
lockedPorts.add(port);
|
|
4264
|
+
return true;
|
|
4265
|
+
} catch (error) {
|
|
4266
|
+
if (error.code !== "EEXIST") return true;
|
|
4267
|
+
try {
|
|
4268
|
+
const held = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
4269
|
+
const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
|
|
4270
|
+
if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
|
|
4271
|
+
fs.unlinkSync(file);
|
|
4272
|
+
} catch {
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
return false;
|
|
4277
|
+
}
|
|
4278
|
+
function releasePortLock(port) {
|
|
4279
|
+
if (!lockedPorts.delete(port)) return;
|
|
4280
|
+
try {
|
|
4281
|
+
const fs = __require("node:fs");
|
|
4282
|
+
const dir = lockDir();
|
|
4283
|
+
if (dir) fs.unlinkSync(__require("node:path").join(dir, `${port}.lock`));
|
|
4284
|
+
} catch {
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4161
4287
|
var projectPortIndex = /* @__PURE__ */ new Map();
|
|
4162
4288
|
function getPortRange(project) {
|
|
4163
4289
|
const key = project;
|
|
@@ -4168,15 +4294,22 @@ function getPortRange(project) {
|
|
|
4168
4294
|
}
|
|
4169
4295
|
async function reservePort(project, preferredPort) {
|
|
4170
4296
|
const range = getPortRange(project);
|
|
4171
|
-
|
|
4297
|
+
const handle = (port) => ({
|
|
4298
|
+
port,
|
|
4299
|
+
// Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
|
|
4300
|
+
releaseSocket: async () => {
|
|
4301
|
+
await releasePort(port);
|
|
4302
|
+
},
|
|
4303
|
+
release: async () => {
|
|
4304
|
+
await releasePort(port);
|
|
4305
|
+
releasePortLock(port);
|
|
4306
|
+
}
|
|
4307
|
+
});
|
|
4308
|
+
const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
|
|
4309
|
+
if (wantsSpecificPort) {
|
|
4172
4310
|
const reservation = await tryReservePort(preferredPort, project);
|
|
4173
4311
|
if (reservation) {
|
|
4174
|
-
return
|
|
4175
|
-
port: preferredPort,
|
|
4176
|
-
release: async () => {
|
|
4177
|
-
await releasePort(preferredPort);
|
|
4178
|
-
}
|
|
4179
|
-
};
|
|
4312
|
+
return handle(preferredPort);
|
|
4180
4313
|
}
|
|
4181
4314
|
console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
|
|
4182
4315
|
}
|
|
@@ -4185,30 +4318,19 @@ async function reservePort(project, preferredPort) {
|
|
|
4185
4318
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4186
4319
|
const port = range.start + index % range.size;
|
|
4187
4320
|
index = (index + 1) % range.size;
|
|
4188
|
-
if (reservedPorts.has(port)) {
|
|
4321
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) {
|
|
4189
4322
|
continue;
|
|
4190
4323
|
}
|
|
4191
4324
|
const reservation = await tryReservePort(port, project);
|
|
4192
4325
|
if (reservation) {
|
|
4193
4326
|
projectPortIndex.set(project, index);
|
|
4194
|
-
return
|
|
4195
|
-
port,
|
|
4196
|
-
release: async () => {
|
|
4197
|
-
await releasePort(port);
|
|
4198
|
-
}
|
|
4199
|
-
};
|
|
4327
|
+
return handle(port);
|
|
4200
4328
|
}
|
|
4201
4329
|
}
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
return {
|
|
4207
|
-
port: dynamicPort,
|
|
4208
|
-
release: async () => {
|
|
4209
|
-
await releasePort(dynamicPort);
|
|
4210
|
-
}
|
|
4211
|
-
};
|
|
4330
|
+
for (let port = 51e3; port < 52e3; port++) {
|
|
4331
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
|
|
4332
|
+
if (await tryReservePort(port, project)) {
|
|
4333
|
+
return handle(port);
|
|
4212
4334
|
}
|
|
4213
4335
|
}
|
|
4214
4336
|
throw new Error(
|
|
@@ -4216,9 +4338,13 @@ async function reservePort(project, preferredPort) {
|
|
|
4216
4338
|
);
|
|
4217
4339
|
}
|
|
4218
4340
|
async function tryReservePort(port, project) {
|
|
4341
|
+
if (!acquirePortLock(port)) {
|
|
4342
|
+
return false;
|
|
4343
|
+
}
|
|
4219
4344
|
return new Promise((resolve) => {
|
|
4220
4345
|
const server = createServer4();
|
|
4221
4346
|
server.once("error", () => {
|
|
4347
|
+
releasePortLock(port);
|
|
4222
4348
|
resolve(false);
|
|
4223
4349
|
});
|
|
4224
4350
|
server.listen(port, () => {
|
|
@@ -4244,31 +4370,6 @@ async function releasePort(port) {
|
|
|
4244
4370
|
});
|
|
4245
4371
|
});
|
|
4246
4372
|
}
|
|
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
4373
|
|
|
4273
4374
|
// libs/testing/src/server/test-server.ts
|
|
4274
4375
|
var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
|
|
@@ -4277,8 +4378,11 @@ var TestServer = class _TestServer {
|
|
|
4277
4378
|
options;
|
|
4278
4379
|
_info;
|
|
4279
4380
|
logs = [];
|
|
4381
|
+
/** Frees the port reservation (socket and cross-process lock); held until stop() */
|
|
4280
4382
|
portRelease = null;
|
|
4281
|
-
|
|
4383
|
+
/** Frees only the reservation socket, just before the server is spawned */
|
|
4384
|
+
portReleaseSocket = null;
|
|
4385
|
+
constructor(options, port, portRelease, portReleaseSocket) {
|
|
4282
4386
|
this.options = {
|
|
4283
4387
|
port,
|
|
4284
4388
|
project: options.project,
|
|
@@ -4290,6 +4394,7 @@ var TestServer = class _TestServer {
|
|
|
4290
4394
|
debug: options.debug ?? DEBUG_SERVER
|
|
4291
4395
|
};
|
|
4292
4396
|
this.portRelease = portRelease ?? null;
|
|
4397
|
+
this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
|
|
4293
4398
|
this._info = {
|
|
4294
4399
|
baseUrl: `http://localhost:${port}`,
|
|
4295
4400
|
port
|
|
@@ -4302,8 +4407,8 @@ var TestServer = class _TestServer {
|
|
|
4302
4407
|
const project = options.project ?? "default";
|
|
4303
4408
|
const maxAttempts = 3;
|
|
4304
4409
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4305
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4306
|
-
const server = new _TestServer(options, port, release);
|
|
4410
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4411
|
+
const server = new _TestServer(options, port, release, releaseSocket);
|
|
4307
4412
|
try {
|
|
4308
4413
|
await server.startProcess();
|
|
4309
4414
|
return server;
|
|
@@ -4343,7 +4448,7 @@ var TestServer = class _TestServer {
|
|
|
4343
4448
|
}
|
|
4344
4449
|
const maxAttempts = 3;
|
|
4345
4450
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4346
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4451
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4347
4452
|
const serverOptions = {
|
|
4348
4453
|
...options,
|
|
4349
4454
|
port,
|
|
@@ -4351,7 +4456,7 @@ var TestServer = class _TestServer {
|
|
|
4351
4456
|
command: `npx nx serve ${project} --port ${port}`,
|
|
4352
4457
|
cwd: options.cwd ?? process.cwd()
|
|
4353
4458
|
};
|
|
4354
|
-
const server = new _TestServer(serverOptions, port, release);
|
|
4459
|
+
const server = new _TestServer(serverOptions, port, release, releaseSocket);
|
|
4355
4460
|
try {
|
|
4356
4461
|
await server.startProcess();
|
|
4357
4462
|
return server;
|
|
@@ -4409,10 +4514,17 @@ var TestServer = class _TestServer {
|
|
|
4409
4514
|
* Stop the test server
|
|
4410
4515
|
*/
|
|
4411
4516
|
async stop() {
|
|
4412
|
-
|
|
4413
|
-
await this.
|
|
4414
|
-
|
|
4517
|
+
try {
|
|
4518
|
+
await this.stopProcess();
|
|
4519
|
+
} finally {
|
|
4520
|
+
if (this.portRelease) {
|
|
4521
|
+
await this.portRelease();
|
|
4522
|
+
this.portRelease = null;
|
|
4523
|
+
}
|
|
4524
|
+
this.portReleaseSocket = null;
|
|
4415
4525
|
}
|
|
4526
|
+
}
|
|
4527
|
+
async stopProcess() {
|
|
4416
4528
|
if (this.process) {
|
|
4417
4529
|
this.log("Stopping server...");
|
|
4418
4530
|
if (this.process.exitCode !== null || this.process.signalCode !== null) {
|
|
@@ -4485,7 +4597,7 @@ var TestServer = class _TestServer {
|
|
|
4485
4597
|
* Restart the server
|
|
4486
4598
|
*/
|
|
4487
4599
|
async restart() {
|
|
4488
|
-
await this.
|
|
4600
|
+
await this.stopProcess();
|
|
4489
4601
|
await this.startProcess();
|
|
4490
4602
|
}
|
|
4491
4603
|
/**
|
|
@@ -4515,9 +4627,9 @@ var TestServer = class _TestServer {
|
|
|
4515
4627
|
PORT: String(this.options.port)
|
|
4516
4628
|
};
|
|
4517
4629
|
const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
|
|
4518
|
-
if (this.
|
|
4519
|
-
await this.
|
|
4520
|
-
this.
|
|
4630
|
+
if (this.portReleaseSocket) {
|
|
4631
|
+
await this.portReleaseSocket();
|
|
4632
|
+
this.portReleaseSocket = null;
|
|
4521
4633
|
await sleep2(300);
|
|
4522
4634
|
}
|
|
4523
4635
|
this.process = spawn(this.options.command, [], {
|
|
@@ -4527,9 +4639,8 @@ var TestServer = class _TestServer {
|
|
|
4527
4639
|
detached: true,
|
|
4528
4640
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4529
4641
|
});
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
}
|
|
4642
|
+
this._info.pid = this.process.pid;
|
|
4643
|
+
this._info.shellPid = this.process.pid;
|
|
4533
4644
|
let processExited = false;
|
|
4534
4645
|
let exitCode = null;
|
|
4535
4646
|
let exitError = null;
|
|
@@ -4632,6 +4743,8 @@ ${allLogs}`);
|
|
|
4632
4743
|
});
|
|
4633
4744
|
if (response.ok || response.status === 404) {
|
|
4634
4745
|
this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
|
|
4746
|
+
const listeningPid = await findListeningPid(this.options.port);
|
|
4747
|
+
if (listeningPid !== void 0) this._info.pid = listeningPid;
|
|
4635
4748
|
return;
|
|
4636
4749
|
}
|
|
4637
4750
|
lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
|
|
@@ -4676,6 +4789,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
|
|
|
4676
4789
|
function sleep2(ms) {
|
|
4677
4790
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4678
4791
|
}
|
|
4792
|
+
function findListeningPid(port) {
|
|
4793
|
+
if (process.platform === "win32") return Promise.resolve(void 0);
|
|
4794
|
+
return new Promise((resolve) => {
|
|
4795
|
+
execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
|
|
4796
|
+
if (error) {
|
|
4797
|
+
resolve(void 0);
|
|
4798
|
+
return;
|
|
4799
|
+
}
|
|
4800
|
+
const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
|
|
4801
|
+
resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
|
|
4802
|
+
});
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4679
4805
|
function withWorkspaceProtocolFallback(env, cwd) {
|
|
4680
4806
|
if (findInstalledProtocolPackageDir(cwd)) {
|
|
4681
4807
|
return env;
|
|
@@ -4947,16 +5073,6 @@ function hasMimeType(result, mimeType) {
|
|
|
4947
5073
|
return result.hasMimeType(mimeType);
|
|
4948
5074
|
}
|
|
4949
5075
|
|
|
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
5076
|
// libs/testing/src/fixtures/gateway-token-binding.ts
|
|
4961
5077
|
import { trimSlashes, trimTrailing } from "@frontmcp/utils";
|
|
4962
5078
|
function gatewayTokenBinding(serverUrl, entryPath) {
|
|
@@ -4967,81 +5083,152 @@ function gatewayTokenBinding(serverUrl, entryPath) {
|
|
|
4967
5083
|
}
|
|
4968
5084
|
|
|
4969
5085
|
// libs/testing/src/fixtures/test-fixture.ts
|
|
4970
|
-
var
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5086
|
+
var jestApi;
|
|
5087
|
+
function jestGlobals() {
|
|
5088
|
+
if (jestApi) return jestApi;
|
|
5089
|
+
try {
|
|
5090
|
+
const api = __require("@jest/globals");
|
|
5091
|
+
jestApi = {
|
|
5092
|
+
describe: api["describe"],
|
|
5093
|
+
beforeAll: api["beforeAll"],
|
|
5094
|
+
beforeEach: api["beforeEach"],
|
|
5095
|
+
afterEach: api["afterEach"],
|
|
5096
|
+
afterAll: api["afterAll"],
|
|
5097
|
+
it: api["it"]
|
|
5098
|
+
};
|
|
5099
|
+
return jestApi;
|
|
5100
|
+
} catch (error) {
|
|
5101
|
+
throw new Error(
|
|
5102
|
+
`@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
5103
|
+
{ cause: error }
|
|
5104
|
+
);
|
|
5105
|
+
}
|
|
4983
5106
|
}
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
5107
|
+
var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
|
|
5108
|
+
function currentScope() {
|
|
5109
|
+
return scopes[scopes.length - 1];
|
|
5110
|
+
}
|
|
5111
|
+
var serverEntries = /* @__PURE__ */ new Map();
|
|
5112
|
+
function resolveConfig(chain) {
|
|
5113
|
+
const merged = {};
|
|
5114
|
+
for (const scope of chain) {
|
|
5115
|
+
const { env, ...rest } = scope.config;
|
|
5116
|
+
Object.assign(merged, rest);
|
|
5117
|
+
if (env) merged.env = { ...merged.env, ...env };
|
|
5118
|
+
}
|
|
5119
|
+
return merged;
|
|
5120
|
+
}
|
|
5121
|
+
function ownerScopeOf(chain) {
|
|
5122
|
+
for (let i = chain.length - 1; i > 0; i--) {
|
|
5123
|
+
if (chain[i].configured) return chain[i];
|
|
5124
|
+
}
|
|
5125
|
+
return chain[0];
|
|
5126
|
+
}
|
|
5127
|
+
function serverKey(config) {
|
|
5128
|
+
const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
5129
|
+
return JSON.stringify({
|
|
5130
|
+
server: config.server,
|
|
5131
|
+
baseUrl: config.baseUrl,
|
|
5132
|
+
project: config.project,
|
|
5133
|
+
port: config.port,
|
|
5134
|
+
entryPath: config.entryPath,
|
|
5135
|
+
startupTimeout: config.startupTimeout,
|
|
5136
|
+
logLevel: config.logLevel,
|
|
5137
|
+
auth: config.auth,
|
|
5138
|
+
env
|
|
5139
|
+
});
|
|
5140
|
+
}
|
|
5141
|
+
function serverEnv(config) {
|
|
5142
|
+
const extra = {};
|
|
5143
|
+
if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
|
|
5144
|
+
if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
|
|
5145
|
+
if (Object.keys(extra).length === 0 && !config.env) return void 0;
|
|
5146
|
+
return { ...extra, ...config.env };
|
|
5147
|
+
}
|
|
5148
|
+
async function startServerEntry(config, owner) {
|
|
5149
|
+
let server;
|
|
5150
|
+
let startedByUs = false;
|
|
5151
|
+
if (config.baseUrl && !config.server) {
|
|
5152
|
+
server = TestServer.connect(config.baseUrl);
|
|
5153
|
+
} else if (config.server) {
|
|
5154
|
+
const serverCommand = resolveServerCommand(config.server);
|
|
5155
|
+
const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
5156
|
+
if (isDebug) {
|
|
5157
|
+
console.log(`[TestFixture] Starting server: ${serverCommand}`);
|
|
5158
|
+
}
|
|
5159
|
+
try {
|
|
5160
|
+
server = await TestServer.start({
|
|
5161
|
+
project: config.project,
|
|
5162
|
+
port: config.port,
|
|
5163
|
+
command: serverCommand,
|
|
5164
|
+
env: serverEnv(config),
|
|
5165
|
+
startupTimeout: config.startupTimeout ?? 3e4,
|
|
5166
|
+
debug: isDebug
|
|
5167
|
+
});
|
|
5168
|
+
startedByUs = true;
|
|
4992
5169
|
if (isDebug) {
|
|
4993
|
-
console.log(`[TestFixture]
|
|
5170
|
+
console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
|
|
4994
5171
|
}
|
|
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.
|
|
5172
|
+
} catch (error) {
|
|
5173
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5174
|
+
throw new Error(
|
|
5175
|
+
`Failed to start test server.
|
|
5012
5176
|
|
|
5013
|
-
Server entry: ${
|
|
5014
|
-
Project: ${
|
|
5177
|
+
Server entry: ${config.server}
|
|
5178
|
+
Project: ${config.project ?? "default"}
|
|
5015
5179
|
Command: ${serverCommand}
|
|
5016
5180
|
|
|
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'
|
|
5181
|
+
Error: ${errMsg}`,
|
|
5182
|
+
{ cause: error }
|
|
5023
5183
|
);
|
|
5024
5184
|
}
|
|
5185
|
+
} else {
|
|
5186
|
+
throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
|
|
5025
5187
|
}
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
}
|
|
5188
|
+
const sharedSecret = config.env?.["JWT_SECRET"];
|
|
5189
|
+
const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
|
|
5190
|
+
const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
|
|
5191
|
+
return { server, startedByUs, tokenFactory: tokenFactory2, owner };
|
|
5031
5192
|
}
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5193
|
+
function acquireServer(chain) {
|
|
5194
|
+
const config = resolveConfig(chain);
|
|
5195
|
+
const key = serverKey(config);
|
|
5196
|
+
let entry = serverEntries.get(key);
|
|
5197
|
+
if (!entry) {
|
|
5198
|
+
entry = startServerEntry(config, ownerScopeOf(chain));
|
|
5199
|
+
serverEntries.set(key, entry);
|
|
5200
|
+
entry.catch(() => serverEntries.delete(key));
|
|
5201
|
+
}
|
|
5202
|
+
return entry;
|
|
5203
|
+
}
|
|
5204
|
+
async function stopServers(owner) {
|
|
5205
|
+
for (const [key, pending] of Array.from(serverEntries)) {
|
|
5206
|
+
let entry;
|
|
5207
|
+
try {
|
|
5208
|
+
entry = await pending;
|
|
5209
|
+
} catch {
|
|
5210
|
+
serverEntries.delete(key);
|
|
5211
|
+
continue;
|
|
5212
|
+
}
|
|
5213
|
+
if (owner && entry.owner !== owner) continue;
|
|
5214
|
+
serverEntries.delete(key);
|
|
5215
|
+
if (entry.startedByUs) {
|
|
5216
|
+
await entry.server.stop();
|
|
5217
|
+
}
|
|
5039
5218
|
}
|
|
5219
|
+
}
|
|
5220
|
+
var fixtureContexts = /* @__PURE__ */ new WeakMap();
|
|
5221
|
+
async function buildFixtures(chain) {
|
|
5222
|
+
const config = resolveConfig(chain);
|
|
5223
|
+
const entry = await acquireServer(chain);
|
|
5224
|
+
const { server, tokenFactory: tokenFactory2 } = entry;
|
|
5225
|
+
const publicMode = config.publicMode ?? config.auth?.mode === "public";
|
|
5226
|
+
const clientConfig = { ...config, publicMode };
|
|
5040
5227
|
const clientInstance = McpTestClient.create({
|
|
5041
|
-
baseUrl: resolveClientBaseUrl(
|
|
5042
|
-
entryPath:
|
|
5043
|
-
transport:
|
|
5044
|
-
publicMode
|
|
5228
|
+
baseUrl: resolveClientBaseUrl(server, clientConfig),
|
|
5229
|
+
entryPath: clientConfig.entryPath,
|
|
5230
|
+
transport: clientConfig.transport ?? "streamable-http",
|
|
5231
|
+
publicMode
|
|
5045
5232
|
}).build();
|
|
5046
5233
|
try {
|
|
5047
5234
|
await clientInstance.connect();
|
|
@@ -5055,17 +5242,18 @@ async function createTestFixtures() {
|
|
|
5055
5242
|
throw err;
|
|
5056
5243
|
}
|
|
5057
5244
|
}
|
|
5058
|
-
const
|
|
5059
|
-
const
|
|
5060
|
-
return {
|
|
5245
|
+
const context = { extraClients: [] };
|
|
5246
|
+
const fixtures = {
|
|
5061
5247
|
mcp: clientInstance,
|
|
5062
|
-
auth,
|
|
5063
|
-
server
|
|
5248
|
+
auth: createAuthFixture(tokenFactory2),
|
|
5249
|
+
server: createServerFixture(server, clientConfig, clientInstance, context)
|
|
5064
5250
|
};
|
|
5251
|
+
fixtureContexts.set(fixtures, context);
|
|
5252
|
+
return fixtures;
|
|
5065
5253
|
}
|
|
5066
5254
|
async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
5067
|
-
if (testFailed
|
|
5068
|
-
const logs =
|
|
5255
|
+
if (testFailed) {
|
|
5256
|
+
const logs = fixtures.server.getLogs();
|
|
5069
5257
|
if (logs.length > 0) {
|
|
5070
5258
|
console.error("\n[TestFixture] === Server Logs (test failed) ===");
|
|
5071
5259
|
const recentLogs = logs.slice(-50);
|
|
@@ -5076,20 +5264,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
|
5076
5264
|
console.error("[TestFixture] === End Server Logs ===\n");
|
|
5077
5265
|
}
|
|
5078
5266
|
}
|
|
5267
|
+
const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
|
|
5268
|
+
for (const client of extras) {
|
|
5269
|
+
if (client.isConnected()) {
|
|
5270
|
+
await client.disconnect().catch(() => void 0);
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5079
5273
|
if (fixtures.mcp.isConnected()) {
|
|
5080
5274
|
await fixtures.mcp.disconnect();
|
|
5081
5275
|
}
|
|
5082
5276
|
}
|
|
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;
|
|
5277
|
+
function resolveClientBaseUrl(server, config) {
|
|
5278
|
+
return config.baseUrl ?? server.info.baseUrl;
|
|
5093
5279
|
}
|
|
5094
5280
|
function createAuthFixture(factory) {
|
|
5095
5281
|
const users = {
|
|
@@ -5135,28 +5321,39 @@ function createAuthFixture(factory) {
|
|
|
5135
5321
|
getAudience: () => factory.getAudience()
|
|
5136
5322
|
};
|
|
5137
5323
|
}
|
|
5138
|
-
function createServerFixture(server) {
|
|
5324
|
+
function createServerFixture(server, config, mcp, context) {
|
|
5139
5325
|
return {
|
|
5140
|
-
|
|
5326
|
+
// Live view: `pid` is filled in once the server is listening and follows restarts
|
|
5327
|
+
get info() {
|
|
5328
|
+
return server.info;
|
|
5329
|
+
},
|
|
5141
5330
|
createClient: async (opts) => {
|
|
5142
|
-
|
|
5143
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5144
|
-
entryPath: opts?.entryPath ??
|
|
5331
|
+
const client = await McpTestClient.create({
|
|
5332
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5333
|
+
entryPath: opts?.entryPath ?? config.entryPath,
|
|
5145
5334
|
transport: opts?.transport ?? "streamable-http",
|
|
5146
5335
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
5147
5336
|
clientInfo: opts?.clientInfo,
|
|
5148
|
-
publicMode:
|
|
5337
|
+
publicMode: config.publicMode
|
|
5149
5338
|
}).buildAndConnect();
|
|
5339
|
+
context.extraClients.push(client);
|
|
5340
|
+
return client;
|
|
5150
5341
|
},
|
|
5151
5342
|
createClientBuilder: () => {
|
|
5152
5343
|
const builder = new McpTestClientBuilder({
|
|
5153
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5154
|
-
entryPath:
|
|
5155
|
-
publicMode:
|
|
5344
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5345
|
+
entryPath: config.entryPath,
|
|
5346
|
+
publicMode: config.publicMode
|
|
5156
5347
|
});
|
|
5157
5348
|
return builder;
|
|
5158
5349
|
},
|
|
5159
|
-
restart: () =>
|
|
5350
|
+
restart: async () => {
|
|
5351
|
+
const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
|
|
5352
|
+
await server.restart();
|
|
5353
|
+
for (const client of reconnect) {
|
|
5354
|
+
await client.reconnect();
|
|
5355
|
+
}
|
|
5356
|
+
},
|
|
5160
5357
|
getLogs: () => server.getLogs(),
|
|
5161
5358
|
clearLogs: () => server.clearLogs()
|
|
5162
5359
|
};
|
|
@@ -5167,12 +5364,12 @@ function resolveServerCommand(server) {
|
|
|
5167
5364
|
}
|
|
5168
5365
|
return `npx tsx ${server}`;
|
|
5169
5366
|
}
|
|
5170
|
-
function runWithFixtures(fn) {
|
|
5171
|
-
return async () => {
|
|
5172
|
-
const fixtures = await
|
|
5367
|
+
function runWithFixtures(fn, chain) {
|
|
5368
|
+
return async (...extra) => {
|
|
5369
|
+
const fixtures = await buildFixtures(chain);
|
|
5173
5370
|
let testFailed = false;
|
|
5174
5371
|
try {
|
|
5175
|
-
await fn(fixtures);
|
|
5372
|
+
await fn(fixtures, ...extra);
|
|
5176
5373
|
} catch (error) {
|
|
5177
5374
|
testFailed = true;
|
|
5178
5375
|
throw error;
|
|
@@ -5181,24 +5378,37 @@ function runWithFixtures(fn) {
|
|
|
5181
5378
|
}
|
|
5182
5379
|
};
|
|
5183
5380
|
}
|
|
5381
|
+
function skipTitle(name, scope) {
|
|
5382
|
+
return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
|
|
5383
|
+
}
|
|
5184
5384
|
function testWithFixtures(name, fn) {
|
|
5185
|
-
const
|
|
5385
|
+
const chain = scopes.slice();
|
|
5386
|
+
const scope = currentScope();
|
|
5186
5387
|
if (scope.skipped) {
|
|
5187
|
-
|
|
5388
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5188
5389
|
return;
|
|
5189
5390
|
}
|
|
5190
|
-
|
|
5391
|
+
jestGlobals().it(name, runWithFixtures(fn, chain));
|
|
5191
5392
|
}
|
|
5192
5393
|
function use(config) {
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5394
|
+
const scope = currentScope();
|
|
5395
|
+
scope.config = {
|
|
5396
|
+
...scope.config,
|
|
5397
|
+
...config,
|
|
5398
|
+
env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
|
|
5399
|
+
};
|
|
5400
|
+
scope.configured = true;
|
|
5401
|
+
if (!scope.cleanupRegistered) {
|
|
5402
|
+
scope.cleanupRegistered = true;
|
|
5403
|
+
jestGlobals().afterAll(async () => {
|
|
5404
|
+
await stopServers(scope === scopes[0] ? void 0 : scope);
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5197
5407
|
}
|
|
5198
5408
|
function skip(nameOrCondition, fnOrReason) {
|
|
5199
5409
|
if (typeof nameOrCondition === "boolean") {
|
|
5200
5410
|
if (nameOrCondition) {
|
|
5201
|
-
const scope =
|
|
5411
|
+
const scope = currentScope();
|
|
5202
5412
|
scope.skipped = true;
|
|
5203
5413
|
scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
|
|
5204
5414
|
}
|
|
@@ -5209,57 +5419,111 @@ function skip(nameOrCondition, fnOrReason) {
|
|
|
5209
5419
|
`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
|
|
5210
5420
|
);
|
|
5211
5421
|
}
|
|
5212
|
-
|
|
5422
|
+
jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
|
|
5213
5423
|
}
|
|
5214
5424
|
function only(name, fn) {
|
|
5215
|
-
const scope =
|
|
5425
|
+
const scope = currentScope();
|
|
5216
5426
|
if (scope.skipped) {
|
|
5217
|
-
|
|
5427
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
|
|
5218
5428
|
return;
|
|
5219
5429
|
}
|
|
5220
|
-
|
|
5430
|
+
jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
|
|
5221
5431
|
}
|
|
5222
5432
|
function todo(name) {
|
|
5223
|
-
|
|
5433
|
+
jestGlobals().it.todo(name);
|
|
5224
5434
|
}
|
|
5225
|
-
function
|
|
5435
|
+
function each(table, ...tagged) {
|
|
5436
|
+
const chain = scopes.slice();
|
|
5437
|
+
const register = jestGlobals().it.each(table, ...tagged);
|
|
5226
5438
|
return (name, fn) => {
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5439
|
+
const scope = currentScope();
|
|
5440
|
+
if (scope.skipped) {
|
|
5441
|
+
const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
|
|
5442
|
+
skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5443
|
+
return;
|
|
5444
|
+
}
|
|
5445
|
+
register(name, runWithFixtures(fn, chain));
|
|
5446
|
+
};
|
|
5447
|
+
}
|
|
5448
|
+
function withScope(fn) {
|
|
5449
|
+
return (...args) => {
|
|
5450
|
+
const parent = currentScope();
|
|
5451
|
+
scopes.push({
|
|
5452
|
+
skipped: parent.skipped,
|
|
5453
|
+
reason: parent.reason,
|
|
5454
|
+
config: {},
|
|
5455
|
+
configured: false,
|
|
5456
|
+
cleanupRegistered: false
|
|
5235
5457
|
});
|
|
5458
|
+
try {
|
|
5459
|
+
fn(...args);
|
|
5460
|
+
} finally {
|
|
5461
|
+
scopes.pop();
|
|
5462
|
+
}
|
|
5236
5463
|
};
|
|
5237
5464
|
}
|
|
5238
|
-
function
|
|
5239
|
-
return (
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
});
|
|
5465
|
+
function scopedRegister(pick) {
|
|
5466
|
+
return (name, fn, timeout) => {
|
|
5467
|
+
pick()(name, withScope(fn), timeout);
|
|
5468
|
+
};
|
|
5243
5469
|
}
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
}
|
|
5470
|
+
function scopedEach(pick) {
|
|
5471
|
+
return (...eachArgs) => (name, fn, timeout) => {
|
|
5472
|
+
const register = pick()(...eachArgs);
|
|
5473
|
+
register(name, withScope(fn), timeout);
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
5476
|
+
var describeWithScope = Object.assign(
|
|
5477
|
+
scopedRegister(() => jestGlobals().describe),
|
|
5478
|
+
{
|
|
5479
|
+
// `describe.skip` still evaluates its callback during collection, so it needs a scope too
|
|
5480
|
+
only: Object.assign(
|
|
5481
|
+
scopedRegister(() => jestGlobals().describe.only),
|
|
5482
|
+
{
|
|
5483
|
+
each: scopedEach(() => jestGlobals().describe.only.each)
|
|
5484
|
+
}
|
|
5485
|
+
),
|
|
5486
|
+
skip: Object.assign(
|
|
5487
|
+
scopedRegister(() => jestGlobals().describe.skip),
|
|
5488
|
+
{
|
|
5489
|
+
each: scopedEach(() => jestGlobals().describe.skip.each)
|
|
5490
|
+
}
|
|
5491
|
+
),
|
|
5492
|
+
each: scopedEach(() => jestGlobals().describe.each)
|
|
5493
|
+
}
|
|
5494
|
+
);
|
|
5249
5495
|
var test = testWithFixtures;
|
|
5250
5496
|
test.use = use;
|
|
5251
|
-
test.describe =
|
|
5252
|
-
test.beforeAll =
|
|
5253
|
-
test.beforeEach =
|
|
5254
|
-
test.afterEach =
|
|
5255
|
-
test.afterAll =
|
|
5497
|
+
test.describe = describeWithScope;
|
|
5498
|
+
test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
|
|
5499
|
+
test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
|
|
5500
|
+
test.afterEach = ((...args) => jestGlobals().afterEach(...args));
|
|
5501
|
+
test.afterAll = ((...args) => jestGlobals().afterAll(...args));
|
|
5256
5502
|
test.skip = skip;
|
|
5257
5503
|
test.only = only;
|
|
5258
5504
|
test.todo = todo;
|
|
5505
|
+
test.each = each;
|
|
5259
5506
|
|
|
5260
5507
|
// libs/testing/src/expect.ts
|
|
5261
|
-
|
|
5262
|
-
|
|
5508
|
+
var resolvedExpect;
|
|
5509
|
+
function resolveJestExpect() {
|
|
5510
|
+
if (resolvedExpect) return resolvedExpect;
|
|
5511
|
+
try {
|
|
5512
|
+
resolvedExpect = __require("@jest/globals").expect;
|
|
5513
|
+
return resolvedExpect;
|
|
5514
|
+
} catch (error) {
|
|
5515
|
+
throw new Error(
|
|
5516
|
+
`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
5517
|
+
{ cause: error }
|
|
5518
|
+
);
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
var expect = new Proxy(function frontmcpExpect() {
|
|
5522
|
+
}, {
|
|
5523
|
+
apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
|
|
5524
|
+
get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
|
|
5525
|
+
has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
|
|
5526
|
+
});
|
|
5263
5527
|
|
|
5264
5528
|
// libs/testing/src/platform/platform-types.ts
|
|
5265
5529
|
function getPlatformMetaNamespace(platform) {
|
|
@@ -5922,34 +6186,44 @@ var HttpInterceptorImpl = class {
|
|
|
5922
6186
|
}
|
|
5923
6187
|
};
|
|
5924
6188
|
}
|
|
5925
|
-
get(url, response) {
|
|
6189
|
+
get(url, response, options) {
|
|
5926
6190
|
return this.mock({
|
|
5927
|
-
match: { url, method: "GET" },
|
|
5928
|
-
response: normalizeResponse(response)
|
|
6191
|
+
match: { url, method: "GET", ...options?.match },
|
|
6192
|
+
response: normalizeResponse(response),
|
|
6193
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6194
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5929
6195
|
});
|
|
5930
6196
|
}
|
|
5931
|
-
post(url, response) {
|
|
6197
|
+
post(url, response, options) {
|
|
5932
6198
|
return this.mock({
|
|
5933
|
-
match: { url, method: "POST" },
|
|
5934
|
-
response: normalizeResponse(response)
|
|
6199
|
+
match: { url, method: "POST", ...options?.match },
|
|
6200
|
+
response: normalizeResponse(response),
|
|
6201
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6202
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5935
6203
|
});
|
|
5936
6204
|
}
|
|
5937
|
-
put(url, response) {
|
|
6205
|
+
put(url, response, options) {
|
|
5938
6206
|
return this.mock({
|
|
5939
|
-
match: { url, method: "PUT" },
|
|
5940
|
-
response: normalizeResponse(response)
|
|
6207
|
+
match: { url, method: "PUT", ...options?.match },
|
|
6208
|
+
response: normalizeResponse(response),
|
|
6209
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6210
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5941
6211
|
});
|
|
5942
6212
|
}
|
|
5943
|
-
delete(url, response) {
|
|
6213
|
+
delete(url, response, options) {
|
|
5944
6214
|
return this.mock({
|
|
5945
|
-
match: { url, method: "DELETE" },
|
|
5946
|
-
response: normalizeResponse(response)
|
|
6215
|
+
match: { url, method: "DELETE", ...options?.match },
|
|
6216
|
+
response: normalizeResponse(response),
|
|
6217
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6218
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5947
6219
|
});
|
|
5948
6220
|
}
|
|
5949
|
-
any(url, response) {
|
|
6221
|
+
any(url, response, options) {
|
|
5950
6222
|
return this.mock({
|
|
5951
|
-
match: { url },
|
|
5952
|
-
response: normalizeResponse(response)
|
|
6223
|
+
match: { url, ...options?.match },
|
|
6224
|
+
response: normalizeResponse(response),
|
|
6225
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6226
|
+
...options?.name !== void 0 && { name: options.name }
|
|
5953
6227
|
});
|
|
5954
6228
|
}
|
|
5955
6229
|
clear() {
|
|
@@ -5981,6 +6255,9 @@ ${descriptions.join("\n")}`);
|
|
|
5981
6255
|
if (index !== -1) {
|
|
5982
6256
|
activeInterceptors.splice(index, 1);
|
|
5983
6257
|
}
|
|
6258
|
+
if (activeInterceptors.length === 0) {
|
|
6259
|
+
httpMock.disable();
|
|
6260
|
+
}
|
|
5984
6261
|
}
|
|
5985
6262
|
// Internal methods
|
|
5986
6263
|
isActive() {
|
|
@@ -6080,43 +6357,49 @@ ${descriptions.join("\n")}`);
|
|
|
6080
6357
|
}
|
|
6081
6358
|
};
|
|
6082
6359
|
async function interceptedFetch(input, init) {
|
|
6083
|
-
const
|
|
6084
|
-
const
|
|
6360
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
6361
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
|
|
6362
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
6085
6363
|
const headers = {};
|
|
6086
|
-
|
|
6087
|
-
if (
|
|
6088
|
-
|
|
6364
|
+
const collectHeaders = (source) => {
|
|
6365
|
+
if (!source) return;
|
|
6366
|
+
if (source instanceof Headers) {
|
|
6367
|
+
source.forEach((value, key) => {
|
|
6089
6368
|
headers[key.toLowerCase()] = value;
|
|
6090
6369
|
});
|
|
6091
|
-
} else if (Array.isArray(
|
|
6092
|
-
for (const [key, value] of
|
|
6370
|
+
} else if (Array.isArray(source)) {
|
|
6371
|
+
for (const [key, value] of source) {
|
|
6093
6372
|
headers[key.toLowerCase()] = value;
|
|
6094
6373
|
}
|
|
6095
6374
|
} else {
|
|
6096
|
-
for (const [key, value] of Object.entries(
|
|
6375
|
+
for (const [key, value] of Object.entries(source)) {
|
|
6097
6376
|
headers[key.toLowerCase()] = value;
|
|
6098
6377
|
}
|
|
6099
6378
|
}
|
|
6100
|
-
}
|
|
6379
|
+
};
|
|
6380
|
+
collectHeaders(init?.headers ?? request?.headers);
|
|
6101
6381
|
let body;
|
|
6102
6382
|
let rawBody;
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6383
|
+
let bodySource = init?.body;
|
|
6384
|
+
if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
|
|
6385
|
+
bodySource = await request.clone().text();
|
|
6386
|
+
}
|
|
6387
|
+
if (bodySource) {
|
|
6388
|
+
if (typeof bodySource === "string") {
|
|
6389
|
+
rawBody = bodySource;
|
|
6390
|
+
} else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
|
|
6391
|
+
rawBody = new TextDecoder().decode(bodySource);
|
|
6392
|
+
} else if (bodySource instanceof URLSearchParams) {
|
|
6393
|
+
rawBody = bodySource.toString();
|
|
6394
|
+
}
|
|
6395
|
+
if (rawBody !== void 0) {
|
|
6113
6396
|
try {
|
|
6114
6397
|
body = JSON.parse(rawBody);
|
|
6115
6398
|
} catch {
|
|
6116
6399
|
body = rawBody;
|
|
6117
6400
|
}
|
|
6118
6401
|
} else {
|
|
6119
|
-
body =
|
|
6402
|
+
body = bodySource;
|
|
6120
6403
|
}
|
|
6121
6404
|
}
|
|
6122
6405
|
const requestInfo = {
|
|
@@ -6146,8 +6429,20 @@ async function interceptedFetch(input, init) {
|
|
|
6146
6429
|
Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
|
|
6147
6430
|
);
|
|
6148
6431
|
}
|
|
6432
|
+
var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
|
|
6433
|
+
function isResponseDescriptor(value) {
|
|
6434
|
+
const keys = Object.keys(value);
|
|
6435
|
+
if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
|
|
6436
|
+
if ("status" in value) {
|
|
6437
|
+
const status = value["status"];
|
|
6438
|
+
if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
|
|
6439
|
+
}
|
|
6440
|
+
return true;
|
|
6441
|
+
}
|
|
6149
6442
|
function normalizeResponse(response) {
|
|
6150
|
-
|
|
6443
|
+
const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
|
|
6444
|
+
if (isNetworkError) return response;
|
|
6445
|
+
if (Array.isArray(response) || !isResponseDescriptor(response)) {
|
|
6151
6446
|
return { body: response };
|
|
6152
6447
|
}
|
|
6153
6448
|
return response;
|
|
@@ -7481,47 +7776,48 @@ function clearGlobalMeasurements() {
|
|
|
7481
7776
|
}
|
|
7482
7777
|
|
|
7483
7778
|
// 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(
|
|
7779
|
+
var currentConfig = {};
|
|
7780
|
+
var serverInstance = null;
|
|
7781
|
+
var tokenFactory = null;
|
|
7782
|
+
var serverStartedByUs = false;
|
|
7783
|
+
async function initializeSharedResources() {
|
|
7784
|
+
if (!tokenFactory) {
|
|
7785
|
+
tokenFactory = new TestTokenFactory();
|
|
7786
|
+
}
|
|
7787
|
+
if (!serverInstance) {
|
|
7788
|
+
if (currentConfig.baseUrl) {
|
|
7789
|
+
serverInstance = TestServer.connect(currentConfig.baseUrl);
|
|
7790
|
+
serverStartedByUs = false;
|
|
7791
|
+
} else if (currentConfig.server) {
|
|
7792
|
+
const serverCommand = resolveServerCommand2(currentConfig.server);
|
|
7498
7793
|
const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
7499
7794
|
if (isDebug) {
|
|
7500
7795
|
console.log(`[PerfTest] Starting server: ${serverCommand}`);
|
|
7501
7796
|
}
|
|
7502
7797
|
try {
|
|
7503
|
-
|
|
7504
|
-
project:
|
|
7505
|
-
port:
|
|
7798
|
+
serverInstance = await TestServer.start({
|
|
7799
|
+
project: currentConfig.project,
|
|
7800
|
+
port: currentConfig.port,
|
|
7506
7801
|
command: serverCommand,
|
|
7507
|
-
env:
|
|
7508
|
-
startupTimeout:
|
|
7802
|
+
env: currentConfig.env,
|
|
7803
|
+
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
7509
7804
|
debug: isDebug
|
|
7510
7805
|
});
|
|
7511
|
-
|
|
7806
|
+
serverStartedByUs = true;
|
|
7512
7807
|
if (isDebug) {
|
|
7513
|
-
console.log(`[PerfTest] Server started at ${
|
|
7808
|
+
console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
|
|
7514
7809
|
}
|
|
7515
7810
|
} catch (error) {
|
|
7516
7811
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
7517
7812
|
throw new Error(
|
|
7518
7813
|
`Failed to start test server.
|
|
7519
7814
|
|
|
7520
|
-
Server entry: ${
|
|
7521
|
-
Project: ${
|
|
7815
|
+
Server entry: ${currentConfig.server}
|
|
7816
|
+
Project: ${currentConfig.project ?? "default"}
|
|
7522
7817
|
Command: ${serverCommand}
|
|
7523
7818
|
|
|
7524
|
-
Error: ${errMsg}
|
|
7819
|
+
Error: ${errMsg}`,
|
|
7820
|
+
{ cause: error }
|
|
7525
7821
|
);
|
|
7526
7822
|
}
|
|
7527
7823
|
} else {
|
|
@@ -7529,23 +7825,23 @@ Error: ${errMsg}`
|
|
|
7529
7825
|
}
|
|
7530
7826
|
}
|
|
7531
7827
|
}
|
|
7532
|
-
async function
|
|
7533
|
-
await
|
|
7534
|
-
if (!
|
|
7828
|
+
async function createTestFixtures(testName) {
|
|
7829
|
+
await initializeSharedResources();
|
|
7830
|
+
if (!serverInstance) {
|
|
7535
7831
|
throw new Error("Server instance not initialized");
|
|
7536
7832
|
}
|
|
7537
|
-
if (!
|
|
7833
|
+
if (!tokenFactory) {
|
|
7538
7834
|
throw new Error("Token factory not initialized");
|
|
7539
7835
|
}
|
|
7540
7836
|
const clientInstance = await McpTestClient.create({
|
|
7541
|
-
baseUrl:
|
|
7542
|
-
transport:
|
|
7543
|
-
publicMode:
|
|
7837
|
+
baseUrl: serverInstance.info.baseUrl,
|
|
7838
|
+
transport: currentConfig.transport ?? "streamable-http",
|
|
7839
|
+
publicMode: currentConfig.publicMode
|
|
7544
7840
|
}).buildAndConnect();
|
|
7545
|
-
const auth = createAuthFixture2(
|
|
7546
|
-
const server = createServerFixture2(
|
|
7547
|
-
const perfImpl = createPerfFixtures(testName,
|
|
7548
|
-
if (
|
|
7841
|
+
const auth = createAuthFixture2(tokenFactory);
|
|
7842
|
+
const server = createServerFixture2(serverInstance);
|
|
7843
|
+
const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
|
|
7844
|
+
if (currentConfig.forceGcOnBaseline !== false) {
|
|
7549
7845
|
await perfImpl.baseline();
|
|
7550
7846
|
}
|
|
7551
7847
|
return {
|
|
@@ -7561,8 +7857,8 @@ async function createTestFixtures2(testName) {
|
|
|
7561
7857
|
async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
7562
7858
|
const measurement = perfImpl.buildMeasurement();
|
|
7563
7859
|
addGlobalMeasurement(measurement);
|
|
7564
|
-
if (testFailed &&
|
|
7565
|
-
const logs =
|
|
7860
|
+
if (testFailed && serverInstance) {
|
|
7861
|
+
const logs = serverInstance.getLogs();
|
|
7566
7862
|
if (logs.length > 0) {
|
|
7567
7863
|
console.error("\n[PerfTest] === Server Logs (test failed) ===");
|
|
7568
7864
|
const recentLogs = logs.slice(-50);
|
|
@@ -7577,13 +7873,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
|
7577
7873
|
await fixtures.mcp.disconnect();
|
|
7578
7874
|
}
|
|
7579
7875
|
}
|
|
7580
|
-
async function
|
|
7581
|
-
if (
|
|
7582
|
-
await
|
|
7876
|
+
async function cleanupSharedResources() {
|
|
7877
|
+
if (serverInstance && serverStartedByUs) {
|
|
7878
|
+
await serverInstance.stop();
|
|
7583
7879
|
}
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7880
|
+
serverInstance = null;
|
|
7881
|
+
tokenFactory = null;
|
|
7882
|
+
serverStartedByUs = false;
|
|
7587
7883
|
}
|
|
7588
7884
|
function createAuthFixture2(factory) {
|
|
7589
7885
|
const users = {
|
|
@@ -7638,13 +7934,13 @@ function createServerFixture2(server) {
|
|
|
7638
7934
|
transport: opts?.transport ?? "streamable-http",
|
|
7639
7935
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
7640
7936
|
clientInfo: opts?.clientInfo,
|
|
7641
|
-
publicMode:
|
|
7937
|
+
publicMode: currentConfig.publicMode
|
|
7642
7938
|
}).buildAndConnect();
|
|
7643
7939
|
},
|
|
7644
7940
|
createClientBuilder: () => {
|
|
7645
7941
|
return new McpTestClientBuilder({
|
|
7646
7942
|
baseUrl: server.info.baseUrl,
|
|
7647
|
-
publicMode:
|
|
7943
|
+
publicMode: currentConfig.publicMode
|
|
7648
7944
|
});
|
|
7649
7945
|
},
|
|
7650
7946
|
restart: () => server.restart(),
|
|
@@ -7660,7 +7956,7 @@ function resolveServerCommand2(server) {
|
|
|
7660
7956
|
}
|
|
7661
7957
|
function perfTestWithFixtures(name, fn) {
|
|
7662
7958
|
it(name, async () => {
|
|
7663
|
-
const { fixtures, perfImpl } = await
|
|
7959
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7664
7960
|
let testFailed = false;
|
|
7665
7961
|
try {
|
|
7666
7962
|
await fn(fixtures);
|
|
@@ -7673,14 +7969,14 @@ function perfTestWithFixtures(name, fn) {
|
|
|
7673
7969
|
});
|
|
7674
7970
|
}
|
|
7675
7971
|
function use2(config) {
|
|
7676
|
-
|
|
7972
|
+
currentConfig = { ...currentConfig, ...config };
|
|
7677
7973
|
afterAll(async () => {
|
|
7678
|
-
await
|
|
7974
|
+
await cleanupSharedResources();
|
|
7679
7975
|
});
|
|
7680
7976
|
}
|
|
7681
7977
|
function skip2(name, fn) {
|
|
7682
7978
|
it.skip(name, async () => {
|
|
7683
|
-
const { fixtures, perfImpl } = await
|
|
7979
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7684
7980
|
let testFailed = false;
|
|
7685
7981
|
try {
|
|
7686
7982
|
await fn(fixtures);
|
|
@@ -7694,7 +7990,7 @@ function skip2(name, fn) {
|
|
|
7694
7990
|
}
|
|
7695
7991
|
function only2(name, fn) {
|
|
7696
7992
|
it.only(name, async () => {
|
|
7697
|
-
const { fixtures, perfImpl } = await
|
|
7993
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7698
7994
|
let testFailed = false;
|
|
7699
7995
|
try {
|
|
7700
7996
|
await fn(fixtures);
|
|
@@ -7711,16 +8007,19 @@ function todo2(name) {
|
|
|
7711
8007
|
}
|
|
7712
8008
|
var perfTest = perfTestWithFixtures;
|
|
7713
8009
|
perfTest.use = use2;
|
|
7714
|
-
|
|
7715
|
-
perfTest
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
8010
|
+
for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
|
|
8011
|
+
Object.defineProperty(perfTest, hook, {
|
|
8012
|
+
get: () => globalThis[hook],
|
|
8013
|
+
enumerable: true,
|
|
8014
|
+
configurable: true
|
|
8015
|
+
});
|
|
8016
|
+
}
|
|
7719
8017
|
perfTest.skip = skip2;
|
|
7720
8018
|
perfTest.only = only2;
|
|
7721
8019
|
perfTest.todo = todo2;
|
|
7722
8020
|
|
|
7723
8021
|
// libs/testing/src/perf/baseline-store.ts
|
|
8022
|
+
import { ensureDir, fileExists, readFile, writeFile } from "@frontmcp/utils";
|
|
7724
8023
|
var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
|
|
7725
8024
|
var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
|
|
7726
8025
|
var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
|
|
@@ -7735,7 +8034,6 @@ var BaselineStore = class {
|
|
|
7735
8034
|
*/
|
|
7736
8035
|
async load() {
|
|
7737
8036
|
try {
|
|
7738
|
-
const { readFile, fileExists } = await import("@frontmcp/utils");
|
|
7739
8037
|
if (!await fileExists(this.baselinePath)) {
|
|
7740
8038
|
return null;
|
|
7741
8039
|
}
|
|
@@ -7750,7 +8048,6 @@ var BaselineStore = class {
|
|
|
7750
8048
|
* Save baseline to local file.
|
|
7751
8049
|
*/
|
|
7752
8050
|
async save(baseline) {
|
|
7753
|
-
const { writeFile, ensureDir } = await import("@frontmcp/utils");
|
|
7754
8051
|
const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
|
|
7755
8052
|
await ensureDir(dir);
|
|
7756
8053
|
await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
@@ -8006,6 +8303,7 @@ function getRegressionDetector(config) {
|
|
|
8006
8303
|
}
|
|
8007
8304
|
|
|
8008
8305
|
// libs/testing/src/perf/report-generator.ts
|
|
8306
|
+
import { ensureDir as ensureDir2, writeFile as writeFile2 } from "@frontmcp/utils";
|
|
8009
8307
|
var ReportGenerator = class {
|
|
8010
8308
|
detector;
|
|
8011
8309
|
constructor() {
|
|
@@ -8261,14 +8559,13 @@ var ReportGenerator = class {
|
|
|
8261
8559
|
}
|
|
8262
8560
|
};
|
|
8263
8561
|
async function saveReports(measurements, outputDir, baseline, gitInfo) {
|
|
8264
|
-
|
|
8265
|
-
await ensureDir(outputDir);
|
|
8562
|
+
await ensureDir2(outputDir);
|
|
8266
8563
|
const generator = new ReportGenerator();
|
|
8267
8564
|
const report = generator.generateReport(measurements, baseline, gitInfo);
|
|
8268
8565
|
const jsonPath = `${outputDir}/report.json`;
|
|
8269
8566
|
const markdownPath = `${outputDir}/report.md`;
|
|
8270
|
-
await
|
|
8271
|
-
await
|
|
8567
|
+
await writeFile2(jsonPath, generator.generateJsonReport(report));
|
|
8568
|
+
await writeFile2(markdownPath, generator.generateMarkdownReport(report));
|
|
8272
8569
|
return { jsonPath, markdownPath };
|
|
8273
8570
|
}
|
|
8274
8571
|
function createReportGenerator() {
|