@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/index.js
CHANGED
|
@@ -460,6 +460,32 @@ var interceptors = {
|
|
|
460
460
|
}
|
|
461
461
|
};
|
|
462
462
|
|
|
463
|
+
// libs/testing/src/transport/error-utils.ts
|
|
464
|
+
function errorName(err) {
|
|
465
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
466
|
+
const name = err.name;
|
|
467
|
+
return typeof name === "string" ? name : void 0;
|
|
468
|
+
}
|
|
469
|
+
function isAbortError(err) {
|
|
470
|
+
return errorName(err) === "AbortError";
|
|
471
|
+
}
|
|
472
|
+
function errorMessage(err, fallback = "Unknown error") {
|
|
473
|
+
if (typeof err === "string" && err) return err;
|
|
474
|
+
if (typeof err === "object" && err !== null) {
|
|
475
|
+
const message = err.message;
|
|
476
|
+
if (typeof message === "string" && message) return message;
|
|
477
|
+
}
|
|
478
|
+
return fallback;
|
|
479
|
+
}
|
|
480
|
+
var interceptedErrors = /* @__PURE__ */ new WeakSet();
|
|
481
|
+
function markInterceptedError(err) {
|
|
482
|
+
interceptedErrors.add(err);
|
|
483
|
+
return err;
|
|
484
|
+
}
|
|
485
|
+
function isInterceptedError(err) {
|
|
486
|
+
return typeof err === "object" && err !== null && interceptedErrors.has(err);
|
|
487
|
+
}
|
|
488
|
+
|
|
463
489
|
// libs/testing/src/transport/streamable-http.transport.ts
|
|
464
490
|
var DEFAULT_TIMEOUT = 3e4;
|
|
465
491
|
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
@@ -568,7 +594,7 @@ var StreamableHttpTransport = class {
|
|
|
568
594
|
return mockResponse2;
|
|
569
595
|
}
|
|
570
596
|
case "error":
|
|
571
|
-
throw interceptResult.error;
|
|
597
|
+
throw markInterceptedError(interceptResult.error);
|
|
572
598
|
case "continue":
|
|
573
599
|
message = interceptResult.request;
|
|
574
600
|
break;
|
|
@@ -629,7 +655,7 @@ var StreamableHttpTransport = class {
|
|
|
629
655
|
return jsonResponse;
|
|
630
656
|
} catch (error) {
|
|
631
657
|
clearTimeout(timeoutId);
|
|
632
|
-
if (error
|
|
658
|
+
if (isAbortError(error)) {
|
|
633
659
|
return {
|
|
634
660
|
jsonrpc: "2.0",
|
|
635
661
|
id: message.id ?? null,
|
|
@@ -668,7 +694,7 @@ var StreamableHttpTransport = class {
|
|
|
668
694
|
}
|
|
669
695
|
} catch (error) {
|
|
670
696
|
clearTimeout(timeoutId);
|
|
671
|
-
if (error
|
|
697
|
+
if (!isAbortError(error)) {
|
|
672
698
|
throw error;
|
|
673
699
|
}
|
|
674
700
|
}
|
|
@@ -709,7 +735,7 @@ var StreamableHttpTransport = class {
|
|
|
709
735
|
error: {
|
|
710
736
|
code: -32700,
|
|
711
737
|
message: "Parse error",
|
|
712
|
-
data: error
|
|
738
|
+
data: errorMessage(error)
|
|
713
739
|
}
|
|
714
740
|
};
|
|
715
741
|
}
|
|
@@ -1079,7 +1105,7 @@ var StreamableHttpTransport = class {
|
|
|
1079
1105
|
if (this.config.clientInfo) {
|
|
1080
1106
|
headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
|
|
1081
1107
|
}
|
|
1082
|
-
if (this.authToken
|
|
1108
|
+
if (this.authToken) {
|
|
1083
1109
|
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1084
1110
|
}
|
|
1085
1111
|
if (this.sessionId) {
|
|
@@ -1421,6 +1447,16 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
1421
1447
|
name: "@frontmcp/testing",
|
|
1422
1448
|
version: "0.4.0"
|
|
1423
1449
|
};
|
|
1450
|
+
var MAX_LIST_PAGES = 1e3;
|
|
1451
|
+
function isAuthFailure(error) {
|
|
1452
|
+
return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
|
|
1453
|
+
}
|
|
1454
|
+
function describeInitializeError(error) {
|
|
1455
|
+
const message = error?.message ?? "Unknown error";
|
|
1456
|
+
const data = error?.data;
|
|
1457
|
+
const detail = typeof data === "string" ? data.trim() : "";
|
|
1458
|
+
return detail ? `${message} - ${detail.slice(0, 500)}` : message;
|
|
1459
|
+
}
|
|
1424
1460
|
function extractEntryPaths(data) {
|
|
1425
1461
|
let payload = data;
|
|
1426
1462
|
if (typeof payload === "string") {
|
|
@@ -1507,19 +1543,22 @@ var McpTestClient = class {
|
|
|
1507
1543
|
async connect() {
|
|
1508
1544
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
1509
1545
|
this.triedDiscoveredEntryPath = false;
|
|
1546
|
+
this.initResult = null;
|
|
1510
1547
|
this.transport = this.createTransport();
|
|
1511
1548
|
await this.transport.connect();
|
|
1512
1549
|
const maxRetries = 3;
|
|
1513
1550
|
const retryDelayMs = 500;
|
|
1514
1551
|
let lastError;
|
|
1515
1552
|
let reportedEntryPaths;
|
|
1553
|
+
let attemptsMade = 0;
|
|
1516
1554
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1555
|
+
attemptsMade = attempt;
|
|
1517
1556
|
const initResponse = await this.initialize();
|
|
1518
1557
|
if (initResponse.success && initResponse.data) {
|
|
1519
1558
|
this.initResult = initResponse.data;
|
|
1520
1559
|
break;
|
|
1521
1560
|
}
|
|
1522
|
-
lastError = initResponse.error
|
|
1561
|
+
lastError = describeInitializeError(initResponse.error);
|
|
1523
1562
|
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
1524
1563
|
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
1525
1564
|
this.triedDiscoveredEntryPath = true;
|
|
@@ -1531,6 +1570,7 @@ var McpTestClient = class {
|
|
|
1531
1570
|
await this.transport.connect();
|
|
1532
1571
|
continue;
|
|
1533
1572
|
}
|
|
1573
|
+
if (isAuthFailure(initResponse.error)) break;
|
|
1534
1574
|
if (attempt < maxRetries) {
|
|
1535
1575
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
1536
1576
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
@@ -1539,7 +1579,7 @@ var McpTestClient = class {
|
|
|
1539
1579
|
if (!this.initResult) {
|
|
1540
1580
|
const served = reportedEntryPaths?.length ? ` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.` : "";
|
|
1541
1581
|
throw new Error(
|
|
1542
|
-
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${
|
|
1582
|
+
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
|
|
1543
1583
|
);
|
|
1544
1584
|
}
|
|
1545
1585
|
this._sessionId = this.transport.getSessionId();
|
|
@@ -1609,18 +1649,47 @@ var McpTestClient = class {
|
|
|
1609
1649
|
return this._authState;
|
|
1610
1650
|
}
|
|
1611
1651
|
/**
|
|
1612
|
-
* Authenticate with a token
|
|
1652
|
+
* Authenticate with a token.
|
|
1653
|
+
*
|
|
1654
|
+
* When connected, this opens a NEW session with the token (the previous session was created
|
|
1655
|
+
* for the previous identity) and rejects if the server refuses it — for example because the
|
|
1656
|
+
* token is expired or badly signed. On rejection the client keeps its previous identity and
|
|
1657
|
+
* session. The token is kept in the client config, so `reconnect()` keeps using it.
|
|
1613
1658
|
*/
|
|
1614
1659
|
async authenticate(token) {
|
|
1660
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
1661
|
+
throw new Error("authenticate() requires a non-empty token");
|
|
1662
|
+
}
|
|
1663
|
+
const previousAuthState = this._authState;
|
|
1664
|
+
const previousConfigAuth = this.config.auth;
|
|
1665
|
+
this.config.auth = { ...previousConfigAuth, token };
|
|
1615
1666
|
this._authState = {
|
|
1616
1667
|
isAnonymous: false,
|
|
1617
1668
|
token,
|
|
1618
1669
|
scopes: this.parseScopesFromToken(token),
|
|
1619
1670
|
user: this.parseUserFromToken(token)
|
|
1620
1671
|
};
|
|
1621
|
-
|
|
1622
|
-
|
|
1672
|
+
const previousTransport = this.transport;
|
|
1673
|
+
if (!previousTransport) {
|
|
1674
|
+
this.log("debug", "Authentication updated (not connected yet)");
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const previousInit = this.initResult;
|
|
1678
|
+
const previousSessionId = this._sessionId;
|
|
1679
|
+
const previousSessionInfo = this._sessionInfo;
|
|
1680
|
+
try {
|
|
1681
|
+
await this.connect();
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
await this.transport?.close().catch(() => void 0);
|
|
1684
|
+
this.transport = previousTransport;
|
|
1685
|
+
this.initResult = previousInit;
|
|
1686
|
+
this._sessionId = previousSessionId;
|
|
1687
|
+
this._sessionInfo = previousSessionInfo;
|
|
1688
|
+
this.config.auth = previousConfigAuth;
|
|
1689
|
+
this._authState = previousAuthState;
|
|
1690
|
+
throw err;
|
|
1623
1691
|
}
|
|
1692
|
+
await previousTransport.close().catch(() => void 0);
|
|
1624
1693
|
this.log("debug", "Authentication updated");
|
|
1625
1694
|
}
|
|
1626
1695
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -1654,13 +1723,12 @@ var McpTestClient = class {
|
|
|
1654
1723
|
/**
|
|
1655
1724
|
* List all available tools
|
|
1656
1725
|
*/
|
|
1657
|
-
list: async () =>
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
},
|
|
1726
|
+
list: async () => this.listAllPages(
|
|
1727
|
+
"tools/list",
|
|
1728
|
+
"tools",
|
|
1729
|
+
(cursor) => this.listTools(cursor),
|
|
1730
|
+
(page) => page.tools
|
|
1731
|
+
),
|
|
1664
1732
|
/**
|
|
1665
1733
|
* Call a tool by name with arguments
|
|
1666
1734
|
*/
|
|
@@ -1676,23 +1744,21 @@ var McpTestClient = class {
|
|
|
1676
1744
|
/**
|
|
1677
1745
|
* List all static resources
|
|
1678
1746
|
*/
|
|
1679
|
-
list: async () =>
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
},
|
|
1747
|
+
list: async () => this.listAllPages(
|
|
1748
|
+
"resources/list",
|
|
1749
|
+
"resources",
|
|
1750
|
+
(cursor) => this.listResources(cursor),
|
|
1751
|
+
(page) => page.resources
|
|
1752
|
+
),
|
|
1686
1753
|
/**
|
|
1687
1754
|
* List all resource templates
|
|
1688
1755
|
*/
|
|
1689
|
-
listTemplates: async () =>
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
},
|
|
1756
|
+
listTemplates: async () => this.listAllPages(
|
|
1757
|
+
"resources/templates/list",
|
|
1758
|
+
"resource templates",
|
|
1759
|
+
(cursor) => this.listResourceTemplates(cursor),
|
|
1760
|
+
(page) => page.resourceTemplates
|
|
1761
|
+
),
|
|
1696
1762
|
/**
|
|
1697
1763
|
* Read a resource by URI
|
|
1698
1764
|
*/
|
|
@@ -1720,13 +1786,12 @@ var McpTestClient = class {
|
|
|
1720
1786
|
/**
|
|
1721
1787
|
* List all available prompts
|
|
1722
1788
|
*/
|
|
1723
|
-
list: async () =>
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
},
|
|
1789
|
+
list: async () => this.listAllPages(
|
|
1790
|
+
"prompts/list",
|
|
1791
|
+
"prompts",
|
|
1792
|
+
(cursor) => this.listPrompts(cursor),
|
|
1793
|
+
(page) => page.prompts
|
|
1794
|
+
),
|
|
1730
1795
|
/**
|
|
1731
1796
|
* Get a prompt with arguments
|
|
1732
1797
|
*/
|
|
@@ -2110,8 +2175,33 @@ var McpTestClient = class {
|
|
|
2110
2175
|
clientInfo: this.config.clientInfo
|
|
2111
2176
|
});
|
|
2112
2177
|
}
|
|
2113
|
-
async listTools() {
|
|
2114
|
-
return this.request("tools/list", {});
|
|
2178
|
+
async listTools(cursor) {
|
|
2179
|
+
return this.request("tools/list", cursor ? { cursor } : {});
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Follow `nextCursor` until the server stops returning one, concatenating the
|
|
2183
|
+
* pages. A server that hands back a cursor it already returned, or never stops
|
|
2184
|
+
* paging, fails the call rather than looping.
|
|
2185
|
+
*/
|
|
2186
|
+
async listAllPages(method, label, fetchPage, itemsOf) {
|
|
2187
|
+
const items = [];
|
|
2188
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
2189
|
+
let cursor;
|
|
2190
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
2191
|
+
const response = await fetchPage(cursor);
|
|
2192
|
+
if (!response.success || !response.data) {
|
|
2193
|
+
throw new Error(`Failed to list ${label}: ${response.error?.message}`);
|
|
2194
|
+
}
|
|
2195
|
+
items.push(...itemsOf(response.data) ?? []);
|
|
2196
|
+
const next = response.data.nextCursor;
|
|
2197
|
+
if (!next) return items;
|
|
2198
|
+
if (seenCursors.has(next)) {
|
|
2199
|
+
throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`);
|
|
2200
|
+
}
|
|
2201
|
+
seenCursors.add(next);
|
|
2202
|
+
cursor = next;
|
|
2203
|
+
}
|
|
2204
|
+
throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`);
|
|
2115
2205
|
}
|
|
2116
2206
|
async callTool(name, args, progressToken) {
|
|
2117
2207
|
return this.request("tools/call", {
|
|
@@ -2136,17 +2226,17 @@ var McpTestClient = class {
|
|
|
2136
2226
|
timestamp
|
|
2137
2227
|
});
|
|
2138
2228
|
};
|
|
2139
|
-
async listResources() {
|
|
2140
|
-
return this.request("resources/list", {});
|
|
2229
|
+
async listResources(cursor) {
|
|
2230
|
+
return this.request("resources/list", cursor ? { cursor } : {});
|
|
2141
2231
|
}
|
|
2142
|
-
async listResourceTemplates() {
|
|
2143
|
-
return this.request("resources/templates/list", {});
|
|
2232
|
+
async listResourceTemplates(cursor) {
|
|
2233
|
+
return this.request("resources/templates/list", cursor ? { cursor } : {});
|
|
2144
2234
|
}
|
|
2145
2235
|
async readResource(uri) {
|
|
2146
2236
|
return this.request("resources/read", { uri });
|
|
2147
2237
|
}
|
|
2148
|
-
async listPrompts() {
|
|
2149
|
-
return this.request("prompts/list", {});
|
|
2238
|
+
async listPrompts(cursor) {
|
|
2239
|
+
return this.request("prompts/list", cursor ? { cursor } : {});
|
|
2150
2240
|
}
|
|
2151
2241
|
async getPrompt(name, args) {
|
|
2152
2242
|
return this.request("prompts/get", {
|
|
@@ -2223,10 +2313,11 @@ var McpTestClient = class {
|
|
|
2223
2313
|
requestId: id
|
|
2224
2314
|
};
|
|
2225
2315
|
} catch (err) {
|
|
2316
|
+
if (isInterceptedError(err)) throw err;
|
|
2226
2317
|
const durationMs = Date.now() - start;
|
|
2227
2318
|
const error = {
|
|
2228
2319
|
code: -32603,
|
|
2229
|
-
message: err
|
|
2320
|
+
message: errorMessage(err)
|
|
2230
2321
|
};
|
|
2231
2322
|
return {
|
|
2232
2323
|
success: false,
|
|
@@ -2374,12 +2465,12 @@ var McpTestClient = class {
|
|
|
2374
2465
|
const payload = this.decodeJwtPayload(token);
|
|
2375
2466
|
if (!payload) return [];
|
|
2376
2467
|
const scope = payload["scope"];
|
|
2377
|
-
const
|
|
2468
|
+
const scopes2 = payload["scopes"];
|
|
2378
2469
|
if (typeof scope === "string") {
|
|
2379
2470
|
return scope.split(" ");
|
|
2380
2471
|
}
|
|
2381
|
-
if (Array.isArray(
|
|
2382
|
-
return
|
|
2472
|
+
if (Array.isArray(scopes2)) {
|
|
2473
|
+
return scopes2;
|
|
2383
2474
|
}
|
|
2384
2475
|
return [];
|
|
2385
2476
|
} catch {
|
|
@@ -2511,14 +2602,15 @@ var TestTokenFactory = class {
|
|
|
2511
2602
|
*/
|
|
2512
2603
|
async createTestToken(options) {
|
|
2513
2604
|
await this.ensureKeys();
|
|
2514
|
-
const
|
|
2605
|
+
const nowMs = Date.now();
|
|
2606
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
2515
2607
|
const exp = options.exp ?? 3600;
|
|
2516
2608
|
const payload = {
|
|
2517
2609
|
iss: options.iss ?? this.issuer,
|
|
2518
2610
|
sub: options.sub,
|
|
2519
2611
|
aud: options.aud ?? this.audience,
|
|
2520
|
-
iat
|
|
2521
|
-
exp:
|
|
2612
|
+
iat,
|
|
2613
|
+
exp: Math.ceil(nowMs / 1e3) + exp,
|
|
2522
2614
|
scope: options.scopes?.join(" "),
|
|
2523
2615
|
...options.claims
|
|
2524
2616
|
};
|
|
@@ -2543,10 +2635,10 @@ var TestTokenFactory = class {
|
|
|
2543
2635
|
/**
|
|
2544
2636
|
* Create a regular user token
|
|
2545
2637
|
*/
|
|
2546
|
-
async createUserToken(sub = "user-001",
|
|
2638
|
+
async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
|
|
2547
2639
|
return this.createTestToken({
|
|
2548
2640
|
sub,
|
|
2549
|
-
scopes,
|
|
2641
|
+
scopes: scopes2,
|
|
2550
2642
|
claims: {
|
|
2551
2643
|
email: "user@test.local",
|
|
2552
2644
|
name: "Test User",
|
|
@@ -2557,10 +2649,11 @@ var TestTokenFactory = class {
|
|
|
2557
2649
|
/**
|
|
2558
2650
|
* Create an anonymous user token
|
|
2559
2651
|
*/
|
|
2560
|
-
async createAnonymousToken() {
|
|
2652
|
+
async createAnonymousToken(expiresIn) {
|
|
2561
2653
|
return this.createTestToken({
|
|
2562
2654
|
sub: `anon:${Date.now()}`,
|
|
2563
2655
|
scopes: ["anonymous"],
|
|
2656
|
+
exp: expiresIn,
|
|
2564
2657
|
claims: {
|
|
2565
2658
|
name: "Anonymous",
|
|
2566
2659
|
role: "anonymous"
|
|
@@ -2771,17 +2864,7 @@ function createTestUser(overrides) {
|
|
|
2771
2864
|
|
|
2772
2865
|
// libs/testing/src/auth/mock-oauth-server.ts
|
|
2773
2866
|
var import_http = require("http");
|
|
2774
|
-
var
|
|
2775
|
-
var _sha256Base64url;
|
|
2776
|
-
var _base64urlEncode;
|
|
2777
|
-
async function loadCryptoUtils() {
|
|
2778
|
-
if (!_randomBytes) {
|
|
2779
|
-
const utils = await import("@frontmcp/utils");
|
|
2780
|
-
_randomBytes = utils.randomBytes;
|
|
2781
|
-
_sha256Base64url = utils.sha256Base64url;
|
|
2782
|
-
_base64urlEncode = utils.base64urlEncode;
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2867
|
+
var import_utils = require("@frontmcp/utils");
|
|
2785
2868
|
var MockOAuthServer = class {
|
|
2786
2869
|
tokenFactory;
|
|
2787
2870
|
options;
|
|
@@ -2796,8 +2879,8 @@ var MockOAuthServer = class {
|
|
|
2796
2879
|
accessTokenTtlSeconds;
|
|
2797
2880
|
/** Refresh token TTL in seconds */
|
|
2798
2881
|
refreshTokenTtlSeconds;
|
|
2799
|
-
constructor(
|
|
2800
|
-
this.tokenFactory =
|
|
2882
|
+
constructor(tokenFactory2, options = {}) {
|
|
2883
|
+
this.tokenFactory = tokenFactory2;
|
|
2801
2884
|
this.options = options;
|
|
2802
2885
|
this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
|
|
2803
2886
|
this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
|
|
@@ -3054,14 +3137,14 @@ ${error.stack}` : String(error);
|
|
|
3054
3137
|
return;
|
|
3055
3138
|
}
|
|
3056
3139
|
const code = this.generateCode();
|
|
3057
|
-
const
|
|
3140
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3058
3141
|
this.authCodes.set(code, {
|
|
3059
3142
|
code,
|
|
3060
3143
|
clientId,
|
|
3061
3144
|
redirectUri,
|
|
3062
3145
|
codeChallenge,
|
|
3063
3146
|
codeChallengeMethod,
|
|
3064
|
-
scopes,
|
|
3147
|
+
scopes: scopes2,
|
|
3065
3148
|
user: testUser,
|
|
3066
3149
|
state,
|
|
3067
3150
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3101,7 +3184,7 @@ ${error.stack}` : String(error);
|
|
|
3101
3184
|
}
|
|
3102
3185
|
const grantType = params.get("grant_type");
|
|
3103
3186
|
if (grantType === "anonymous") {
|
|
3104
|
-
const token = await this.tokenFactory.createAnonymousToken();
|
|
3187
|
+
const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
|
|
3105
3188
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3106
3189
|
res.end(
|
|
3107
3190
|
JSON.stringify({
|
|
@@ -3233,6 +3316,8 @@ ${error.stack}` : String(error);
|
|
|
3233
3316
|
codeRecord.used = true;
|
|
3234
3317
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3235
3318
|
sub: codeRecord.user.sub,
|
|
3319
|
+
scopes: codeRecord.scopes,
|
|
3320
|
+
exp: this.accessTokenTtlSeconds,
|
|
3236
3321
|
claims: {
|
|
3237
3322
|
email: codeRecord.user.email,
|
|
3238
3323
|
name: codeRecord.user.name,
|
|
@@ -3241,6 +3326,7 @@ ${error.stack}` : String(error);
|
|
|
3241
3326
|
});
|
|
3242
3327
|
const idToken = await this.tokenFactory.createTestToken({
|
|
3243
3328
|
sub: codeRecord.user.sub,
|
|
3329
|
+
exp: this.accessTokenTtlSeconds,
|
|
3244
3330
|
claims: {
|
|
3245
3331
|
email: codeRecord.user.email,
|
|
3246
3332
|
name: codeRecord.user.name,
|
|
@@ -3318,6 +3404,8 @@ ${error.stack}` : String(error);
|
|
|
3318
3404
|
}
|
|
3319
3405
|
const accessToken = await this.tokenFactory.createTestToken({
|
|
3320
3406
|
sub: tokenRecord.user.sub,
|
|
3407
|
+
scopes: tokenRecord.scopes,
|
|
3408
|
+
exp: this.accessTokenTtlSeconds,
|
|
3321
3409
|
claims: {
|
|
3322
3410
|
email: tokenRecord.user.email,
|
|
3323
3411
|
name: tokenRecord.user.name,
|
|
@@ -3422,14 +3510,14 @@ ${error.stack}` : String(error);
|
|
|
3422
3510
|
name
|
|
3423
3511
|
};
|
|
3424
3512
|
const code = this.generateCode();
|
|
3425
|
-
const
|
|
3513
|
+
const scopes2 = scope.split(" ").filter(Boolean);
|
|
3426
3514
|
this.authCodes.set(code, {
|
|
3427
3515
|
code,
|
|
3428
3516
|
clientId,
|
|
3429
3517
|
redirectUri,
|
|
3430
3518
|
codeChallenge,
|
|
3431
3519
|
codeChallengeMethod,
|
|
3432
|
-
scopes,
|
|
3520
|
+
scopes: scopes2,
|
|
3433
3521
|
user: testUser,
|
|
3434
3522
|
state,
|
|
3435
3523
|
expiresAt: Date.now() + 5 * 60 * 1e3,
|
|
@@ -3535,8 +3623,7 @@ ${error.stack}` : String(error);
|
|
|
3535
3623
|
* Generate a random authorization code
|
|
3536
3624
|
*/
|
|
3537
3625
|
async generateCodeAsync() {
|
|
3538
|
-
|
|
3539
|
-
return _base64urlEncode(_randomBytes(32));
|
|
3626
|
+
return (0, import_utils.base64urlEncode)((0, import_utils.randomBytes)(32));
|
|
3540
3627
|
}
|
|
3541
3628
|
/**
|
|
3542
3629
|
* Generate a random authorization code (sync wrapper for compatibility)
|
|
@@ -3552,8 +3639,7 @@ ${error.stack}` : String(error);
|
|
|
3552
3639
|
*/
|
|
3553
3640
|
async computeCodeChallengeAsync(verifier, method) {
|
|
3554
3641
|
if (method === "S256") {
|
|
3555
|
-
|
|
3556
|
-
return _sha256Base64url(verifier);
|
|
3642
|
+
return (0, import_utils.sha256Base64url)(verifier);
|
|
3557
3643
|
}
|
|
3558
3644
|
return verifier;
|
|
3559
3645
|
}
|
|
@@ -4141,7 +4227,7 @@ var MockCimdServer = class {
|
|
|
4141
4227
|
|
|
4142
4228
|
// libs/testing/src/server/test-server.ts
|
|
4143
4229
|
var import_child_process = require("child_process");
|
|
4144
|
-
var
|
|
4230
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
4145
4231
|
|
|
4146
4232
|
// libs/testing/src/errors/index.ts
|
|
4147
4233
|
var TestClientError = class extends Error {
|
|
@@ -4247,6 +4333,8 @@ var E2E_PORT_RANGES = {
|
|
|
4247
4333
|
"demo-e2e-distributed": { start: 50440, size: 20 },
|
|
4248
4334
|
// Protocol revision E2E tests (50460-50479)
|
|
4249
4335
|
"demo-e2e-protocol-20260728": { start: 50460, size: 20 },
|
|
4336
|
+
// Testing library E2E tests (50480-50499)
|
|
4337
|
+
"demo-e2e-testing": { start: 50480, size: 20 },
|
|
4250
4338
|
// Mock servers and utilities (50900-50999)
|
|
4251
4339
|
"mock-oauth": { start: 50900, size: 10 },
|
|
4252
4340
|
"mock-api": { start: 50910, size: 10 },
|
|
@@ -4255,6 +4343,66 @@ var E2E_PORT_RANGES = {
|
|
|
4255
4343
|
default: { start: 51e3, size: 100 }
|
|
4256
4344
|
};
|
|
4257
4345
|
var reservedPorts = /* @__PURE__ */ new Map();
|
|
4346
|
+
var lockedPorts = /* @__PURE__ */ new Set();
|
|
4347
|
+
var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
4348
|
+
function lockDir() {
|
|
4349
|
+
try {
|
|
4350
|
+
const os = require("node:os");
|
|
4351
|
+
const path = require("node:path");
|
|
4352
|
+
return path.join(os.tmpdir(), "frontmcp-testing-ports");
|
|
4353
|
+
} catch {
|
|
4354
|
+
return void 0;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
function pidIsAlive(pid) {
|
|
4358
|
+
try {
|
|
4359
|
+
process.kill(pid, 0);
|
|
4360
|
+
return true;
|
|
4361
|
+
} catch (error) {
|
|
4362
|
+
return error.code === "EPERM";
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
function acquirePortLock(port) {
|
|
4366
|
+
if (lockedPorts.has(port)) return false;
|
|
4367
|
+
let fs;
|
|
4368
|
+
let file;
|
|
4369
|
+
try {
|
|
4370
|
+
fs = require("node:fs");
|
|
4371
|
+
const dir = lockDir();
|
|
4372
|
+
if (!dir) return true;
|
|
4373
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4374
|
+
file = require("node:path").join(dir, `${port}.lock`);
|
|
4375
|
+
} catch {
|
|
4376
|
+
return true;
|
|
4377
|
+
}
|
|
4378
|
+
const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
|
|
4379
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4380
|
+
try {
|
|
4381
|
+
fs.writeFileSync(file, payload, { flag: "wx" });
|
|
4382
|
+
lockedPorts.add(port);
|
|
4383
|
+
return true;
|
|
4384
|
+
} catch (error) {
|
|
4385
|
+
if (error.code !== "EEXIST") return true;
|
|
4386
|
+
try {
|
|
4387
|
+
const held = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
4388
|
+
const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
|
|
4389
|
+
if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
|
|
4390
|
+
fs.unlinkSync(file);
|
|
4391
|
+
} catch {
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
return false;
|
|
4396
|
+
}
|
|
4397
|
+
function releasePortLock(port) {
|
|
4398
|
+
if (!lockedPorts.delete(port)) return;
|
|
4399
|
+
try {
|
|
4400
|
+
const fs = require("node:fs");
|
|
4401
|
+
const dir = lockDir();
|
|
4402
|
+
if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
|
|
4403
|
+
} catch {
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4258
4406
|
var projectPortIndex = /* @__PURE__ */ new Map();
|
|
4259
4407
|
function getPortRange(project) {
|
|
4260
4408
|
const key = project;
|
|
@@ -4265,15 +4413,22 @@ function getPortRange(project) {
|
|
|
4265
4413
|
}
|
|
4266
4414
|
async function reservePort(project, preferredPort) {
|
|
4267
4415
|
const range = getPortRange(project);
|
|
4268
|
-
|
|
4416
|
+
const handle = (port) => ({
|
|
4417
|
+
port,
|
|
4418
|
+
// Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
|
|
4419
|
+
releaseSocket: async () => {
|
|
4420
|
+
await releasePort(port);
|
|
4421
|
+
},
|
|
4422
|
+
release: async () => {
|
|
4423
|
+
await releasePort(port);
|
|
4424
|
+
releasePortLock(port);
|
|
4425
|
+
}
|
|
4426
|
+
});
|
|
4427
|
+
const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
|
|
4428
|
+
if (wantsSpecificPort) {
|
|
4269
4429
|
const reservation = await tryReservePort(preferredPort, project);
|
|
4270
4430
|
if (reservation) {
|
|
4271
|
-
return
|
|
4272
|
-
port: preferredPort,
|
|
4273
|
-
release: async () => {
|
|
4274
|
-
await releasePort(preferredPort);
|
|
4275
|
-
}
|
|
4276
|
-
};
|
|
4431
|
+
return handle(preferredPort);
|
|
4277
4432
|
}
|
|
4278
4433
|
console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
|
|
4279
4434
|
}
|
|
@@ -4282,30 +4437,19 @@ async function reservePort(project, preferredPort) {
|
|
|
4282
4437
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4283
4438
|
const port = range.start + index % range.size;
|
|
4284
4439
|
index = (index + 1) % range.size;
|
|
4285
|
-
if (reservedPorts.has(port)) {
|
|
4440
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) {
|
|
4286
4441
|
continue;
|
|
4287
4442
|
}
|
|
4288
4443
|
const reservation = await tryReservePort(port, project);
|
|
4289
4444
|
if (reservation) {
|
|
4290
4445
|
projectPortIndex.set(project, index);
|
|
4291
|
-
return
|
|
4292
|
-
port,
|
|
4293
|
-
release: async () => {
|
|
4294
|
-
await releasePort(port);
|
|
4295
|
-
}
|
|
4296
|
-
};
|
|
4446
|
+
return handle(port);
|
|
4297
4447
|
}
|
|
4298
4448
|
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
return {
|
|
4304
|
-
port: dynamicPort,
|
|
4305
|
-
release: async () => {
|
|
4306
|
-
await releasePort(dynamicPort);
|
|
4307
|
-
}
|
|
4308
|
-
};
|
|
4449
|
+
for (let port = 51e3; port < 52e3; port++) {
|
|
4450
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
|
|
4451
|
+
if (await tryReservePort(port, project)) {
|
|
4452
|
+
return handle(port);
|
|
4309
4453
|
}
|
|
4310
4454
|
}
|
|
4311
4455
|
throw new Error(
|
|
@@ -4313,9 +4457,13 @@ async function reservePort(project, preferredPort) {
|
|
|
4313
4457
|
);
|
|
4314
4458
|
}
|
|
4315
4459
|
async function tryReservePort(port, project) {
|
|
4460
|
+
if (!acquirePortLock(port)) {
|
|
4461
|
+
return false;
|
|
4462
|
+
}
|
|
4316
4463
|
return new Promise((resolve) => {
|
|
4317
4464
|
const server = (0, import_net.createServer)();
|
|
4318
4465
|
server.once("error", () => {
|
|
4466
|
+
releasePortLock(port);
|
|
4319
4467
|
resolve(false);
|
|
4320
4468
|
});
|
|
4321
4469
|
server.listen(port, () => {
|
|
@@ -4341,31 +4489,6 @@ async function releasePort(port) {
|
|
|
4341
4489
|
});
|
|
4342
4490
|
});
|
|
4343
4491
|
}
|
|
4344
|
-
async function findAvailablePortInRange(start, end) {
|
|
4345
|
-
for (let port = start; port < end; port++) {
|
|
4346
|
-
if (reservedPorts.has(port)) {
|
|
4347
|
-
continue;
|
|
4348
|
-
}
|
|
4349
|
-
const available = await isPortAvailable(port);
|
|
4350
|
-
if (available) {
|
|
4351
|
-
return port;
|
|
4352
|
-
}
|
|
4353
|
-
}
|
|
4354
|
-
return null;
|
|
4355
|
-
}
|
|
4356
|
-
async function isPortAvailable(port) {
|
|
4357
|
-
return new Promise((resolve) => {
|
|
4358
|
-
const server = (0, import_net.createServer)();
|
|
4359
|
-
server.once("error", () => {
|
|
4360
|
-
resolve(false);
|
|
4361
|
-
});
|
|
4362
|
-
server.listen(port, () => {
|
|
4363
|
-
server.close(() => {
|
|
4364
|
-
resolve(true);
|
|
4365
|
-
});
|
|
4366
|
-
});
|
|
4367
|
-
});
|
|
4368
|
-
}
|
|
4369
4492
|
|
|
4370
4493
|
// libs/testing/src/server/test-server.ts
|
|
4371
4494
|
var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
|
|
@@ -4374,8 +4497,11 @@ var TestServer = class _TestServer {
|
|
|
4374
4497
|
options;
|
|
4375
4498
|
_info;
|
|
4376
4499
|
logs = [];
|
|
4500
|
+
/** Frees the port reservation (socket and cross-process lock); held until stop() */
|
|
4377
4501
|
portRelease = null;
|
|
4378
|
-
|
|
4502
|
+
/** Frees only the reservation socket, just before the server is spawned */
|
|
4503
|
+
portReleaseSocket = null;
|
|
4504
|
+
constructor(options, port, portRelease, portReleaseSocket) {
|
|
4379
4505
|
this.options = {
|
|
4380
4506
|
port,
|
|
4381
4507
|
project: options.project,
|
|
@@ -4387,6 +4513,7 @@ var TestServer = class _TestServer {
|
|
|
4387
4513
|
debug: options.debug ?? DEBUG_SERVER
|
|
4388
4514
|
};
|
|
4389
4515
|
this.portRelease = portRelease ?? null;
|
|
4516
|
+
this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
|
|
4390
4517
|
this._info = {
|
|
4391
4518
|
baseUrl: `http://localhost:${port}`,
|
|
4392
4519
|
port
|
|
@@ -4399,8 +4526,8 @@ var TestServer = class _TestServer {
|
|
|
4399
4526
|
const project = options.project ?? "default";
|
|
4400
4527
|
const maxAttempts = 3;
|
|
4401
4528
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4402
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4403
|
-
const server = new _TestServer(options, port, release);
|
|
4529
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4530
|
+
const server = new _TestServer(options, port, release, releaseSocket);
|
|
4404
4531
|
try {
|
|
4405
4532
|
await server.startProcess();
|
|
4406
4533
|
return server;
|
|
@@ -4440,7 +4567,7 @@ var TestServer = class _TestServer {
|
|
|
4440
4567
|
}
|
|
4441
4568
|
const maxAttempts = 3;
|
|
4442
4569
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4443
|
-
const { port, release } = await reservePort(project, options.port);
|
|
4570
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
4444
4571
|
const serverOptions = {
|
|
4445
4572
|
...options,
|
|
4446
4573
|
port,
|
|
@@ -4448,7 +4575,7 @@ var TestServer = class _TestServer {
|
|
|
4448
4575
|
command: `npx nx serve ${project} --port ${port}`,
|
|
4449
4576
|
cwd: options.cwd ?? process.cwd()
|
|
4450
4577
|
};
|
|
4451
|
-
const server = new _TestServer(serverOptions, port, release);
|
|
4578
|
+
const server = new _TestServer(serverOptions, port, release, releaseSocket);
|
|
4452
4579
|
try {
|
|
4453
4580
|
await server.startProcess();
|
|
4454
4581
|
return server;
|
|
@@ -4506,10 +4633,17 @@ var TestServer = class _TestServer {
|
|
|
4506
4633
|
* Stop the test server
|
|
4507
4634
|
*/
|
|
4508
4635
|
async stop() {
|
|
4509
|
-
|
|
4510
|
-
await this.
|
|
4511
|
-
|
|
4636
|
+
try {
|
|
4637
|
+
await this.stopProcess();
|
|
4638
|
+
} finally {
|
|
4639
|
+
if (this.portRelease) {
|
|
4640
|
+
await this.portRelease();
|
|
4641
|
+
this.portRelease = null;
|
|
4642
|
+
}
|
|
4643
|
+
this.portReleaseSocket = null;
|
|
4512
4644
|
}
|
|
4645
|
+
}
|
|
4646
|
+
async stopProcess() {
|
|
4513
4647
|
if (this.process) {
|
|
4514
4648
|
this.log("Stopping server...");
|
|
4515
4649
|
if (this.process.exitCode !== null || this.process.signalCode !== null) {
|
|
@@ -4582,7 +4716,7 @@ var TestServer = class _TestServer {
|
|
|
4582
4716
|
* Restart the server
|
|
4583
4717
|
*/
|
|
4584
4718
|
async restart() {
|
|
4585
|
-
await this.
|
|
4719
|
+
await this.stopProcess();
|
|
4586
4720
|
await this.startProcess();
|
|
4587
4721
|
}
|
|
4588
4722
|
/**
|
|
@@ -4612,9 +4746,9 @@ var TestServer = class _TestServer {
|
|
|
4612
4746
|
PORT: String(this.options.port)
|
|
4613
4747
|
};
|
|
4614
4748
|
const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
|
|
4615
|
-
if (this.
|
|
4616
|
-
await this.
|
|
4617
|
-
this.
|
|
4749
|
+
if (this.portReleaseSocket) {
|
|
4750
|
+
await this.portReleaseSocket();
|
|
4751
|
+
this.portReleaseSocket = null;
|
|
4618
4752
|
await sleep2(300);
|
|
4619
4753
|
}
|
|
4620
4754
|
this.process = (0, import_child_process.spawn)(this.options.command, [], {
|
|
@@ -4624,9 +4758,8 @@ var TestServer = class _TestServer {
|
|
|
4624
4758
|
detached: true,
|
|
4625
4759
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4626
4760
|
});
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
}
|
|
4761
|
+
this._info.pid = this.process.pid;
|
|
4762
|
+
this._info.shellPid = this.process.pid;
|
|
4630
4763
|
let processExited = false;
|
|
4631
4764
|
let exitCode = null;
|
|
4632
4765
|
let exitError = null;
|
|
@@ -4729,6 +4862,8 @@ ${allLogs}`);
|
|
|
4729
4862
|
});
|
|
4730
4863
|
if (response.ok || response.status === 404) {
|
|
4731
4864
|
this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
|
|
4865
|
+
const listeningPid = await findListeningPid(this.options.port);
|
|
4866
|
+
if (listeningPid !== void 0) this._info.pid = listeningPid;
|
|
4732
4867
|
return;
|
|
4733
4868
|
}
|
|
4734
4869
|
lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
|
|
@@ -4773,6 +4908,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
|
|
|
4773
4908
|
function sleep2(ms) {
|
|
4774
4909
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4775
4910
|
}
|
|
4911
|
+
function findListeningPid(port) {
|
|
4912
|
+
if (process.platform === "win32") return Promise.resolve(void 0);
|
|
4913
|
+
return new Promise((resolve) => {
|
|
4914
|
+
(0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
|
|
4915
|
+
if (error) {
|
|
4916
|
+
resolve(void 0);
|
|
4917
|
+
return;
|
|
4918
|
+
}
|
|
4919
|
+
const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
|
|
4920
|
+
resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
|
|
4921
|
+
});
|
|
4922
|
+
});
|
|
4923
|
+
}
|
|
4776
4924
|
function withWorkspaceProtocolFallback(env, cwd) {
|
|
4777
4925
|
if (findInstalledProtocolPackageDir(cwd)) {
|
|
4778
4926
|
return env;
|
|
@@ -4822,7 +4970,7 @@ function withProtocolNodePathAlias(env, cwd, workspacePackageDir) {
|
|
|
4822
4970
|
const fs = require("node:fs");
|
|
4823
4971
|
const os = require("node:os");
|
|
4824
4972
|
const path = require("node:path");
|
|
4825
|
-
const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0,
|
|
4973
|
+
const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0, import_utils2.sha256Hex)(cwd).slice(0, 12));
|
|
4826
4974
|
const scopeDir = path.join(aliasRoot, "@frontmcp");
|
|
4827
4975
|
const aliasPackageDir = path.join(scopeDir, "protocol");
|
|
4828
4976
|
fs.mkdirSync(scopeDir, { recursive: true });
|
|
@@ -5044,94 +5192,162 @@ function hasMimeType(result, mimeType) {
|
|
|
5044
5192
|
return result.hasMimeType(mimeType);
|
|
5045
5193
|
}
|
|
5046
5194
|
|
|
5047
|
-
// libs/testing/src/fixtures/test-fixture.ts
|
|
5048
|
-
var import_globals = require("@jest/globals");
|
|
5049
|
-
|
|
5050
5195
|
// libs/testing/src/fixtures/gateway-token-binding.ts
|
|
5051
|
-
var
|
|
5196
|
+
var import_utils3 = require("@frontmcp/utils");
|
|
5052
5197
|
function gatewayTokenBinding(serverUrl, entryPath) {
|
|
5053
|
-
const base = (0,
|
|
5054
|
-
const prefix = (0,
|
|
5198
|
+
const base = (0, import_utils3.trimTrailing)(serverUrl, "/");
|
|
5199
|
+
const prefix = (0, import_utils3.trimSlashes)(entryPath ?? "");
|
|
5055
5200
|
const mcpUrl = prefix ? `${base}/${prefix}` : base;
|
|
5056
5201
|
return { issuer: mcpUrl, audience: mcpUrl };
|
|
5057
5202
|
}
|
|
5058
5203
|
|
|
5059
5204
|
// libs/testing/src/fixtures/test-fixture.ts
|
|
5060
|
-
var
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5205
|
+
var jestApi;
|
|
5206
|
+
function jestGlobals() {
|
|
5207
|
+
if (jestApi) return jestApi;
|
|
5208
|
+
try {
|
|
5209
|
+
const api = require("@jest/globals");
|
|
5210
|
+
jestApi = {
|
|
5211
|
+
describe: api["describe"],
|
|
5212
|
+
beforeAll: api["beforeAll"],
|
|
5213
|
+
beforeEach: api["beforeEach"],
|
|
5214
|
+
afterEach: api["afterEach"],
|
|
5215
|
+
afterAll: api["afterAll"],
|
|
5216
|
+
it: api["it"]
|
|
5217
|
+
};
|
|
5218
|
+
return jestApi;
|
|
5219
|
+
} catch (error) {
|
|
5220
|
+
throw new Error(
|
|
5221
|
+
`@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
5222
|
+
{ cause: error }
|
|
5223
|
+
);
|
|
5224
|
+
}
|
|
5073
5225
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5226
|
+
var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
|
|
5227
|
+
function currentScope() {
|
|
5228
|
+
return scopes[scopes.length - 1];
|
|
5229
|
+
}
|
|
5230
|
+
var serverEntries = /* @__PURE__ */ new Map();
|
|
5231
|
+
function resolveConfig(chain) {
|
|
5232
|
+
const merged = {};
|
|
5233
|
+
for (const scope of chain) {
|
|
5234
|
+
const { env, ...rest } = scope.config;
|
|
5235
|
+
Object.assign(merged, rest);
|
|
5236
|
+
if (env) merged.env = { ...merged.env, ...env };
|
|
5237
|
+
}
|
|
5238
|
+
return merged;
|
|
5239
|
+
}
|
|
5240
|
+
function ownerScopeOf(chain) {
|
|
5241
|
+
for (let i = chain.length - 1; i > 0; i--) {
|
|
5242
|
+
if (chain[i].configured) return chain[i];
|
|
5243
|
+
}
|
|
5244
|
+
return chain[0];
|
|
5245
|
+
}
|
|
5246
|
+
function serverKey(config) {
|
|
5247
|
+
const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
5248
|
+
return JSON.stringify({
|
|
5249
|
+
server: config.server,
|
|
5250
|
+
baseUrl: config.baseUrl,
|
|
5251
|
+
project: config.project,
|
|
5252
|
+
port: config.port,
|
|
5253
|
+
entryPath: config.entryPath,
|
|
5254
|
+
startupTimeout: config.startupTimeout,
|
|
5255
|
+
logLevel: config.logLevel,
|
|
5256
|
+
auth: config.auth,
|
|
5257
|
+
env
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
function serverEnv(config) {
|
|
5261
|
+
const extra = {};
|
|
5262
|
+
if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
|
|
5263
|
+
if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
|
|
5264
|
+
if (Object.keys(extra).length === 0 && !config.env) return void 0;
|
|
5265
|
+
return { ...extra, ...config.env };
|
|
5266
|
+
}
|
|
5267
|
+
async function startServerEntry(config, owner) {
|
|
5268
|
+
let server;
|
|
5269
|
+
let startedByUs = false;
|
|
5270
|
+
if (config.baseUrl && !config.server) {
|
|
5271
|
+
server = TestServer.connect(config.baseUrl);
|
|
5272
|
+
} else if (config.server) {
|
|
5273
|
+
const serverCommand = resolveServerCommand(config.server);
|
|
5274
|
+
const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
5275
|
+
if (isDebug) {
|
|
5276
|
+
console.log(`[TestFixture] Starting server: ${serverCommand}`);
|
|
5277
|
+
}
|
|
5278
|
+
try {
|
|
5279
|
+
server = await TestServer.start({
|
|
5280
|
+
project: config.project,
|
|
5281
|
+
port: config.port,
|
|
5282
|
+
command: serverCommand,
|
|
5283
|
+
env: serverEnv(config),
|
|
5284
|
+
startupTimeout: config.startupTimeout ?? 3e4,
|
|
5285
|
+
debug: isDebug
|
|
5286
|
+
});
|
|
5287
|
+
startedByUs = true;
|
|
5082
5288
|
if (isDebug) {
|
|
5083
|
-
console.log(`[TestFixture]
|
|
5289
|
+
console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
|
|
5084
5290
|
}
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
command: serverCommand,
|
|
5090
|
-
env: currentConfig.env,
|
|
5091
|
-
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
5092
|
-
debug: isDebug
|
|
5093
|
-
});
|
|
5094
|
-
serverStartedByUs = true;
|
|
5095
|
-
if (isDebug) {
|
|
5096
|
-
console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
|
|
5097
|
-
}
|
|
5098
|
-
} catch (error) {
|
|
5099
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5100
|
-
throw new Error(
|
|
5101
|
-
`Failed to start test server.
|
|
5291
|
+
} catch (error) {
|
|
5292
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5293
|
+
throw new Error(
|
|
5294
|
+
`Failed to start test server.
|
|
5102
5295
|
|
|
5103
|
-
Server entry: ${
|
|
5104
|
-
Project: ${
|
|
5296
|
+
Server entry: ${config.server}
|
|
5297
|
+
Project: ${config.project ?? "default"}
|
|
5105
5298
|
Command: ${serverCommand}
|
|
5106
5299
|
|
|
5107
|
-
Error: ${errMsg}
|
|
5108
|
-
|
|
5109
|
-
}
|
|
5110
|
-
} else {
|
|
5111
|
-
throw new Error(
|
|
5112
|
-
'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
|
|
5300
|
+
Error: ${errMsg}`,
|
|
5301
|
+
{ cause: error }
|
|
5113
5302
|
);
|
|
5114
5303
|
}
|
|
5304
|
+
} else {
|
|
5305
|
+
throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
|
|
5115
5306
|
}
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
}
|
|
5307
|
+
const sharedSecret = config.env?.["JWT_SECRET"];
|
|
5308
|
+
const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
|
|
5309
|
+
const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
|
|
5310
|
+
return { server, startedByUs, tokenFactory: tokenFactory2, owner };
|
|
5121
5311
|
}
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5312
|
+
function acquireServer(chain) {
|
|
5313
|
+
const config = resolveConfig(chain);
|
|
5314
|
+
const key = serverKey(config);
|
|
5315
|
+
let entry = serverEntries.get(key);
|
|
5316
|
+
if (!entry) {
|
|
5317
|
+
entry = startServerEntry(config, ownerScopeOf(chain));
|
|
5318
|
+
serverEntries.set(key, entry);
|
|
5319
|
+
entry.catch(() => serverEntries.delete(key));
|
|
5320
|
+
}
|
|
5321
|
+
return entry;
|
|
5322
|
+
}
|
|
5323
|
+
async function stopServers(owner) {
|
|
5324
|
+
for (const [key, pending] of Array.from(serverEntries)) {
|
|
5325
|
+
let entry;
|
|
5326
|
+
try {
|
|
5327
|
+
entry = await pending;
|
|
5328
|
+
} catch {
|
|
5329
|
+
serverEntries.delete(key);
|
|
5330
|
+
continue;
|
|
5331
|
+
}
|
|
5332
|
+
if (owner && entry.owner !== owner) continue;
|
|
5333
|
+
serverEntries.delete(key);
|
|
5334
|
+
if (entry.startedByUs) {
|
|
5335
|
+
await entry.server.stop();
|
|
5336
|
+
}
|
|
5129
5337
|
}
|
|
5338
|
+
}
|
|
5339
|
+
var fixtureContexts = /* @__PURE__ */ new WeakMap();
|
|
5340
|
+
async function buildFixtures(chain) {
|
|
5341
|
+
const config = resolveConfig(chain);
|
|
5342
|
+
const entry = await acquireServer(chain);
|
|
5343
|
+
const { server, tokenFactory: tokenFactory2 } = entry;
|
|
5344
|
+
const publicMode = config.publicMode ?? config.auth?.mode === "public";
|
|
5345
|
+
const clientConfig = { ...config, publicMode };
|
|
5130
5346
|
const clientInstance = McpTestClient.create({
|
|
5131
|
-
baseUrl: resolveClientBaseUrl(
|
|
5132
|
-
entryPath:
|
|
5133
|
-
transport:
|
|
5134
|
-
publicMode
|
|
5347
|
+
baseUrl: resolveClientBaseUrl(server, clientConfig),
|
|
5348
|
+
entryPath: clientConfig.entryPath,
|
|
5349
|
+
transport: clientConfig.transport ?? "streamable-http",
|
|
5350
|
+
publicMode
|
|
5135
5351
|
}).build();
|
|
5136
5352
|
try {
|
|
5137
5353
|
await clientInstance.connect();
|
|
@@ -5145,17 +5361,18 @@ async function createTestFixtures() {
|
|
|
5145
5361
|
throw err;
|
|
5146
5362
|
}
|
|
5147
5363
|
}
|
|
5148
|
-
const
|
|
5149
|
-
const
|
|
5150
|
-
return {
|
|
5364
|
+
const context = { extraClients: [] };
|
|
5365
|
+
const fixtures = {
|
|
5151
5366
|
mcp: clientInstance,
|
|
5152
|
-
auth,
|
|
5153
|
-
server
|
|
5367
|
+
auth: createAuthFixture(tokenFactory2),
|
|
5368
|
+
server: createServerFixture(server, clientConfig, clientInstance, context)
|
|
5154
5369
|
};
|
|
5370
|
+
fixtureContexts.set(fixtures, context);
|
|
5371
|
+
return fixtures;
|
|
5155
5372
|
}
|
|
5156
5373
|
async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
5157
|
-
if (testFailed
|
|
5158
|
-
const logs =
|
|
5374
|
+
if (testFailed) {
|
|
5375
|
+
const logs = fixtures.server.getLogs();
|
|
5159
5376
|
if (logs.length > 0) {
|
|
5160
5377
|
console.error("\n[TestFixture] === Server Logs (test failed) ===");
|
|
5161
5378
|
const recentLogs = logs.slice(-50);
|
|
@@ -5166,20 +5383,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
|
|
|
5166
5383
|
console.error("[TestFixture] === End Server Logs ===\n");
|
|
5167
5384
|
}
|
|
5168
5385
|
}
|
|
5386
|
+
const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
|
|
5387
|
+
for (const client of extras) {
|
|
5388
|
+
if (client.isConnected()) {
|
|
5389
|
+
await client.disconnect().catch(() => void 0);
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5169
5392
|
if (fixtures.mcp.isConnected()) {
|
|
5170
5393
|
await fixtures.mcp.disconnect();
|
|
5171
5394
|
}
|
|
5172
5395
|
}
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
await serverInstance.stop();
|
|
5176
|
-
}
|
|
5177
|
-
serverInstance = null;
|
|
5178
|
-
tokenFactory = null;
|
|
5179
|
-
serverStartedByUs = false;
|
|
5180
|
-
}
|
|
5181
|
-
function resolveClientBaseUrl(server) {
|
|
5182
|
-
return currentConfig.baseUrl ?? server.info.baseUrl;
|
|
5396
|
+
function resolveClientBaseUrl(server, config) {
|
|
5397
|
+
return config.baseUrl ?? server.info.baseUrl;
|
|
5183
5398
|
}
|
|
5184
5399
|
function createAuthFixture(factory) {
|
|
5185
5400
|
const users = {
|
|
@@ -5225,28 +5440,39 @@ function createAuthFixture(factory) {
|
|
|
5225
5440
|
getAudience: () => factory.getAudience()
|
|
5226
5441
|
};
|
|
5227
5442
|
}
|
|
5228
|
-
function createServerFixture(server) {
|
|
5443
|
+
function createServerFixture(server, config, mcp, context) {
|
|
5229
5444
|
return {
|
|
5230
|
-
|
|
5445
|
+
// Live view: `pid` is filled in once the server is listening and follows restarts
|
|
5446
|
+
get info() {
|
|
5447
|
+
return server.info;
|
|
5448
|
+
},
|
|
5231
5449
|
createClient: async (opts) => {
|
|
5232
|
-
|
|
5233
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5234
|
-
entryPath: opts?.entryPath ??
|
|
5450
|
+
const client = await McpTestClient.create({
|
|
5451
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5452
|
+
entryPath: opts?.entryPath ?? config.entryPath,
|
|
5235
5453
|
transport: opts?.transport ?? "streamable-http",
|
|
5236
5454
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
5237
5455
|
clientInfo: opts?.clientInfo,
|
|
5238
|
-
publicMode:
|
|
5456
|
+
publicMode: config.publicMode
|
|
5239
5457
|
}).buildAndConnect();
|
|
5458
|
+
context.extraClients.push(client);
|
|
5459
|
+
return client;
|
|
5240
5460
|
},
|
|
5241
5461
|
createClientBuilder: () => {
|
|
5242
5462
|
const builder = new McpTestClientBuilder({
|
|
5243
|
-
baseUrl: resolveClientBaseUrl(server),
|
|
5244
|
-
entryPath:
|
|
5245
|
-
publicMode:
|
|
5463
|
+
baseUrl: resolveClientBaseUrl(server, config),
|
|
5464
|
+
entryPath: config.entryPath,
|
|
5465
|
+
publicMode: config.publicMode
|
|
5246
5466
|
});
|
|
5247
5467
|
return builder;
|
|
5248
5468
|
},
|
|
5249
|
-
restart: () =>
|
|
5469
|
+
restart: async () => {
|
|
5470
|
+
const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
|
|
5471
|
+
await server.restart();
|
|
5472
|
+
for (const client of reconnect) {
|
|
5473
|
+
await client.reconnect();
|
|
5474
|
+
}
|
|
5475
|
+
},
|
|
5250
5476
|
getLogs: () => server.getLogs(),
|
|
5251
5477
|
clearLogs: () => server.clearLogs()
|
|
5252
5478
|
};
|
|
@@ -5257,12 +5483,12 @@ function resolveServerCommand(server) {
|
|
|
5257
5483
|
}
|
|
5258
5484
|
return `npx tsx ${server}`;
|
|
5259
5485
|
}
|
|
5260
|
-
function runWithFixtures(fn) {
|
|
5261
|
-
return async () => {
|
|
5262
|
-
const fixtures = await
|
|
5486
|
+
function runWithFixtures(fn, chain) {
|
|
5487
|
+
return async (...extra) => {
|
|
5488
|
+
const fixtures = await buildFixtures(chain);
|
|
5263
5489
|
let testFailed = false;
|
|
5264
5490
|
try {
|
|
5265
|
-
await fn(fixtures);
|
|
5491
|
+
await fn(fixtures, ...extra);
|
|
5266
5492
|
} catch (error) {
|
|
5267
5493
|
testFailed = true;
|
|
5268
5494
|
throw error;
|
|
@@ -5271,24 +5497,37 @@ function runWithFixtures(fn) {
|
|
|
5271
5497
|
}
|
|
5272
5498
|
};
|
|
5273
5499
|
}
|
|
5500
|
+
function skipTitle(name, scope) {
|
|
5501
|
+
return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
|
|
5502
|
+
}
|
|
5274
5503
|
function testWithFixtures(name, fn) {
|
|
5275
|
-
const
|
|
5504
|
+
const chain = scopes.slice();
|
|
5505
|
+
const scope = currentScope();
|
|
5276
5506
|
if (scope.skipped) {
|
|
5277
|
-
|
|
5507
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5278
5508
|
return;
|
|
5279
5509
|
}
|
|
5280
|
-
|
|
5510
|
+
jestGlobals().it(name, runWithFixtures(fn, chain));
|
|
5281
5511
|
}
|
|
5282
5512
|
function use(config) {
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5513
|
+
const scope = currentScope();
|
|
5514
|
+
scope.config = {
|
|
5515
|
+
...scope.config,
|
|
5516
|
+
...config,
|
|
5517
|
+
env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
|
|
5518
|
+
};
|
|
5519
|
+
scope.configured = true;
|
|
5520
|
+
if (!scope.cleanupRegistered) {
|
|
5521
|
+
scope.cleanupRegistered = true;
|
|
5522
|
+
jestGlobals().afterAll(async () => {
|
|
5523
|
+
await stopServers(scope === scopes[0] ? void 0 : scope);
|
|
5524
|
+
});
|
|
5525
|
+
}
|
|
5287
5526
|
}
|
|
5288
5527
|
function skip(nameOrCondition, fnOrReason) {
|
|
5289
5528
|
if (typeof nameOrCondition === "boolean") {
|
|
5290
5529
|
if (nameOrCondition) {
|
|
5291
|
-
const scope =
|
|
5530
|
+
const scope = currentScope();
|
|
5292
5531
|
scope.skipped = true;
|
|
5293
5532
|
scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
|
|
5294
5533
|
}
|
|
@@ -5299,57 +5538,111 @@ function skip(nameOrCondition, fnOrReason) {
|
|
|
5299
5538
|
`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
|
|
5300
5539
|
);
|
|
5301
5540
|
}
|
|
5302
|
-
|
|
5541
|
+
jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
|
|
5303
5542
|
}
|
|
5304
5543
|
function only(name, fn) {
|
|
5305
|
-
const scope =
|
|
5544
|
+
const scope = currentScope();
|
|
5306
5545
|
if (scope.skipped) {
|
|
5307
|
-
|
|
5546
|
+
jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
|
|
5308
5547
|
return;
|
|
5309
5548
|
}
|
|
5310
|
-
|
|
5549
|
+
jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
|
|
5311
5550
|
}
|
|
5312
5551
|
function todo(name) {
|
|
5313
|
-
|
|
5552
|
+
jestGlobals().it.todo(name);
|
|
5314
5553
|
}
|
|
5315
|
-
function
|
|
5554
|
+
function each(table, ...tagged) {
|
|
5555
|
+
const chain = scopes.slice();
|
|
5556
|
+
const register = jestGlobals().it.each(table, ...tagged);
|
|
5316
5557
|
return (name, fn) => {
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5558
|
+
const scope = currentScope();
|
|
5559
|
+
if (scope.skipped) {
|
|
5560
|
+
const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
|
|
5561
|
+
skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
|
|
5562
|
+
return;
|
|
5563
|
+
}
|
|
5564
|
+
register(name, runWithFixtures(fn, chain));
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5567
|
+
function withScope(fn) {
|
|
5568
|
+
return (...args) => {
|
|
5569
|
+
const parent = currentScope();
|
|
5570
|
+
scopes.push({
|
|
5571
|
+
skipped: parent.skipped,
|
|
5572
|
+
reason: parent.reason,
|
|
5573
|
+
config: {},
|
|
5574
|
+
configured: false,
|
|
5575
|
+
cleanupRegistered: false
|
|
5325
5576
|
});
|
|
5577
|
+
try {
|
|
5578
|
+
fn(...args);
|
|
5579
|
+
} finally {
|
|
5580
|
+
scopes.pop();
|
|
5581
|
+
}
|
|
5326
5582
|
};
|
|
5327
5583
|
}
|
|
5328
|
-
function
|
|
5329
|
-
return (
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
});
|
|
5584
|
+
function scopedRegister(pick) {
|
|
5585
|
+
return (name, fn, timeout) => {
|
|
5586
|
+
pick()(name, withScope(fn), timeout);
|
|
5587
|
+
};
|
|
5333
5588
|
}
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
}
|
|
5589
|
+
function scopedEach(pick) {
|
|
5590
|
+
return (...eachArgs) => (name, fn, timeout) => {
|
|
5591
|
+
const register = pick()(...eachArgs);
|
|
5592
|
+
register(name, withScope(fn), timeout);
|
|
5593
|
+
};
|
|
5594
|
+
}
|
|
5595
|
+
var describeWithScope = Object.assign(
|
|
5596
|
+
scopedRegister(() => jestGlobals().describe),
|
|
5597
|
+
{
|
|
5598
|
+
// `describe.skip` still evaluates its callback during collection, so it needs a scope too
|
|
5599
|
+
only: Object.assign(
|
|
5600
|
+
scopedRegister(() => jestGlobals().describe.only),
|
|
5601
|
+
{
|
|
5602
|
+
each: scopedEach(() => jestGlobals().describe.only.each)
|
|
5603
|
+
}
|
|
5604
|
+
),
|
|
5605
|
+
skip: Object.assign(
|
|
5606
|
+
scopedRegister(() => jestGlobals().describe.skip),
|
|
5607
|
+
{
|
|
5608
|
+
each: scopedEach(() => jestGlobals().describe.skip.each)
|
|
5609
|
+
}
|
|
5610
|
+
),
|
|
5611
|
+
each: scopedEach(() => jestGlobals().describe.each)
|
|
5612
|
+
}
|
|
5613
|
+
);
|
|
5339
5614
|
var test = testWithFixtures;
|
|
5340
5615
|
test.use = use;
|
|
5341
|
-
test.describe =
|
|
5342
|
-
test.beforeAll =
|
|
5343
|
-
test.beforeEach =
|
|
5344
|
-
test.afterEach =
|
|
5345
|
-
test.afterAll =
|
|
5616
|
+
test.describe = describeWithScope;
|
|
5617
|
+
test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
|
|
5618
|
+
test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
|
|
5619
|
+
test.afterEach = ((...args) => jestGlobals().afterEach(...args));
|
|
5620
|
+
test.afterAll = ((...args) => jestGlobals().afterAll(...args));
|
|
5346
5621
|
test.skip = skip;
|
|
5347
5622
|
test.only = only;
|
|
5348
5623
|
test.todo = todo;
|
|
5624
|
+
test.each = each;
|
|
5349
5625
|
|
|
5350
5626
|
// libs/testing/src/expect.ts
|
|
5351
|
-
var
|
|
5352
|
-
|
|
5627
|
+
var resolvedExpect;
|
|
5628
|
+
function resolveJestExpect() {
|
|
5629
|
+
if (resolvedExpect) return resolvedExpect;
|
|
5630
|
+
try {
|
|
5631
|
+
resolvedExpect = require("@jest/globals").expect;
|
|
5632
|
+
return resolvedExpect;
|
|
5633
|
+
} catch (error) {
|
|
5634
|
+
throw new Error(
|
|
5635
|
+
`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
|
|
5636
|
+
{ cause: error }
|
|
5637
|
+
);
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5640
|
+
var expect = new Proxy(function frontmcpExpect() {
|
|
5641
|
+
}, {
|
|
5642
|
+
apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
|
|
5643
|
+
get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
|
|
5644
|
+
has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
|
|
5645
|
+
});
|
|
5353
5646
|
|
|
5354
5647
|
// libs/testing/src/platform/platform-types.ts
|
|
5355
5648
|
function getPlatformMetaNamespace(platform) {
|
|
@@ -6012,34 +6305,44 @@ var HttpInterceptorImpl = class {
|
|
|
6012
6305
|
}
|
|
6013
6306
|
};
|
|
6014
6307
|
}
|
|
6015
|
-
get(url, response) {
|
|
6308
|
+
get(url, response, options) {
|
|
6016
6309
|
return this.mock({
|
|
6017
|
-
match: { url, method: "GET" },
|
|
6018
|
-
response: normalizeResponse(response)
|
|
6310
|
+
match: { url, method: "GET", ...options?.match },
|
|
6311
|
+
response: normalizeResponse(response),
|
|
6312
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6313
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6019
6314
|
});
|
|
6020
6315
|
}
|
|
6021
|
-
post(url, response) {
|
|
6316
|
+
post(url, response, options) {
|
|
6022
6317
|
return this.mock({
|
|
6023
|
-
match: { url, method: "POST" },
|
|
6024
|
-
response: normalizeResponse(response)
|
|
6318
|
+
match: { url, method: "POST", ...options?.match },
|
|
6319
|
+
response: normalizeResponse(response),
|
|
6320
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6321
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6025
6322
|
});
|
|
6026
6323
|
}
|
|
6027
|
-
put(url, response) {
|
|
6324
|
+
put(url, response, options) {
|
|
6028
6325
|
return this.mock({
|
|
6029
|
-
match: { url, method: "PUT" },
|
|
6030
|
-
response: normalizeResponse(response)
|
|
6326
|
+
match: { url, method: "PUT", ...options?.match },
|
|
6327
|
+
response: normalizeResponse(response),
|
|
6328
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6329
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6031
6330
|
});
|
|
6032
6331
|
}
|
|
6033
|
-
delete(url, response) {
|
|
6332
|
+
delete(url, response, options) {
|
|
6034
6333
|
return this.mock({
|
|
6035
|
-
match: { url, method: "DELETE" },
|
|
6036
|
-
response: normalizeResponse(response)
|
|
6334
|
+
match: { url, method: "DELETE", ...options?.match },
|
|
6335
|
+
response: normalizeResponse(response),
|
|
6336
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6337
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6037
6338
|
});
|
|
6038
6339
|
}
|
|
6039
|
-
any(url, response) {
|
|
6340
|
+
any(url, response, options) {
|
|
6040
6341
|
return this.mock({
|
|
6041
|
-
match: { url },
|
|
6042
|
-
response: normalizeResponse(response)
|
|
6342
|
+
match: { url, ...options?.match },
|
|
6343
|
+
response: normalizeResponse(response),
|
|
6344
|
+
...options?.times !== void 0 && { times: options.times },
|
|
6345
|
+
...options?.name !== void 0 && { name: options.name }
|
|
6043
6346
|
});
|
|
6044
6347
|
}
|
|
6045
6348
|
clear() {
|
|
@@ -6071,6 +6374,9 @@ ${descriptions.join("\n")}`);
|
|
|
6071
6374
|
if (index !== -1) {
|
|
6072
6375
|
activeInterceptors.splice(index, 1);
|
|
6073
6376
|
}
|
|
6377
|
+
if (activeInterceptors.length === 0) {
|
|
6378
|
+
httpMock.disable();
|
|
6379
|
+
}
|
|
6074
6380
|
}
|
|
6075
6381
|
// Internal methods
|
|
6076
6382
|
isActive() {
|
|
@@ -6170,43 +6476,49 @@ ${descriptions.join("\n")}`);
|
|
|
6170
6476
|
}
|
|
6171
6477
|
};
|
|
6172
6478
|
async function interceptedFetch(input, init) {
|
|
6173
|
-
const
|
|
6174
|
-
const
|
|
6479
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
6480
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
|
|
6481
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
6175
6482
|
const headers = {};
|
|
6176
|
-
|
|
6177
|
-
if (
|
|
6178
|
-
|
|
6483
|
+
const collectHeaders = (source) => {
|
|
6484
|
+
if (!source) return;
|
|
6485
|
+
if (source instanceof Headers) {
|
|
6486
|
+
source.forEach((value, key) => {
|
|
6179
6487
|
headers[key.toLowerCase()] = value;
|
|
6180
6488
|
});
|
|
6181
|
-
} else if (Array.isArray(
|
|
6182
|
-
for (const [key, value] of
|
|
6489
|
+
} else if (Array.isArray(source)) {
|
|
6490
|
+
for (const [key, value] of source) {
|
|
6183
6491
|
headers[key.toLowerCase()] = value;
|
|
6184
6492
|
}
|
|
6185
6493
|
} else {
|
|
6186
|
-
for (const [key, value] of Object.entries(
|
|
6494
|
+
for (const [key, value] of Object.entries(source)) {
|
|
6187
6495
|
headers[key.toLowerCase()] = value;
|
|
6188
6496
|
}
|
|
6189
6497
|
}
|
|
6190
|
-
}
|
|
6498
|
+
};
|
|
6499
|
+
collectHeaders(init?.headers ?? request?.headers);
|
|
6191
6500
|
let body;
|
|
6192
6501
|
let rawBody;
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6502
|
+
let bodySource = init?.body;
|
|
6503
|
+
if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
|
|
6504
|
+
bodySource = await request.clone().text();
|
|
6505
|
+
}
|
|
6506
|
+
if (bodySource) {
|
|
6507
|
+
if (typeof bodySource === "string") {
|
|
6508
|
+
rawBody = bodySource;
|
|
6509
|
+
} else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
|
|
6510
|
+
rawBody = new TextDecoder().decode(bodySource);
|
|
6511
|
+
} else if (bodySource instanceof URLSearchParams) {
|
|
6512
|
+
rawBody = bodySource.toString();
|
|
6513
|
+
}
|
|
6514
|
+
if (rawBody !== void 0) {
|
|
6203
6515
|
try {
|
|
6204
6516
|
body = JSON.parse(rawBody);
|
|
6205
6517
|
} catch {
|
|
6206
6518
|
body = rawBody;
|
|
6207
6519
|
}
|
|
6208
6520
|
} else {
|
|
6209
|
-
body =
|
|
6521
|
+
body = bodySource;
|
|
6210
6522
|
}
|
|
6211
6523
|
}
|
|
6212
6524
|
const requestInfo = {
|
|
@@ -6236,8 +6548,20 @@ async function interceptedFetch(input, init) {
|
|
|
6236
6548
|
Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
|
|
6237
6549
|
);
|
|
6238
6550
|
}
|
|
6551
|
+
var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
|
|
6552
|
+
function isResponseDescriptor(value) {
|
|
6553
|
+
const keys = Object.keys(value);
|
|
6554
|
+
if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
|
|
6555
|
+
if ("status" in value) {
|
|
6556
|
+
const status = value["status"];
|
|
6557
|
+
if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
|
|
6558
|
+
}
|
|
6559
|
+
return true;
|
|
6560
|
+
}
|
|
6239
6561
|
function normalizeResponse(response) {
|
|
6240
|
-
|
|
6562
|
+
const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
|
|
6563
|
+
if (isNetworkError) return response;
|
|
6564
|
+
if (Array.isArray(response) || !isResponseDescriptor(response)) {
|
|
6241
6565
|
return { body: response };
|
|
6242
6566
|
}
|
|
6243
6567
|
return response;
|
|
@@ -7571,47 +7895,48 @@ function clearGlobalMeasurements() {
|
|
|
7571
7895
|
}
|
|
7572
7896
|
|
|
7573
7897
|
// libs/testing/src/perf/perf-test.ts
|
|
7574
|
-
var
|
|
7575
|
-
var
|
|
7576
|
-
var
|
|
7577
|
-
var
|
|
7578
|
-
async function
|
|
7579
|
-
if (!
|
|
7580
|
-
|
|
7581
|
-
}
|
|
7582
|
-
if (!
|
|
7583
|
-
if (
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
} else if (
|
|
7587
|
-
const serverCommand = resolveServerCommand2(
|
|
7898
|
+
var currentConfig = {};
|
|
7899
|
+
var serverInstance = null;
|
|
7900
|
+
var tokenFactory = null;
|
|
7901
|
+
var serverStartedByUs = false;
|
|
7902
|
+
async function initializeSharedResources() {
|
|
7903
|
+
if (!tokenFactory) {
|
|
7904
|
+
tokenFactory = new TestTokenFactory();
|
|
7905
|
+
}
|
|
7906
|
+
if (!serverInstance) {
|
|
7907
|
+
if (currentConfig.baseUrl) {
|
|
7908
|
+
serverInstance = TestServer.connect(currentConfig.baseUrl);
|
|
7909
|
+
serverStartedByUs = false;
|
|
7910
|
+
} else if (currentConfig.server) {
|
|
7911
|
+
const serverCommand = resolveServerCommand2(currentConfig.server);
|
|
7588
7912
|
const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
|
|
7589
7913
|
if (isDebug) {
|
|
7590
7914
|
console.log(`[PerfTest] Starting server: ${serverCommand}`);
|
|
7591
7915
|
}
|
|
7592
7916
|
try {
|
|
7593
|
-
|
|
7594
|
-
project:
|
|
7595
|
-
port:
|
|
7917
|
+
serverInstance = await TestServer.start({
|
|
7918
|
+
project: currentConfig.project,
|
|
7919
|
+
port: currentConfig.port,
|
|
7596
7920
|
command: serverCommand,
|
|
7597
|
-
env:
|
|
7598
|
-
startupTimeout:
|
|
7921
|
+
env: currentConfig.env,
|
|
7922
|
+
startupTimeout: currentConfig.startupTimeout ?? 3e4,
|
|
7599
7923
|
debug: isDebug
|
|
7600
7924
|
});
|
|
7601
|
-
|
|
7925
|
+
serverStartedByUs = true;
|
|
7602
7926
|
if (isDebug) {
|
|
7603
|
-
console.log(`[PerfTest] Server started at ${
|
|
7927
|
+
console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
|
|
7604
7928
|
}
|
|
7605
7929
|
} catch (error) {
|
|
7606
7930
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
7607
7931
|
throw new Error(
|
|
7608
7932
|
`Failed to start test server.
|
|
7609
7933
|
|
|
7610
|
-
Server entry: ${
|
|
7611
|
-
Project: ${
|
|
7934
|
+
Server entry: ${currentConfig.server}
|
|
7935
|
+
Project: ${currentConfig.project ?? "default"}
|
|
7612
7936
|
Command: ${serverCommand}
|
|
7613
7937
|
|
|
7614
|
-
Error: ${errMsg}
|
|
7938
|
+
Error: ${errMsg}`,
|
|
7939
|
+
{ cause: error }
|
|
7615
7940
|
);
|
|
7616
7941
|
}
|
|
7617
7942
|
} else {
|
|
@@ -7619,23 +7944,23 @@ Error: ${errMsg}`
|
|
|
7619
7944
|
}
|
|
7620
7945
|
}
|
|
7621
7946
|
}
|
|
7622
|
-
async function
|
|
7623
|
-
await
|
|
7624
|
-
if (!
|
|
7947
|
+
async function createTestFixtures(testName) {
|
|
7948
|
+
await initializeSharedResources();
|
|
7949
|
+
if (!serverInstance) {
|
|
7625
7950
|
throw new Error("Server instance not initialized");
|
|
7626
7951
|
}
|
|
7627
|
-
if (!
|
|
7952
|
+
if (!tokenFactory) {
|
|
7628
7953
|
throw new Error("Token factory not initialized");
|
|
7629
7954
|
}
|
|
7630
7955
|
const clientInstance = await McpTestClient.create({
|
|
7631
|
-
baseUrl:
|
|
7632
|
-
transport:
|
|
7633
|
-
publicMode:
|
|
7956
|
+
baseUrl: serverInstance.info.baseUrl,
|
|
7957
|
+
transport: currentConfig.transport ?? "streamable-http",
|
|
7958
|
+
publicMode: currentConfig.publicMode
|
|
7634
7959
|
}).buildAndConnect();
|
|
7635
|
-
const auth = createAuthFixture2(
|
|
7636
|
-
const server = createServerFixture2(
|
|
7637
|
-
const perfImpl = createPerfFixtures(testName,
|
|
7638
|
-
if (
|
|
7960
|
+
const auth = createAuthFixture2(tokenFactory);
|
|
7961
|
+
const server = createServerFixture2(serverInstance);
|
|
7962
|
+
const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
|
|
7963
|
+
if (currentConfig.forceGcOnBaseline !== false) {
|
|
7639
7964
|
await perfImpl.baseline();
|
|
7640
7965
|
}
|
|
7641
7966
|
return {
|
|
@@ -7651,8 +7976,8 @@ async function createTestFixtures2(testName) {
|
|
|
7651
7976
|
async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
7652
7977
|
const measurement = perfImpl.buildMeasurement();
|
|
7653
7978
|
addGlobalMeasurement(measurement);
|
|
7654
|
-
if (testFailed &&
|
|
7655
|
-
const logs =
|
|
7979
|
+
if (testFailed && serverInstance) {
|
|
7980
|
+
const logs = serverInstance.getLogs();
|
|
7656
7981
|
if (logs.length > 0) {
|
|
7657
7982
|
console.error("\n[PerfTest] === Server Logs (test failed) ===");
|
|
7658
7983
|
const recentLogs = logs.slice(-50);
|
|
@@ -7667,13 +7992,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
|
|
|
7667
7992
|
await fixtures.mcp.disconnect();
|
|
7668
7993
|
}
|
|
7669
7994
|
}
|
|
7670
|
-
async function
|
|
7671
|
-
if (
|
|
7672
|
-
await
|
|
7995
|
+
async function cleanupSharedResources() {
|
|
7996
|
+
if (serverInstance && serverStartedByUs) {
|
|
7997
|
+
await serverInstance.stop();
|
|
7673
7998
|
}
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7999
|
+
serverInstance = null;
|
|
8000
|
+
tokenFactory = null;
|
|
8001
|
+
serverStartedByUs = false;
|
|
7677
8002
|
}
|
|
7678
8003
|
function createAuthFixture2(factory) {
|
|
7679
8004
|
const users = {
|
|
@@ -7728,13 +8053,13 @@ function createServerFixture2(server) {
|
|
|
7728
8053
|
transport: opts?.transport ?? "streamable-http",
|
|
7729
8054
|
auth: opts?.token ? { token: opts.token } : void 0,
|
|
7730
8055
|
clientInfo: opts?.clientInfo,
|
|
7731
|
-
publicMode:
|
|
8056
|
+
publicMode: currentConfig.publicMode
|
|
7732
8057
|
}).buildAndConnect();
|
|
7733
8058
|
},
|
|
7734
8059
|
createClientBuilder: () => {
|
|
7735
8060
|
return new McpTestClientBuilder({
|
|
7736
8061
|
baseUrl: server.info.baseUrl,
|
|
7737
|
-
publicMode:
|
|
8062
|
+
publicMode: currentConfig.publicMode
|
|
7738
8063
|
});
|
|
7739
8064
|
},
|
|
7740
8065
|
restart: () => server.restart(),
|
|
@@ -7750,7 +8075,7 @@ function resolveServerCommand2(server) {
|
|
|
7750
8075
|
}
|
|
7751
8076
|
function perfTestWithFixtures(name, fn) {
|
|
7752
8077
|
it(name, async () => {
|
|
7753
|
-
const { fixtures, perfImpl } = await
|
|
8078
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7754
8079
|
let testFailed = false;
|
|
7755
8080
|
try {
|
|
7756
8081
|
await fn(fixtures);
|
|
@@ -7763,14 +8088,14 @@ function perfTestWithFixtures(name, fn) {
|
|
|
7763
8088
|
});
|
|
7764
8089
|
}
|
|
7765
8090
|
function use2(config) {
|
|
7766
|
-
|
|
8091
|
+
currentConfig = { ...currentConfig, ...config };
|
|
7767
8092
|
afterAll(async () => {
|
|
7768
|
-
await
|
|
8093
|
+
await cleanupSharedResources();
|
|
7769
8094
|
});
|
|
7770
8095
|
}
|
|
7771
8096
|
function skip2(name, fn) {
|
|
7772
8097
|
it.skip(name, async () => {
|
|
7773
|
-
const { fixtures, perfImpl } = await
|
|
8098
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7774
8099
|
let testFailed = false;
|
|
7775
8100
|
try {
|
|
7776
8101
|
await fn(fixtures);
|
|
@@ -7784,7 +8109,7 @@ function skip2(name, fn) {
|
|
|
7784
8109
|
}
|
|
7785
8110
|
function only2(name, fn) {
|
|
7786
8111
|
it.only(name, async () => {
|
|
7787
|
-
const { fixtures, perfImpl } = await
|
|
8112
|
+
const { fixtures, perfImpl } = await createTestFixtures(name);
|
|
7788
8113
|
let testFailed = false;
|
|
7789
8114
|
try {
|
|
7790
8115
|
await fn(fixtures);
|
|
@@ -7801,16 +8126,19 @@ function todo2(name) {
|
|
|
7801
8126
|
}
|
|
7802
8127
|
var perfTest = perfTestWithFixtures;
|
|
7803
8128
|
perfTest.use = use2;
|
|
7804
|
-
|
|
7805
|
-
perfTest
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
8129
|
+
for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
|
|
8130
|
+
Object.defineProperty(perfTest, hook, {
|
|
8131
|
+
get: () => globalThis[hook],
|
|
8132
|
+
enumerable: true,
|
|
8133
|
+
configurable: true
|
|
8134
|
+
});
|
|
8135
|
+
}
|
|
7809
8136
|
perfTest.skip = skip2;
|
|
7810
8137
|
perfTest.only = only2;
|
|
7811
8138
|
perfTest.todo = todo2;
|
|
7812
8139
|
|
|
7813
8140
|
// libs/testing/src/perf/baseline-store.ts
|
|
8141
|
+
var import_utils4 = require("@frontmcp/utils");
|
|
7814
8142
|
var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
|
|
7815
8143
|
var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
|
|
7816
8144
|
var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
|
|
@@ -7825,11 +8153,10 @@ var BaselineStore = class {
|
|
|
7825
8153
|
*/
|
|
7826
8154
|
async load() {
|
|
7827
8155
|
try {
|
|
7828
|
-
|
|
7829
|
-
if (!await fileExists(this.baselinePath)) {
|
|
8156
|
+
if (!await (0, import_utils4.fileExists)(this.baselinePath)) {
|
|
7830
8157
|
return null;
|
|
7831
8158
|
}
|
|
7832
|
-
const content = await readFile(this.baselinePath);
|
|
8159
|
+
const content = await (0, import_utils4.readFile)(this.baselinePath);
|
|
7833
8160
|
this.baseline = JSON.parse(content);
|
|
7834
8161
|
return this.baseline;
|
|
7835
8162
|
} catch {
|
|
@@ -7840,10 +8167,9 @@ var BaselineStore = class {
|
|
|
7840
8167
|
* Save baseline to local file.
|
|
7841
8168
|
*/
|
|
7842
8169
|
async save(baseline) {
|
|
7843
|
-
const { writeFile, ensureDir } = await import("@frontmcp/utils");
|
|
7844
8170
|
const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
|
|
7845
|
-
await ensureDir(dir);
|
|
7846
|
-
await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
8171
|
+
await (0, import_utils4.ensureDir)(dir);
|
|
8172
|
+
await (0, import_utils4.writeFile)(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
7847
8173
|
this.baseline = baseline;
|
|
7848
8174
|
}
|
|
7849
8175
|
/**
|
|
@@ -8096,6 +8422,7 @@ function getRegressionDetector(config) {
|
|
|
8096
8422
|
}
|
|
8097
8423
|
|
|
8098
8424
|
// libs/testing/src/perf/report-generator.ts
|
|
8425
|
+
var import_utils5 = require("@frontmcp/utils");
|
|
8099
8426
|
var ReportGenerator = class {
|
|
8100
8427
|
detector;
|
|
8101
8428
|
constructor() {
|
|
@@ -8351,14 +8678,13 @@ var ReportGenerator = class {
|
|
|
8351
8678
|
}
|
|
8352
8679
|
};
|
|
8353
8680
|
async function saveReports(measurements, outputDir, baseline, gitInfo) {
|
|
8354
|
-
|
|
8355
|
-
await ensureDir(outputDir);
|
|
8681
|
+
await (0, import_utils5.ensureDir)(outputDir);
|
|
8356
8682
|
const generator = new ReportGenerator();
|
|
8357
8683
|
const report = generator.generateReport(measurements, baseline, gitInfo);
|
|
8358
8684
|
const jsonPath = `${outputDir}/report.json`;
|
|
8359
8685
|
const markdownPath = `${outputDir}/report.md`;
|
|
8360
|
-
await writeFile(jsonPath, generator.generateJsonReport(report));
|
|
8361
|
-
await writeFile(markdownPath, generator.generateMarkdownReport(report));
|
|
8686
|
+
await (0, import_utils5.writeFile)(jsonPath, generator.generateJsonReport(report));
|
|
8687
|
+
await (0, import_utils5.writeFile)(markdownPath, generator.generateMarkdownReport(report));
|
|
8362
8688
|
return { jsonPath, markdownPath };
|
|
8363
8689
|
}
|
|
8364
8690
|
function createReportGenerator() {
|