@frontmcp/testing 0.12.1 → 1.0.0-beta.1
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/assertions/mcp-assertions.d.ts +1 -1
- package/assertions/mcp-assertions.d.ts.map +1 -1
- package/client/mcp-test-client.builder.d.ts +1 -1
- package/client/mcp-test-client.d.ts.map +1 -1
- package/client/mcp-test-client.types.d.ts +3 -3
- package/client/mcp-test-client.types.d.ts.map +1 -1
- package/errors/index.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +239 -36
- package/esm/index.mjs +267 -112
- package/esm/matchers/index.mjs +8 -32
- package/esm/package.json +5 -5
- package/esm/perf/index.mjs +239 -36
- package/esm/setup.mjs +8 -32
- package/example-tools/index.d.ts +1 -1
- package/example-tools/index.d.ts.map +1 -1
- package/example-tools/tool-configs.d.ts +4 -10
- package/example-tools/tool-configs.d.ts.map +1 -1
- package/fixtures/index.js +232 -36
- package/index.d.ts +2 -1
- package/index.d.ts.map +1 -1
- package/index.js +264 -114
- package/matchers/index.js +8 -32
- package/package.json +5 -5
- package/perf/index.js +232 -36
- package/platform/platform-client-info.d.ts +2 -1
- package/platform/platform-client-info.d.ts.map +1 -1
- package/platform/platform-types.d.ts +11 -16
- package/platform/platform-types.d.ts.map +1 -1
- package/playwright/index.d.ts +1 -1
- package/raw-client/index.d.ts +7 -0
- package/raw-client/index.d.ts.map +1 -0
- package/server/index.d.ts +1 -1
- package/server/index.d.ts.map +1 -1
- package/server/port-registry.d.ts +24 -6
- package/server/port-registry.d.ts.map +1 -1
- package/server/test-server.d.ts +1 -1
- package/server/test-server.d.ts.map +1 -1
- package/setup.js +8 -32
- package/ui/ui-assertions.d.ts +3 -4
- package/ui/ui-assertions.d.ts.map +1 -1
- package/ui/ui-matchers.d.ts +1 -2
- package/ui/ui-matchers.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -39,14 +39,16 @@ __export(index_exports, {
|
|
|
39
39
|
DefaultMockRegistry: () => DefaultMockRegistry,
|
|
40
40
|
EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS: () => EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS,
|
|
41
41
|
EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS: () => EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS,
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
EXPECTED_GENERIC_TOOLS_LIST_META_KEYS: () => EXPECTED_GENERIC_TOOLS_LIST_META_KEYS,
|
|
43
|
+
EXPECTED_GENERIC_TOOL_CALL_META_KEYS: () => EXPECTED_GENERIC_TOOL_CALL_META_KEYS,
|
|
44
44
|
EXPECTED_OPENAI_TOOLS_LIST_META_KEYS: () => EXPECTED_OPENAI_TOOLS_LIST_META_KEYS,
|
|
45
45
|
EXPECTED_OPENAI_TOOL_CALL_META_KEYS: () => EXPECTED_OPENAI_TOOL_CALL_META_KEYS,
|
|
46
46
|
FULL_UI_TOOL_CONFIG: () => FULL_UI_TOOL_CONFIG,
|
|
47
47
|
LeakDetector: () => LeakDetector,
|
|
48
48
|
McpAssertions: () => McpAssertions,
|
|
49
|
+
McpClient: () => import_protocol.Client,
|
|
49
50
|
McpProtocolError: () => McpProtocolError,
|
|
51
|
+
McpStdioClientTransport: () => import_protocol2.StdioClientTransport,
|
|
50
52
|
McpTestClient: () => McpTestClient,
|
|
51
53
|
McpTestClientBuilder: () => McpTestClientBuilder,
|
|
52
54
|
MetricsCollector: () => MetricsCollector,
|
|
@@ -206,7 +208,7 @@ function getPlatformCapabilities(platform) {
|
|
|
206
208
|
...baseCapabilities,
|
|
207
209
|
experimental: {
|
|
208
210
|
[MCP_APPS_EXTENSION_KEY]: {
|
|
209
|
-
mimeTypes: ["text/html
|
|
211
|
+
mimeTypes: ["text/html;profile=mcp-app"]
|
|
210
212
|
}
|
|
211
213
|
}
|
|
212
214
|
};
|
|
@@ -327,7 +329,7 @@ var McpTestClientBuilder = class {
|
|
|
327
329
|
* .withCapabilities({
|
|
328
330
|
* sampling: {},
|
|
329
331
|
* experimental: {
|
|
330
|
-
* 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html
|
|
332
|
+
* 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
|
|
331
333
|
* }
|
|
332
334
|
* })
|
|
333
335
|
* .buildAndConnect();
|
|
@@ -1373,11 +1375,24 @@ var McpTestClient = class {
|
|
|
1373
1375
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
1374
1376
|
this.transport = this.createTransport();
|
|
1375
1377
|
await this.transport.connect();
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1378
|
+
const maxRetries = 3;
|
|
1379
|
+
const retryDelayMs = 500;
|
|
1380
|
+
let lastError;
|
|
1381
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1382
|
+
const initResponse = await this.initialize();
|
|
1383
|
+
if (initResponse.success && initResponse.data) {
|
|
1384
|
+
this.initResult = initResponse.data;
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
lastError = initResponse.error?.message ?? "Unknown error";
|
|
1388
|
+
if (attempt < maxRetries) {
|
|
1389
|
+
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
1390
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (!this.initResult) {
|
|
1394
|
+
throw new Error(`Failed to initialize MCP connection after ${maxRetries} attempts: ${lastError}`);
|
|
1379
1395
|
}
|
|
1380
|
-
this.initResult = initResponse.data;
|
|
1381
1396
|
this._sessionId = this.transport.getSessionId();
|
|
1382
1397
|
this._sessionInfo = {
|
|
1383
1398
|
id: this._sessionId ?? `session-${Date.now()}`,
|
|
@@ -2068,7 +2083,7 @@ var McpTestClient = class {
|
|
|
2068
2083
|
const raw = response.data ?? { content: [] };
|
|
2069
2084
|
const isError2 = !response.success || raw.isError === true;
|
|
2070
2085
|
const meta = raw._meta;
|
|
2071
|
-
const hasUI = meta?.["ui/html"] !== void 0 || meta?.["ui/component"] !== void 0
|
|
2086
|
+
const hasUI = meta?.["ui/html"] !== void 0 || meta?.["ui/component"] !== void 0;
|
|
2072
2087
|
const structuredContent = raw["structuredContent"];
|
|
2073
2088
|
return {
|
|
2074
2089
|
raw,
|
|
@@ -3922,6 +3937,7 @@ var MockCimdServer = class {
|
|
|
3922
3937
|
|
|
3923
3938
|
// libs/testing/src/server/test-server.ts
|
|
3924
3939
|
var import_child_process = require("child_process");
|
|
3940
|
+
var import_utils = require("@frontmcp/utils");
|
|
3925
3941
|
|
|
3926
3942
|
// libs/testing/src/errors/index.ts
|
|
3927
3943
|
var TestClientError = class extends Error {
|
|
@@ -4001,9 +4017,16 @@ var E2E_PORT_RANGES = {
|
|
|
4001
4017
|
"demo-e2e-agents": { start: 50270, size: 10 },
|
|
4002
4018
|
"demo-e2e-transport-recreation": { start: 50280, size: 10 },
|
|
4003
4019
|
"demo-e2e-jobs": { start: 50290, size: 10 },
|
|
4004
|
-
// Infrastructure E2E tests (50300-
|
|
4020
|
+
// Infrastructure E2E tests (50300-50409)
|
|
4005
4021
|
"demo-e2e-redis": { start: 50300, size: 10 },
|
|
4006
4022
|
"demo-e2e-serverless": { start: 50310, size: 10 },
|
|
4023
|
+
"demo-e2e-uipack": { start: 50320, size: 10 },
|
|
4024
|
+
"demo-e2e-agent-adapters": { start: 50330, size: 10 },
|
|
4025
|
+
"demo-e2e-guard": { start: 50340, size: 10 },
|
|
4026
|
+
// ESM E2E tests (50400-50449)
|
|
4027
|
+
"esm-package-server": { start: 50400, size: 10 },
|
|
4028
|
+
"esm-package-server-hot-reload": { start: 50410, size: 10 },
|
|
4029
|
+
"esm-package-server-cli": { start: 50420, size: 10 },
|
|
4007
4030
|
// Mock servers and utilities (50900-50999)
|
|
4008
4031
|
"mock-oauth": { start: 50900, size: 10 },
|
|
4009
4032
|
"mock-api": { start: 50910, size: 10 },
|
|
@@ -4075,7 +4098,7 @@ async function tryReservePort(port, project) {
|
|
|
4075
4098
|
server.once("error", () => {
|
|
4076
4099
|
resolve(false);
|
|
4077
4100
|
});
|
|
4078
|
-
server.listen(port,
|
|
4101
|
+
server.listen(port, () => {
|
|
4079
4102
|
reservedPorts.set(port, {
|
|
4080
4103
|
port,
|
|
4081
4104
|
project,
|
|
@@ -4116,7 +4139,7 @@ async function isPortAvailable(port) {
|
|
|
4116
4139
|
server.once("error", () => {
|
|
4117
4140
|
resolve(false);
|
|
4118
4141
|
});
|
|
4119
|
-
server.listen(port,
|
|
4142
|
+
server.listen(port, () => {
|
|
4120
4143
|
server.close(() => {
|
|
4121
4144
|
resolve(true);
|
|
4122
4145
|
});
|
|
@@ -4154,15 +4177,37 @@ var TestServer = class _TestServer {
|
|
|
4154
4177
|
*/
|
|
4155
4178
|
static async start(options) {
|
|
4156
4179
|
const project = options.project ?? "default";
|
|
4157
|
-
const
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4180
|
+
const maxAttempts = 3;
|
|
4181
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4182
|
+
const { port, release } = await reservePort(project, options.port);
|
|
4183
|
+
const server = new _TestServer(options, port, release);
|
|
4184
|
+
try {
|
|
4185
|
+
await server.startProcess();
|
|
4186
|
+
return server;
|
|
4187
|
+
} catch (error) {
|
|
4188
|
+
try {
|
|
4189
|
+
await server.stop();
|
|
4190
|
+
} catch (cleanupError) {
|
|
4191
|
+
if (options.debug || DEBUG_SERVER) {
|
|
4192
|
+
const msg = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
4193
|
+
console.warn(`[TestServer] Cleanup failed after startup error: ${msg}`);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
const isEADDRINUSE = error instanceof Error && (error.message.includes("EADDRINUSE") || server.getLogs().some((l) => l.includes("EADDRINUSE")));
|
|
4197
|
+
if (isEADDRINUSE && attempt < maxAttempts) {
|
|
4198
|
+
const delayMs = attempt * 500;
|
|
4199
|
+
if (options.debug || DEBUG_SERVER) {
|
|
4200
|
+
console.warn(
|
|
4201
|
+
`[TestServer] EADDRINUSE on port ${port}, retrying in ${delayMs}ms (attempt ${attempt}/${maxAttempts})`
|
|
4202
|
+
);
|
|
4203
|
+
}
|
|
4204
|
+
await sleep2(delayMs);
|
|
4205
|
+
continue;
|
|
4206
|
+
}
|
|
4207
|
+
throw error;
|
|
4208
|
+
}
|
|
4164
4209
|
}
|
|
4165
|
-
|
|
4210
|
+
throw new Error(`[TestServer] Failed to start after ${maxAttempts} attempts`);
|
|
4166
4211
|
}
|
|
4167
4212
|
/**
|
|
4168
4213
|
* Start an Nx project as test server
|
|
@@ -4173,22 +4218,44 @@ var TestServer = class _TestServer {
|
|
|
4173
4218
|
`Invalid project name: ${project}. Must contain only alphanumeric, underscore, and hyphen characters.`
|
|
4174
4219
|
);
|
|
4175
4220
|
}
|
|
4176
|
-
const
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4221
|
+
const maxAttempts = 3;
|
|
4222
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4223
|
+
const { port, release } = await reservePort(project, options.port);
|
|
4224
|
+
const serverOptions = {
|
|
4225
|
+
...options,
|
|
4226
|
+
port,
|
|
4227
|
+
project,
|
|
4228
|
+
command: `npx nx serve ${project} --port ${port}`,
|
|
4229
|
+
cwd: options.cwd ?? process.cwd()
|
|
4230
|
+
};
|
|
4231
|
+
const server = new _TestServer(serverOptions, port, release);
|
|
4232
|
+
try {
|
|
4233
|
+
await server.startProcess();
|
|
4234
|
+
return server;
|
|
4235
|
+
} catch (error) {
|
|
4236
|
+
try {
|
|
4237
|
+
await server.stop();
|
|
4238
|
+
} catch (cleanupError) {
|
|
4239
|
+
if (options.debug || DEBUG_SERVER) {
|
|
4240
|
+
const msg = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
4241
|
+
console.warn(`[TestServer] Cleanup failed after startup error: ${msg}`);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
const isEADDRINUSE = error instanceof Error && (error.message.includes("EADDRINUSE") || server.getLogs().some((l) => l.includes("EADDRINUSE")));
|
|
4245
|
+
if (isEADDRINUSE && attempt < maxAttempts) {
|
|
4246
|
+
const delayMs = attempt * 500;
|
|
4247
|
+
if (options.debug || DEBUG_SERVER) {
|
|
4248
|
+
console.warn(
|
|
4249
|
+
`[TestServer] EADDRINUSE on port ${port}, retrying in ${delayMs}ms (attempt ${attempt}/${maxAttempts})`
|
|
4250
|
+
);
|
|
4251
|
+
}
|
|
4252
|
+
await sleep2(delayMs);
|
|
4253
|
+
continue;
|
|
4254
|
+
}
|
|
4255
|
+
throw error;
|
|
4256
|
+
}
|
|
4190
4257
|
}
|
|
4191
|
-
|
|
4258
|
+
throw new Error(`[TestServer] Failed to start after ${maxAttempts} attempts`);
|
|
4192
4259
|
}
|
|
4193
4260
|
/**
|
|
4194
4261
|
* Create a test server connected to an already running server
|
|
@@ -4225,7 +4292,21 @@ var TestServer = class _TestServer {
|
|
|
4225
4292
|
}
|
|
4226
4293
|
if (this.process) {
|
|
4227
4294
|
this.log("Stopping server...");
|
|
4228
|
-
this.process.
|
|
4295
|
+
if (this.process.exitCode !== null || this.process.signalCode !== null) {
|
|
4296
|
+
this.log(`Server already exited (code: ${this.process.exitCode}, signal: ${this.process.signalCode})`);
|
|
4297
|
+
this.process = null;
|
|
4298
|
+
return;
|
|
4299
|
+
}
|
|
4300
|
+
const pid = this.process.pid;
|
|
4301
|
+
try {
|
|
4302
|
+
if (pid !== void 0) {
|
|
4303
|
+
process.kill(-pid, "SIGTERM");
|
|
4304
|
+
} else {
|
|
4305
|
+
this.process.kill("SIGTERM");
|
|
4306
|
+
}
|
|
4307
|
+
} catch {
|
|
4308
|
+
this.process.kill("SIGTERM");
|
|
4309
|
+
}
|
|
4229
4310
|
const exitPromise = new Promise((resolve) => {
|
|
4230
4311
|
if (this.process) {
|
|
4231
4312
|
this.process.once("exit", () => resolve());
|
|
@@ -4236,7 +4317,16 @@ var TestServer = class _TestServer {
|
|
|
4236
4317
|
const killTimeout = setTimeout(() => {
|
|
4237
4318
|
if (this.process) {
|
|
4238
4319
|
this.log("Force killing server after timeout...");
|
|
4239
|
-
this.process.
|
|
4320
|
+
const killPid = this.process.pid;
|
|
4321
|
+
try {
|
|
4322
|
+
if (killPid !== void 0) {
|
|
4323
|
+
process.kill(-killPid, "SIGKILL");
|
|
4324
|
+
} else {
|
|
4325
|
+
this.process.kill("SIGKILL");
|
|
4326
|
+
}
|
|
4327
|
+
} catch {
|
|
4328
|
+
this.process.kill("SIGKILL");
|
|
4329
|
+
}
|
|
4240
4330
|
}
|
|
4241
4331
|
}, 5e3);
|
|
4242
4332
|
await exitPromise;
|
|
@@ -4301,14 +4391,17 @@ var TestServer = class _TestServer {
|
|
|
4301
4391
|
...this.options.env,
|
|
4302
4392
|
PORT: String(this.options.port)
|
|
4303
4393
|
};
|
|
4394
|
+
const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
|
|
4304
4395
|
if (this.portRelease) {
|
|
4305
4396
|
await this.portRelease();
|
|
4306
4397
|
this.portRelease = null;
|
|
4398
|
+
await sleep2(300);
|
|
4307
4399
|
}
|
|
4308
4400
|
this.process = (0, import_child_process.spawn)(this.options.command, [], {
|
|
4309
4401
|
cwd: this.options.cwd,
|
|
4310
|
-
env,
|
|
4402
|
+
env: runtimeEnv,
|
|
4311
4403
|
shell: true,
|
|
4404
|
+
detached: true,
|
|
4312
4405
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4313
4406
|
});
|
|
4314
4407
|
if (this.process.pid !== void 0) {
|
|
@@ -4460,6 +4553,111 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
|
|
|
4460
4553
|
function sleep2(ms) {
|
|
4461
4554
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4462
4555
|
}
|
|
4556
|
+
function withWorkspaceProtocolFallback(env, cwd) {
|
|
4557
|
+
if (findInstalledProtocolPackageDir(cwd)) {
|
|
4558
|
+
return env;
|
|
4559
|
+
}
|
|
4560
|
+
try {
|
|
4561
|
+
const workspacePackageDir = findWorkspaceProtocolDir(cwd);
|
|
4562
|
+
if (!workspacePackageDir) {
|
|
4563
|
+
return env;
|
|
4564
|
+
}
|
|
4565
|
+
ensureWorkspaceProtocolLink(cwd, workspacePackageDir);
|
|
4566
|
+
if (findInstalledProtocolPackageDir(cwd)) {
|
|
4567
|
+
return env;
|
|
4568
|
+
}
|
|
4569
|
+
return withProtocolNodePathAlias(env, cwd, workspacePackageDir);
|
|
4570
|
+
} catch (err) {
|
|
4571
|
+
if (DEBUG_SERVER) {
|
|
4572
|
+
console.error(
|
|
4573
|
+
`[TestServer] Workspace protocol fallback failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
return env;
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
function ensureWorkspaceProtocolLink(cwd, workspacePackageDir) {
|
|
4580
|
+
const fs = require("node:fs");
|
|
4581
|
+
const path = require("node:path");
|
|
4582
|
+
const nodeModulesDir = findWorkspaceNodeModulesDir(cwd);
|
|
4583
|
+
if (!nodeModulesDir) {
|
|
4584
|
+
return;
|
|
4585
|
+
}
|
|
4586
|
+
const scopeDir = path.join(nodeModulesDir, "@frontmcp");
|
|
4587
|
+
const aliasPackageDir = path.join(scopeDir, "protocol");
|
|
4588
|
+
if (fs.existsSync(aliasPackageDir)) {
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
4591
|
+
fs.mkdirSync(scopeDir, { recursive: true });
|
|
4592
|
+
try {
|
|
4593
|
+
fs.symlinkSync(workspacePackageDir, aliasPackageDir, process.platform === "win32" ? "junction" : "dir");
|
|
4594
|
+
} catch (error) {
|
|
4595
|
+
const err = error;
|
|
4596
|
+
if (err.code !== "EEXIST") {
|
|
4597
|
+
throw error;
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
function withProtocolNodePathAlias(env, cwd, workspacePackageDir) {
|
|
4602
|
+
const fs = require("node:fs");
|
|
4603
|
+
const os = require("node:os");
|
|
4604
|
+
const path = require("node:path");
|
|
4605
|
+
const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0, import_utils.sha256Hex)(cwd).slice(0, 12));
|
|
4606
|
+
const scopeDir = path.join(aliasRoot, "@frontmcp");
|
|
4607
|
+
const aliasPackageDir = path.join(scopeDir, "protocol");
|
|
4608
|
+
fs.mkdirSync(scopeDir, { recursive: true });
|
|
4609
|
+
try {
|
|
4610
|
+
if (!fs.existsSync(aliasPackageDir)) {
|
|
4611
|
+
fs.symlinkSync(workspacePackageDir, aliasPackageDir, process.platform === "win32" ? "junction" : "dir");
|
|
4612
|
+
}
|
|
4613
|
+
} catch (error) {
|
|
4614
|
+
const err = error;
|
|
4615
|
+
if (err.code !== "EEXIST") throw error;
|
|
4616
|
+
}
|
|
4617
|
+
const existingNodePath = env["NODE_PATH"];
|
|
4618
|
+
const nodePathEntries = [aliasRoot, ...existingNodePath ? existingNodePath.split(path.delimiter) : []].filter(
|
|
4619
|
+
Boolean
|
|
4620
|
+
);
|
|
4621
|
+
return {
|
|
4622
|
+
...env,
|
|
4623
|
+
NODE_PATH: [...new Set(nodePathEntries)].join(path.delimiter)
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
function findUp(startDir, testFn) {
|
|
4627
|
+
const path = require("node:path");
|
|
4628
|
+
let currentDir = startDir;
|
|
4629
|
+
while (true) {
|
|
4630
|
+
const result = testFn(currentDir);
|
|
4631
|
+
if (result !== void 0) return result;
|
|
4632
|
+
const parentDir = path.dirname(currentDir);
|
|
4633
|
+
if (parentDir === currentDir) return void 0;
|
|
4634
|
+
currentDir = parentDir;
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
function findWorkspaceProtocolDir(startDir) {
|
|
4638
|
+
const fs = require("node:fs");
|
|
4639
|
+
const path = require("node:path");
|
|
4640
|
+
return findUp(startDir, (dir) => {
|
|
4641
|
+
const candidate = path.join(dir, "libs", "protocol");
|
|
4642
|
+
return fs.existsSync(path.join(candidate, "dist", "index.js")) ? candidate : void 0;
|
|
4643
|
+
});
|
|
4644
|
+
}
|
|
4645
|
+
function findInstalledProtocolPackageDir(startDir) {
|
|
4646
|
+
const fs = require("node:fs");
|
|
4647
|
+
const path = require("node:path");
|
|
4648
|
+
return findUp(startDir, (dir) => {
|
|
4649
|
+
const candidate = path.join(dir, "node_modules", "@frontmcp", "protocol");
|
|
4650
|
+
return fs.existsSync(path.join(candidate, "package.json")) ? candidate : void 0;
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
function findWorkspaceNodeModulesDir(startDir) {
|
|
4654
|
+
const fs = require("node:fs");
|
|
4655
|
+
const path = require("node:path");
|
|
4656
|
+
return findUp(startDir, (dir) => {
|
|
4657
|
+
const candidate = path.join(dir, "node_modules");
|
|
4658
|
+
return fs.existsSync(candidate) ? candidate : void 0;
|
|
4659
|
+
});
|
|
4660
|
+
}
|
|
4463
4661
|
|
|
4464
4662
|
// libs/testing/src/assertions/mcp-assertions.ts
|
|
4465
4663
|
var McpAssertions = {
|
|
@@ -4865,15 +5063,10 @@ var expect = import_globals.expect;
|
|
|
4865
5063
|
|
|
4866
5064
|
// libs/testing/src/platform/platform-types.ts
|
|
4867
5065
|
function getPlatformMetaNamespace(platform) {
|
|
4868
|
-
|
|
4869
|
-
case "openai":
|
|
4870
|
-
return "openai";
|
|
4871
|
-
default:
|
|
4872
|
-
return "ui";
|
|
4873
|
-
}
|
|
5066
|
+
return "ui";
|
|
4874
5067
|
}
|
|
4875
5068
|
function getPlatformMimeType(platform) {
|
|
4876
|
-
return
|
|
5069
|
+
return "text/html;profile=mcp-app";
|
|
4877
5070
|
}
|
|
4878
5071
|
function isOpenAIPlatform(platform) {
|
|
4879
5072
|
return platform === "openai";
|
|
@@ -4882,31 +5075,16 @@ function isExtAppsPlatform(platform) {
|
|
|
4882
5075
|
return platform === "ext-apps";
|
|
4883
5076
|
}
|
|
4884
5077
|
function isUiPlatform(platform) {
|
|
4885
|
-
return
|
|
5078
|
+
return true;
|
|
4886
5079
|
}
|
|
4887
5080
|
function getToolsListMetaPrefixes(platform) {
|
|
4888
|
-
|
|
4889
|
-
case "openai":
|
|
4890
|
-
return ["openai/"];
|
|
4891
|
-
default:
|
|
4892
|
-
return ["ui/"];
|
|
4893
|
-
}
|
|
5081
|
+
return ["ui/"];
|
|
4894
5082
|
}
|
|
4895
5083
|
function getToolCallMetaPrefixes(platform) {
|
|
4896
|
-
|
|
4897
|
-
case "openai":
|
|
4898
|
-
return ["openai/"];
|
|
4899
|
-
default:
|
|
4900
|
-
return ["ui/"];
|
|
4901
|
-
}
|
|
5084
|
+
return ["ui/"];
|
|
4902
5085
|
}
|
|
4903
5086
|
function getForbiddenMetaPrefixes(platform) {
|
|
4904
|
-
|
|
4905
|
-
case "openai":
|
|
4906
|
-
return ["ui/", "frontmcp/"];
|
|
4907
|
-
default:
|
|
4908
|
-
return ["openai/", "frontmcp/"];
|
|
4909
|
-
}
|
|
5087
|
+
return ["openai/", "frontmcp/"];
|
|
4910
5088
|
}
|
|
4911
5089
|
|
|
4912
5090
|
// libs/testing/src/ui/ui-matchers.ts
|
|
@@ -5013,12 +5191,12 @@ var toHaveWidgetMetadata = function(received) {
|
|
|
5013
5191
|
};
|
|
5014
5192
|
}
|
|
5015
5193
|
const hasUiHtml = Boolean(meta["ui/html"]);
|
|
5016
|
-
const hasOutputTemplate = Boolean(meta["openai/outputTemplate"]);
|
|
5017
5194
|
const hasMimeType2 = Boolean(meta["ui/mimeType"]);
|
|
5018
|
-
const
|
|
5195
|
+
const hasUiObject = Boolean(meta["ui"] && typeof meta["ui"] === "object");
|
|
5196
|
+
const pass = hasUiHtml || hasMimeType2 || hasUiObject;
|
|
5019
5197
|
return {
|
|
5020
5198
|
pass,
|
|
5021
|
-
message: () => pass ? "Expected result not to have widget metadata" : "Expected _meta to have widget metadata (ui/html,
|
|
5199
|
+
message: () => pass ? "Expected result not to have widget metadata" : "Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)"
|
|
5022
5200
|
};
|
|
5023
5201
|
};
|
|
5024
5202
|
var toHaveCssClass = function(received, className) {
|
|
@@ -5181,14 +5359,7 @@ var toHavePlatformMimeType = function(received, platform) {
|
|
|
5181
5359
|
message: () => `Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`
|
|
5182
5360
|
};
|
|
5183
5361
|
}
|
|
5184
|
-
|
|
5185
|
-
switch (platform) {
|
|
5186
|
-
case "openai":
|
|
5187
|
-
mimeTypeKey = "openai/mimeType";
|
|
5188
|
-
break;
|
|
5189
|
-
default:
|
|
5190
|
-
mimeTypeKey = "ui/mimeType";
|
|
5191
|
-
}
|
|
5362
|
+
const mimeTypeKey = "ui/mimeType";
|
|
5192
5363
|
const actualMimeType = meta[mimeTypeKey];
|
|
5193
5364
|
const pass = actualMimeType === expectedMimeType;
|
|
5194
5365
|
return {
|
|
@@ -5204,14 +5375,7 @@ var toHavePlatformHtml = function(received, platform) {
|
|
|
5204
5375
|
message: () => `Expected _meta to have platform HTML for "${platform}", but no _meta found`
|
|
5205
5376
|
};
|
|
5206
5377
|
}
|
|
5207
|
-
|
|
5208
|
-
switch (platform) {
|
|
5209
|
-
case "openai":
|
|
5210
|
-
htmlKey = "openai/html";
|
|
5211
|
-
break;
|
|
5212
|
-
default:
|
|
5213
|
-
htmlKey = "ui/html";
|
|
5214
|
-
}
|
|
5378
|
+
const htmlKey = "ui/html";
|
|
5215
5379
|
const html = meta[htmlKey];
|
|
5216
5380
|
const pass = typeof html === "string" && html.length > 0;
|
|
5217
5381
|
return {
|
|
@@ -6075,7 +6239,7 @@ var UIAssertions = {
|
|
|
6075
6239
|
},
|
|
6076
6240
|
/**
|
|
6077
6241
|
* Assert that widget metadata is present in the result.
|
|
6078
|
-
* Checks for ui/html,
|
|
6242
|
+
* Checks for ui/html, ui/mimeType, or nested ui object with resourceUri.
|
|
6079
6243
|
* @param result - The tool result wrapper
|
|
6080
6244
|
* @throws Error if widget metadata is missing
|
|
6081
6245
|
*/
|
|
@@ -6085,10 +6249,10 @@ var UIAssertions = {
|
|
|
6085
6249
|
throw new Error("Expected tool result to have _meta with widget metadata");
|
|
6086
6250
|
}
|
|
6087
6251
|
const hasUiHtml = Boolean(meta["ui/html"]);
|
|
6088
|
-
const hasOutputTemplate = Boolean(meta["openai/outputTemplate"]);
|
|
6089
6252
|
const hasMimeType2 = Boolean(meta["ui/mimeType"]);
|
|
6090
|
-
|
|
6091
|
-
|
|
6253
|
+
const hasUiObject = Boolean(meta["ui"] && typeof meta["ui"] === "object");
|
|
6254
|
+
if (!hasUiHtml && !hasMimeType2 && !hasUiObject) {
|
|
6255
|
+
throw new Error("Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)");
|
|
6092
6256
|
}
|
|
6093
6257
|
},
|
|
6094
6258
|
/**
|
|
@@ -6116,7 +6280,7 @@ var UIAssertions = {
|
|
|
6116
6280
|
// ═══════════════════════════════════════════════════════════════════
|
|
6117
6281
|
/**
|
|
6118
6282
|
* Assert tool result has correct meta keys for OpenAI platform.
|
|
6119
|
-
* Verifies
|
|
6283
|
+
* Verifies ui/* keys are present and openai/*, frontmcp/* keys are absent.
|
|
6120
6284
|
* @param result - The tool result wrapper
|
|
6121
6285
|
* @throws Error if meta keys don't match OpenAI expectations
|
|
6122
6286
|
*/
|
|
@@ -6199,14 +6363,7 @@ var UIAssertions = {
|
|
|
6199
6363
|
if (!meta) {
|
|
6200
6364
|
throw new Error(`Expected tool result to have _meta with MIME type for platform "${platform}"`);
|
|
6201
6365
|
}
|
|
6202
|
-
|
|
6203
|
-
switch (platform) {
|
|
6204
|
-
case "openai":
|
|
6205
|
-
mimeTypeKey = "openai/mimeType";
|
|
6206
|
-
break;
|
|
6207
|
-
default:
|
|
6208
|
-
mimeTypeKey = "ui/mimeType";
|
|
6209
|
-
}
|
|
6366
|
+
const mimeTypeKey = "ui/mimeType";
|
|
6210
6367
|
const actualMimeType = meta[mimeTypeKey];
|
|
6211
6368
|
if (actualMimeType !== expectedMimeType) {
|
|
6212
6369
|
throw new Error(
|
|
@@ -6226,14 +6383,7 @@ var UIAssertions = {
|
|
|
6226
6383
|
if (!meta) {
|
|
6227
6384
|
throw new Error(`Expected tool result to have _meta with platform HTML for "${platform}"`);
|
|
6228
6385
|
}
|
|
6229
|
-
|
|
6230
|
-
switch (platform) {
|
|
6231
|
-
case "openai":
|
|
6232
|
-
htmlKey = "openai/html";
|
|
6233
|
-
break;
|
|
6234
|
-
default:
|
|
6235
|
-
htmlKey = "ui/html";
|
|
6236
|
-
}
|
|
6386
|
+
const htmlKey = "ui/html";
|
|
6237
6387
|
const html = meta[htmlKey];
|
|
6238
6388
|
if (typeof html !== "string" || html.length === 0) {
|
|
6239
6389
|
throw new Error(
|
|
@@ -6373,18 +6523,12 @@ function generateFullUIToolOutput(input) {
|
|
|
6373
6523
|
timestamp: Date.now()
|
|
6374
6524
|
};
|
|
6375
6525
|
}
|
|
6376
|
-
var EXPECTED_OPENAI_TOOLS_LIST_META_KEYS = [
|
|
6377
|
-
|
|
6378
|
-
"openai/resultCanProduceWidget",
|
|
6379
|
-
"openai/widgetAccessible"
|
|
6380
|
-
];
|
|
6381
|
-
var EXPECTED_OPENAI_TOOL_CALL_META_KEYS = ["openai/html", "openai/mimeType", "openai/type"];
|
|
6526
|
+
var EXPECTED_OPENAI_TOOLS_LIST_META_KEYS = ["ui/resourceUri", "ui/mimeType", "ui/cdn", "ui/type"];
|
|
6527
|
+
var EXPECTED_OPENAI_TOOL_CALL_META_KEYS = ["ui/html", "ui/mimeType", "ui/type"];
|
|
6382
6528
|
var EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS = ["ui/resourceUri", "ui/mimeType", "ui/cdn", "ui/type"];
|
|
6383
6529
|
var EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS = ["ui/html", "ui/mimeType", "ui/type"];
|
|
6384
6530
|
var EXPECTED_GENERIC_TOOLS_LIST_META_KEYS = ["ui/resourceUri", "ui/mimeType", "ui/cdn", "ui/type"];
|
|
6385
6531
|
var EXPECTED_GENERIC_TOOL_CALL_META_KEYS = ["ui/html", "ui/mimeType", "ui/type"];
|
|
6386
|
-
var EXPECTED_FRONTMCP_TOOLS_LIST_META_KEYS = EXPECTED_GENERIC_TOOLS_LIST_META_KEYS;
|
|
6387
|
-
var EXPECTED_FRONTMCP_TOOL_CALL_META_KEYS = EXPECTED_GENERIC_TOOL_CALL_META_KEYS;
|
|
6388
6532
|
|
|
6389
6533
|
// libs/testing/src/perf/metrics-collector.ts
|
|
6390
6534
|
function isGcAvailable() {
|
|
@@ -7929,6 +8073,10 @@ async function saveReports(measurements, outputDir, baseline, gitInfo) {
|
|
|
7929
8073
|
function createReportGenerator() {
|
|
7930
8074
|
return new ReportGenerator();
|
|
7931
8075
|
}
|
|
8076
|
+
|
|
8077
|
+
// libs/testing/src/raw-client/index.ts
|
|
8078
|
+
var import_protocol = require("@frontmcp/protocol");
|
|
8079
|
+
var import_protocol2 = require("@frontmcp/protocol");
|
|
7932
8080
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7933
8081
|
0 && (module.exports = {
|
|
7934
8082
|
AssertionError,
|
|
@@ -7940,14 +8088,16 @@ function createReportGenerator() {
|
|
|
7940
8088
|
DefaultMockRegistry,
|
|
7941
8089
|
EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS,
|
|
7942
8090
|
EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS,
|
|
7943
|
-
|
|
7944
|
-
|
|
8091
|
+
EXPECTED_GENERIC_TOOLS_LIST_META_KEYS,
|
|
8092
|
+
EXPECTED_GENERIC_TOOL_CALL_META_KEYS,
|
|
7945
8093
|
EXPECTED_OPENAI_TOOLS_LIST_META_KEYS,
|
|
7946
8094
|
EXPECTED_OPENAI_TOOL_CALL_META_KEYS,
|
|
7947
8095
|
FULL_UI_TOOL_CONFIG,
|
|
7948
8096
|
LeakDetector,
|
|
7949
8097
|
McpAssertions,
|
|
8098
|
+
McpClient,
|
|
7950
8099
|
McpProtocolError,
|
|
8100
|
+
McpStdioClientTransport,
|
|
7951
8101
|
McpTestClient,
|
|
7952
8102
|
McpTestClientBuilder,
|
|
7953
8103
|
MetricsCollector,
|
package/matchers/index.js
CHANGED
|
@@ -26,23 +26,13 @@ module.exports = __toCommonJS(matchers_exports);
|
|
|
26
26
|
|
|
27
27
|
// libs/testing/src/platform/platform-types.ts
|
|
28
28
|
function getPlatformMimeType(platform) {
|
|
29
|
-
return
|
|
29
|
+
return "text/html;profile=mcp-app";
|
|
30
30
|
}
|
|
31
31
|
function getToolCallMetaPrefixes(platform) {
|
|
32
|
-
|
|
33
|
-
case "openai":
|
|
34
|
-
return ["openai/"];
|
|
35
|
-
default:
|
|
36
|
-
return ["ui/"];
|
|
37
|
-
}
|
|
32
|
+
return ["ui/"];
|
|
38
33
|
}
|
|
39
34
|
function getForbiddenMetaPrefixes(platform) {
|
|
40
|
-
|
|
41
|
-
case "openai":
|
|
42
|
-
return ["ui/", "frontmcp/"];
|
|
43
|
-
default:
|
|
44
|
-
return ["openai/", "frontmcp/"];
|
|
45
|
-
}
|
|
35
|
+
return ["openai/", "frontmcp/"];
|
|
46
36
|
}
|
|
47
37
|
|
|
48
38
|
// libs/testing/src/ui/ui-matchers.ts
|
|
@@ -149,12 +139,12 @@ var toHaveWidgetMetadata = function(received) {
|
|
|
149
139
|
};
|
|
150
140
|
}
|
|
151
141
|
const hasUiHtml = Boolean(meta["ui/html"]);
|
|
152
|
-
const hasOutputTemplate = Boolean(meta["openai/outputTemplate"]);
|
|
153
142
|
const hasMimeType = Boolean(meta["ui/mimeType"]);
|
|
154
|
-
const
|
|
143
|
+
const hasUiObject = Boolean(meta["ui"] && typeof meta["ui"] === "object");
|
|
144
|
+
const pass = hasUiHtml || hasMimeType || hasUiObject;
|
|
155
145
|
return {
|
|
156
146
|
pass,
|
|
157
|
-
message: () => pass ? "Expected result not to have widget metadata" : "Expected _meta to have widget metadata (ui/html,
|
|
147
|
+
message: () => pass ? "Expected result not to have widget metadata" : "Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)"
|
|
158
148
|
};
|
|
159
149
|
};
|
|
160
150
|
var toHaveCssClass = function(received, className) {
|
|
@@ -317,14 +307,7 @@ var toHavePlatformMimeType = function(received, platform) {
|
|
|
317
307
|
message: () => `Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`
|
|
318
308
|
};
|
|
319
309
|
}
|
|
320
|
-
|
|
321
|
-
switch (platform) {
|
|
322
|
-
case "openai":
|
|
323
|
-
mimeTypeKey = "openai/mimeType";
|
|
324
|
-
break;
|
|
325
|
-
default:
|
|
326
|
-
mimeTypeKey = "ui/mimeType";
|
|
327
|
-
}
|
|
310
|
+
const mimeTypeKey = "ui/mimeType";
|
|
328
311
|
const actualMimeType = meta[mimeTypeKey];
|
|
329
312
|
const pass = actualMimeType === expectedMimeType;
|
|
330
313
|
return {
|
|
@@ -340,14 +323,7 @@ var toHavePlatformHtml = function(received, platform) {
|
|
|
340
323
|
message: () => `Expected _meta to have platform HTML for "${platform}", but no _meta found`
|
|
341
324
|
};
|
|
342
325
|
}
|
|
343
|
-
|
|
344
|
-
switch (platform) {
|
|
345
|
-
case "openai":
|
|
346
|
-
htmlKey = "openai/html";
|
|
347
|
-
break;
|
|
348
|
-
default:
|
|
349
|
-
htmlKey = "ui/html";
|
|
350
|
-
}
|
|
326
|
+
const htmlKey = "ui/html";
|
|
351
327
|
const html = meta[htmlKey];
|
|
352
328
|
const pass = typeof html === "string" && html.length > 0;
|
|
353
329
|
return {
|