@frontmcp/testing 1.8.6 → 1.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auth/mock-oauth-server.d.ts.map +1 -1
- package/auth/token-factory.d.ts +2 -2
- package/auth/token-factory.d.ts.map +1 -1
- package/client/mcp-test-client.d.ts +6 -1
- package/client/mcp-test-client.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +487 -224
- package/esm/index.mjs +644 -347
- package/esm/package.json +6 -6
- package/esm/perf/index.mjs +407 -272
- package/expect.d.ts +12 -11
- package/expect.d.ts.map +1 -1
- package/fixtures/fixture-types.d.ts +22 -4
- package/fixtures/fixture-types.d.ts.map +1 -1
- package/fixtures/index.js +486 -216
- package/fixtures/test-fixture.d.ts +6 -5
- package/fixtures/test-fixture.d.ts.map +1 -1
- package/http-mock/http-mock.d.ts +5 -3
- package/http-mock/http-mock.d.ts.map +1 -1
- package/http-mock/http-mock.types.d.ts +17 -6
- package/http-mock/http-mock.types.d.ts.map +1 -1
- package/http-mock/index.d.ts +1 -1
- package/http-mock/index.d.ts.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +652 -348
- package/package.json +6 -6
- package/perf/baseline-store.d.ts +1 -1
- package/perf/baseline-store.d.ts.map +1 -1
- package/perf/index.js +410 -285
- package/perf/perf-test.d.ts.map +1 -1
- package/perf/report-generator.d.ts +1 -1
- package/perf/report-generator.d.ts.map +1 -1
- package/server/port-registry.d.ts +7 -1
- package/server/port-registry.d.ts.map +1 -1
- package/server/test-server.d.ts +10 -1
- package/server/test-server.d.ts.map +1 -1
- package/transport/error-utils.d.ts +15 -0
- package/transport/error-utils.d.ts.map +1 -0
- package/transport/streamable-http.transport.d.ts.map +1 -1
package/perf/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// libs/testing/src/perf/index.ts
|
|
@@ -834,6 +824,192 @@ function getMeasurementsForProject(project) {
|
|
|
834
824
|
return getGlobalMeasurementsArray().filter((m) => m.project === project);
|
|
835
825
|
}
|
|
836
826
|
|
|
827
|
+
// libs/testing/src/auth/token-factory.ts
|
|
828
|
+
var import_jose = require("jose");
|
|
829
|
+
var TestTokenFactory = class {
|
|
830
|
+
issuer;
|
|
831
|
+
audience;
|
|
832
|
+
privateKey = null;
|
|
833
|
+
publicKey = null;
|
|
834
|
+
jwk = null;
|
|
835
|
+
keyId;
|
|
836
|
+
/** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
|
|
837
|
+
hmacSecret;
|
|
838
|
+
constructor(options = {}) {
|
|
839
|
+
this.issuer = options.issuer ?? "https://test.frontmcp.local";
|
|
840
|
+
this.audience = options.audience ?? "frontmcp-test";
|
|
841
|
+
this.keyId = `test-key-${Date.now()}`;
|
|
842
|
+
this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
|
|
843
|
+
}
|
|
844
|
+
/** True when this factory signs HS256 with a shared secret (gateway mode). */
|
|
845
|
+
get isHmac() {
|
|
846
|
+
return this.hmacSecret !== null;
|
|
847
|
+
}
|
|
848
|
+
/** The JWS `alg` this factory signs with. */
|
|
849
|
+
get alg() {
|
|
850
|
+
return this.isHmac ? "HS256" : "RS256";
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* The signing key: the shared HMAC secret in gateway mode, otherwise the
|
|
854
|
+
* generated RSA private key (lazily created via ensureKeys()).
|
|
855
|
+
*/
|
|
856
|
+
signingKey() {
|
|
857
|
+
if (this.hmacSecret) return this.hmacSecret;
|
|
858
|
+
if (!this.privateKey) {
|
|
859
|
+
throw new Error("Private key not initialized");
|
|
860
|
+
}
|
|
861
|
+
return this.privateKey;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Initialize the key pair (called automatically on first use). No-op in HS256
|
|
865
|
+
* mode — there is no asymmetric keypair, just the shared secret.
|
|
866
|
+
*/
|
|
867
|
+
async ensureKeys() {
|
|
868
|
+
if (this.isHmac) return;
|
|
869
|
+
if (this.privateKey && this.publicKey) return;
|
|
870
|
+
const { publicKey, privateKey } = await (0, import_jose.generateKeyPair)("RS256", {
|
|
871
|
+
extractable: true
|
|
872
|
+
});
|
|
873
|
+
this.privateKey = privateKey;
|
|
874
|
+
this.publicKey = publicKey;
|
|
875
|
+
this.jwk = await (0, import_jose.exportJWK)(publicKey);
|
|
876
|
+
this.jwk.kid = this.keyId;
|
|
877
|
+
this.jwk.use = "sig";
|
|
878
|
+
this.jwk.alg = "RS256";
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Create a JWT token with the specified claims
|
|
882
|
+
*/
|
|
883
|
+
async createTestToken(options) {
|
|
884
|
+
await this.ensureKeys();
|
|
885
|
+
const nowMs = Date.now();
|
|
886
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
887
|
+
const exp = options.exp ?? 3600;
|
|
888
|
+
const payload = {
|
|
889
|
+
iss: options.iss ?? this.issuer,
|
|
890
|
+
sub: options.sub,
|
|
891
|
+
aud: options.aud ?? this.audience,
|
|
892
|
+
iat,
|
|
893
|
+
exp: Math.ceil(nowMs / 1e3) + exp,
|
|
894
|
+
scope: options.scopes?.join(" "),
|
|
895
|
+
...options.claims
|
|
896
|
+
};
|
|
897
|
+
const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
|
|
898
|
+
const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
|
|
899
|
+
return token;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Create an admin token with full access
|
|
903
|
+
*/
|
|
904
|
+
async createAdminToken(sub = "admin-001") {
|
|
905
|
+
return this.createTestToken({
|
|
906
|
+
sub,
|
|
907
|
+
scopes: ["admin:*", "read", "write", "delete"],
|
|
908
|
+
claims: {
|
|
909
|
+
email: "admin@test.local",
|
|
910
|
+
name: "Test Admin",
|
|
911
|
+
role: "admin"
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Create a regular user token
|
|
917
|
+
*/
|
|
918
|
+
async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
|
|
919
|
+
return this.createTestToken({
|
|
920
|
+
sub,
|
|
921
|
+
scopes,
|
|
922
|
+
claims: {
|
|
923
|
+
email: "user@test.local",
|
|
924
|
+
name: "Test User",
|
|
925
|
+
role: "user"
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Create an anonymous user token
|
|
931
|
+
*/
|
|
932
|
+
async createAnonymousToken(expiresIn) {
|
|
933
|
+
return this.createTestToken({
|
|
934
|
+
sub: `anon:${Date.now()}`,
|
|
935
|
+
scopes: ["anonymous"],
|
|
936
|
+
exp: expiresIn,
|
|
937
|
+
claims: {
|
|
938
|
+
name: "Anonymous",
|
|
939
|
+
role: "anonymous"
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Create an expired token (for testing token expiration)
|
|
945
|
+
*/
|
|
946
|
+
async createExpiredToken(options) {
|
|
947
|
+
await this.ensureKeys();
|
|
948
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
949
|
+
const payload = {
|
|
950
|
+
iss: this.issuer,
|
|
951
|
+
sub: options.sub,
|
|
952
|
+
aud: this.audience,
|
|
953
|
+
iat: now - 7200,
|
|
954
|
+
// 2 hours ago
|
|
955
|
+
exp: now - 3600
|
|
956
|
+
// Expired 1 hour ago
|
|
957
|
+
};
|
|
958
|
+
const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
|
|
959
|
+
const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
|
|
960
|
+
return token;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Create a token with an invalid signature (for testing signature validation)
|
|
964
|
+
*/
|
|
965
|
+
createTokenWithInvalidSignature(options) {
|
|
966
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
967
|
+
const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
|
|
968
|
+
const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
|
|
969
|
+
const payload = Buffer.from(
|
|
970
|
+
JSON.stringify({
|
|
971
|
+
iss: this.issuer,
|
|
972
|
+
sub: options.sub,
|
|
973
|
+
aud: this.audience,
|
|
974
|
+
iat: now,
|
|
975
|
+
exp: now + 3600
|
|
976
|
+
})
|
|
977
|
+
).toString("base64url");
|
|
978
|
+
const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
|
|
979
|
+
return `${header}.${payload}.${signature}`;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Get the public JWKS for verifying tokens.
|
|
983
|
+
*
|
|
984
|
+
* Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
|
|
985
|
+
* the shared secret, not a published public key, so there is no JWKS to serve.
|
|
986
|
+
*/
|
|
987
|
+
async getPublicJwks() {
|
|
988
|
+
if (this.isHmac) {
|
|
989
|
+
throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
|
|
990
|
+
}
|
|
991
|
+
await this.ensureKeys();
|
|
992
|
+
if (!this.jwk) {
|
|
993
|
+
throw new Error("JWK not initialized");
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
keys: [this.jwk]
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Get the issuer URL
|
|
1001
|
+
*/
|
|
1002
|
+
getIssuer() {
|
|
1003
|
+
return this.issuer;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Get the audience
|
|
1007
|
+
*/
|
|
1008
|
+
getAudience() {
|
|
1009
|
+
return this.audience;
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
|
|
837
1013
|
// libs/testing/src/interceptor/mock-registry.ts
|
|
838
1014
|
var DefaultMockRegistry = class {
|
|
839
1015
|
mocks = [];
|
|
@@ -1106,6 +1282,32 @@ function sleep3(ms) {
|
|
|
1106
1282
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1107
1283
|
}
|
|
1108
1284
|
|
|
1285
|
+
// libs/testing/src/transport/error-utils.ts
|
|
1286
|
+
function errorName(err) {
|
|
1287
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
1288
|
+
const name = err.name;
|
|
1289
|
+
return typeof name === "string" ? name : void 0;
|
|
1290
|
+
}
|
|
1291
|
+
function isAbortError(err) {
|
|
1292
|
+
return errorName(err) === "AbortError";
|
|
1293
|
+
}
|
|
1294
|
+
function errorMessage(err, fallback = "Unknown error") {
|
|
1295
|
+
if (typeof err === "string" && err) return err;
|
|
1296
|
+
if (typeof err === "object" && err !== null) {
|
|
1297
|
+
const message = err.message;
|
|
1298
|
+
if (typeof message === "string" && message) return message;
|
|
1299
|
+
}
|
|
1300
|
+
return fallback;
|
|
1301
|
+
}
|
|
1302
|
+
var interceptedErrors = /* @__PURE__ */ new WeakSet();
|
|
1303
|
+
function markInterceptedError(err) {
|
|
1304
|
+
interceptedErrors.add(err);
|
|
1305
|
+
return err;
|
|
1306
|
+
}
|
|
1307
|
+
function isInterceptedError(err) {
|
|
1308
|
+
return typeof err === "object" && err !== null && interceptedErrors.has(err);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1109
1311
|
// libs/testing/src/transport/streamable-http.transport.ts
|
|
1110
1312
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1111
1313
|
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
@@ -1214,7 +1416,7 @@ var StreamableHttpTransport = class {
|
|
|
1214
1416
|
return mockResponse2;
|
|
1215
1417
|
}
|
|
1216
1418
|
case "error":
|
|
1217
|
-
throw interceptResult.error;
|
|
1419
|
+
throw markInterceptedError(interceptResult.error);
|
|
1218
1420
|
case "continue":
|
|
1219
1421
|
message = interceptResult.request;
|
|
1220
1422
|
break;
|
|
@@ -1275,7 +1477,7 @@ var StreamableHttpTransport = class {
|
|
|
1275
1477
|
return jsonResponse;
|
|
1276
1478
|
} catch (error) {
|
|
1277
1479
|
clearTimeout(timeoutId);
|
|
1278
|
-
if (error
|
|
1480
|
+
if (isAbortError(error)) {
|
|
1279
1481
|
return {
|
|
1280
1482
|
jsonrpc: "2.0",
|
|
1281
1483
|
id: message.id ?? null,
|
|
@@ -1314,7 +1516,7 @@ var StreamableHttpTransport = class {
|
|
|
1314
1516
|
}
|
|
1315
1517
|
} catch (error) {
|
|
1316
1518
|
clearTimeout(timeoutId);
|
|
1317
|
-
if (error
|
|
1519
|
+
if (!isAbortError(error)) {
|
|
1318
1520
|
throw error;
|
|
1319
1521
|
}
|
|
1320
1522
|
}
|
|
@@ -1355,7 +1557,7 @@ var StreamableHttpTransport = class {
|
|
|
1355
1557
|
error: {
|
|
1356
1558
|
code: -32700,
|
|
1357
1559
|
message: "Parse error",
|
|
1358
|
-
data: error
|
|
1560
|
+
data: errorMessage(error)
|
|
1359
1561
|
}
|
|
1360
1562
|
};
|
|
1361
1563
|
}
|
|
@@ -1725,7 +1927,7 @@ var StreamableHttpTransport = class {
|
|
|
1725
1927
|
if (this.config.clientInfo) {
|
|
1726
1928
|
headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
|
|
1727
1929
|
}
|
|
1728
|
-
if (this.authToken
|
|
1930
|
+
if (this.authToken) {
|
|
1729
1931
|
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1730
1932
|
}
|
|
1731
1933
|
if (this.sessionId) {
|
|
@@ -2049,6 +2251,15 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
2049
2251
|
version: "0.4.0"
|
|
2050
2252
|
};
|
|
2051
2253
|
var MAX_LIST_PAGES = 1e3;
|
|
2254
|
+
function isAuthFailure(error) {
|
|
2255
|
+
return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
|
|
2256
|
+
}
|
|
2257
|
+
function describeInitializeError(error) {
|
|
2258
|
+
const message = error?.message ?? "Unknown error";
|
|
2259
|
+
const data = error?.data;
|
|
2260
|
+
const detail = typeof data === "string" ? data.trim() : "";
|
|
2261
|
+
return detail ? `${message} - ${detail.slice(0, 500)}` : message;
|
|
2262
|
+
}
|
|
2052
2263
|
function extractEntryPaths(data) {
|
|
2053
2264
|
let payload = data;
|
|
2054
2265
|
if (typeof payload === "string") {
|
|
@@ -2135,19 +2346,22 @@ var McpTestClient = class {
|
|
|
2135
2346
|
async connect() {
|
|
2136
2347
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
2137
2348
|
this.triedDiscoveredEntryPath = false;
|
|
2349
|
+
this.initResult = null;
|
|
2138
2350
|
this.transport = this.createTransport();
|
|
2139
2351
|
await this.transport.connect();
|
|
2140
2352
|
const maxRetries = 3;
|
|
2141
2353
|
const retryDelayMs = 500;
|
|
2142
2354
|
let lastError;
|
|
2143
2355
|
let reportedEntryPaths;
|
|
2356
|
+
let attemptsMade = 0;
|
|
2144
2357
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2358
|
+
attemptsMade = attempt;
|
|
2145
2359
|
const initResponse = await this.initialize();
|
|
2146
2360
|
if (initResponse.success && initResponse.data) {
|
|
2147
2361
|
this.initResult = initResponse.data;
|
|
2148
2362
|
break;
|
|
2149
2363
|
}
|
|
2150
|
-
lastError = initResponse.error
|
|
2364
|
+
lastError = describeInitializeError(initResponse.error);
|
|
2151
2365
|
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
2152
2366
|
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
2153
2367
|
this.triedDiscoveredEntryPath = true;
|
|
@@ -2159,6 +2373,7 @@ var McpTestClient = class {
|
|
|
2159
2373
|
await this.transport.connect();
|
|
2160
2374
|
continue;
|
|
2161
2375
|
}
|
|
2376
|
+
if (isAuthFailure(initResponse.error)) break;
|
|
2162
2377
|
if (attempt < maxRetries) {
|
|
2163
2378
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
2164
2379
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
@@ -2167,7 +2382,7 @@ var McpTestClient = class {
|
|
|
2167
2382
|
if (!this.initResult) {
|
|
2168
2383
|
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.` : "";
|
|
2169
2384
|
throw new Error(
|
|
2170
|
-
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${
|
|
2385
|
+
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
|
|
2171
2386
|
);
|
|
2172
2387
|
}
|
|
2173
2388
|
this._sessionId = this.transport.getSessionId();
|
|
@@ -2237,18 +2452,47 @@ var McpTestClient = class {
|
|
|
2237
2452
|
return this._authState;
|
|
2238
2453
|
}
|
|
2239
2454
|
/**
|
|
2240
|
-
* Authenticate with a token
|
|
2455
|
+
* Authenticate with a token.
|
|
2456
|
+
*
|
|
2457
|
+
* When connected, this opens a NEW session with the token (the previous session was created
|
|
2458
|
+
* for the previous identity) and rejects if the server refuses it — for example because the
|
|
2459
|
+
* token is expired or badly signed. On rejection the client keeps its previous identity and
|
|
2460
|
+
* session. The token is kept in the client config, so `reconnect()` keeps using it.
|
|
2241
2461
|
*/
|
|
2242
2462
|
async authenticate(token) {
|
|
2463
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
2464
|
+
throw new Error("authenticate() requires a non-empty token");
|
|
2465
|
+
}
|
|
2466
|
+
const previousAuthState = this._authState;
|
|
2467
|
+
const previousConfigAuth = this.config.auth;
|
|
2468
|
+
this.config.auth = { ...previousConfigAuth, token };
|
|
2243
2469
|
this._authState = {
|
|
2244
2470
|
isAnonymous: false,
|
|
2245
2471
|
token,
|
|
2246
2472
|
scopes: this.parseScopesFromToken(token),
|
|
2247
2473
|
user: this.parseUserFromToken(token)
|
|
2248
2474
|
};
|
|
2249
|
-
|
|
2250
|
-
|
|
2475
|
+
const previousTransport = this.transport;
|
|
2476
|
+
if (!previousTransport) {
|
|
2477
|
+
this.log("debug", "Authentication updated (not connected yet)");
|
|
2478
|
+
return;
|
|
2251
2479
|
}
|
|
2480
|
+
const previousInit = this.initResult;
|
|
2481
|
+
const previousSessionId = this._sessionId;
|
|
2482
|
+
const previousSessionInfo = this._sessionInfo;
|
|
2483
|
+
try {
|
|
2484
|
+
await this.connect();
|
|
2485
|
+
} catch (err) {
|
|
2486
|
+
await this.transport?.close().catch(() => void 0);
|
|
2487
|
+
this.transport = previousTransport;
|
|
2488
|
+
this.initResult = previousInit;
|
|
2489
|
+
this._sessionId = previousSessionId;
|
|
2490
|
+
this._sessionInfo = previousSessionInfo;
|
|
2491
|
+
this.config.auth = previousConfigAuth;
|
|
2492
|
+
this._authState = previousAuthState;
|
|
2493
|
+
throw err;
|
|
2494
|
+
}
|
|
2495
|
+
await previousTransport.close().catch(() => void 0);
|
|
2252
2496
|
this.log("debug", "Authentication updated");
|
|
2253
2497
|
}
|
|
2254
2498
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -2872,10 +3116,11 @@ var McpTestClient = class {
|
|
|
2872
3116
|
requestId: id
|
|
2873
3117
|
};
|
|
2874
3118
|
} catch (err) {
|
|
3119
|
+
if (isInterceptedError(err)) throw err;
|
|
2875
3120
|
const durationMs = Date.now() - start;
|
|
2876
3121
|
const error = {
|
|
2877
3122
|
code: -32603,
|
|
2878
|
-
message: err
|
|
3123
|
+
message: errorMessage(err)
|
|
2879
3124
|
};
|
|
2880
3125
|
return {
|
|
2881
3126
|
success: false,
|
|
@@ -3102,190 +3347,6 @@ var ProgressCollector = class {
|
|
|
3102
3347
|
}
|
|
3103
3348
|
};
|
|
3104
3349
|
|
|
3105
|
-
// libs/testing/src/auth/token-factory.ts
|
|
3106
|
-
var import_jose = require("jose");
|
|
3107
|
-
var TestTokenFactory = class {
|
|
3108
|
-
issuer;
|
|
3109
|
-
audience;
|
|
3110
|
-
privateKey = null;
|
|
3111
|
-
publicKey = null;
|
|
3112
|
-
jwk = null;
|
|
3113
|
-
keyId;
|
|
3114
|
-
/** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
|
|
3115
|
-
hmacSecret;
|
|
3116
|
-
constructor(options = {}) {
|
|
3117
|
-
this.issuer = options.issuer ?? "https://test.frontmcp.local";
|
|
3118
|
-
this.audience = options.audience ?? "frontmcp-test";
|
|
3119
|
-
this.keyId = `test-key-${Date.now()}`;
|
|
3120
|
-
this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
|
|
3121
|
-
}
|
|
3122
|
-
/** True when this factory signs HS256 with a shared secret (gateway mode). */
|
|
3123
|
-
get isHmac() {
|
|
3124
|
-
return this.hmacSecret !== null;
|
|
3125
|
-
}
|
|
3126
|
-
/** The JWS `alg` this factory signs with. */
|
|
3127
|
-
get alg() {
|
|
3128
|
-
return this.isHmac ? "HS256" : "RS256";
|
|
3129
|
-
}
|
|
3130
|
-
/**
|
|
3131
|
-
* The signing key: the shared HMAC secret in gateway mode, otherwise the
|
|
3132
|
-
* generated RSA private key (lazily created via ensureKeys()).
|
|
3133
|
-
*/
|
|
3134
|
-
signingKey() {
|
|
3135
|
-
if (this.hmacSecret) return this.hmacSecret;
|
|
3136
|
-
if (!this.privateKey) {
|
|
3137
|
-
throw new Error("Private key not initialized");
|
|
3138
|
-
}
|
|
3139
|
-
return this.privateKey;
|
|
3140
|
-
}
|
|
3141
|
-
/**
|
|
3142
|
-
* Initialize the key pair (called automatically on first use). No-op in HS256
|
|
3143
|
-
* mode — there is no asymmetric keypair, just the shared secret.
|
|
3144
|
-
*/
|
|
3145
|
-
async ensureKeys() {
|
|
3146
|
-
if (this.isHmac) return;
|
|
3147
|
-
if (this.privateKey && this.publicKey) return;
|
|
3148
|
-
const { publicKey, privateKey } = await (0, import_jose.generateKeyPair)("RS256", {
|
|
3149
|
-
extractable: true
|
|
3150
|
-
});
|
|
3151
|
-
this.privateKey = privateKey;
|
|
3152
|
-
this.publicKey = publicKey;
|
|
3153
|
-
this.jwk = await (0, import_jose.exportJWK)(publicKey);
|
|
3154
|
-
this.jwk.kid = this.keyId;
|
|
3155
|
-
this.jwk.use = "sig";
|
|
3156
|
-
this.jwk.alg = "RS256";
|
|
3157
|
-
}
|
|
3158
|
-
/**
|
|
3159
|
-
* Create a JWT token with the specified claims
|
|
3160
|
-
*/
|
|
3161
|
-
async createTestToken(options) {
|
|
3162
|
-
await this.ensureKeys();
|
|
3163
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
3164
|
-
const exp = options.exp ?? 3600;
|
|
3165
|
-
const payload = {
|
|
3166
|
-
iss: options.iss ?? this.issuer,
|
|
3167
|
-
sub: options.sub,
|
|
3168
|
-
aud: options.aud ?? this.audience,
|
|
3169
|
-
iat: now,
|
|
3170
|
-
exp: now + exp,
|
|
3171
|
-
scope: options.scopes?.join(" "),
|
|
3172
|
-
...options.claims
|
|
3173
|
-
};
|
|
3174
|
-
const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
|
|
3175
|
-
const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
|
|
3176
|
-
return token;
|
|
3177
|
-
}
|
|
3178
|
-
/**
|
|
3179
|
-
* Create an admin token with full access
|
|
3180
|
-
*/
|
|
3181
|
-
async createAdminToken(sub = "admin-001") {
|
|
3182
|
-
return this.createTestToken({
|
|
3183
|
-
sub,
|
|
3184
|
-
scopes: ["admin:*", "read", "write", "delete"],
|
|
3185
|
-
claims: {
|
|
3186
|
-
email: "admin@test.local",
|
|
3187
|
-
name: "Test Admin",
|
|
3188
|
-
role: "admin"
|
|
3189
|
-
}
|
|
3190
|
-
});
|
|
3191
|
-
}
|
|
3192
|
-
/**
|
|
3193
|
-
* Create a regular user token
|
|
3194
|
-
*/
|
|
3195
|
-
async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
|
|
3196
|
-
return this.createTestToken({
|
|
3197
|
-
sub,
|
|
3198
|
-
scopes,
|
|
3199
|
-
claims: {
|
|
3200
|
-
email: "user@test.local",
|
|
3201
|
-
name: "Test User",
|
|
3202
|
-
role: "user"
|
|
3203
|
-
}
|
|
3204
|
-
});
|
|
3205
|
-
}
|
|
3206
|
-
/**
|
|
3207
|
-
* Create an anonymous user token
|
|
3208
|
-
*/
|
|
3209
|
-
async createAnonymousToken() {
|
|
3210
|
-
return this.createTestToken({
|
|
3211
|
-
sub: `anon:${Date.now()}`,
|
|
3212
|
-
scopes: ["anonymous"],
|
|
3213
|
-
claims: {
|
|
3214
|
-
name: "Anonymous",
|
|
3215
|
-
role: "anonymous"
|
|
3216
|
-
}
|
|
3217
|
-
});
|
|
3218
|
-
}
|
|
3219
|
-
/**
|
|
3220
|
-
* Create an expired token (for testing token expiration)
|
|
3221
|
-
*/
|
|
3222
|
-
async createExpiredToken(options) {
|
|
3223
|
-
await this.ensureKeys();
|
|
3224
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
3225
|
-
const payload = {
|
|
3226
|
-
iss: this.issuer,
|
|
3227
|
-
sub: options.sub,
|
|
3228
|
-
aud: this.audience,
|
|
3229
|
-
iat: now - 7200,
|
|
3230
|
-
// 2 hours ago
|
|
3231
|
-
exp: now - 3600
|
|
3232
|
-
// Expired 1 hour ago
|
|
3233
|
-
};
|
|
3234
|
-
const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
|
|
3235
|
-
const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
|
|
3236
|
-
return token;
|
|
3237
|
-
}
|
|
3238
|
-
/**
|
|
3239
|
-
* Create a token with an invalid signature (for testing signature validation)
|
|
3240
|
-
*/
|
|
3241
|
-
createTokenWithInvalidSignature(options) {
|
|
3242
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
3243
|
-
const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
|
|
3244
|
-
const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
|
|
3245
|
-
const payload = Buffer.from(
|
|
3246
|
-
JSON.stringify({
|
|
3247
|
-
iss: this.issuer,
|
|
3248
|
-
sub: options.sub,
|
|
3249
|
-
aud: this.audience,
|
|
3250
|
-
iat: now,
|
|
3251
|
-
exp: now + 3600
|
|
3252
|
-
})
|
|
3253
|
-
).toString("base64url");
|
|
3254
|
-
const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
|
|
3255
|
-
return `${header}.${payload}.${signature}`;
|
|
3256
|
-
}
|
|
3257
|
-
/**
|
|
3258
|
-
* Get the public JWKS for verifying tokens.
|
|
3259
|
-
*
|
|
3260
|
-
* Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
|
|
3261
|
-
* the shared secret, not a published public key, so there is no JWKS to serve.
|
|
3262
|
-
*/
|
|
3263
|
-
async getPublicJwks() {
|
|
3264
|
-
if (this.isHmac) {
|
|
3265
|
-
throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
|
|
3266
|
-
}
|
|
3267
|
-
await this.ensureKeys();
|
|
3268
|
-
if (!this.jwk) {
|
|
3269
|
-
throw new Error("JWK not initialized");
|
|
3270
|
-
}
|
|
3271
|
-
return {
|
|
3272
|
-
keys: [this.jwk]
|
|
3273
|
-
};
|
|
3274
|
-
}
|
|
3275
|
-
/**
|
|
3276
|
-
* Get the issuer URL
|
|
3277
|
-
*/
|
|
3278
|
-
getIssuer() {
|
|
3279
|
-
return this.issuer;
|
|
3280
|
-
}
|
|
3281
|
-
/**
|
|
3282
|
-
* Get the audience
|
|
3283
|
-
*/
|
|
3284
|
-
getAudience() {
|
|
3285
|
-
return this.audience;
|
|
3286
|
-
}
|
|
3287
|
-
};
|
|
3288
|
-
|
|
3289
3350
|
// libs/testing/src/server/test-server.ts
|
|
3290
3351
|
var import_child_process = require("child_process");
|
|
3291
3352
|
var import_utils = require("@frontmcp/utils");
|
|
@@ -3358,6 +3419,8 @@ var E2E_PORT_RANGES = {
|
|
|
3358
3419
|
"demo-e2e-distributed": { start: 50440, size: 20 },
|
|
3359
3420
|
// Protocol revision E2E tests (50460-50479)
|
|
3360
3421
|
"demo-e2e-protocol-20260728": { start: 50460, size: 20 },
|
|
3422
|
+
// Testing library E2E tests (50480-50499)
|
|
3423
|
+
"demo-e2e-testing": { start: 50480, size: 20 },
|
|
3361
3424
|
// Mock servers and utilities (50900-50999)
|
|
3362
3425
|
"mock-oauth": { start: 50900, size: 10 },
|
|
3363
3426
|
"mock-api": { start: 50910, size: 10 },
|
|
@@ -3366,6 +3429,66 @@ var E2E_PORT_RANGES = {
|
|
|
3366
3429
|
default: { start: 51e3, size: 100 }
|
|
3367
3430
|
};
|
|
3368
3431
|
var reservedPorts = /* @__PURE__ */ new Map();
|
|
3432
|
+
var lockedPorts = /* @__PURE__ */ new Set();
|
|
3433
|
+
var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
3434
|
+
function lockDir() {
|
|
3435
|
+
try {
|
|
3436
|
+
const os = require("node:os");
|
|
3437
|
+
const path = require("node:path");
|
|
3438
|
+
return path.join(os.tmpdir(), "frontmcp-testing-ports");
|
|
3439
|
+
} catch {
|
|
3440
|
+
return void 0;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
function pidIsAlive(pid) {
|
|
3444
|
+
try {
|
|
3445
|
+
process.kill(pid, 0);
|
|
3446
|
+
return true;
|
|
3447
|
+
} catch (error) {
|
|
3448
|
+
return error.code === "EPERM";
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
function acquirePortLock(port) {
|
|
3452
|
+
if (lockedPorts.has(port)) return false;
|
|
3453
|
+
let fs;
|
|
3454
|
+
let file;
|
|
3455
|
+
try {
|
|
3456
|
+
fs = require("node:fs");
|
|
3457
|
+
const dir = lockDir();
|
|
3458
|
+
if (!dir) return true;
|
|
3459
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3460
|
+
file = require("node:path").join(dir, `${port}.lock`);
|
|
3461
|
+
} catch {
|
|
3462
|
+
return true;
|
|
3463
|
+
}
|
|
3464
|
+
const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
|
|
3465
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3466
|
+
try {
|
|
3467
|
+
fs.writeFileSync(file, payload, { flag: "wx" });
|
|
3468
|
+
lockedPorts.add(port);
|
|
3469
|
+
return true;
|
|
3470
|
+
} catch (error) {
|
|
3471
|
+
if (error.code !== "EEXIST") return true;
|
|
3472
|
+
try {
|
|
3473
|
+
const held = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
3474
|
+
const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
|
|
3475
|
+
if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
|
|
3476
|
+
fs.unlinkSync(file);
|
|
3477
|
+
} catch {
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
return false;
|
|
3482
|
+
}
|
|
3483
|
+
function releasePortLock(port) {
|
|
3484
|
+
if (!lockedPorts.delete(port)) return;
|
|
3485
|
+
try {
|
|
3486
|
+
const fs = require("node:fs");
|
|
3487
|
+
const dir = lockDir();
|
|
3488
|
+
if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
|
|
3489
|
+
} catch {
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3369
3492
|
var projectPortIndex = /* @__PURE__ */ new Map();
|
|
3370
3493
|
function getPortRange(project) {
|
|
3371
3494
|
const key = project;
|
|
@@ -3376,15 +3499,22 @@ function getPortRange(project) {
|
|
|
3376
3499
|
}
|
|
3377
3500
|
async function reservePort(project, preferredPort) {
|
|
3378
3501
|
const range = getPortRange(project);
|
|
3379
|
-
|
|
3502
|
+
const handle = (port) => ({
|
|
3503
|
+
port,
|
|
3504
|
+
// Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
|
|
3505
|
+
releaseSocket: async () => {
|
|
3506
|
+
await releasePort(port);
|
|
3507
|
+
},
|
|
3508
|
+
release: async () => {
|
|
3509
|
+
await releasePort(port);
|
|
3510
|
+
releasePortLock(port);
|
|
3511
|
+
}
|
|
3512
|
+
});
|
|
3513
|
+
const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
|
|
3514
|
+
if (wantsSpecificPort) {
|
|
3380
3515
|
const reservation = await tryReservePort(preferredPort, project);
|
|
3381
3516
|
if (reservation) {
|
|
3382
|
-
return
|
|
3383
|
-
port: preferredPort,
|
|
3384
|
-
release: async () => {
|
|
3385
|
-
await releasePort(preferredPort);
|
|
3386
|
-
}
|
|
3387
|
-
};
|
|
3517
|
+
return handle(preferredPort);
|
|
3388
3518
|
}
|
|
3389
3519
|
console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
|
|
3390
3520
|
}
|
|
@@ -3393,30 +3523,19 @@ async function reservePort(project, preferredPort) {
|
|
|
3393
3523
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3394
3524
|
const port = range.start + index % range.size;
|
|
3395
3525
|
index = (index + 1) % range.size;
|
|
3396
|
-
if (reservedPorts.has(port)) {
|
|
3526
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) {
|
|
3397
3527
|
continue;
|
|
3398
3528
|
}
|
|
3399
3529
|
const reservation = await tryReservePort(port, project);
|
|
3400
3530
|
if (reservation) {
|
|
3401
3531
|
projectPortIndex.set(project, index);
|
|
3402
|
-
return
|
|
3403
|
-
port,
|
|
3404
|
-
release: async () => {
|
|
3405
|
-
await releasePort(port);
|
|
3406
|
-
}
|
|
3407
|
-
};
|
|
3532
|
+
return handle(port);
|
|
3408
3533
|
}
|
|
3409
3534
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
return {
|
|
3415
|
-
port: dynamicPort,
|
|
3416
|
-
release: async () => {
|
|
3417
|
-
await releasePort(dynamicPort);
|
|
3418
|
-
}
|
|
3419
|
-
};
|
|
3535
|
+
for (let port = 51e3; port < 52e3; port++) {
|
|
3536
|
+
if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
|
|
3537
|
+
if (await tryReservePort(port, project)) {
|
|
3538
|
+
return handle(port);
|
|
3420
3539
|
}
|
|
3421
3540
|
}
|
|
3422
3541
|
throw new Error(
|
|
@@ -3424,9 +3543,13 @@ async function reservePort(project, preferredPort) {
|
|
|
3424
3543
|
);
|
|
3425
3544
|
}
|
|
3426
3545
|
async function tryReservePort(port, project) {
|
|
3546
|
+
if (!acquirePortLock(port)) {
|
|
3547
|
+
return false;
|
|
3548
|
+
}
|
|
3427
3549
|
return new Promise((resolve) => {
|
|
3428
3550
|
const server = (0, import_net.createServer)();
|
|
3429
3551
|
server.once("error", () => {
|
|
3552
|
+
releasePortLock(port);
|
|
3430
3553
|
resolve(false);
|
|
3431
3554
|
});
|
|
3432
3555
|
server.listen(port, () => {
|
|
@@ -3452,31 +3575,6 @@ async function releasePort(port) {
|
|
|
3452
3575
|
});
|
|
3453
3576
|
});
|
|
3454
3577
|
}
|
|
3455
|
-
async function findAvailablePortInRange(start, end) {
|
|
3456
|
-
for (let port = start; port < end; port++) {
|
|
3457
|
-
if (reservedPorts.has(port)) {
|
|
3458
|
-
continue;
|
|
3459
|
-
}
|
|
3460
|
-
const available = await isPortAvailable(port);
|
|
3461
|
-
if (available) {
|
|
3462
|
-
return port;
|
|
3463
|
-
}
|
|
3464
|
-
}
|
|
3465
|
-
return null;
|
|
3466
|
-
}
|
|
3467
|
-
async function isPortAvailable(port) {
|
|
3468
|
-
return new Promise((resolve) => {
|
|
3469
|
-
const server = (0, import_net.createServer)();
|
|
3470
|
-
server.once("error", () => {
|
|
3471
|
-
resolve(false);
|
|
3472
|
-
});
|
|
3473
|
-
server.listen(port, () => {
|
|
3474
|
-
server.close(() => {
|
|
3475
|
-
resolve(true);
|
|
3476
|
-
});
|
|
3477
|
-
});
|
|
3478
|
-
});
|
|
3479
|
-
}
|
|
3480
3578
|
|
|
3481
3579
|
// libs/testing/src/server/test-server.ts
|
|
3482
3580
|
var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
|
|
@@ -3485,8 +3583,11 @@ var TestServer = class _TestServer {
|
|
|
3485
3583
|
options;
|
|
3486
3584
|
_info;
|
|
3487
3585
|
logs = [];
|
|
3586
|
+
/** Frees the port reservation (socket and cross-process lock); held until stop() */
|
|
3488
3587
|
portRelease = null;
|
|
3489
|
-
|
|
3588
|
+
/** Frees only the reservation socket, just before the server is spawned */
|
|
3589
|
+
portReleaseSocket = null;
|
|
3590
|
+
constructor(options, port, portRelease, portReleaseSocket) {
|
|
3490
3591
|
this.options = {
|
|
3491
3592
|
port,
|
|
3492
3593
|
project: options.project,
|
|
@@ -3498,6 +3599,7 @@ var TestServer = class _TestServer {
|
|
|
3498
3599
|
debug: options.debug ?? DEBUG_SERVER
|
|
3499
3600
|
};
|
|
3500
3601
|
this.portRelease = portRelease ?? null;
|
|
3602
|
+
this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
|
|
3501
3603
|
this._info = {
|
|
3502
3604
|
baseUrl: `http://localhost:${port}`,
|
|
3503
3605
|
port
|
|
@@ -3510,8 +3612,8 @@ var TestServer = class _TestServer {
|
|
|
3510
3612
|
const project = options.project ?? "default";
|
|
3511
3613
|
const maxAttempts = 3;
|
|
3512
3614
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
3513
|
-
const { port, release } = await reservePort(project, options.port);
|
|
3514
|
-
const server = new _TestServer(options, port, release);
|
|
3615
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
3616
|
+
const server = new _TestServer(options, port, release, releaseSocket);
|
|
3515
3617
|
try {
|
|
3516
3618
|
await server.startProcess();
|
|
3517
3619
|
return server;
|
|
@@ -3551,7 +3653,7 @@ var TestServer = class _TestServer {
|
|
|
3551
3653
|
}
|
|
3552
3654
|
const maxAttempts = 3;
|
|
3553
3655
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
3554
|
-
const { port, release } = await reservePort(project, options.port);
|
|
3656
|
+
const { port, release, releaseSocket } = await reservePort(project, options.port);
|
|
3555
3657
|
const serverOptions = {
|
|
3556
3658
|
...options,
|
|
3557
3659
|
port,
|
|
@@ -3559,7 +3661,7 @@ var TestServer = class _TestServer {
|
|
|
3559
3661
|
command: `npx nx serve ${project} --port ${port}`,
|
|
3560
3662
|
cwd: options.cwd ?? process.cwd()
|
|
3561
3663
|
};
|
|
3562
|
-
const server = new _TestServer(serverOptions, port, release);
|
|
3664
|
+
const server = new _TestServer(serverOptions, port, release, releaseSocket);
|
|
3563
3665
|
try {
|
|
3564
3666
|
await server.startProcess();
|
|
3565
3667
|
return server;
|
|
@@ -3617,10 +3719,17 @@ var TestServer = class _TestServer {
|
|
|
3617
3719
|
* Stop the test server
|
|
3618
3720
|
*/
|
|
3619
3721
|
async stop() {
|
|
3620
|
-
|
|
3621
|
-
await this.
|
|
3622
|
-
|
|
3722
|
+
try {
|
|
3723
|
+
await this.stopProcess();
|
|
3724
|
+
} finally {
|
|
3725
|
+
if (this.portRelease) {
|
|
3726
|
+
await this.portRelease();
|
|
3727
|
+
this.portRelease = null;
|
|
3728
|
+
}
|
|
3729
|
+
this.portReleaseSocket = null;
|
|
3623
3730
|
}
|
|
3731
|
+
}
|
|
3732
|
+
async stopProcess() {
|
|
3624
3733
|
if (this.process) {
|
|
3625
3734
|
this.log("Stopping server...");
|
|
3626
3735
|
if (this.process.exitCode !== null || this.process.signalCode !== null) {
|
|
@@ -3693,7 +3802,7 @@ var TestServer = class _TestServer {
|
|
|
3693
3802
|
* Restart the server
|
|
3694
3803
|
*/
|
|
3695
3804
|
async restart() {
|
|
3696
|
-
await this.
|
|
3805
|
+
await this.stopProcess();
|
|
3697
3806
|
await this.startProcess();
|
|
3698
3807
|
}
|
|
3699
3808
|
/**
|
|
@@ -3723,9 +3832,9 @@ var TestServer = class _TestServer {
|
|
|
3723
3832
|
PORT: String(this.options.port)
|
|
3724
3833
|
};
|
|
3725
3834
|
const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
|
|
3726
|
-
if (this.
|
|
3727
|
-
await this.
|
|
3728
|
-
this.
|
|
3835
|
+
if (this.portReleaseSocket) {
|
|
3836
|
+
await this.portReleaseSocket();
|
|
3837
|
+
this.portReleaseSocket = null;
|
|
3729
3838
|
await sleep4(300);
|
|
3730
3839
|
}
|
|
3731
3840
|
this.process = (0, import_child_process.spawn)(this.options.command, [], {
|
|
@@ -3735,9 +3844,8 @@ var TestServer = class _TestServer {
|
|
|
3735
3844
|
detached: true,
|
|
3736
3845
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3737
3846
|
});
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
}
|
|
3847
|
+
this._info.pid = this.process.pid;
|
|
3848
|
+
this._info.shellPid = this.process.pid;
|
|
3741
3849
|
let processExited = false;
|
|
3742
3850
|
let exitCode = null;
|
|
3743
3851
|
let exitError = null;
|
|
@@ -3840,6 +3948,8 @@ ${allLogs}`);
|
|
|
3840
3948
|
});
|
|
3841
3949
|
if (response.ok || response.status === 404) {
|
|
3842
3950
|
this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
|
|
3951
|
+
const listeningPid = await findListeningPid(this.options.port);
|
|
3952
|
+
if (listeningPid !== void 0) this._info.pid = listeningPid;
|
|
3843
3953
|
return;
|
|
3844
3954
|
}
|
|
3845
3955
|
lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
|
|
@@ -3884,6 +3994,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
|
|
|
3884
3994
|
function sleep4(ms) {
|
|
3885
3995
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3886
3996
|
}
|
|
3997
|
+
function findListeningPid(port) {
|
|
3998
|
+
if (process.platform === "win32") return Promise.resolve(void 0);
|
|
3999
|
+
return new Promise((resolve) => {
|
|
4000
|
+
(0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
|
|
4001
|
+
if (error) {
|
|
4002
|
+
resolve(void 0);
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
|
|
4006
|
+
resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
|
|
4007
|
+
});
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
3887
4010
|
function withWorkspaceProtocolFallback(env, cwd) {
|
|
3888
4011
|
if (findInstalledProtocolPackageDir(cwd)) {
|
|
3889
4012
|
return env;
|
|
@@ -4031,7 +4154,8 @@ Server entry: ${currentConfig.server}
|
|
|
4031
4154
|
Project: ${currentConfig.project ?? "default"}
|
|
4032
4155
|
Command: ${serverCommand}
|
|
4033
4156
|
|
|
4034
|
-
Error: ${errMsg}
|
|
4157
|
+
Error: ${errMsg}`,
|
|
4158
|
+
{ cause: error }
|
|
4035
4159
|
);
|
|
4036
4160
|
}
|
|
4037
4161
|
} else {
|
|
@@ -4221,16 +4345,19 @@ function todo(name) {
|
|
|
4221
4345
|
}
|
|
4222
4346
|
var perfTest = perfTestWithFixtures;
|
|
4223
4347
|
perfTest.use = use;
|
|
4224
|
-
|
|
4225
|
-
perfTest
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4348
|
+
for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
|
|
4349
|
+
Object.defineProperty(perfTest, hook, {
|
|
4350
|
+
get: () => globalThis[hook],
|
|
4351
|
+
enumerable: true,
|
|
4352
|
+
configurable: true
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
4229
4355
|
perfTest.skip = skip;
|
|
4230
4356
|
perfTest.only = only;
|
|
4231
4357
|
perfTest.todo = todo;
|
|
4232
4358
|
|
|
4233
4359
|
// libs/testing/src/perf/baseline-store.ts
|
|
4360
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
4234
4361
|
var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
|
|
4235
4362
|
var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
|
|
4236
4363
|
var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
|
|
@@ -4245,11 +4372,10 @@ var BaselineStore = class {
|
|
|
4245
4372
|
*/
|
|
4246
4373
|
async load() {
|
|
4247
4374
|
try {
|
|
4248
|
-
|
|
4249
|
-
if (!await fileExists(this.baselinePath)) {
|
|
4375
|
+
if (!await (0, import_utils2.fileExists)(this.baselinePath)) {
|
|
4250
4376
|
return null;
|
|
4251
4377
|
}
|
|
4252
|
-
const content = await readFile(this.baselinePath);
|
|
4378
|
+
const content = await (0, import_utils2.readFile)(this.baselinePath);
|
|
4253
4379
|
this.baseline = JSON.parse(content);
|
|
4254
4380
|
return this.baseline;
|
|
4255
4381
|
} catch {
|
|
@@ -4260,10 +4386,9 @@ var BaselineStore = class {
|
|
|
4260
4386
|
* Save baseline to local file.
|
|
4261
4387
|
*/
|
|
4262
4388
|
async save(baseline) {
|
|
4263
|
-
const { writeFile, ensureDir } = await import("@frontmcp/utils");
|
|
4264
4389
|
const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
|
|
4265
|
-
await ensureDir(dir);
|
|
4266
|
-
await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
4390
|
+
await (0, import_utils2.ensureDir)(dir);
|
|
4391
|
+
await (0, import_utils2.writeFile)(this.baselinePath, JSON.stringify(baseline, null, 2));
|
|
4267
4392
|
this.baseline = baseline;
|
|
4268
4393
|
}
|
|
4269
4394
|
/**
|
|
@@ -4539,6 +4664,7 @@ function resetRegressionDetector() {
|
|
|
4539
4664
|
}
|
|
4540
4665
|
|
|
4541
4666
|
// libs/testing/src/perf/report-generator.ts
|
|
4667
|
+
var import_utils3 = require("@frontmcp/utils");
|
|
4542
4668
|
var ReportGenerator = class {
|
|
4543
4669
|
detector;
|
|
4544
4670
|
constructor() {
|
|
@@ -4794,14 +4920,13 @@ var ReportGenerator = class {
|
|
|
4794
4920
|
}
|
|
4795
4921
|
};
|
|
4796
4922
|
async function saveReports(measurements, outputDir, baseline, gitInfo) {
|
|
4797
|
-
|
|
4798
|
-
await ensureDir(outputDir);
|
|
4923
|
+
await (0, import_utils3.ensureDir)(outputDir);
|
|
4799
4924
|
const generator = new ReportGenerator();
|
|
4800
4925
|
const report = generator.generateReport(measurements, baseline, gitInfo);
|
|
4801
4926
|
const jsonPath = `${outputDir}/report.json`;
|
|
4802
4927
|
const markdownPath = `${outputDir}/report.md`;
|
|
4803
|
-
await writeFile(jsonPath, generator.generateJsonReport(report));
|
|
4804
|
-
await writeFile(markdownPath, generator.generateMarkdownReport(report));
|
|
4928
|
+
await (0, import_utils3.writeFile)(jsonPath, generator.generateJsonReport(report));
|
|
4929
|
+
await (0, import_utils3.writeFile)(markdownPath, generator.generateMarkdownReport(report));
|
|
4805
4930
|
return { jsonPath, markdownPath };
|
|
4806
4931
|
}
|
|
4807
4932
|
function createReportGenerator() {
|