@frontmcp/testing 1.7.2 → 1.8.0

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/index.js CHANGED
@@ -124,250 +124,339 @@ __export(index_exports, {
124
124
  });
125
125
  module.exports = __toCommonJS(index_exports);
126
126
 
127
- // libs/testing/src/platform/platform-client-info.ts
128
- var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
129
- function getPlatformClientInfo(platform) {
130
- switch (platform) {
131
- case "openai":
132
- return {
133
- name: "ChatGPT",
134
- version: "1.0"
135
- };
136
- case "ext-apps":
137
- return {
138
- name: "mcp-ext-apps",
139
- version: "1.0"
140
- };
141
- case "claude":
142
- return {
143
- name: "claude-desktop",
144
- version: "1.0"
145
- };
146
- case "cursor":
147
- return {
148
- name: "cursor",
149
- version: "1.0"
150
- };
151
- case "continue":
152
- return {
153
- name: "continue",
154
- version: "1.0"
155
- };
156
- case "cody":
157
- return {
158
- name: "cody",
159
- version: "1.0"
160
- };
161
- case "gemini":
162
- return {
163
- name: "gemini",
164
- version: "1.0"
165
- };
166
- case "generic-mcp":
167
- return {
168
- name: "generic-mcp-client",
169
- version: "1.0"
170
- };
171
- case "unknown":
172
- default:
173
- return {
174
- name: "mcp-test-client",
175
- version: "1.0"
176
- };
177
- }
178
- }
179
- function buildUserAgent(clientInfo) {
180
- return `${clientInfo.name}/${clientInfo.version}`;
181
- }
182
- function getPlatformUserAgent(platform) {
183
- return buildUserAgent(getPlatformClientInfo(platform));
184
- }
185
- var PLATFORM_DETECTION_PATTERNS = {
186
- openai: /chatgpt/i,
187
- "ext-apps": /mcp-ext-apps/i,
188
- // Note: Actual detection uses capabilities
189
- claude: /claude|claude-desktop/i,
190
- cursor: /cursor/i,
191
- continue: /continue/i,
192
- cody: /cody/i,
193
- gemini: /gemini/i,
194
- "generic-mcp": /generic-mcp/i,
195
- unknown: /.*/
196
- // Matches anything (fallback)
197
- };
198
- function getPlatformCapabilities(platform) {
199
- const baseCapabilities = {
200
- sampling: {},
201
- // Include elicitation.form by default for testing elicitation workflows
202
- // Note: MCP SDK expects form to be an object, not boolean
203
- elicitation: {
204
- form: {}
205
- }
206
- };
207
- if (platform === "ext-apps") {
127
+ // libs/testing/src/interceptor/mock-registry.ts
128
+ var DefaultMockRegistry = class {
129
+ mocks = [];
130
+ add(mock) {
131
+ const entry = {
132
+ definition: mock,
133
+ callCount: 0,
134
+ calls: [],
135
+ remainingUses: mock.times ?? Infinity
136
+ };
137
+ this.mocks.push(entry);
208
138
  return {
209
- ...baseCapabilities,
210
- experimental: {
211
- [MCP_APPS_EXTENSION_KEY]: {
212
- mimeTypes: ["text/html;profile=mcp-app"]
139
+ remove: () => {
140
+ const index = this.mocks.indexOf(entry);
141
+ if (index !== -1) {
142
+ this.mocks.splice(index, 1);
213
143
  }
214
- }
144
+ },
145
+ callCount: () => entry.callCount,
146
+ calls: () => [...entry.calls]
215
147
  };
216
148
  }
217
- return baseCapabilities;
218
- }
219
-
220
- // libs/testing/src/client/mcp-test-client.builder.ts
221
- var McpTestClientBuilder = class {
222
- config;
223
- constructor(config) {
224
- this.config = { ...config };
149
+ clear() {
150
+ this.mocks = [];
151
+ }
152
+ getAll() {
153
+ return this.mocks.map((e) => e.definition);
154
+ }
155
+ match(request) {
156
+ for (const entry of this.mocks) {
157
+ if (entry.remainingUses <= 0) continue;
158
+ const { definition } = entry;
159
+ if (definition.method !== request.method) continue;
160
+ if (definition.params !== void 0) {
161
+ const params = request.params ?? {};
162
+ if (typeof definition.params === "function") {
163
+ if (!definition.params(params)) continue;
164
+ } else {
165
+ if (!this.paramsMatch(definition.params, params)) continue;
166
+ }
167
+ }
168
+ entry.callCount++;
169
+ entry.calls.push(request);
170
+ entry.remainingUses--;
171
+ return definition;
172
+ }
173
+ return void 0;
225
174
  }
226
175
  /**
227
- * Set the authentication configuration
176
+ * Check if request params match the mock params definition
228
177
  */
229
- withAuth(auth) {
230
- this.config.auth = { ...this.config.auth, ...auth };
231
- return this;
178
+ paramsMatch(expected, actual) {
179
+ for (const [key, value] of Object.entries(expected)) {
180
+ if (!(key in actual)) return false;
181
+ const actualValue = actual[key];
182
+ if (Array.isArray(value)) {
183
+ if (!Array.isArray(actualValue)) return false;
184
+ if (value.length !== actualValue.length) return false;
185
+ for (let i = 0; i < value.length; i++) {
186
+ const expectedItem = value[i];
187
+ const actualItem = actualValue[i];
188
+ if (typeof expectedItem === "object" && expectedItem !== null) {
189
+ if (typeof actualItem !== "object" || actualItem === null) return false;
190
+ if (!this.paramsMatch(expectedItem, actualItem)) {
191
+ return false;
192
+ }
193
+ } else if (actualItem !== expectedItem) {
194
+ return false;
195
+ }
196
+ }
197
+ } else if (typeof value === "object" && value !== null) {
198
+ if (typeof actualValue !== "object" || actualValue === null) return false;
199
+ if (!this.paramsMatch(value, actualValue)) {
200
+ return false;
201
+ }
202
+ } else if (actualValue !== value) {
203
+ return false;
204
+ }
205
+ }
206
+ return true;
232
207
  }
208
+ };
209
+ var mockResponse = {
233
210
  /**
234
- * Set the bearer token for authentication
211
+ * Create a successful JSON-RPC response
235
212
  */
236
- withToken(token) {
237
- this.config.auth = { ...this.config.auth, token };
238
- return this;
239
- }
213
+ success(result, id = 1) {
214
+ return {
215
+ jsonrpc: "2.0",
216
+ id,
217
+ result
218
+ };
219
+ },
240
220
  /**
241
- * Add custom headers to all requests
221
+ * Create an error JSON-RPC response
242
222
  */
243
- withHeaders(headers) {
244
- this.config.auth = {
245
- ...this.config.auth,
246
- headers: { ...this.config.auth?.headers, ...headers }
223
+ error(code, message, data, id = 1) {
224
+ return {
225
+ jsonrpc: "2.0",
226
+ id,
227
+ error: { code, message, data }
247
228
  };
248
- return this;
249
- }
229
+ },
250
230
  /**
251
- * Set the transport type
231
+ * Create a tool result response
252
232
  */
253
- withTransport(transport) {
254
- this.config.transport = transport;
255
- return this;
256
- }
233
+ toolResult(content, id = 1) {
234
+ return {
235
+ jsonrpc: "2.0",
236
+ id,
237
+ result: { content }
238
+ };
239
+ },
257
240
  /**
258
- * Set the request timeout in milliseconds
241
+ * Create a tools/list response
259
242
  */
260
- withTimeout(timeoutMs) {
261
- this.config.timeout = timeoutMs;
262
- return this;
263
- }
243
+ toolsList(tools, id = 1) {
244
+ return {
245
+ jsonrpc: "2.0",
246
+ id,
247
+ result: { tools }
248
+ };
249
+ },
264
250
  /**
265
- * Enable debug logging
251
+ * Create a resources/list response
266
252
  */
267
- withDebug(enabled = true) {
268
- this.config.debug = enabled;
269
- return this;
270
- }
253
+ resourcesList(resources, id = 1) {
254
+ return {
255
+ jsonrpc: "2.0",
256
+ id,
257
+ result: { resources }
258
+ };
259
+ },
271
260
  /**
272
- * Enable public mode - skip authentication entirely.
273
- * When true, no Authorization header is sent and anonymous token is not requested.
274
- * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
261
+ * Create a resources/read response
275
262
  */
276
- withPublicMode(enabled = true) {
277
- this.config.publicMode = enabled;
278
- return this;
279
- }
263
+ resourceRead(contents, id = 1) {
264
+ return {
265
+ jsonrpc: "2.0",
266
+ id,
267
+ result: { contents }
268
+ };
269
+ },
280
270
  /**
281
- * Set the MCP protocol version to request
271
+ * Common MCP errors
282
272
  */
283
- withProtocolVersion(version) {
284
- this.config.protocolVersion = version;
285
- return this;
273
+ errors: {
274
+ methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
275
+ invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
276
+ internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
277
+ resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
278
+ toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
279
+ unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
280
+ forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
281
+ }
282
+ };
283
+
284
+ // libs/testing/src/interceptor/interceptor-chain.ts
285
+ var DefaultInterceptorChain = class {
286
+ request = [];
287
+ response = [];
288
+ mocks;
289
+ constructor() {
290
+ this.mocks = new DefaultMockRegistry();
286
291
  }
287
292
  /**
288
- * Set the client info sent during initialization
293
+ * Add a request interceptor
289
294
  */
290
- withClientInfo(info) {
291
- this.config.clientInfo = info;
292
- return this;
295
+ addRequestInterceptor(interceptor) {
296
+ this.request.push(interceptor);
297
+ return () => {
298
+ const index = this.request.indexOf(interceptor);
299
+ if (index !== -1) {
300
+ this.request.splice(index, 1);
301
+ }
302
+ };
293
303
  }
294
304
  /**
295
- * Set the platform type for testing platform-specific meta keys.
296
- * Automatically configures clientInfo and capabilities for platform detection.
297
- *
298
- * Platform-specific behavior:
299
- * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
300
- * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
301
- * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
302
- * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
303
- * - Other platforms follow similar patterns
304
- *
305
- * @example
306
- * ```typescript
307
- * const client = await McpTestClient.create({ baseUrl })
308
- * .withPlatform('openai')
309
- * .buildAndConnect();
310
- *
311
- * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
312
- * const extAppsClient = await McpTestClient.create({ baseUrl })
313
- * .withPlatform('ext-apps')
314
- * .buildAndConnect();
315
- * ```
305
+ * Add a response interceptor
316
306
  */
317
- withPlatform(platform) {
318
- this.config.platform = platform;
319
- this.config.clientInfo = getPlatformClientInfo(platform);
320
- this.config.capabilities = getPlatformCapabilities(platform);
321
- return this;
307
+ addResponseInterceptor(interceptor) {
308
+ this.response.push(interceptor);
309
+ return () => {
310
+ const index = this.response.indexOf(interceptor);
311
+ if (index !== -1) {
312
+ this.response.splice(index, 1);
313
+ }
314
+ };
322
315
  }
323
316
  /**
324
- * Set custom client capabilities for MCP initialization.
325
- * Use this for fine-grained control over capabilities sent during initialization.
326
- *
327
- * @example
328
- * ```typescript
329
- * const client = await McpTestClient.create({ baseUrl })
330
- * .withCapabilities({
331
- * sampling: {},
332
- * experimental: {
333
- * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
334
- * }
335
- * })
336
- * .buildAndConnect();
337
- * ```
317
+ * Process a request through the interceptor chain
318
+ * Returns either:
319
+ * - { type: 'continue', request } - continue with (possibly modified) request
320
+ * - { type: 'mock', response } - return mock response immediately
321
+ * - { type: 'error', error } - throw error
338
322
  */
339
- withCapabilities(capabilities) {
340
- this.config.capabilities = capabilities;
341
- return this;
323
+ async processRequest(request, meta) {
324
+ let currentRequest = request;
325
+ const mockDef = this.mocks.match(request);
326
+ if (mockDef) {
327
+ if (mockDef.delay && mockDef.delay > 0) {
328
+ await sleep(mockDef.delay);
329
+ }
330
+ let mockResponse2;
331
+ if (typeof mockDef.response === "function") {
332
+ mockResponse2 = await mockDef.response(request);
333
+ } else {
334
+ mockResponse2 = mockDef.response;
335
+ }
336
+ return {
337
+ type: "mock",
338
+ response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
339
+ };
340
+ }
341
+ for (const interceptor of this.request) {
342
+ const ctx = {
343
+ request: currentRequest,
344
+ meta
345
+ };
346
+ const result = await interceptor(ctx);
347
+ switch (result.action) {
348
+ case "passthrough":
349
+ break;
350
+ case "modify":
351
+ currentRequest = result.request;
352
+ break;
353
+ case "mock":
354
+ return {
355
+ type: "mock",
356
+ response: { ...result.response, id: request.id ?? result.response.id }
357
+ };
358
+ case "error":
359
+ return { type: "error", error: result.error };
360
+ }
361
+ }
362
+ return { type: "continue", request: currentRequest };
342
363
  }
343
364
  /**
344
- * Set query parameters to append to the connection URL.
345
- * Useful for testing mode switches like `?mode=skills_only`.
346
- *
347
- * @example
348
- * ```typescript
349
- * const client = await McpTestClient.create({ baseUrl })
350
- * .withQueryParams({ mode: 'skills_only' })
351
- * .buildAndConnect();
352
- * ```
365
+ * Process a response through the interceptor chain
353
366
  */
354
- withQueryParams(params) {
355
- this.config.queryParams = { ...this.config.queryParams, ...params };
356
- return this;
367
+ async processResponse(request, response, durationMs) {
368
+ let currentResponse = response;
369
+ for (const interceptor of this.response) {
370
+ const ctx = {
371
+ request,
372
+ response: currentResponse,
373
+ durationMs
374
+ };
375
+ const result = await interceptor(ctx);
376
+ switch (result.action) {
377
+ case "passthrough":
378
+ break;
379
+ case "modify":
380
+ currentResponse = result.response;
381
+ break;
382
+ }
383
+ }
384
+ return currentResponse;
357
385
  }
358
386
  /**
359
- * Build the McpTestClient instance (does not connect)
387
+ * Clear all interceptors and mocks
360
388
  */
361
- build() {
362
- return new McpTestClient(this.config);
389
+ clear() {
390
+ this.request = [];
391
+ this.response = [];
392
+ this.mocks.clear();
363
393
  }
394
+ };
395
+ function sleep(ms) {
396
+ return new Promise((resolve) => setTimeout(resolve, ms));
397
+ }
398
+ var interceptors = {
364
399
  /**
365
- * Build the McpTestClient and connect to the server
400
+ * Create an interceptor that logs all requests
366
401
  */
367
- async buildAndConnect() {
368
- const client = this.build();
369
- await client.connect();
370
- return client;
402
+ logger(logFn = console.log) {
403
+ return (ctx) => {
404
+ logFn(`[MCP Request] ${ctx.request.method}`, ctx.request.params);
405
+ return { action: "passthrough" };
406
+ };
407
+ },
408
+ /**
409
+ * Create an interceptor that adds latency to all requests
410
+ */
411
+ delay(ms) {
412
+ return async () => {
413
+ await sleep(ms);
414
+ return { action: "passthrough" };
415
+ };
416
+ },
417
+ /**
418
+ * Create an interceptor that fails requests matching a condition
419
+ */
420
+ failWhen(condition, error) {
421
+ return (ctx) => {
422
+ if (condition(ctx)) {
423
+ const err = typeof error === "string" ? new Error(error) : error;
424
+ return { action: "error", error: err };
425
+ }
426
+ return { action: "passthrough" };
427
+ };
428
+ },
429
+ /**
430
+ * Create an interceptor that modifies specific methods
431
+ */
432
+ modifyMethod(method, modifier) {
433
+ return (ctx) => {
434
+ if (ctx.request.method === method) {
435
+ return { action: "modify", request: modifier(ctx.request) };
436
+ }
437
+ return { action: "passthrough" };
438
+ };
439
+ },
440
+ /**
441
+ * Create a response interceptor that logs responses
442
+ */
443
+ responseLogger(logFn = console.log) {
444
+ return (ctx) => {
445
+ const status = ctx.response.error ? "ERROR" : "OK";
446
+ logFn(`[MCP Response] ${ctx.request.method} ${status} (${ctx.durationMs}ms)`, ctx.response);
447
+ return { action: "passthrough" };
448
+ };
449
+ },
450
+ /**
451
+ * Create a response interceptor that modifies specific responses
452
+ */
453
+ modifyResponse(method, modifier) {
454
+ return (ctx) => {
455
+ if (ctx.request.method === method) {
456
+ return { action: "modify", response: modifier(ctx.response) };
457
+ }
458
+ return { action: "passthrough" };
459
+ };
371
460
  }
372
461
  };
373
462
 
@@ -388,6 +477,7 @@ var StreamableHttpTransport = class {
388
477
  this.config = {
389
478
  baseUrl: config.baseUrl.replace(/\/$/, ""),
390
479
  // Remove trailing slash
480
+ entryPath: config.entryPath,
391
481
  timeout: config.timeout ?? DEFAULT_TIMEOUT,
392
482
  auth: config.auth ?? {},
393
483
  publicMode: config.publicMode ?? false,
@@ -480,7 +570,7 @@ var StreamableHttpTransport = class {
480
570
  }
481
571
  const headers = this.buildHeaders();
482
572
  this.lastRequestHeaders = headers;
483
- const url = `${this.config.baseUrl}/`;
573
+ const url = this.mcpUrl();
484
574
  this.log(`POST ${url}`, message);
485
575
  const controller = new AbortController();
486
576
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -550,7 +640,7 @@ var StreamableHttpTransport = class {
550
640
  this.ensureConnected();
551
641
  const headers = this.buildHeaders();
552
642
  this.lastRequestHeaders = headers;
553
- const url = `${this.config.baseUrl}/`;
643
+ const url = this.mcpUrl();
554
644
  this.log(`POST ${url} (notification)`, message);
555
645
  const controller = new AbortController();
556
646
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -581,7 +671,7 @@ var StreamableHttpTransport = class {
581
671
  this.ensureConnected();
582
672
  const headers = this.buildHeaders();
583
673
  this.lastRequestHeaders = headers;
584
- const url = `${this.config.baseUrl}/`;
674
+ const url = this.mcpUrl();
585
675
  this.log(`POST ${url} (raw)`, data);
586
676
  const controller = new AbortController();
587
677
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -864,7 +954,7 @@ var StreamableHttpTransport = class {
864
954
  */
865
955
  async sendElicitationResponse(requestId, response) {
866
956
  const headers = this.buildHeaders();
867
- const url = `${this.config.baseUrl}/`;
957
+ const url = this.mcpUrl();
868
958
  const rpcResponse = {
869
959
  jsonrpc: "2.0",
870
960
  id: requestId,
@@ -884,6 +974,18 @@ var StreamableHttpTransport = class {
884
974
  this.log("Failed to send elicitation response:", error);
885
975
  }
886
976
  }
977
+ /**
978
+ * The MCP endpoint URL. `entryPath` is where the server mounts MCP; the
979
+ * trailing slash is kept because both hosts normalize it and the original
980
+ * behaviour (`${baseUrl}/`) relied on it (issue #543).
981
+ */
982
+ mcpUrl() {
983
+ const url = new URL(this.config.baseUrl);
984
+ const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
985
+ const basePath = url.pathname.replace(/\/+$/, "");
986
+ url.pathname = entry ? `${basePath}/${entry}` : `${basePath}/`;
987
+ return url.toString();
988
+ }
887
989
  buildHeaders() {
888
990
  const headers = {
889
991
  "Content-Type": "application/json",
@@ -971,339 +1073,250 @@ var StreamableHttpTransport = class {
971
1073
  }
972
1074
  };
973
1075
 
974
- // libs/testing/src/interceptor/mock-registry.ts
975
- var DefaultMockRegistry = class {
976
- mocks = [];
977
- add(mock) {
978
- const entry = {
979
- definition: mock,
980
- callCount: 0,
981
- calls: [],
982
- remainingUses: mock.times ?? Infinity
983
- };
984
- this.mocks.push(entry);
985
- return {
986
- remove: () => {
987
- const index = this.mocks.indexOf(entry);
988
- if (index !== -1) {
989
- this.mocks.splice(index, 1);
990
- }
991
- },
992
- callCount: () => entry.callCount,
993
- calls: () => [...entry.calls]
994
- };
995
- }
996
- clear() {
997
- this.mocks = [];
998
- }
999
- getAll() {
1000
- return this.mocks.map((e) => e.definition);
1076
+ // libs/testing/src/platform/platform-client-info.ts
1077
+ var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
1078
+ function getPlatformClientInfo(platform) {
1079
+ switch (platform) {
1080
+ case "openai":
1081
+ return {
1082
+ name: "ChatGPT",
1083
+ version: "1.0"
1084
+ };
1085
+ case "ext-apps":
1086
+ return {
1087
+ name: "mcp-ext-apps",
1088
+ version: "1.0"
1089
+ };
1090
+ case "claude":
1091
+ return {
1092
+ name: "claude-desktop",
1093
+ version: "1.0"
1094
+ };
1095
+ case "cursor":
1096
+ return {
1097
+ name: "cursor",
1098
+ version: "1.0"
1099
+ };
1100
+ case "continue":
1101
+ return {
1102
+ name: "continue",
1103
+ version: "1.0"
1104
+ };
1105
+ case "cody":
1106
+ return {
1107
+ name: "cody",
1108
+ version: "1.0"
1109
+ };
1110
+ case "gemini":
1111
+ return {
1112
+ name: "gemini",
1113
+ version: "1.0"
1114
+ };
1115
+ case "generic-mcp":
1116
+ return {
1117
+ name: "generic-mcp-client",
1118
+ version: "1.0"
1119
+ };
1120
+ case "unknown":
1121
+ default:
1122
+ return {
1123
+ name: "mcp-test-client",
1124
+ version: "1.0"
1125
+ };
1001
1126
  }
1002
- match(request) {
1003
- for (const entry of this.mocks) {
1004
- if (entry.remainingUses <= 0) continue;
1005
- const { definition } = entry;
1006
- if (definition.method !== request.method) continue;
1007
- if (definition.params !== void 0) {
1008
- const params = request.params ?? {};
1009
- if (typeof definition.params === "function") {
1010
- if (!definition.params(params)) continue;
1011
- } else {
1012
- if (!this.paramsMatch(definition.params, params)) continue;
1013
- }
1014
- }
1015
- entry.callCount++;
1016
- entry.calls.push(request);
1017
- entry.remainingUses--;
1018
- return definition;
1127
+ }
1128
+ function buildUserAgent(clientInfo) {
1129
+ return `${clientInfo.name}/${clientInfo.version}`;
1130
+ }
1131
+ function getPlatformUserAgent(platform) {
1132
+ return buildUserAgent(getPlatformClientInfo(platform));
1133
+ }
1134
+ var PLATFORM_DETECTION_PATTERNS = {
1135
+ openai: /chatgpt/i,
1136
+ "ext-apps": /mcp-ext-apps/i,
1137
+ // Note: Actual detection uses capabilities
1138
+ claude: /claude|claude-desktop/i,
1139
+ cursor: /cursor/i,
1140
+ continue: /continue/i,
1141
+ cody: /cody/i,
1142
+ gemini: /gemini/i,
1143
+ "generic-mcp": /generic-mcp/i,
1144
+ unknown: /.*/
1145
+ // Matches anything (fallback)
1146
+ };
1147
+ function getPlatformCapabilities(platform) {
1148
+ const baseCapabilities = {
1149
+ sampling: {},
1150
+ // Include elicitation.form by default for testing elicitation workflows
1151
+ // Note: MCP SDK expects form to be an object, not boolean
1152
+ elicitation: {
1153
+ form: {}
1019
1154
  }
1020
- return void 0;
1021
- }
1022
- /**
1023
- * Check if request params match the mock params definition
1024
- */
1025
- paramsMatch(expected, actual) {
1026
- for (const [key, value] of Object.entries(expected)) {
1027
- if (!(key in actual)) return false;
1028
- const actualValue = actual[key];
1029
- if (Array.isArray(value)) {
1030
- if (!Array.isArray(actualValue)) return false;
1031
- if (value.length !== actualValue.length) return false;
1032
- for (let i = 0; i < value.length; i++) {
1033
- const expectedItem = value[i];
1034
- const actualItem = actualValue[i];
1035
- if (typeof expectedItem === "object" && expectedItem !== null) {
1036
- if (typeof actualItem !== "object" || actualItem === null) return false;
1037
- if (!this.paramsMatch(expectedItem, actualItem)) {
1038
- return false;
1039
- }
1040
- } else if (actualItem !== expectedItem) {
1041
- return false;
1042
- }
1043
- }
1044
- } else if (typeof value === "object" && value !== null) {
1045
- if (typeof actualValue !== "object" || actualValue === null) return false;
1046
- if (!this.paramsMatch(value, actualValue)) {
1047
- return false;
1155
+ };
1156
+ if (platform === "ext-apps") {
1157
+ return {
1158
+ ...baseCapabilities,
1159
+ experimental: {
1160
+ [MCP_APPS_EXTENSION_KEY]: {
1161
+ mimeTypes: ["text/html;profile=mcp-app"]
1048
1162
  }
1049
- } else if (actualValue !== value) {
1050
- return false;
1051
1163
  }
1052
- }
1053
- return true;
1054
- }
1055
- };
1056
- var mockResponse = {
1057
- /**
1058
- * Create a successful JSON-RPC response
1059
- */
1060
- success(result, id = 1) {
1061
- return {
1062
- jsonrpc: "2.0",
1063
- id,
1064
- result
1065
- };
1066
- },
1067
- /**
1068
- * Create an error JSON-RPC response
1069
- */
1070
- error(code, message, data, id = 1) {
1071
- return {
1072
- jsonrpc: "2.0",
1073
- id,
1074
- error: { code, message, data }
1075
1164
  };
1076
- },
1077
- /**
1078
- * Create a tool result response
1079
- */
1080
- toolResult(content, id = 1) {
1081
- return {
1082
- jsonrpc: "2.0",
1083
- id,
1084
- result: { content }
1085
- };
1086
- },
1087
- /**
1088
- * Create a tools/list response
1089
- */
1090
- toolsList(tools, id = 1) {
1091
- return {
1092
- jsonrpc: "2.0",
1093
- id,
1094
- result: { tools }
1095
- };
1096
- },
1097
- /**
1098
- * Create a resources/list response
1099
- */
1100
- resourcesList(resources, id = 1) {
1101
- return {
1102
- jsonrpc: "2.0",
1103
- id,
1104
- result: { resources }
1105
- };
1106
- },
1107
- /**
1108
- * Create a resources/read response
1109
- */
1110
- resourceRead(contents, id = 1) {
1111
- return {
1112
- jsonrpc: "2.0",
1113
- id,
1114
- result: { contents }
1115
- };
1116
- },
1117
- /**
1118
- * Common MCP errors
1119
- */
1120
- errors: {
1121
- methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
1122
- invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
1123
- internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
1124
- resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
1125
- toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
1126
- unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
1127
- forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
1128
1165
  }
1129
- };
1166
+ return baseCapabilities;
1167
+ }
1130
1168
 
1131
- // libs/testing/src/interceptor/interceptor-chain.ts
1132
- var DefaultInterceptorChain = class {
1133
- request = [];
1134
- response = [];
1135
- mocks;
1136
- constructor() {
1137
- this.mocks = new DefaultMockRegistry();
1138
- }
1139
- /**
1140
- * Add a request interceptor
1141
- */
1142
- addRequestInterceptor(interceptor) {
1143
- this.request.push(interceptor);
1144
- return () => {
1145
- const index = this.request.indexOf(interceptor);
1146
- if (index !== -1) {
1147
- this.request.splice(index, 1);
1148
- }
1149
- };
1150
- }
1151
- /**
1152
- * Add a response interceptor
1153
- */
1154
- addResponseInterceptor(interceptor) {
1155
- this.response.push(interceptor);
1156
- return () => {
1157
- const index = this.response.indexOf(interceptor);
1158
- if (index !== -1) {
1159
- this.response.splice(index, 1);
1160
- }
1161
- };
1169
+ // libs/testing/src/client/mcp-test-client.builder.ts
1170
+ var McpTestClientBuilder = class {
1171
+ config;
1172
+ constructor(config) {
1173
+ this.config = { ...config };
1162
1174
  }
1163
1175
  /**
1164
- * Process a request through the interceptor chain
1165
- * Returns either:
1166
- * - { type: 'continue', request } - continue with (possibly modified) request
1167
- * - { type: 'mock', response } - return mock response immediately
1168
- * - { type: 'error', error } - throw error
1176
+ * Set the authentication configuration
1169
1177
  */
1170
- async processRequest(request, meta) {
1171
- let currentRequest = request;
1172
- const mockDef = this.mocks.match(request);
1173
- if (mockDef) {
1174
- if (mockDef.delay && mockDef.delay > 0) {
1175
- await sleep(mockDef.delay);
1176
- }
1177
- let mockResponse2;
1178
- if (typeof mockDef.response === "function") {
1179
- mockResponse2 = await mockDef.response(request);
1180
- } else {
1181
- mockResponse2 = mockDef.response;
1182
- }
1183
- return {
1184
- type: "mock",
1185
- response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
1186
- };
1187
- }
1188
- for (const interceptor of this.request) {
1189
- const ctx = {
1190
- request: currentRequest,
1191
- meta
1192
- };
1193
- const result = await interceptor(ctx);
1194
- switch (result.action) {
1195
- case "passthrough":
1196
- break;
1197
- case "modify":
1198
- currentRequest = result.request;
1199
- break;
1200
- case "mock":
1201
- return {
1202
- type: "mock",
1203
- response: { ...result.response, id: request.id ?? result.response.id }
1204
- };
1205
- case "error":
1206
- return { type: "error", error: result.error };
1207
- }
1208
- }
1209
- return { type: "continue", request: currentRequest };
1178
+ withAuth(auth) {
1179
+ this.config.auth = { ...this.config.auth, ...auth };
1180
+ return this;
1210
1181
  }
1211
1182
  /**
1212
- * Process a response through the interceptor chain
1183
+ * Set the bearer token for authentication
1213
1184
  */
1214
- async processResponse(request, response, durationMs) {
1215
- let currentResponse = response;
1216
- for (const interceptor of this.response) {
1217
- const ctx = {
1218
- request,
1219
- response: currentResponse,
1220
- durationMs
1221
- };
1222
- const result = await interceptor(ctx);
1223
- switch (result.action) {
1224
- case "passthrough":
1225
- break;
1226
- case "modify":
1227
- currentResponse = result.response;
1228
- break;
1229
- }
1230
- }
1231
- return currentResponse;
1185
+ withToken(token) {
1186
+ this.config.auth = { ...this.config.auth, token };
1187
+ return this;
1232
1188
  }
1233
1189
  /**
1234
- * Clear all interceptors and mocks
1190
+ * Add custom headers to all requests
1235
1191
  */
1236
- clear() {
1237
- this.request = [];
1238
- this.response = [];
1239
- this.mocks.clear();
1192
+ withHeaders(headers) {
1193
+ this.config.auth = {
1194
+ ...this.config.auth,
1195
+ headers: { ...this.config.auth?.headers, ...headers }
1196
+ };
1197
+ return this;
1240
1198
  }
1241
- };
1242
- function sleep(ms) {
1243
- return new Promise((resolve) => setTimeout(resolve, ms));
1244
- }
1245
- var interceptors = {
1246
1199
  /**
1247
- * Create an interceptor that logs all requests
1200
+ * Set the transport type
1248
1201
  */
1249
- logger(logFn = console.log) {
1250
- return (ctx) => {
1251
- logFn(`[MCP Request] ${ctx.request.method}`, ctx.request.params);
1252
- return { action: "passthrough" };
1253
- };
1254
- },
1202
+ withTransport(transport) {
1203
+ this.config.transport = transport;
1204
+ return this;
1205
+ }
1255
1206
  /**
1256
- * Create an interceptor that adds latency to all requests
1207
+ * Set the request timeout in milliseconds
1257
1208
  */
1258
- delay(ms) {
1259
- return async () => {
1260
- await sleep(ms);
1261
- return { action: "passthrough" };
1262
- };
1263
- },
1209
+ withTimeout(timeoutMs) {
1210
+ this.config.timeout = timeoutMs;
1211
+ return this;
1212
+ }
1264
1213
  /**
1265
- * Create an interceptor that fails requests matching a condition
1214
+ * Enable debug logging
1266
1215
  */
1267
- failWhen(condition, error) {
1268
- return (ctx) => {
1269
- if (condition(ctx)) {
1270
- const err = typeof error === "string" ? new Error(error) : error;
1271
- return { action: "error", error: err };
1272
- }
1273
- return { action: "passthrough" };
1274
- };
1275
- },
1216
+ withDebug(enabled = true) {
1217
+ this.config.debug = enabled;
1218
+ return this;
1219
+ }
1276
1220
  /**
1277
- * Create an interceptor that modifies specific methods
1221
+ * Enable public mode - skip authentication entirely.
1222
+ * When true, no Authorization header is sent and anonymous token is not requested.
1223
+ * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
1278
1224
  */
1279
- modifyMethod(method, modifier) {
1280
- return (ctx) => {
1281
- if (ctx.request.method === method) {
1282
- return { action: "modify", request: modifier(ctx.request) };
1283
- }
1284
- return { action: "passthrough" };
1285
- };
1286
- },
1225
+ withPublicMode(enabled = true) {
1226
+ this.config.publicMode = enabled;
1227
+ return this;
1228
+ }
1287
1229
  /**
1288
- * Create a response interceptor that logs responses
1230
+ * Set the MCP protocol version to request
1289
1231
  */
1290
- responseLogger(logFn = console.log) {
1291
- return (ctx) => {
1292
- const status = ctx.response.error ? "ERROR" : "OK";
1293
- logFn(`[MCP Response] ${ctx.request.method} ${status} (${ctx.durationMs}ms)`, ctx.response);
1294
- return { action: "passthrough" };
1295
- };
1296
- },
1232
+ withProtocolVersion(version) {
1233
+ this.config.protocolVersion = version;
1234
+ return this;
1235
+ }
1297
1236
  /**
1298
- * Create a response interceptor that modifies specific responses
1237
+ * Set the client info sent during initialization
1299
1238
  */
1300
- modifyResponse(method, modifier) {
1301
- return (ctx) => {
1302
- if (ctx.request.method === method) {
1303
- return { action: "modify", response: modifier(ctx.response) };
1304
- }
1305
- return { action: "passthrough" };
1306
- };
1239
+ withClientInfo(info) {
1240
+ this.config.clientInfo = info;
1241
+ return this;
1242
+ }
1243
+ /**
1244
+ * Set the platform type for testing platform-specific meta keys.
1245
+ * Automatically configures clientInfo and capabilities for platform detection.
1246
+ *
1247
+ * Platform-specific behavior:
1248
+ * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
1249
+ * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
1250
+ * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
1251
+ * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
1252
+ * - Other platforms follow similar patterns
1253
+ *
1254
+ * @example
1255
+ * ```typescript
1256
+ * const client = await McpTestClient.create({ baseUrl })
1257
+ * .withPlatform('openai')
1258
+ * .buildAndConnect();
1259
+ *
1260
+ * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
1261
+ * const extAppsClient = await McpTestClient.create({ baseUrl })
1262
+ * .withPlatform('ext-apps')
1263
+ * .buildAndConnect();
1264
+ * ```
1265
+ */
1266
+ withPlatform(platform) {
1267
+ this.config.platform = platform;
1268
+ this.config.clientInfo = getPlatformClientInfo(platform);
1269
+ this.config.capabilities = getPlatformCapabilities(platform);
1270
+ return this;
1271
+ }
1272
+ /**
1273
+ * Set custom client capabilities for MCP initialization.
1274
+ * Use this for fine-grained control over capabilities sent during initialization.
1275
+ *
1276
+ * @example
1277
+ * ```typescript
1278
+ * const client = await McpTestClient.create({ baseUrl })
1279
+ * .withCapabilities({
1280
+ * sampling: {},
1281
+ * experimental: {
1282
+ * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
1283
+ * }
1284
+ * })
1285
+ * .buildAndConnect();
1286
+ * ```
1287
+ */
1288
+ withCapabilities(capabilities) {
1289
+ this.config.capabilities = capabilities;
1290
+ return this;
1291
+ }
1292
+ /**
1293
+ * Set query parameters to append to the connection URL.
1294
+ * Useful for testing mode switches like `?mode=skills_only`.
1295
+ *
1296
+ * @example
1297
+ * ```typescript
1298
+ * const client = await McpTestClient.create({ baseUrl })
1299
+ * .withQueryParams({ mode: 'skills_only' })
1300
+ * .buildAndConnect();
1301
+ * ```
1302
+ */
1303
+ withQueryParams(params) {
1304
+ this.config.queryParams = { ...this.config.queryParams, ...params };
1305
+ return this;
1306
+ }
1307
+ /**
1308
+ * Build the McpTestClient instance (does not connect)
1309
+ */
1310
+ build() {
1311
+ return new McpTestClient(this.config);
1312
+ }
1313
+ /**
1314
+ * Build the McpTestClient and connect to the server
1315
+ */
1316
+ async buildAndConnect() {
1317
+ const client = this.build();
1318
+ await client.connect();
1319
+ return client;
1307
1320
  }
1308
1321
  };
1309
1322
 
@@ -1314,6 +1327,21 @@ var DEFAULT_CLIENT_INFO = {
1314
1327
  name: "@frontmcp/testing",
1315
1328
  version: "0.4.0"
1316
1329
  };
1330
+ function extractEntryPaths(data) {
1331
+ let payload = data;
1332
+ if (typeof payload === "string") {
1333
+ try {
1334
+ payload = JSON.parse(payload);
1335
+ } catch {
1336
+ return void 0;
1337
+ }
1338
+ }
1339
+ if (!payload || typeof payload !== "object") return void 0;
1340
+ const paths = payload.entryPaths;
1341
+ if (!Array.isArray(paths)) return void 0;
1342
+ const strings = paths.filter((p) => typeof p === "string");
1343
+ return strings.length > 0 ? strings : void 0;
1344
+ }
1317
1345
  var McpTestClient = class {
1318
1346
  // Platform, capabilities, and queryParams are optional - only set when needed
1319
1347
  config;
@@ -1322,6 +1350,8 @@ var McpTestClient = class {
1322
1350
  requestIdCounter = 0;
1323
1351
  _lastRequestId = 0;
1324
1352
  _sessionId;
1353
+ /** #543 — discovery is attempted at most once per connect. */
1354
+ triedDiscoveredEntryPath = false;
1325
1355
  _sessionInfo = null;
1326
1356
  _authState = { isAnonymous: true, scopes: [] };
1327
1357
  // Logging and tracing
@@ -1339,6 +1369,7 @@ var McpTestClient = class {
1339
1369
  constructor(config) {
1340
1370
  this.config = {
1341
1371
  baseUrl: config.baseUrl,
1372
+ entryPath: config.entryPath,
1342
1373
  transport: config.transport ?? "streamable-http",
1343
1374
  auth: config.auth ?? {},
1344
1375
  publicMode: config.publicMode ?? false,
@@ -1374,11 +1405,13 @@ var McpTestClient = class {
1374
1405
  */
1375
1406
  async connect() {
1376
1407
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1408
+ this.triedDiscoveredEntryPath = false;
1377
1409
  this.transport = this.createTransport();
1378
1410
  await this.transport.connect();
1379
1411
  const maxRetries = 3;
1380
1412
  const retryDelayMs = 500;
1381
1413
  let lastError;
1414
+ let reportedEntryPaths;
1382
1415
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1383
1416
  const initResponse = await this.initialize();
1384
1417
  if (initResponse.success && initResponse.data) {
@@ -1386,13 +1419,27 @@ var McpTestClient = class {
1386
1419
  break;
1387
1420
  }
1388
1421
  lastError = initResponse.error?.message ?? "Unknown error";
1422
+ reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1423
+ if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1424
+ this.triedDiscoveredEntryPath = true;
1425
+ const discovered = reportedEntryPaths[0];
1426
+ this.log("info", `Server serves MCP at ${discovered}; reconnecting there (configured: ${this.mcpPathLabel()})`);
1427
+ this.config.entryPath = discovered;
1428
+ await this.transport.close();
1429
+ this.transport = this.createTransport();
1430
+ await this.transport.connect();
1431
+ continue;
1432
+ }
1389
1433
  if (attempt < maxRetries) {
1390
1434
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1391
1435
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
1392
1436
  }
1393
1437
  }
1394
1438
  if (!this.initResult) {
1395
- throw new Error(`Failed to initialize MCP connection after ${maxRetries} attempts: ${lastError}`);
1439
+ 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.` : "";
1440
+ throw new Error(
1441
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1442
+ );
1396
1443
  }
1397
1444
  this._sessionId = this.transport.getSessionId();
1398
1445
  this._sessionInfo = {
@@ -1990,6 +2037,11 @@ var McpTestClient = class {
1990
2037
  // ═══════════════════════════════════════════════════════════════════
1991
2038
  // PRIVATE: TRANSPORT & REQUEST HELPERS
1992
2039
  // ═══════════════════════════════════════════════════════════════════
2040
+ /** The MCP path this client is currently pointed at, for log and error text. */
2041
+ mcpPathLabel() {
2042
+ const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
2043
+ return entry ? `/${entry}` : "/";
2044
+ }
1993
2045
  createTransport() {
1994
2046
  let baseUrl = this.config.baseUrl;
1995
2047
  if (this.config.queryParams && Object.keys(this.config.queryParams).length > 0) {
@@ -2003,6 +2055,7 @@ var McpTestClient = class {
2003
2055
  case "streamable-http":
2004
2056
  return new StreamableHttpTransport({
2005
2057
  baseUrl,
2058
+ entryPath: this.config.entryPath,
2006
2059
  timeout: this.config.timeout,
2007
2060
  auth: this.config.auth,
2008
2061
  publicMode: this.config.publicMode,
@@ -4882,13 +4935,17 @@ var currentConfig = {};
4882
4935
  var serverInstance = null;
4883
4936
  var tokenFactory = null;
4884
4937
  var serverStartedByUs = false;
4938
+ var skipScopes = [{ skipped: false }];
4939
+ function currentSkipScope() {
4940
+ return skipScopes[skipScopes.length - 1];
4941
+ }
4885
4942
  async function initializeSharedResources() {
4886
4943
  if (!tokenFactory) {
4887
4944
  const sharedSecret = currentConfig.env?.["JWT_SECRET"];
4888
4945
  tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret } : {});
4889
4946
  }
4890
4947
  if (!serverInstance) {
4891
- if (currentConfig.baseUrl) {
4948
+ if (currentConfig.baseUrl && !currentConfig.server) {
4892
4949
  serverInstance = TestServer.connect(currentConfig.baseUrl);
4893
4950
  serverStartedByUs = false;
4894
4951
  } else if (currentConfig.server) {
@@ -4938,7 +4995,8 @@ async function createTestFixtures() {
4938
4995
  throw new Error("Token factory not initialized");
4939
4996
  }
4940
4997
  const clientInstance = McpTestClient.create({
4941
- baseUrl: serverInstance.info.baseUrl,
4998
+ baseUrl: resolveClientBaseUrl(serverInstance),
4999
+ entryPath: currentConfig.entryPath,
4942
5000
  transport: currentConfig.transport ?? "streamable-http",
4943
5001
  publicMode: currentConfig.publicMode
4944
5002
  }).build();
@@ -4987,6 +5045,9 @@ async function cleanupSharedResources() {
4987
5045
  tokenFactory = null;
4988
5046
  serverStartedByUs = false;
4989
5047
  }
5048
+ function resolveClientBaseUrl(server) {
5049
+ return currentConfig.baseUrl ?? server.info.baseUrl;
5050
+ }
4990
5051
  function createAuthFixture(factory) {
4991
5052
  const users = {
4992
5053
  admin: {
@@ -5036,7 +5097,8 @@ function createServerFixture(server) {
5036
5097
  info: server.info,
5037
5098
  createClient: async (opts) => {
5038
5099
  return McpTestClient.create({
5039
- baseUrl: server.info.baseUrl,
5100
+ baseUrl: resolveClientBaseUrl(server),
5101
+ entryPath: opts?.entryPath ?? currentConfig.entryPath,
5040
5102
  transport: opts?.transport ?? "streamable-http",
5041
5103
  auth: opts?.token ? { token: opts.token } : void 0,
5042
5104
  clientInfo: opts?.clientInfo,
@@ -5045,7 +5107,8 @@ function createServerFixture(server) {
5045
5107
  },
5046
5108
  createClientBuilder: () => {
5047
5109
  const builder = new McpTestClientBuilder({
5048
- baseUrl: server.info.baseUrl,
5110
+ baseUrl: resolveClientBaseUrl(server),
5111
+ entryPath: currentConfig.entryPath,
5049
5112
  publicMode: currentConfig.publicMode
5050
5113
  });
5051
5114
  return builder;
@@ -5061,8 +5124,8 @@ function resolveServerCommand(server) {
5061
5124
  }
5062
5125
  return `npx tsx ${server}`;
5063
5126
  }
5064
- function testWithFixtures(name, fn) {
5065
- it2(name, async () => {
5127
+ function runWithFixtures(fn) {
5128
+ return async () => {
5066
5129
  const fixtures = await createTestFixtures();
5067
5130
  let testFailed = false;
5068
5131
  try {
@@ -5073,7 +5136,15 @@ function testWithFixtures(name, fn) {
5073
5136
  } finally {
5074
5137
  await cleanupTestFixtures(fixtures, testFailed);
5075
5138
  }
5076
- });
5139
+ };
5140
+ }
5141
+ function testWithFixtures(name, fn) {
5142
+ const scope = currentSkipScope();
5143
+ if (scope.skipped) {
5144
+ it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5145
+ return;
5146
+ }
5147
+ it2(name, runWithFixtures(fn));
5077
5148
  }
5078
5149
  function use(config) {
5079
5150
  currentConfig = { ...currentConfig, ...config };
@@ -5081,40 +5152,60 @@ function use(config) {
5081
5152
  await cleanupSharedResources();
5082
5153
  });
5083
5154
  }
5084
- function skip(name, fn) {
5085
- it2.skip(name, async () => {
5086
- const fixtures = await createTestFixtures();
5087
- let testFailed = false;
5088
- try {
5089
- await fn(fixtures);
5090
- } catch (error) {
5091
- testFailed = true;
5092
- throw error;
5093
- } finally {
5094
- await cleanupTestFixtures(fixtures, testFailed);
5155
+ function skip(nameOrCondition, fnOrReason) {
5156
+ if (typeof nameOrCondition === "boolean") {
5157
+ if (nameOrCondition) {
5158
+ const scope = currentSkipScope();
5159
+ scope.skipped = true;
5160
+ scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
5095
5161
  }
5096
- });
5162
+ return;
5163
+ }
5164
+ if (typeof fnOrReason !== "function") {
5165
+ throw new TypeError(
5166
+ `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
5167
+ );
5168
+ }
5169
+ it2.skip(nameOrCondition, runWithFixtures(fnOrReason));
5097
5170
  }
5098
5171
  function only(name, fn) {
5099
- it2.only(name, async () => {
5100
- const fixtures = await createTestFixtures();
5101
- let testFailed = false;
5102
- try {
5103
- await fn(fixtures);
5104
- } catch (error) {
5105
- testFailed = true;
5106
- throw error;
5107
- } finally {
5108
- await cleanupTestFixtures(fixtures, testFailed);
5109
- }
5110
- });
5172
+ const scope = currentSkipScope();
5173
+ if (scope.skipped) {
5174
+ it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5175
+ return;
5176
+ }
5177
+ it2.only(name, runWithFixtures(fn));
5111
5178
  }
5112
5179
  function todo(name) {
5113
5180
  it2.todo(name);
5114
5181
  }
5182
+ function withSkipScope(register) {
5183
+ return (name, fn) => {
5184
+ register(name, () => {
5185
+ const parent = currentSkipScope();
5186
+ skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
5187
+ try {
5188
+ fn();
5189
+ } finally {
5190
+ skipScopes.pop();
5191
+ }
5192
+ });
5193
+ };
5194
+ }
5195
+ function eachWithSkipScope(each) {
5196
+ return ((...eachArgs) => {
5197
+ const register = each(...eachArgs);
5198
+ return withSkipScope(register);
5199
+ });
5200
+ }
5201
+ var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
5202
+ only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
5203
+ skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
5204
+ each: eachWithSkipScope(describe2.each)
5205
+ });
5115
5206
  var test = testWithFixtures;
5116
5207
  test.use = use;
5117
- test.describe = describe2;
5208
+ test.describe = describeWithSkipScope;
5118
5209
  test.beforeAll = beforeAll2;
5119
5210
  test.beforeEach = beforeEach2;
5120
5211
  test.afterEach = afterEach2;