@frontmcp/testing 1.8.5 → 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 +12 -1
- package/client/mcp-test-client.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +545 -260
- package/esm/index.mjs +702 -383
- package/esm/package.json +6 -6
- package/esm/perf/index.mjs +465 -308
- 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 +544 -252
- 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 +710 -384
- 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 +468 -321
- 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) {
|
|
@@ -1302,6 +1328,16 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
1302
1328
|
name: "@frontmcp/testing",
|
|
1303
1329
|
version: "0.4.0"
|
|
1304
1330
|
};
|
|
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
|
+
}
|
|
1305
1341
|
function extractEntryPaths(data) {
|
|
1306
1342
|
let payload = data;
|
|
1307
1343
|
if (typeof payload === "string") {
|
|
@@ -1388,19 +1424,22 @@ var McpTestClient = class {
|
|
|
1388
1424
|
async connect() {
|
|
1389
1425
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
1390
1426
|
this.triedDiscoveredEntryPath = false;
|
|
1427
|
+
this.initResult = null;
|
|
1391
1428
|
this.transport = this.createTransport();
|
|
1392
1429
|
await this.transport.connect();
|
|
1393
1430
|
const maxRetries = 3;
|
|
1394
1431
|
const retryDelayMs = 500;
|
|
1395
1432
|
let lastError;
|
|
1396
1433
|
let reportedEntryPaths;
|
|
1434
|
+
let attemptsMade = 0;
|
|
1397
1435
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1436
|
+
attemptsMade = attempt;
|
|
1398
1437
|
const initResponse = await this.initialize();
|
|
1399
1438
|
if (initResponse.success && initResponse.data) {
|
|
1400
1439
|
this.initResult = initResponse.data;
|
|
1401
1440
|
break;
|
|
1402
1441
|
}
|
|
1403
|
-
lastError = initResponse.error
|
|
1442
|
+
lastError = describeInitializeError(initResponse.error);
|
|
1404
1443
|
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
1405
1444
|
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
1406
1445
|
this.triedDiscoveredEntryPath = true;
|
|
@@ -1412,6 +1451,7 @@ var McpTestClient = class {
|
|
|
1412
1451
|
await this.transport.connect();
|
|
1413
1452
|
continue;
|
|
1414
1453
|
}
|
|
1454
|
+
if (isAuthFailure(initResponse.error)) break;
|
|
1415
1455
|
if (attempt < maxRetries) {
|
|
1416
1456
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
1417
1457
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
@@ -1420,7 +1460,7 @@ var McpTestClient = class {
|
|
|
1420
1460
|
if (!this.initResult) {
|
|
1421
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.` : "";
|
|
1422
1462
|
throw new Error(
|
|
1423
|
-
`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}`
|
|
1424
1464
|
);
|
|
1425
1465
|
}
|
|
1426
1466
|
this._sessionId = this.transport.getSessionId();
|
|
@@ -1490,18 +1530,47 @@ var McpTestClient = class {
|
|
|
1490
1530
|
return this._authState;
|
|
1491
1531
|
}
|
|
1492
1532
|
/**
|
|
1493
|
-
* 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.
|
|
1494
1539
|
*/
|
|
1495
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 };
|
|
1496
1547
|
this._authState = {
|
|
1497
1548
|
isAnonymous: false,
|
|
1498
1549
|
token,
|
|
1499
1550
|
scopes: this.parseScopesFromToken(token),
|
|
1500
1551
|
user: this.parseUserFromToken(token)
|
|
1501
1552
|
};
|
|
1502
|
-
|
|
1503
|
-
|
|
1553
|
+
const previousTransport = this.transport;
|
|
1554
|
+
if (!previousTransport) {
|
|
1555
|
+
this.log("debug", "Authentication updated (not connected yet)");
|
|
1556
|
+
return;
|
|
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;
|
|
1504
1572
|
}
|
|
1573
|
+
await previousTransport.close().catch(() => void 0);
|
|
1505
1574
|
this.log("debug", "Authentication updated");
|
|
1506
1575
|
}
|
|
1507
1576
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -1535,13 +1604,12 @@ var McpTestClient = class {
|
|
|
1535
1604
|
/**
|
|
1536
1605
|
* List all available tools
|
|
1537
1606
|
*/
|
|
1538
|
-
list: async () =>
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
},
|
|
1607
|
+
list: async () => this.listAllPages(
|
|
1608
|
+
"tools/list",
|
|
1609
|
+
"tools",
|
|
1610
|
+
(cursor) => this.listTools(cursor),
|
|
1611
|
+
(page) => page.tools
|
|
1612
|
+
),
|
|
1545
1613
|
/**
|
|
1546
1614
|
* Call a tool by name with arguments
|
|
1547
1615
|
*/
|
|
@@ -1557,23 +1625,21 @@ var McpTestClient = class {
|
|
|
1557
1625
|
/**
|
|
1558
1626
|
* List all static resources
|
|
1559
1627
|
*/
|
|
1560
|
-
list: async () =>
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
},
|
|
1628
|
+
list: async () => this.listAllPages(
|
|
1629
|
+
"resources/list",
|
|
1630
|
+
"resources",
|
|
1631
|
+
(cursor) => this.listResources(cursor),
|
|
1632
|
+
(page) => page.resources
|
|
1633
|
+
),
|
|
1567
1634
|
/**
|
|
1568
1635
|
* List all resource templates
|
|
1569
1636
|
*/
|
|
1570
|
-
listTemplates: async () =>
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
},
|
|
1637
|
+
listTemplates: async () => this.listAllPages(
|
|
1638
|
+
"resources/templates/list",
|
|
1639
|
+
"resource templates",
|
|
1640
|
+
(cursor) => this.listResourceTemplates(cursor),
|
|
1641
|
+
(page) => page.resourceTemplates
|
|
1642
|
+
),
|
|
1577
1643
|
/**
|
|
1578
1644
|
* Read a resource by URI
|
|
1579
1645
|
*/
|
|
@@ -1601,13 +1667,12 @@ var McpTestClient = class {
|
|
|
1601
1667
|
/**
|
|
1602
1668
|
* List all available prompts
|
|
1603
1669
|
*/
|
|
1604
|
-
list: async () =>
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
},
|
|
1670
|
+
list: async () => this.listAllPages(
|
|
1671
|
+
"prompts/list",
|
|
1672
|
+
"prompts",
|
|
1673
|
+
(cursor) => this.listPrompts(cursor),
|
|
1674
|
+
(page) => page.prompts
|
|
1675
|
+
),
|
|
1611
1676
|
/**
|
|
1612
1677
|
* Get a prompt with arguments
|
|
1613
1678
|
*/
|
|
@@ -1991,8 +2056,33 @@ var McpTestClient = class {
|
|
|
1991
2056
|
clientInfo: this.config.clientInfo
|
|
1992
2057
|
});
|
|
1993
2058
|
}
|
|
1994
|
-
async listTools() {
|
|
1995
|
-
return this.request("tools/list", {});
|
|
2059
|
+
async listTools(cursor) {
|
|
2060
|
+
return this.request("tools/list", cursor ? { cursor } : {});
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Follow `nextCursor` until the server stops returning one, concatenating the
|
|
2064
|
+
* pages. A server that hands back a cursor it already returned, or never stops
|
|
2065
|
+
* paging, fails the call rather than looping.
|
|
2066
|
+
*/
|
|
2067
|
+
async listAllPages(method, label, fetchPage, itemsOf) {
|
|
2068
|
+
const items = [];
|
|
2069
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
2070
|
+
let cursor;
|
|
2071
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
2072
|
+
const response = await fetchPage(cursor);
|
|
2073
|
+
if (!response.success || !response.data) {
|
|
2074
|
+
throw new Error(`Failed to list ${label}: ${response.error?.message}`);
|
|
2075
|
+
}
|
|
2076
|
+
items.push(...itemsOf(response.data) ?? []);
|
|
2077
|
+
const next = response.data.nextCursor;
|
|
2078
|
+
if (!next) return items;
|
|
2079
|
+
if (seenCursors.has(next)) {
|
|
2080
|
+
throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`);
|
|
2081
|
+
}
|
|
2082
|
+
seenCursors.add(next);
|
|
2083
|
+
cursor = next;
|
|
2084
|
+
}
|
|
2085
|
+
throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`);
|
|
1996
2086
|
}
|
|
1997
2087
|
async callTool(name, args, progressToken) {
|
|
1998
2088
|
return this.request("tools/call", {
|
|
@@ -2017,17 +2107,17 @@ var McpTestClient = class {
|
|
|
2017
2107
|
timestamp
|
|
2018
2108
|
});
|
|
2019
2109
|
};
|
|
2020
|
-
async listResources() {
|
|
2021
|
-
return this.request("resources/list", {});
|
|
2110
|
+
async listResources(cursor) {
|
|
2111
|
+
return this.request("resources/list", cursor ? { cursor } : {});
|
|
2022
2112
|
}
|
|
2023
|
-
async listResourceTemplates() {
|
|
2024
|
-
return this.request("resources/templates/list", {});
|
|
2113
|
+
async listResourceTemplates(cursor) {
|
|
2114
|
+
return this.request("resources/templates/list", cursor ? { cursor } : {});
|
|
2025
2115
|
}
|
|
2026
2116
|
async readResource(uri) {
|
|
2027
2117
|
return this.request("resources/read", { uri });
|
|
2028
2118
|
}
|
|
2029
|
-
async listPrompts() {
|
|
2030
|
-
return this.request("prompts/list", {});
|
|
2119
|
+
async listPrompts(cursor) {
|
|
2120
|
+
return this.request("prompts/list", cursor ? { cursor } : {});
|
|
2031
2121
|
}
|
|
2032
2122
|
async getPrompt(name, args) {
|
|
2033
2123
|
return this.request("prompts/get", {
|
|
@@ -2104,10 +2194,11 @@ var McpTestClient = class {
|
|
|
2104
2194
|
requestId: id
|
|
2105
2195
|
};
|
|
2106
2196
|
} catch (err) {
|
|
2197
|
+
if (isInterceptedError(err)) throw err;
|
|
2107
2198
|
const durationMs = Date.now() - start;
|
|
2108
2199
|
const error = {
|
|
2109
2200
|
code: -32603,
|
|
2110
|
-
message: err
|
|
2201
|
+
message: errorMessage(err)
|
|
2111
2202
|
};
|
|
2112
2203
|
return {
|
|
2113
2204
|
success: false,
|
|
@@ -2255,12 +2346,12 @@ var McpTestClient = class {
|
|
|
2255
2346
|
const payload = this.decodeJwtPayload(token);
|
|
2256
2347
|
if (!payload) return [];
|
|
2257
2348
|
const scope = payload["scope"];
|
|
2258
|
-
const
|
|
2349
|
+
const scopes2 = payload["scopes"];
|
|
2259
2350
|
if (typeof scope === "string") {
|
|
2260
2351
|
return scope.split(" ");
|
|
2261
2352
|
}
|
|
2262
|
-
if (Array.isArray(
|
|
2263
|
-
return
|
|
2353
|
+
if (Array.isArray(scopes2)) {
|
|
2354
|
+
return scopes2;
|
|
2264
2355
|
}
|
|
2265
2356
|
return [];
|
|
2266
2357
|
} catch {
|
|
@@ -2392,14 +2483,15 @@ var TestTokenFactory = class {
|
|
|
2392
2483
|
*/
|
|
2393
2484
|
async createTestToken(options) {
|
|
2394
2485
|
await this.ensureKeys();
|
|
2395
|
-
const
|
|
2486
|
+
const nowMs = Date.now();
|
|
2487
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
2396
2488
|
const exp = options.exp ?? 3600;
|
|
2397
2489
|
const payload = {
|
|
2398
2490
|
iss: options.iss ?? this.issuer,
|
|
2399
2491
|
sub: options.sub,
|
|
2400
2492
|
aud: options.aud ?? this.audience,
|
|
2401
|
-
iat
|
|
2402
|
-
exp:
|
|
2493
|
+
iat,
|
|
2494
|
+
exp: Math.ceil(nowMs / 1e3) + exp,
|
|
2403
2495
|
scope: options.scopes?.join(" "),
|
|
2404
2496
|
...options.claims
|
|
2405
2497
|
};
|
|
@@ -2424,10 +2516,10 @@ var TestTokenFactory = class {
|
|
|
2424
2516
|
/**
|
|
2425
2517
|
* Create a regular user token
|
|
2426
2518
|
*/
|
|
2427
|
-
async createUserToken(sub = "user-001",
|
|
2519
|
+
async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
|
|
2428
2520
|
return this.createTestToken({
|
|
2429
2521
|
sub,
|
|
2430
|
-
scopes,
|
|
2522
|
+
scopes: scopes2,
|
|
2431
2523
|
claims: {
|
|
2432
2524
|
email: "user@test.local",
|
|
2433
2525
|
name: "Test User",
|
|
@@ -2438,10 +2530,11 @@ var TestTokenFactory = class {
|
|
|
2438
2530
|
/**
|
|
2439
2531
|
* Create an anonymous user token
|
|
2440
2532
|
*/
|
|
2441
|
-
async createAnonymousToken() {
|
|
2533
|
+
async createAnonymousToken(expiresIn) {
|
|
2442
2534
|
return this.createTestToken({
|
|
2443
2535
|
sub: `anon:${Date.now()}`,
|
|
2444
2536
|
scopes: ["anonymous"],
|
|
2537
|
+
exp: expiresIn,
|
|
2445
2538
|
claims: {
|
|
2446
2539
|
name: "Anonymous",
|
|
2447
2540
|
role: "anonymous"
|
|
@@ -2652,17 +2745,7 @@ function createTestUser(overrides) {
|
|
|
2652
2745
|
|
|
2653
2746
|
// libs/testing/src/auth/mock-oauth-server.ts
|
|
2654
2747
|
import { createServer } from "http";
|
|
2655
|
-
|
|
2656
|
-
var _sha256Base64url;
|
|
2657
|
-
var _base64urlEncode;
|
|
2658
|
-
async function loadCryptoUtils() {
|
|
2659
|
-
if (!_randomBytes) {
|
|
2660
|
-
const utils = await import("@frontmcp/utils");
|
|
2661
|
-
_randomBytes = utils.randomBytes;
|
|
2662
|
-
_sha256Base64url = utils.sha256Base64url;
|
|
2663
|
-
_base64urlEncode = utils.base64urlEncode;
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2748
|
+
import { base64urlEncode, randomBytes, sha256Base64url } from "@frontmcp/utils";
|
|
2666
2749
|
var MockOAuthServer = class {
|
|
2667
2750
|
tokenFactory;
|
|
2668
2751
|
options;
|
|
@@ -2677,8 +2760,8 @@ var MockOAuthServer = class {
|
|
|
2677
2760
|
accessTokenTtlSeconds;
|
|
2678
2761
|
/** Refresh token TTL in seconds */
|
|
2679
2762
|
refreshTokenTtlSeconds;
|
|
2680
|
-
constructor(
|
|
2681
|
-
this.tokenFactory =
|
|
2763
|
+
constructor(tokenFactory2, options = {}) {
|
|
2764
|
+
this.tokenFactory = tokenFactory2;
|
|
2682
2765
|
this.options = options;
|
|
2683
2766
|
this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
|
|
2684
2767
|
this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
|
|
@@ -2935,14 +3018,14 @@ ${error.stack}` : String(error);
|
|
|
2935
3018
|
return;
|
|
2936
3019
|
}
|
|
2937
3020
|
const code = this.generateCode();
|
|
2938
|
-
const
|
|
3021
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
2939
3022
|
this.authCodes.set(code, {
|
|
2940
3023
|
code,
|
|
2941
3024
|
clientId,
|
|
2942
3025
|
redirectUri,
|
|
2943
3026
|
codeChallenge,
|
|
2944
3027
|
codeChallengeMethod,
|
|
2945
|
-
scopes,
|
|
3028
|
+
scopes: scopes2,
|
|
2946
3029
|
user: testUser,
|
|
2947
3030
|
state,
|
|
2948
3031
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -2982,7 +3065,7 @@ ${error.stack}` : String(error);
|
|
|
2982
3065
|
}
|
|
2983
3066
|
const grantType = params.get("grant_type");
|
|
2984
3067
|
if (grantType === "anonymous") {
|
|
2985
|
-
const token = await this.tokenFactory.createAnonymousToken();
|
|
3068
|
+
const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
|
|
2986
3069
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2987
3070
|
res.end(
|
|
2988
3071
|
JSON.stringify({
|
|
@@ -3114,6 +3197,8 @@ ${error.stack}` : String(error);
|
|
|
3114
3197
|
codeRecord.used = true;
|
|
3115
3198
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3116
3199
|
sub: codeRecord.user.sub,
|
|
3200
|
+
scopes: codeRecord.scopes,
|
|
3201
|
+
exp: this.accessTokenTtlSeconds,
|
|
3117
3202
|
claims: {
|
|
3118
3203
|
email: codeRecord.user.email,
|
|
3119
3204
|
name: codeRecord.user.name,
|
|
@@ -3122,6 +3207,7 @@ ${error.stack}` : String(error);
|
|
|
3122
3207
|
});
|
|
3123
3208
|
const idToken = await this.tokenFactory.createTestToken({
|
|
3124
3209
|
sub: codeRecord.user.sub,
|
|
3210
|
+
exp: this.accessTokenTtlSeconds,
|
|
3125
3211
|
claims: {
|
|
3126
3212
|
email: codeRecord.user.email,
|
|
3127
3213
|
name: codeRecord.user.name,
|
|
@@ -3199,6 +3285,8 @@ ${error.stack}` : String(error);
|
|
|
3199
3285
|
}
|
|
3200
3286
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3201
3287
|
sub: tokenRecord.user.sub,
|
|
3288
|
+
scopes: tokenRecord.scopes,
|
|
3289
|
+
exp: this.accessTokenTtlSeconds,
|
|
3202
3290
|
claims: {
|
|
3203
3291
|
email: tokenRecord.user.email,
|
|
3204
3292
|
name: tokenRecord.user.name,
|
|
@@ -3303,14 +3391,14 @@ ${error.stack}` : String(error);
|
|
|
3303
3391
|
name
|
|
3304
3392
|
};
|
|
3305
3393
|
const code = this.generateCode();
|
|
3306
|
-
const
|
|
3394
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3307
3395
|
this.authCodes.set(code, {
|
|
3308
3396
|
code,
|
|
3309
3397
|
clientId,
|
|
3310
3398
|
redirectUri,
|
|
3311
3399
|
codeChallenge,
|
|
3312
3400
|
codeChallengeMethod,
|
|
3313
|
-
scopes,
|
|
3401
|
+
scopes: scopes2,
|
|
3314
3402
|
user: testUser,
|
|
3315
3403
|
state,
|
|
3316
3404
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3416,8 +3504,7 @@ ${error.stack}` : String(error);
|
|
|
3416
3504
|
* Generate a random authorization code
|
|
3417
3505
|
*/
|
|
3418
3506
|
async generateCodeAsync() {
|
|
3419
|
-
|
|
3420
|
-
return _base64urlEncode(_randomBytes(32));
|
|
3507
|
+
return base64urlEncode(randomBytes(32));
|
|
3421
3508
|
}
|
|
3422
3509
|
/**
|
|
3423
3510
|
* Generate a random authorization code (sync wrapper for compatibility)
|
|
@@ -3433,8 +3520,7 @@ ${error.stack}` : String(error);
|
|
|
3433
3520
|
*/
|
|
3434
3521
|
async computeCodeChallengeAsync(verifier, method) {
|
|
3435
3522
|
if (method === "S256") {
|
|
3436
|
-
|
|
3437
|
-
return _sha256Base64url(verifier);
|
|
3523
|
+
return sha256Base64url(verifier);
|
|
3438
3524
|
}
|
|
3439
3525
|
return verifier;
|
|
3440
3526
|
}
|
|
@@ -4021,7 +4107,7 @@ var MockCimdServer = class {
|
|
|
4021
4107
|
};
|
|
4022
4108
|
|
|
4023
4109
|
// libs/testing/src/server/test-server.ts
|
|
4024
|
-
import { spawn } from "child_process";
|
|
4110
|
+
import { execFile, spawn } from "child_process";
|
|
4025
4111
|
import { sha256Hex } from "@frontmcp/utils";
|
|
4026
4112
|
|
|
4027
4113
|
// libs/testing/src/errors/index.ts
|
|
@@ -4128,6 +4214,8 @@ var E2E_PORT_RANGES = {
|
|
|
4128
4214
|
"demo-e2e-distributed": { start: 50440, size: 20 },
|
|
4129
4215
|
// Protocol revision E2E tests (50460-50479)
|
|
4130
4216
|
"demo-e2e-protocol-20260728": { start: 50460, size: 20 },
|
|
4217
|
+
// Testing library E2E tests (50480-50499)
|
|
4218
|
+
"demo-e2e-testing": { start: 50480, size: 20 },
|
|
4131
4219
|
// Mock servers and utilities (50900-50999)
|
|
4132
4220
|
"mock-oauth": { start: 50900, size: 10 },
|
|
4133
4221
|
"mock-api": { start: 50910, size: 10 },
|
|
@@ -4136,6 +4224,66 @@ var E2E_PORT_RANGES = {
|
|
|
4136
4224
|
default: { start: 51e3, size: 100 }
|
|
4137
4225
|
};
|
|
4138
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
|
+
}
|
|
4139
4287
|
var projectPortIndex = /* @__PURE__ */ new Map();
|
|
4140
4288
|
function getPortRange(project) {
|
|
4141
4289
|
const key = project;
|
|
@@ -4146,15 +4294,22 @@ function getPortRange(project) {
|
|
|
4146
4294
|
}
|
|
4147
4295
|
async function reservePort(project, preferredPort) {
|
|
4148
4296
|
const range = getPortRange(project);
|
|
4149
|
-
|
|
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) {
|
|
4150
4310
|
const reservation = await tryReservePort(preferredPort, project);
|
|
4151
4311
|
if (reservation) {
|
|
4152
|
-
return
|
|
4153
|
-
port: preferredPort,
|
|
4154
|
-
release: async () => {
|
|
4155
|
-
await releasePort(preferredPort);
|
|
4156
|
-
}
|
|
4157
|
-
};
|
|
4312
|
+
return handle(preferredPort);
|
|
4158
4313
|
}
|
|
4159
4314
|
console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
|
|
4160
4315
|
}
|
|
@@ -4163,30 +4318,19 @@ async function reservePort(project, preferredPort) {
|
|
|
4163
4318
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4164
4319
|
const port = range.start + index % range.size;
|
|
4165
4320
|
index = (index + 1) % range.size;
|
|
4166
|
-
if (reservedPorts.has(port)) {
|
|
4321
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) {
|
|
4167
4322
|
continue;
|
|
4168
4323
|
}
|
|
4169
4324
|
const reservation = await tryReservePort(port, project);
|
|
4170
4325
|
if (reservation) {
|
|
4171
4326
|
projectPortIndex.set(project, index);
|
|
4172
|
-
return
|
|
4173
|
-
port,
|
|
4174
|
-
release: async () => {
|
|
4175
|
-
await releasePort(port);
|
|
4176
|
-
}
|
|
4177
|
-
};
|
|
4327
|
+
return handle(port);
|
|
4178
4328
|
}
|
|
4179
4329
|
}
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
return {
|
|
4185
|
-
port: dynamicPort,
|
|
4186
|
-
release: async () => {
|
|
4187
|
-
await releasePort(dynamicPort);
|
|
4188
|
-
}
|
|
4189
|
-
};
|
|
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);
|
|
4190
4334
|
}
|
|
4191
4335
|
}
|
|
4192
4336
|
throw new Error(
|
|
@@ -4194,9 +4338,13 @@ async function reservePort(project, preferredPort) {
|
|
|
4194
4338
|
);
|
|
4195
4339
|
}
|
|
4196
4340
|
async function tryReservePort(port, project) {
|
|
4341
|
+
if (!acquirePortLock(port)) {
|
|
4342
|
+
return false;
|
|
4343
|
+
}
|
|
4197
4344
|
return new Promise((resolve) => {
|
|
4198
4345
|
const server = createServer4();
|
|
4199
4346
|
server.once("error", () => {
|
|
4347
|
+
releasePortLock(port);
|
|
4200
4348
|
resolve(false);
|
|
4201
4349
|
});
|
|
4202
4350
|
server.listen(port, () => {
|
|
@@ -4222,31 +4370,6 @@ async function releasePort(port) {
|
|
|
4222
4370
|
});
|
|
4223
4371
|
});
|
|
4224
4372
|
}
|
|
4225
|
-
async function findAvailablePortInRange(start, end) {
|
|
4226
|
-
for (let port = start; port < end; port++) {
|
|
4227
|
-
if (reservedPorts.has(port)) {
|
|
4228
|
-
continue;
|
|
4229
|
-
}
|
|
4230
|
-
const available = await isPortAvailable(port);
|
|
4231
|
-
if (available) {
|
|
4232
|
-
return port;
|
|
4233
|
-
}
|
|
4234
|
-
}
|
|
4235
|
-
return null;
|
|
4236
|
-
}
|
|
4237
|
-
async function isPortAvailable(port) {
|
|
4238
|
-
return new Promise((resolve) => {
|
|
4239
|
-
const server = createServer4();
|
|
4240
|
-
server.once("error", () => {
|
|
4241
|
-
resolve(false);
|
|
4242
|
-
});
|
|
4243
|
-
server.listen(port, () => {
|
|
4244
|
-
server.close(() => {
|
|
4245
|
-
resolve(true);
|
|
4246
|
-
});
|
|
4247
|
-
});
|
|
4248
|
-
});
|
|
4249
|
-
}
|
|
4250
4373
|
|
|
4251
4374
|
// libs/testing/src/server/test-server.ts
|
|
4252
4375
|
var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
|
|
@@ -4255,8 +4378,11 @@ var TestServer = class _TestServer {
|
|
|
4255
4378
|
options;
|
|
4256
4379
|
_info;
|
|
4257
4380
|
logs = [];
|
|
4381
|
+
/** Frees the port reservation (socket and cross-process lock); held until stop() */
|
|
4258
4382
|
portRelease = null;
|
|
4259
|
-
|
|
4383
|
+
/** Frees only the reservation socket, just before the server is spawned */
|
|
4384
|
+
portReleaseSocket = null;
|
|
4385
|
+
constructor(options, port, portRelease, portReleaseSocket) {
|
|
4260
4386
|
this.options = {
|
|
4261
4387
|
port,
|
|
4262
4388
|
project: options.project,
|
|
@@ -4268,6 +4394,7 @@ var TestServer = class _TestServer {
|
|
|
4268
4394
|
debug: options.debug ?? DEBUG_SERVER
|
|
4269
4395
|
};
|
|
4270
4396
|
this.portRelease = portRelease ?? null;
|
|
4397
|
+
this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
|
|
4271
4398
|
this._info = {
|
|
4272
4399
|
baseUrl: `http://localhost:${port}`,
|
|
4273
4400
|
port
|
|
@@ -4280,8 +4407,8 @@ var TestServer = class _TestServer {
|
|
|
4280
4407
|
const project = options.project ?? "default";
|
|
4281
4408
|
const maxAttempts = 3;
|
|
4282
4409
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4283
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4284
|
-
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);
|
|
4285
4412
|
try {
|
|
4286
4413
|
await server.startProcess();
|
|
4287
4414
|
return server;
|
|
@@ -4321,7 +4448,7 @@ var TestServer = class _TestServer {
|
|
|
4321
4448
|
}
|
|
4322
4449
|
const maxAttempts = 3;
|
|
4323
4450
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4324
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4451
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4325
4452
|
const serverOptions = {
|
|
4326
4453
|
...options,
|
|
4327
4454
|
port,
|
|
@@ -4329,7 +4456,7 @@ var TestServer = class _TestServer {
|
|
|
4329
4456
|
command: `npx nx serve ${project} --port ${port}`,
|
|
4330
4457
|
cwd: options.cwd ?? process.cwd()
|
|
4331
4458
|
};
|
|
4332
|
-
const server = new _TestServer(serverOptions, port, release);
|
|
4459
|
+
const server = new _TestServer(serverOptions, port, release, releaseSocket);
|
|
4333
4460
|
try {
|
|
4334
4461
|
await server.startProcess();
|
|
4335
4462
|
return server;
|
|
@@ -4387,10 +4514,17 @@ var TestServer = class _TestServer {
|
|
|
4387
4514
|
* Stop the test server
|
|
4388
4515
|
*/
|
|
4389
4516
|
async stop() {
|
|
4390
|
-
|
|
4391
|
-
await this.
|
|
4392
|
-
|
|
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;
|
|
4393
4525
|
}
|
|
4526
|
+
}
|
|
4527
|
+
async stopProcess() {
|
|
4394
4528
|
if (this.process) {
|
|
4395
4529
|
this.log("Stopping server...");
|
|
4396
4530
|
if (this.process.exitCode !== null || this.process.signalCode !== null) {
|
|
@@ -4463,7 +4597,7 @@ var TestServer = class _TestServer {
|
|
|
4463
4597
|
* Restart the server
|
|
4464
4598
|
*/
|
|
4465
4599
|
async restart() {
|
|
4466
|
-
await this.
|
|
4600
|
+
await this.stopProcess();
|
|
4467
4601
|
await this.startProcess();
|
|
4468
4602
|
}
|
|
4469
4603
|
/**
|
|
@@ -4493,9 +4627,9 @@ var TestServer = class _TestServer {
|
|
|
4493
4627
|
PORT: String(this.options.port)
|
|
4494
4628
|
};
|
|
4495
4629
|
const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
|
|
4496
|
-
if (this.
|
|
4497
|
-
await this.
|
|
4498
|
-
this.
|
|
4630
|
+
if (this.portReleaseSocket) {
|
|
4631
|
+
await this.portReleaseSocket();
|
|
4632
|
+
this.portReleaseSocket = null;
|
|
4499
4633
|
await sleep2(300);
|
|
4500
4634
|
}
|
|
4501
4635
|
this.process = spawn(this.options.command, [], {
|
|
@@ -4505,9 +4639,8 @@ var TestServer = class _TestServer {
|
|
|
4505
4639
|
detached: true,
|
|
4506
4640
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4507
4641
|
});
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
}
|
|
4642
|
+
this._info.pid = this.process.pid;
|
|
4643
|
+
this._info.shellPid = this.process.pid;
|
|
4511
4644
|
let processExited = false;
|
|
4512
4645
|
let exitCode = null;
|
|
4513
4646
|
let exitError = null;
|
|
@@ -4610,6 +4743,8 @@ ${allLogs}`);
|
|
|
4610
4743
|
});
|
|
4611
4744
|
if (response.ok || response.status === 404) {
|
|
4612
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;
|
|
4613
4748
|
return;
|
|
4614
4749
|
}
|
|
4615
4750
|
lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
|
|
@@ -4654,6 +4789,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
|
|
|
4654
4789
|
function sleep2(ms) {
|
|
4655
4790
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4656
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
|
+
}
|
|
4657
4805
|
function withWorkspaceProtocolFallback(env, cwd) {
|
|
4658
4806
|
if (findInstalledProtocolPackageDir(cwd)) {
|
|
4659
4807
|
return env;
|
|
@@ -4925,16 +5073,6 @@ function hasMimeType(result, mimeType) {
|
|
|
4925
5073
|
return result.hasMimeType(mimeType);
|
|
4926
5074
|
}
|
|
4927
5075
|
|
|
4928
|
-
// libs/testing/src/fixtures/test-fixture.ts
|
|
4929
|
-
import {
|
|
4930
|
-
afterAll as _afterAll,
|
|
4931
|
-
afterEach as _afterEach,
|
|
4932
|
-
beforeAll as _beforeAll,
|
|
4933
|
-
beforeEach as _beforeEach,
|
|
4934
|
-
describe as _describe,
|
|
4935
|
-
it as _it
|
|
4936
|
-
} from "@jest/globals";
|
|
4937
|
-
|
|
4938
5076
|
// libs/testing/src/fixtures/gateway-token-binding.ts
|
|
4939
5077
|
import { trimSlashes, trimTrailing } from "@frontmcp/utils";
|
|
4940
5078
|
function gatewayTokenBinding(serverUrl, entryPath) {
|
|
@@ -4945,81 +5083,152 @@ function gatewayTokenBinding(serverUrl, entryPath) {
|
|
|
4945
5083
|
}
|
|
4946
5084
|
|
|
4947
5085
|
// libs/testing/src/fixtures/test-fixture.ts
|
|
4948
|
-
var
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
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
|
+
}
|
|
4961
5106
|
}
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
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;
|
|
4970
5169
|
if (isDebug) {
|
|
4971
|
-
console.log(`[TestFixture]
|
|
5170
|
+
console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
|
|
4972
5171
|
}
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
command: serverCommand,
|
|
4978
|
-
env: currentConfig.env,
|
|
4979
|
-
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
4980
|
-
debug: isDebug
|
|
4981
|
-
});
|
|
4982
|
-
serverStartedByUs = true;
|
|
4983
|
-
if (isDebug) {
|
|
4984
|
-
console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
|
|
4985
|
-
}
|
|
4986
|
-
} catch (error) {
|
|
4987
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
4988
|
-
throw new Error(
|
|
4989
|
-
`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.
|
|
4990
5176
|
|
|
4991
|
-
Server entry: ${
|
|
4992
|
-
Project: ${
|
|
5177
|
+
Server entry: ${config.server}
|
|
5178
|
+
Project: ${config.project ?? "default"}
|
|
4993
5179
|
Command: ${serverCommand}
|
|
4994
5180
|
|
|
4995
|
-
Error: ${errMsg}
|
|
4996
|
-
|
|
4997
|
-
}
|
|
4998
|
-
} else {
|
|
4999
|
-
throw new Error(
|
|
5000
|
-
'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
|
|
5181
|
+
Error: ${errMsg}`,
|
|
5182
|
+
{ cause: error }
|
|
5001
5183
|
);
|
|
5002
5184
|
}
|
|
5185
|
+
} else {
|
|
5186
|
+
throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
|
|
5003
5187
|
}
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
}
|
|
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 };
|
|
5009
5192
|
}
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
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
|
+
}
|
|
5017
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 };
|
|
5018
5227
|
const clientInstance = McpTestClient.create({
|
|
5019
|
-
baseUrl: resolveClientBaseUrl(
|
|
5020
|
-
entryPath:
|
|
5021
|
-
transport:
|
|
5022
|
-
publicMode
|
|
5228
|
+
baseUrl: resolveClientBaseUrl(server, clientConfig),
|
|
5229
|
+
entryPath: clientConfig.entryPath,
|
|
5230
|
+
transport: clientConfig.transport ?? "streamable-http",
|
|
5231
|
+
publicMode
|
|
5023
5232
|
}).build();
|
|
5024
5233
|
try {
|
|
5025
5234
|
await clientInstance.connect();
|
|
@@ -5033,17 +5242,18 @@ async function createTestFixtures() {
|
|
|
5033
5242
|
throw err;
|
|
5034
5243
|
}
|
|
5035
5244
|
}
|
|
5036
|
-
const
|
|
5037
|
-
const
|
|
5038
|
-
return {
|
|
5245
|
+
const context = { extraClients: [] };
|
|
5246
|
+
const fixtures = {
|
|
5039
5247
|
mcp: clientInstance,
|
|
5040
|
-
auth,
|
|
5041
|
-
server
|
|
5248
|
+
auth: createAuthFixture(tokenFactory2),
|
|
5249
|
+
server: createServerFixture(server, clientConfig, clientInstance, context)
|
|
5042
5250
|
};
|
|
5251
|
+
fixtureContexts.set(fixtures, context);
|
|
5252
|
+
return fixtures;
|
|
5043
5253
|
}
|
|
5044
5254
|
async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
5045
|
-
if (testFailed
|
|
5046
|
-
const logs =
|
|
5255
|
+
if (testFailed) {
|
|
5256
|
+
const logs = fixtures.server.getLogs();
|
|
5047
5257
|
if (logs.length > 0) {
|
|
5048
5258
|
console.error("\n[TestFixture] === Server Logs (test failed) ===");
|
|
5049
5259
|
const recentLogs = logs.slice(-50);
|
|
@@ -5054,20 +5264,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
|
5054
5264
|
console.error("[TestFixture] === End Server Logs ===\n");
|
|
5055
5265
|
}
|
|
5056
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
|
+
}
|
|
5057
5273
|
if (fixtures.mcp.isConnected()) {
|
|
5058
5274
|
await fixtures.mcp.disconnect();
|
|
5059
5275
|
}
|
|
5060
5276
|
}
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
await serverInstance.stop();
|
|
5064
|
-
}
|
|
5065
|
-
serverInstance = null;
|
|
5066
|
-
tokenFactory = null;
|
|
5067
|
-
serverStartedByUs = false;
|
|
5068
|
-
}
|
|
5069
|
-
function resolveClientBaseUrl(server) {
|
|
5070
|
-
return currentConfig.baseUrl ?? server.info.baseUrl;
|
|
5277
|
+
function resolveClientBaseUrl(server, config) {
|
|
5278
|
+
return config.baseUrl ?? server.info.baseUrl;
|
|
5071
5279
|
}
|
|
5072
5280
|
function createAuthFixture(factory) {
|
|
5073
5281
|
const users = {
|
|
@@ -5113,28 +5321,39 @@ function createAuthFixture(factory) {
|
|
|
5113
5321
|
getAudience: () => factory.getAudience()
|
|
5114
5322
|
};
|
|
5115
5323
|
}
|
|
5116
|
-
function createServerFixture(server) {
|
|
5324
|
+
function createServerFixture(server, config, mcp, context) {
|
|
5117
5325
|
return {
|
|
5118
|
-
|
|
5326
|
+
// Live view: `pid` is filled in once the server is listening and follows restarts
|
|
5327
|
+
get info() {
|
|
5328
|
+
return server.info;
|
|
5329
|
+
},
|
|
5119
5330
|
createClient: async (opts) => {
|
|
5120
|
-
|
|
5121
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5122
|
-
entryPath: opts?.entryPath ??
|
|
5331
|
+
const client = await McpTestClient.create({
|
|
5332
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5333
|
+
entryPath: opts?.entryPath ?? config.entryPath,
|
|
5123
5334
|
transport: opts?.transport ?? "streamable-http",
|
|
5124
5335
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
5125
5336
|
clientInfo: opts?.clientInfo,
|
|
5126
|
-
publicMode:
|
|
5337
|
+
publicMode: config.publicMode
|
|
5127
5338
|
}).buildAndConnect();
|
|
5339
|
+
context.extraClients.push(client);
|
|
5340
|
+
return client;
|
|
5128
5341
|
},
|
|
5129
5342
|
createClientBuilder: () => {
|
|
5130
5343
|
const builder = new McpTestClientBuilder({
|
|
5131
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5132
|
-
entryPath:
|
|
5133
|
-
publicMode:
|
|
5344
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5345
|
+
entryPath: config.entryPath,
|
|
5346
|
+
publicMode: config.publicMode
|
|
5134
5347
|
});
|
|
5135
5348
|
return builder;
|
|
5136
5349
|
},
|
|
5137
|
-
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
|
+
},
|
|
5138
5357
|
getLogs: () => server.getLogs(),
|
|
5139
5358
|
clearLogs: () => server.clearLogs()
|
|
5140
5359
|
};
|
|
@@ -5145,12 +5364,12 @@ function resolveServerCommand(server) {
|
|
|
5145
5364
|
}
|
|
5146
5365
|
return `npx tsx ${server}`;
|
|
5147
5366
|
}
|
|
5148
|
-
function runWithFixtures(fn) {
|
|
5149
|
-
return async () => {
|
|
5150
|
-
const fixtures = await
|
|
5367
|
+
function runWithFixtures(fn, chain) {
|
|
5368
|
+
return async (...extra) => {
|
|
5369
|
+
const fixtures = await buildFixtures(chain);
|
|
5151
5370
|
let testFailed = false;
|
|
5152
5371
|
try {
|
|
5153
|
-
await fn(fixtures);
|
|
5372
|
+
await fn(fixtures, ...extra);
|
|
5154
5373
|
} catch (error) {
|
|
5155
5374
|
testFailed = true;
|
|
5156
5375
|
throw error;
|
|
@@ -5159,24 +5378,37 @@ function runWithFixtures(fn) {
|
|
|
5159
5378
|
}
|
|
5160
5379
|
};
|
|
5161
5380
|
}
|
|
5381
|
+
function skipTitle(name, scope) {
|
|
5382
|
+
return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
|
|
5383
|
+
}
|
|
5162
5384
|
function testWithFixtures(name, fn) {
|
|
5163
|
-
const
|
|
5385
|
+
const chain = scopes.slice();
|
|
5386
|
+
const scope = currentScope();
|
|
5164
5387
|
if (scope.skipped) {
|
|
5165
|
-
|
|
5388
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5166
5389
|
return;
|
|
5167
5390
|
}
|
|
5168
|
-
|
|
5391
|
+
jestGlobals().it(name, runWithFixtures(fn, chain));
|
|
5169
5392
|
}
|
|
5170
5393
|
function use(config) {
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
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
|
+
}
|
|
5175
5407
|
}
|
|
5176
5408
|
function skip(nameOrCondition, fnOrReason) {
|
|
5177
5409
|
if (typeof nameOrCondition === "boolean") {
|
|
5178
5410
|
if (nameOrCondition) {
|
|
5179
|
-
const scope =
|
|
5411
|
+
const scope = currentScope();
|
|
5180
5412
|
scope.skipped = true;
|
|
5181
5413
|
scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
|
|
5182
5414
|
}
|
|
@@ -5187,57 +5419,111 @@ function skip(nameOrCondition, fnOrReason) {
|
|
|
5187
5419
|
`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
|
|
5188
5420
|
);
|
|
5189
5421
|
}
|
|
5190
|
-
|
|
5422
|
+
jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
|
|
5191
5423
|
}
|
|
5192
5424
|
function only(name, fn) {
|
|
5193
|
-
const scope =
|
|
5425
|
+
const scope = currentScope();
|
|
5194
5426
|
if (scope.skipped) {
|
|
5195
|
-
|
|
5427
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
|
|
5196
5428
|
return;
|
|
5197
5429
|
}
|
|
5198
|
-
|
|
5430
|
+
jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
|
|
5199
5431
|
}
|
|
5200
5432
|
function todo(name) {
|
|
5201
|
-
|
|
5433
|
+
jestGlobals().it.todo(name);
|
|
5202
5434
|
}
|
|
5203
|
-
function
|
|
5435
|
+
function each(table, ...tagged) {
|
|
5436
|
+
const chain = scopes.slice();
|
|
5437
|
+
const register = jestGlobals().it.each(table, ...tagged);
|
|
5204
5438
|
return (name, fn) => {
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
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
|
|
5213
5457
|
});
|
|
5458
|
+
try {
|
|
5459
|
+
fn(...args);
|
|
5460
|
+
} finally {
|
|
5461
|
+
scopes.pop();
|
|
5462
|
+
}
|
|
5214
5463
|
};
|
|
5215
5464
|
}
|
|
5216
|
-
function
|
|
5217
|
-
return (
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
});
|
|
5465
|
+
function scopedRegister(pick) {
|
|
5466
|
+
return (name, fn, timeout) => {
|
|
5467
|
+
pick()(name, withScope(fn), timeout);
|
|
5468
|
+
};
|
|
5221
5469
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
}
|
|
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
|
+
);
|
|
5227
5495
|
var test = testWithFixtures;
|
|
5228
5496
|
test.use = use;
|
|
5229
|
-
test.describe =
|
|
5230
|
-
test.beforeAll =
|
|
5231
|
-
test.beforeEach =
|
|
5232
|
-
test.afterEach =
|
|
5233
|
-
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));
|
|
5234
5502
|
test.skip = skip;
|
|
5235
5503
|
test.only = only;
|
|
5236
5504
|
test.todo = todo;
|
|
5505
|
+
test.each = each;
|
|
5237
5506
|
|
|
5238
5507
|
// libs/testing/src/expect.ts
|
|
5239
|
-
|
|
5240
|
-
|
|
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
|
+
});
|
|
5241
5527
|
|
|
5242
5528
|
// libs/testing/src/platform/platform-types.ts
|
|
5243
5529
|
function getPlatformMetaNamespace(platform) {
|
|
@@ -5900,34 +6186,44 @@ var HttpInterceptorImpl = class {
|
|
|
5900
6186
|
}
|
|
5901
6187
|
};
|
|
5902
6188
|
}
|
|
5903
|
-
get(url, response) {
|
|
6189
|
+
get(url, response, options) {
|
|
5904
6190
|
return this.mock({
|
|
5905
|
-
match: { url, method: "GET" },
|
|
5906
|
-
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 }
|
|
5907
6195
|
});
|
|
5908
6196
|
}
|
|
5909
|
-
post(url, response) {
|
|
6197
|
+
post(url, response, options) {
|
|
5910
6198
|
return this.mock({
|
|
5911
|
-
match: { url, method: "POST" },
|
|
5912
|
-
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 }
|
|
5913
6203
|
});
|
|
5914
6204
|
}
|
|
5915
|
-
put(url, response) {
|
|
6205
|
+
put(url, response, options) {
|
|
5916
6206
|
return this.mock({
|
|
5917
|
-
match: { url, method: "PUT" },
|
|
5918
|
-
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 }
|
|
5919
6211
|
});
|
|
5920
6212
|
}
|
|
5921
|
-
delete(url, response) {
|
|
6213
|
+
delete(url, response, options) {
|
|
5922
6214
|
return this.mock({
|
|
5923
|
-
match: { url, method: "DELETE" },
|
|
5924
|
-
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 }
|
|
5925
6219
|
});
|
|
5926
6220
|
}
|
|
5927
|
-
any(url, response) {
|
|
6221
|
+
any(url, response, options) {
|
|
5928
6222
|
return this.mock({
|
|
5929
|
-
match: { url },
|
|
5930
|
-
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 }
|
|
5931
6227
|
});
|
|
5932
6228
|
}
|
|
5933
6229
|
clear() {
|
|
@@ -5959,6 +6255,9 @@ ${descriptions.join("\n")}`);
|
|
|
5959
6255
|
if (index !== -1) {
|
|
5960
6256
|
activeInterceptors.splice(index, 1);
|
|
5961
6257
|
}
|
|
6258
|
+
if (activeInterceptors.length === 0) {
|
|
6259
|
+
httpMock.disable();
|
|
6260
|
+
}
|
|
5962
6261
|
}
|
|
5963
6262
|
// Internal methods
|
|
5964
6263
|
isActive() {
|
|
@@ -6058,43 +6357,49 @@ ${descriptions.join("\n")}`);
|
|
|
6058
6357
|
}
|
|
6059
6358
|
};
|
|
6060
6359
|
async function interceptedFetch(input, init) {
|
|
6061
|
-
const
|
|
6062
|
-
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();
|
|
6063
6363
|
const headers = {};
|
|
6064
|
-
|
|
6065
|
-
if (
|
|
6066
|
-
|
|
6364
|
+
const collectHeaders = (source) => {
|
|
6365
|
+
if (!source) return;
|
|
6366
|
+
if (source instanceof Headers) {
|
|
6367
|
+
source.forEach((value, key) => {
|
|
6067
6368
|
headers[key.toLowerCase()] = value;
|
|
6068
6369
|
});
|
|
6069
|
-
} else if (Array.isArray(
|
|
6070
|
-
for (const [key, value] of
|
|
6370
|
+
} else if (Array.isArray(source)) {
|
|
6371
|
+
for (const [key, value] of source) {
|
|
6071
6372
|
headers[key.toLowerCase()] = value;
|
|
6072
6373
|
}
|
|
6073
6374
|
} else {
|
|
6074
|
-
for (const [key, value] of Object.entries(
|
|
6375
|
+
for (const [key, value] of Object.entries(source)) {
|
|
6075
6376
|
headers[key.toLowerCase()] = value;
|
|
6076
6377
|
}
|
|
6077
6378
|
}
|
|
6078
|
-
}
|
|
6379
|
+
};
|
|
6380
|
+
collectHeaders(init?.headers ?? request?.headers);
|
|
6079
6381
|
let body;
|
|
6080
6382
|
let rawBody;
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
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) {
|
|
6091
6396
|
try {
|
|
6092
6397
|
body = JSON.parse(rawBody);
|
|
6093
6398
|
} catch {
|
|
6094
6399
|
body = rawBody;
|
|
6095
6400
|
}
|
|
6096
6401
|
} else {
|
|
6097
|
-
body =
|
|
6402
|
+
body = bodySource;
|
|
6098
6403
|
}
|
|
6099
6404
|
}
|
|
6100
6405
|
const requestInfo = {
|
|
@@ -6124,8 +6429,20 @@ async function interceptedFetch(input, init) {
|
|
|
6124
6429
|
Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
|
|
6125
6430
|
);
|
|
6126
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
|
+
}
|
|
6127
6442
|
function normalizeResponse(response) {
|
|
6128
|
-
|
|
6443
|
+
const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
|
|
6444
|
+
if (isNetworkError) return response;
|
|
6445
|
+
if (Array.isArray(response) || !isResponseDescriptor(response)) {
|
|
6129
6446
|
return { body: response };
|
|
6130
6447
|
}
|
|
6131
6448
|
return response;
|
|
@@ -7459,47 +7776,48 @@ function clearGlobalMeasurements() {
|
|
|
7459
7776
|
}
|
|
7460
7777
|
|
|
7461
7778
|
// libs/testing/src/perf/perf-test.ts
|
|
7462
|
-
var
|
|
7463
|
-
var
|
|
7464
|
-
var
|
|
7465
|
-
var
|
|
7466
|
-
async function
|
|
7467
|
-
if (!
|
|
7468
|
-
|
|
7469
|
-
}
|
|
7470
|
-
if (!
|
|
7471
|
-
if (
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
} else if (
|
|
7475
|
-
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);
|
|
7476
7793
|
const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
7477
7794
|
if (isDebug) {
|
|
7478
7795
|
console.log(`[PerfTest] Starting server: ${serverCommand}`);
|
|
7479
7796
|
}
|
|
7480
7797
|
try {
|
|
7481
|
-
|
|
7482
|
-
project:
|
|
7483
|
-
port:
|
|
7798
|
+
serverInstance = await TestServer.start({
|
|
7799
|
+
project: currentConfig.project,
|
|
7800
|
+
port: currentConfig.port,
|
|
7484
7801
|
command: serverCommand,
|
|
7485
|
-
env:
|
|
7486
|
-
startupTimeout:
|
|
7802
|
+
env: currentConfig.env,
|
|
7803
|
+
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
7487
7804
|
debug: isDebug
|
|
7488
7805
|
});
|
|
7489
|
-
|
|
7806
|
+
serverStartedByUs = true;
|
|
7490
7807
|
if (isDebug) {
|
|
7491
|
-
console.log(`[PerfTest] Server started at ${
|
|
7808
|
+
console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
|
|
7492
7809
|
}
|
|
7493
7810
|
} catch (error) {
|
|
7494
7811
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
7495
7812
|
throw new Error(
|
|
7496
7813
|
`Failed to start test server.
|
|
7497
7814
|
|
|
7498
|
-
Server entry: ${
|
|
7499
|
-
Project: ${
|
|
7815
|
+
Server entry: ${currentConfig.server}
|
|
7816
|
+
Project: ${currentConfig.project ?? "default"}
|
|
7500
7817
|
Command: ${serverCommand}
|
|
7501
7818
|
|
|
7502
|
-
Error: ${errMsg}
|
|
7819
|
+
Error: ${errMsg}`,
|
|
7820
|
+
{ cause: error }
|
|
7503
7821
|
);
|
|
7504
7822
|
}
|
|
7505
7823
|
} else {
|
|
@@ -7507,23 +7825,23 @@ Error: ${errMsg}`
|
|
|
7507
7825
|
}
|
|
7508
7826
|
}
|
|
7509
7827
|
}
|
|
7510
|
-
async function
|
|
7511
|
-
await
|
|
7512
|
-
if (!
|
|
7828
|
+
async function createTestFixtures(testName) {
|
|
7829
|
+
await initializeSharedResources();
|
|
7830
|
+
if (!serverInstance) {
|
|
7513
7831
|
throw new Error("Server instance not initialized");
|
|
7514
7832
|
}
|
|
7515
|
-
if (!
|
|
7833
|
+
if (!tokenFactory) {
|
|
7516
7834
|
throw new Error("Token factory not initialized");
|
|
7517
7835
|
}
|
|
7518
7836
|
const clientInstance = await McpTestClient.create({
|
|
7519
|
-
baseUrl:
|
|
7520
|
-
transport:
|
|
7521
|
-
publicMode:
|
|
7837
|
+
baseUrl: serverInstance.info.baseUrl,
|
|
7838
|
+
transport: currentConfig.transport ?? "streamable-http",
|
|
7839
|
+
publicMode: currentConfig.publicMode
|
|
7522
7840
|
}).buildAndConnect();
|
|
7523
|
-
const auth = createAuthFixture2(
|
|
7524
|
-
const server = createServerFixture2(
|
|
7525
|
-
const perfImpl = createPerfFixtures(testName,
|
|
7526
|
-
if (
|
|
7841
|
+
const auth = createAuthFixture2(tokenFactory);
|
|
7842
|
+
const server = createServerFixture2(serverInstance);
|
|
7843
|
+
const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
|
|
7844
|
+
if (currentConfig.forceGcOnBaseline !== false) {
|
|
7527
7845
|
await perfImpl.baseline();
|
|
7528
7846
|
}
|
|
7529
7847
|
return {
|
|
@@ -7539,8 +7857,8 @@ async function createTestFixtures2(testName) {
|
|
|
7539
7857
|
async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
7540
7858
|
const measurement = perfImpl.buildMeasurement();
|
|
7541
7859
|
addGlobalMeasurement(measurement);
|
|
7542
|
-
if (testFailed &&
|
|
7543
|
-
const logs =
|
|
7860
|
+
if (testFailed && serverInstance) {
|
|
7861
|
+
const logs = serverInstance.getLogs();
|
|
7544
7862
|
if (logs.length > 0) {
|
|
7545
7863
|
console.error("\n[PerfTest] === Server Logs (test failed) ===");
|
|
7546
7864
|
const recentLogs = logs.slice(-50);
|
|
@@ -7555,13 +7873,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
|
7555
7873
|
await fixtures.mcp.disconnect();
|
|
7556
7874
|
}
|
|
7557
7875
|
}
|
|
7558
|
-
async function
|
|
7559
|
-
if (
|
|
7560
|
-
await
|
|
7876
|
+
async function cleanupSharedResources() {
|
|
7877
|
+
if (serverInstance && serverStartedByUs) {
|
|
7878
|
+
await serverInstance.stop();
|
|
7561
7879
|
}
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7880
|
+
serverInstance = null;
|
|
7881
|
+
tokenFactory = null;
|
|
7882
|
+
serverStartedByUs = false;
|
|
7565
7883
|
}
|
|
7566
7884
|
function createAuthFixture2(factory) {
|
|
7567
7885
|
const users = {
|
|
@@ -7616,13 +7934,13 @@ function createServerFixture2(server) {
|
|
|
7616
7934
|
transport: opts?.transport ?? "streamable-http",
|
|
7617
7935
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
7618
7936
|
clientInfo: opts?.clientInfo,
|
|
7619
|
-
publicMode:
|
|
7937
|
+
publicMode: currentConfig.publicMode
|
|
7620
7938
|
}).buildAndConnect();
|
|
7621
7939
|
},
|
|
7622
7940
|
createClientBuilder: () => {
|
|
7623
7941
|
return new McpTestClientBuilder({
|
|
7624
7942
|
baseUrl: server.info.baseUrl,
|
|
7625
|
-
publicMode:
|
|
7943
|
+
publicMode: currentConfig.publicMode
|
|
7626
7944
|
});
|
|
7627
7945
|
},
|
|
7628
7946
|
restart: () => server.restart(),
|
|
@@ -7638,7 +7956,7 @@ function resolveServerCommand2(server) {
|
|
|
7638
7956
|
}
|
|
7639
7957
|
function perfTestWithFixtures(name, fn) {
|
|
7640
7958
|
it(name, async () => {
|
|
7641
|
-
const { fixtures, perfImpl } = await
|
|
7959
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7642
7960
|
let testFailed = false;
|
|
7643
7961
|
try {
|
|
7644
7962
|
await fn(fixtures);
|
|
@@ -7651,14 +7969,14 @@ function perfTestWithFixtures(name, fn) {
|
|
|
7651
7969
|
});
|
|
7652
7970
|
}
|
|
7653
7971
|
function use2(config) {
|
|
7654
|
-
|
|
7972
|
+
currentConfig = { ...currentConfig, ...config };
|
|
7655
7973
|
afterAll(async () => {
|
|
7656
|
-
await
|
|
7974
|
+
await cleanupSharedResources();
|
|
7657
7975
|
});
|
|
7658
7976
|
}
|
|
7659
7977
|
function skip2(name, fn) {
|
|
7660
7978
|
it.skip(name, async () => {
|
|
7661
|
-
const { fixtures, perfImpl } = await
|
|
7979
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7662
7980
|
let testFailed = false;
|
|
7663
7981
|
try {
|
|
7664
7982
|
await fn(fixtures);
|
|
@@ -7672,7 +7990,7 @@ function skip2(name, fn) {
|
|
|
7672
7990
|
}
|
|
7673
7991
|
function only2(name, fn) {
|
|
7674
7992
|
it.only(name, async () => {
|
|
7675
|
-
const { fixtures, perfImpl } = await
|
|
7993
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7676
7994
|
let testFailed = false;
|
|
7677
7995
|
try {
|
|
7678
7996
|
await fn(fixtures);
|
|
@@ -7689,16 +8007,19 @@ function todo2(name) {
|
|
|
7689
8007
|
}
|
|
7690
8008
|
var perfTest = perfTestWithFixtures;
|
|
7691
8009
|
perfTest.use = use2;
|
|
7692
|
-
|
|
7693
|
-
perfTest
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
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
|
+
}
|
|
7697
8017
|
perfTest.skip = skip2;
|
|
7698
8018
|
perfTest.only = only2;
|
|
7699
8019
|
perfTest.todo = todo2;
|
|
7700
8020
|
|
|
7701
8021
|
// libs/testing/src/perf/baseline-store.ts
|
|
8022
|
+
import { ensureDir, fileExists, readFile, writeFile } from "@frontmcp/utils";
|
|
7702
8023
|
var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
|
|
7703
8024
|
var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
|
|
7704
8025
|
var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
|
|
@@ -7713,7 +8034,6 @@ var BaselineStore = class {
|
|
|
7713
8034
|
*/
|
|
7714
8035
|
async load() {
|
|
7715
8036
|
try {
|
|
7716
|
-
const { readFile, fileExists } = await import("@frontmcp/utils");
|
|
7717
8037
|
if (!await fileExists(this.baselinePath)) {
|
|
7718
8038
|
return null;
|
|
7719
8039
|
}
|
|
@@ -7728,7 +8048,6 @@ var BaselineStore = class {
|
|
|
7728
8048
|
* Save baseline to local file.
|
|
7729
8049
|
*/
|
|
7730
8050
|
async save(baseline) {
|
|
7731
|
-
const { writeFile, ensureDir } = await import("@frontmcp/utils");
|
|
7732
8051
|
const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
|
|
7733
8052
|
await ensureDir(dir);
|
|
7734
8053
|
await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
@@ -7984,6 +8303,7 @@ function getRegressionDetector(config) {
|
|
|
7984
8303
|
}
|
|
7985
8304
|
|
|
7986
8305
|
// libs/testing/src/perf/report-generator.ts
|
|
8306
|
+
import { ensureDir as ensureDir2, writeFile as writeFile2 } from "@frontmcp/utils";
|
|
7987
8307
|
var ReportGenerator = class {
|
|
7988
8308
|
detector;
|
|
7989
8309
|
constructor() {
|
|
@@ -8239,14 +8559,13 @@ var ReportGenerator = class {
|
|
|
8239
8559
|
}
|
|
8240
8560
|
};
|
|
8241
8561
|
async function saveReports(measurements, outputDir, baseline, gitInfo) {
|
|
8242
|
-
|
|
8243
|
-
await ensureDir(outputDir);
|
|
8562
|
+
await ensureDir2(outputDir);
|
|
8244
8563
|
const generator = new ReportGenerator();
|
|
8245
8564
|
const report = generator.generateReport(measurements, baseline, gitInfo);
|
|
8246
8565
|
const jsonPath = `${outputDir}/report.json`;
|
|
8247
8566
|
const markdownPath = `${outputDir}/report.md`;
|
|
8248
|
-
await
|
|
8249
|
-
await
|
|
8567
|
+
await writeFile2(jsonPath, generator.generateJsonReport(report));
|
|
8568
|
+
await writeFile2(markdownPath, generator.generateMarkdownReport(report));
|
|
8250
8569
|
return { jsonPath, markdownPath };
|
|
8251
8570
|
}
|
|
8252
8571
|
function createReportGenerator() {
|