@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/esm/index.mjs CHANGED
@@ -5,250 +5,339 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
- // libs/testing/src/platform/platform-client-info.ts
9
- var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
10
- function getPlatformClientInfo(platform) {
11
- switch (platform) {
12
- case "openai":
13
- return {
14
- name: "ChatGPT",
15
- version: "1.0"
16
- };
17
- case "ext-apps":
18
- return {
19
- name: "mcp-ext-apps",
20
- version: "1.0"
21
- };
22
- case "claude":
23
- return {
24
- name: "claude-desktop",
25
- version: "1.0"
26
- };
27
- case "cursor":
28
- return {
29
- name: "cursor",
30
- version: "1.0"
31
- };
32
- case "continue":
33
- return {
34
- name: "continue",
35
- version: "1.0"
36
- };
37
- case "cody":
38
- return {
39
- name: "cody",
40
- version: "1.0"
41
- };
42
- case "gemini":
43
- return {
44
- name: "gemini",
45
- version: "1.0"
46
- };
47
- case "generic-mcp":
48
- return {
49
- name: "generic-mcp-client",
50
- version: "1.0"
51
- };
52
- case "unknown":
53
- default:
54
- return {
55
- name: "mcp-test-client",
56
- version: "1.0"
57
- };
58
- }
59
- }
60
- function buildUserAgent(clientInfo) {
61
- return `${clientInfo.name}/${clientInfo.version}`;
62
- }
63
- function getPlatformUserAgent(platform) {
64
- return buildUserAgent(getPlatformClientInfo(platform));
65
- }
66
- var PLATFORM_DETECTION_PATTERNS = {
67
- openai: /chatgpt/i,
68
- "ext-apps": /mcp-ext-apps/i,
69
- // Note: Actual detection uses capabilities
70
- claude: /claude|claude-desktop/i,
71
- cursor: /cursor/i,
72
- continue: /continue/i,
73
- cody: /cody/i,
74
- gemini: /gemini/i,
75
- "generic-mcp": /generic-mcp/i,
76
- unknown: /.*/
77
- // Matches anything (fallback)
78
- };
79
- function getPlatformCapabilities(platform) {
80
- const baseCapabilities = {
81
- sampling: {},
82
- // Include elicitation.form by default for testing elicitation workflows
83
- // Note: MCP SDK expects form to be an object, not boolean
84
- elicitation: {
85
- form: {}
86
- }
87
- };
88
- if (platform === "ext-apps") {
8
+ // libs/testing/src/interceptor/mock-registry.ts
9
+ var DefaultMockRegistry = class {
10
+ mocks = [];
11
+ add(mock) {
12
+ const entry = {
13
+ definition: mock,
14
+ callCount: 0,
15
+ calls: [],
16
+ remainingUses: mock.times ?? Infinity
17
+ };
18
+ this.mocks.push(entry);
89
19
  return {
90
- ...baseCapabilities,
91
- experimental: {
92
- [MCP_APPS_EXTENSION_KEY]: {
93
- mimeTypes: ["text/html;profile=mcp-app"]
20
+ remove: () => {
21
+ const index = this.mocks.indexOf(entry);
22
+ if (index !== -1) {
23
+ this.mocks.splice(index, 1);
94
24
  }
95
- }
25
+ },
26
+ callCount: () => entry.callCount,
27
+ calls: () => [...entry.calls]
96
28
  };
97
29
  }
98
- return baseCapabilities;
99
- }
100
-
101
- // libs/testing/src/client/mcp-test-client.builder.ts
102
- var McpTestClientBuilder = class {
103
- config;
104
- constructor(config) {
105
- this.config = { ...config };
30
+ clear() {
31
+ this.mocks = [];
32
+ }
33
+ getAll() {
34
+ return this.mocks.map((e) => e.definition);
35
+ }
36
+ match(request) {
37
+ for (const entry of this.mocks) {
38
+ if (entry.remainingUses <= 0) continue;
39
+ const { definition } = entry;
40
+ if (definition.method !== request.method) continue;
41
+ if (definition.params !== void 0) {
42
+ const params = request.params ?? {};
43
+ if (typeof definition.params === "function") {
44
+ if (!definition.params(params)) continue;
45
+ } else {
46
+ if (!this.paramsMatch(definition.params, params)) continue;
47
+ }
48
+ }
49
+ entry.callCount++;
50
+ entry.calls.push(request);
51
+ entry.remainingUses--;
52
+ return definition;
53
+ }
54
+ return void 0;
106
55
  }
107
56
  /**
108
- * Set the authentication configuration
57
+ * Check if request params match the mock params definition
109
58
  */
110
- withAuth(auth) {
111
- this.config.auth = { ...this.config.auth, ...auth };
112
- return this;
59
+ paramsMatch(expected, actual) {
60
+ for (const [key, value] of Object.entries(expected)) {
61
+ if (!(key in actual)) return false;
62
+ const actualValue = actual[key];
63
+ if (Array.isArray(value)) {
64
+ if (!Array.isArray(actualValue)) return false;
65
+ if (value.length !== actualValue.length) return false;
66
+ for (let i = 0; i < value.length; i++) {
67
+ const expectedItem = value[i];
68
+ const actualItem = actualValue[i];
69
+ if (typeof expectedItem === "object" && expectedItem !== null) {
70
+ if (typeof actualItem !== "object" || actualItem === null) return false;
71
+ if (!this.paramsMatch(expectedItem, actualItem)) {
72
+ return false;
73
+ }
74
+ } else if (actualItem !== expectedItem) {
75
+ return false;
76
+ }
77
+ }
78
+ } else if (typeof value === "object" && value !== null) {
79
+ if (typeof actualValue !== "object" || actualValue === null) return false;
80
+ if (!this.paramsMatch(value, actualValue)) {
81
+ return false;
82
+ }
83
+ } else if (actualValue !== value) {
84
+ return false;
85
+ }
86
+ }
87
+ return true;
113
88
  }
89
+ };
90
+ var mockResponse = {
114
91
  /**
115
- * Set the bearer token for authentication
92
+ * Create a successful JSON-RPC response
116
93
  */
117
- withToken(token) {
118
- this.config.auth = { ...this.config.auth, token };
119
- return this;
120
- }
94
+ success(result, id = 1) {
95
+ return {
96
+ jsonrpc: "2.0",
97
+ id,
98
+ result
99
+ };
100
+ },
121
101
  /**
122
- * Add custom headers to all requests
102
+ * Create an error JSON-RPC response
123
103
  */
124
- withHeaders(headers) {
125
- this.config.auth = {
126
- ...this.config.auth,
127
- headers: { ...this.config.auth?.headers, ...headers }
104
+ error(code, message, data, id = 1) {
105
+ return {
106
+ jsonrpc: "2.0",
107
+ id,
108
+ error: { code, message, data }
128
109
  };
129
- return this;
130
- }
110
+ },
131
111
  /**
132
- * Set the transport type
112
+ * Create a tool result response
133
113
  */
134
- withTransport(transport) {
135
- this.config.transport = transport;
136
- return this;
137
- }
114
+ toolResult(content, id = 1) {
115
+ return {
116
+ jsonrpc: "2.0",
117
+ id,
118
+ result: { content }
119
+ };
120
+ },
138
121
  /**
139
- * Set the request timeout in milliseconds
122
+ * Create a tools/list response
140
123
  */
141
- withTimeout(timeoutMs) {
142
- this.config.timeout = timeoutMs;
143
- return this;
144
- }
124
+ toolsList(tools, id = 1) {
125
+ return {
126
+ jsonrpc: "2.0",
127
+ id,
128
+ result: { tools }
129
+ };
130
+ },
145
131
  /**
146
- * Enable debug logging
132
+ * Create a resources/list response
147
133
  */
148
- withDebug(enabled = true) {
149
- this.config.debug = enabled;
150
- return this;
151
- }
134
+ resourcesList(resources, id = 1) {
135
+ return {
136
+ jsonrpc: "2.0",
137
+ id,
138
+ result: { resources }
139
+ };
140
+ },
152
141
  /**
153
- * Enable public mode - skip authentication entirely.
154
- * When true, no Authorization header is sent and anonymous token is not requested.
155
- * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
142
+ * Create a resources/read response
156
143
  */
157
- withPublicMode(enabled = true) {
158
- this.config.publicMode = enabled;
159
- return this;
160
- }
144
+ resourceRead(contents, id = 1) {
145
+ return {
146
+ jsonrpc: "2.0",
147
+ id,
148
+ result: { contents }
149
+ };
150
+ },
161
151
  /**
162
- * Set the MCP protocol version to request
152
+ * Common MCP errors
163
153
  */
164
- withProtocolVersion(version) {
165
- this.config.protocolVersion = version;
166
- return this;
154
+ errors: {
155
+ methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
156
+ invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
157
+ internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
158
+ resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
159
+ toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
160
+ unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
161
+ forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
162
+ }
163
+ };
164
+
165
+ // libs/testing/src/interceptor/interceptor-chain.ts
166
+ var DefaultInterceptorChain = class {
167
+ request = [];
168
+ response = [];
169
+ mocks;
170
+ constructor() {
171
+ this.mocks = new DefaultMockRegistry();
167
172
  }
168
173
  /**
169
- * Set the client info sent during initialization
174
+ * Add a request interceptor
170
175
  */
171
- withClientInfo(info) {
172
- this.config.clientInfo = info;
173
- return this;
176
+ addRequestInterceptor(interceptor) {
177
+ this.request.push(interceptor);
178
+ return () => {
179
+ const index = this.request.indexOf(interceptor);
180
+ if (index !== -1) {
181
+ this.request.splice(index, 1);
182
+ }
183
+ };
174
184
  }
175
185
  /**
176
- * Set the platform type for testing platform-specific meta keys.
177
- * Automatically configures clientInfo and capabilities for platform detection.
178
- *
179
- * Platform-specific behavior:
180
- * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
181
- * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
182
- * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
183
- * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
184
- * - Other platforms follow similar patterns
185
- *
186
- * @example
187
- * ```typescript
188
- * const client = await McpTestClient.create({ baseUrl })
189
- * .withPlatform('openai')
190
- * .buildAndConnect();
191
- *
192
- * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
193
- * const extAppsClient = await McpTestClient.create({ baseUrl })
194
- * .withPlatform('ext-apps')
195
- * .buildAndConnect();
196
- * ```
186
+ * Add a response interceptor
197
187
  */
198
- withPlatform(platform) {
199
- this.config.platform = platform;
200
- this.config.clientInfo = getPlatformClientInfo(platform);
201
- this.config.capabilities = getPlatformCapabilities(platform);
202
- return this;
188
+ addResponseInterceptor(interceptor) {
189
+ this.response.push(interceptor);
190
+ return () => {
191
+ const index = this.response.indexOf(interceptor);
192
+ if (index !== -1) {
193
+ this.response.splice(index, 1);
194
+ }
195
+ };
203
196
  }
204
197
  /**
205
- * Set custom client capabilities for MCP initialization.
206
- * Use this for fine-grained control over capabilities sent during initialization.
207
- *
208
- * @example
209
- * ```typescript
210
- * const client = await McpTestClient.create({ baseUrl })
211
- * .withCapabilities({
212
- * sampling: {},
213
- * experimental: {
214
- * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
215
- * }
216
- * })
217
- * .buildAndConnect();
218
- * ```
198
+ * Process a request through the interceptor chain
199
+ * Returns either:
200
+ * - { type: 'continue', request } - continue with (possibly modified) request
201
+ * - { type: 'mock', response } - return mock response immediately
202
+ * - { type: 'error', error } - throw error
219
203
  */
220
- withCapabilities(capabilities) {
221
- this.config.capabilities = capabilities;
222
- return this;
204
+ async processRequest(request, meta) {
205
+ let currentRequest = request;
206
+ const mockDef = this.mocks.match(request);
207
+ if (mockDef) {
208
+ if (mockDef.delay && mockDef.delay > 0) {
209
+ await sleep(mockDef.delay);
210
+ }
211
+ let mockResponse2;
212
+ if (typeof mockDef.response === "function") {
213
+ mockResponse2 = await mockDef.response(request);
214
+ } else {
215
+ mockResponse2 = mockDef.response;
216
+ }
217
+ return {
218
+ type: "mock",
219
+ response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
220
+ };
221
+ }
222
+ for (const interceptor of this.request) {
223
+ const ctx = {
224
+ request: currentRequest,
225
+ meta
226
+ };
227
+ const result = await interceptor(ctx);
228
+ switch (result.action) {
229
+ case "passthrough":
230
+ break;
231
+ case "modify":
232
+ currentRequest = result.request;
233
+ break;
234
+ case "mock":
235
+ return {
236
+ type: "mock",
237
+ response: { ...result.response, id: request.id ?? result.response.id }
238
+ };
239
+ case "error":
240
+ return { type: "error", error: result.error };
241
+ }
242
+ }
243
+ return { type: "continue", request: currentRequest };
223
244
  }
224
245
  /**
225
- * Set query parameters to append to the connection URL.
226
- * Useful for testing mode switches like `?mode=skills_only`.
227
- *
228
- * @example
229
- * ```typescript
230
- * const client = await McpTestClient.create({ baseUrl })
231
- * .withQueryParams({ mode: 'skills_only' })
232
- * .buildAndConnect();
233
- * ```
246
+ * Process a response through the interceptor chain
234
247
  */
235
- withQueryParams(params) {
236
- this.config.queryParams = { ...this.config.queryParams, ...params };
237
- return this;
248
+ async processResponse(request, response, durationMs) {
249
+ let currentResponse = response;
250
+ for (const interceptor of this.response) {
251
+ const ctx = {
252
+ request,
253
+ response: currentResponse,
254
+ durationMs
255
+ };
256
+ const result = await interceptor(ctx);
257
+ switch (result.action) {
258
+ case "passthrough":
259
+ break;
260
+ case "modify":
261
+ currentResponse = result.response;
262
+ break;
263
+ }
264
+ }
265
+ return currentResponse;
238
266
  }
239
267
  /**
240
- * Build the McpTestClient instance (does not connect)
268
+ * Clear all interceptors and mocks
241
269
  */
242
- build() {
243
- return new McpTestClient(this.config);
270
+ clear() {
271
+ this.request = [];
272
+ this.response = [];
273
+ this.mocks.clear();
244
274
  }
275
+ };
276
+ function sleep(ms) {
277
+ return new Promise((resolve) => setTimeout(resolve, ms));
278
+ }
279
+ var interceptors = {
245
280
  /**
246
- * Build the McpTestClient and connect to the server
281
+ * Create an interceptor that logs all requests
247
282
  */
248
- async buildAndConnect() {
249
- const client = this.build();
250
- await client.connect();
251
- return client;
283
+ logger(logFn = console.log) {
284
+ return (ctx) => {
285
+ logFn(`[MCP Request] ${ctx.request.method}`, ctx.request.params);
286
+ return { action: "passthrough" };
287
+ };
288
+ },
289
+ /**
290
+ * Create an interceptor that adds latency to all requests
291
+ */
292
+ delay(ms) {
293
+ return async () => {
294
+ await sleep(ms);
295
+ return { action: "passthrough" };
296
+ };
297
+ },
298
+ /**
299
+ * Create an interceptor that fails requests matching a condition
300
+ */
301
+ failWhen(condition, error) {
302
+ return (ctx) => {
303
+ if (condition(ctx)) {
304
+ const err = typeof error === "string" ? new Error(error) : error;
305
+ return { action: "error", error: err };
306
+ }
307
+ return { action: "passthrough" };
308
+ };
309
+ },
310
+ /**
311
+ * Create an interceptor that modifies specific methods
312
+ */
313
+ modifyMethod(method, modifier) {
314
+ return (ctx) => {
315
+ if (ctx.request.method === method) {
316
+ return { action: "modify", request: modifier(ctx.request) };
317
+ }
318
+ return { action: "passthrough" };
319
+ };
320
+ },
321
+ /**
322
+ * Create a response interceptor that logs responses
323
+ */
324
+ responseLogger(logFn = console.log) {
325
+ return (ctx) => {
326
+ const status = ctx.response.error ? "ERROR" : "OK";
327
+ logFn(`[MCP Response] ${ctx.request.method} ${status} (${ctx.durationMs}ms)`, ctx.response);
328
+ return { action: "passthrough" };
329
+ };
330
+ },
331
+ /**
332
+ * Create a response interceptor that modifies specific responses
333
+ */
334
+ modifyResponse(method, modifier) {
335
+ return (ctx) => {
336
+ if (ctx.request.method === method) {
337
+ return { action: "modify", response: modifier(ctx.response) };
338
+ }
339
+ return { action: "passthrough" };
340
+ };
252
341
  }
253
342
  };
254
343
 
@@ -269,6 +358,7 @@ var StreamableHttpTransport = class {
269
358
  this.config = {
270
359
  baseUrl: config.baseUrl.replace(/\/$/, ""),
271
360
  // Remove trailing slash
361
+ entryPath: config.entryPath,
272
362
  timeout: config.timeout ?? DEFAULT_TIMEOUT,
273
363
  auth: config.auth ?? {},
274
364
  publicMode: config.publicMode ?? false,
@@ -361,7 +451,7 @@ var StreamableHttpTransport = class {
361
451
  }
362
452
  const headers = this.buildHeaders();
363
453
  this.lastRequestHeaders = headers;
364
- const url = `${this.config.baseUrl}/`;
454
+ const url = this.mcpUrl();
365
455
  this.log(`POST ${url}`, message);
366
456
  const controller = new AbortController();
367
457
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -431,7 +521,7 @@ var StreamableHttpTransport = class {
431
521
  this.ensureConnected();
432
522
  const headers = this.buildHeaders();
433
523
  this.lastRequestHeaders = headers;
434
- const url = `${this.config.baseUrl}/`;
524
+ const url = this.mcpUrl();
435
525
  this.log(`POST ${url} (notification)`, message);
436
526
  const controller = new AbortController();
437
527
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -462,7 +552,7 @@ var StreamableHttpTransport = class {
462
552
  this.ensureConnected();
463
553
  const headers = this.buildHeaders();
464
554
  this.lastRequestHeaders = headers;
465
- const url = `${this.config.baseUrl}/`;
555
+ const url = this.mcpUrl();
466
556
  this.log(`POST ${url} (raw)`, data);
467
557
  const controller = new AbortController();
468
558
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -745,7 +835,7 @@ var StreamableHttpTransport = class {
745
835
  */
746
836
  async sendElicitationResponse(requestId, response) {
747
837
  const headers = this.buildHeaders();
748
- const url = `${this.config.baseUrl}/`;
838
+ const url = this.mcpUrl();
749
839
  const rpcResponse = {
750
840
  jsonrpc: "2.0",
751
841
  id: requestId,
@@ -765,6 +855,18 @@ var StreamableHttpTransport = class {
765
855
  this.log("Failed to send elicitation response:", error);
766
856
  }
767
857
  }
858
+ /**
859
+ * The MCP endpoint URL. `entryPath` is where the server mounts MCP; the
860
+ * trailing slash is kept because both hosts normalize it and the original
861
+ * behaviour (`${baseUrl}/`) relied on it (issue #543).
862
+ */
863
+ mcpUrl() {
864
+ const url = new URL(this.config.baseUrl);
865
+ const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
866
+ const basePath = url.pathname.replace(/\/+$/, "");
867
+ url.pathname = entry ? `${basePath}/${entry}` : `${basePath}/`;
868
+ return url.toString();
869
+ }
768
870
  buildHeaders() {
769
871
  const headers = {
770
872
  "Content-Type": "application/json",
@@ -852,339 +954,250 @@ var StreamableHttpTransport = class {
852
954
  }
853
955
  };
854
956
 
855
- // libs/testing/src/interceptor/mock-registry.ts
856
- var DefaultMockRegistry = class {
857
- mocks = [];
858
- add(mock) {
859
- const entry = {
860
- definition: mock,
861
- callCount: 0,
862
- calls: [],
863
- remainingUses: mock.times ?? Infinity
864
- };
865
- this.mocks.push(entry);
866
- return {
867
- remove: () => {
868
- const index = this.mocks.indexOf(entry);
869
- if (index !== -1) {
870
- this.mocks.splice(index, 1);
871
- }
872
- },
873
- callCount: () => entry.callCount,
874
- calls: () => [...entry.calls]
875
- };
876
- }
877
- clear() {
878
- this.mocks = [];
879
- }
880
- getAll() {
881
- return this.mocks.map((e) => e.definition);
957
+ // libs/testing/src/platform/platform-client-info.ts
958
+ var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
959
+ function getPlatformClientInfo(platform) {
960
+ switch (platform) {
961
+ case "openai":
962
+ return {
963
+ name: "ChatGPT",
964
+ version: "1.0"
965
+ };
966
+ case "ext-apps":
967
+ return {
968
+ name: "mcp-ext-apps",
969
+ version: "1.0"
970
+ };
971
+ case "claude":
972
+ return {
973
+ name: "claude-desktop",
974
+ version: "1.0"
975
+ };
976
+ case "cursor":
977
+ return {
978
+ name: "cursor",
979
+ version: "1.0"
980
+ };
981
+ case "continue":
982
+ return {
983
+ name: "continue",
984
+ version: "1.0"
985
+ };
986
+ case "cody":
987
+ return {
988
+ name: "cody",
989
+ version: "1.0"
990
+ };
991
+ case "gemini":
992
+ return {
993
+ name: "gemini",
994
+ version: "1.0"
995
+ };
996
+ case "generic-mcp":
997
+ return {
998
+ name: "generic-mcp-client",
999
+ version: "1.0"
1000
+ };
1001
+ case "unknown":
1002
+ default:
1003
+ return {
1004
+ name: "mcp-test-client",
1005
+ version: "1.0"
1006
+ };
882
1007
  }
883
- match(request) {
884
- for (const entry of this.mocks) {
885
- if (entry.remainingUses <= 0) continue;
886
- const { definition } = entry;
887
- if (definition.method !== request.method) continue;
888
- if (definition.params !== void 0) {
889
- const params = request.params ?? {};
890
- if (typeof definition.params === "function") {
891
- if (!definition.params(params)) continue;
892
- } else {
893
- if (!this.paramsMatch(definition.params, params)) continue;
894
- }
895
- }
896
- entry.callCount++;
897
- entry.calls.push(request);
898
- entry.remainingUses--;
899
- return definition;
1008
+ }
1009
+ function buildUserAgent(clientInfo) {
1010
+ return `${clientInfo.name}/${clientInfo.version}`;
1011
+ }
1012
+ function getPlatformUserAgent(platform) {
1013
+ return buildUserAgent(getPlatformClientInfo(platform));
1014
+ }
1015
+ var PLATFORM_DETECTION_PATTERNS = {
1016
+ openai: /chatgpt/i,
1017
+ "ext-apps": /mcp-ext-apps/i,
1018
+ // Note: Actual detection uses capabilities
1019
+ claude: /claude|claude-desktop/i,
1020
+ cursor: /cursor/i,
1021
+ continue: /continue/i,
1022
+ cody: /cody/i,
1023
+ gemini: /gemini/i,
1024
+ "generic-mcp": /generic-mcp/i,
1025
+ unknown: /.*/
1026
+ // Matches anything (fallback)
1027
+ };
1028
+ function getPlatformCapabilities(platform) {
1029
+ const baseCapabilities = {
1030
+ sampling: {},
1031
+ // Include elicitation.form by default for testing elicitation workflows
1032
+ // Note: MCP SDK expects form to be an object, not boolean
1033
+ elicitation: {
1034
+ form: {}
900
1035
  }
901
- return void 0;
902
- }
903
- /**
904
- * Check if request params match the mock params definition
905
- */
906
- paramsMatch(expected, actual) {
907
- for (const [key, value] of Object.entries(expected)) {
908
- if (!(key in actual)) return false;
909
- const actualValue = actual[key];
910
- if (Array.isArray(value)) {
911
- if (!Array.isArray(actualValue)) return false;
912
- if (value.length !== actualValue.length) return false;
913
- for (let i = 0; i < value.length; i++) {
914
- const expectedItem = value[i];
915
- const actualItem = actualValue[i];
916
- if (typeof expectedItem === "object" && expectedItem !== null) {
917
- if (typeof actualItem !== "object" || actualItem === null) return false;
918
- if (!this.paramsMatch(expectedItem, actualItem)) {
919
- return false;
920
- }
921
- } else if (actualItem !== expectedItem) {
922
- return false;
923
- }
924
- }
925
- } else if (typeof value === "object" && value !== null) {
926
- if (typeof actualValue !== "object" || actualValue === null) return false;
927
- if (!this.paramsMatch(value, actualValue)) {
928
- return false;
1036
+ };
1037
+ if (platform === "ext-apps") {
1038
+ return {
1039
+ ...baseCapabilities,
1040
+ experimental: {
1041
+ [MCP_APPS_EXTENSION_KEY]: {
1042
+ mimeTypes: ["text/html;profile=mcp-app"]
929
1043
  }
930
- } else if (actualValue !== value) {
931
- return false;
932
1044
  }
933
- }
934
- return true;
935
- }
936
- };
937
- var mockResponse = {
938
- /**
939
- * Create a successful JSON-RPC response
940
- */
941
- success(result, id = 1) {
942
- return {
943
- jsonrpc: "2.0",
944
- id,
945
- result
946
- };
947
- },
948
- /**
949
- * Create an error JSON-RPC response
950
- */
951
- error(code, message, data, id = 1) {
952
- return {
953
- jsonrpc: "2.0",
954
- id,
955
- error: { code, message, data }
956
1045
  };
957
- },
958
- /**
959
- * Create a tool result response
960
- */
961
- toolResult(content, id = 1) {
962
- return {
963
- jsonrpc: "2.0",
964
- id,
965
- result: { content }
966
- };
967
- },
968
- /**
969
- * Create a tools/list response
970
- */
971
- toolsList(tools, id = 1) {
972
- return {
973
- jsonrpc: "2.0",
974
- id,
975
- result: { tools }
976
- };
977
- },
978
- /**
979
- * Create a resources/list response
980
- */
981
- resourcesList(resources, id = 1) {
982
- return {
983
- jsonrpc: "2.0",
984
- id,
985
- result: { resources }
986
- };
987
- },
988
- /**
989
- * Create a resources/read response
990
- */
991
- resourceRead(contents, id = 1) {
992
- return {
993
- jsonrpc: "2.0",
994
- id,
995
- result: { contents }
996
- };
997
- },
998
- /**
999
- * Common MCP errors
1000
- */
1001
- errors: {
1002
- methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
1003
- invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
1004
- internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
1005
- resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
1006
- toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
1007
- unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
1008
- forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
1009
1046
  }
1010
- };
1047
+ return baseCapabilities;
1048
+ }
1011
1049
 
1012
- // libs/testing/src/interceptor/interceptor-chain.ts
1013
- var DefaultInterceptorChain = class {
1014
- request = [];
1015
- response = [];
1016
- mocks;
1017
- constructor() {
1018
- this.mocks = new DefaultMockRegistry();
1019
- }
1020
- /**
1021
- * Add a request interceptor
1022
- */
1023
- addRequestInterceptor(interceptor) {
1024
- this.request.push(interceptor);
1025
- return () => {
1026
- const index = this.request.indexOf(interceptor);
1027
- if (index !== -1) {
1028
- this.request.splice(index, 1);
1029
- }
1030
- };
1031
- }
1032
- /**
1033
- * Add a response interceptor
1034
- */
1035
- addResponseInterceptor(interceptor) {
1036
- this.response.push(interceptor);
1037
- return () => {
1038
- const index = this.response.indexOf(interceptor);
1039
- if (index !== -1) {
1040
- this.response.splice(index, 1);
1041
- }
1042
- };
1050
+ // libs/testing/src/client/mcp-test-client.builder.ts
1051
+ var McpTestClientBuilder = class {
1052
+ config;
1053
+ constructor(config) {
1054
+ this.config = { ...config };
1043
1055
  }
1044
1056
  /**
1045
- * Process a request through the interceptor chain
1046
- * Returns either:
1047
- * - { type: 'continue', request } - continue with (possibly modified) request
1048
- * - { type: 'mock', response } - return mock response immediately
1049
- * - { type: 'error', error } - throw error
1057
+ * Set the authentication configuration
1050
1058
  */
1051
- async processRequest(request, meta) {
1052
- let currentRequest = request;
1053
- const mockDef = this.mocks.match(request);
1054
- if (mockDef) {
1055
- if (mockDef.delay && mockDef.delay > 0) {
1056
- await sleep(mockDef.delay);
1057
- }
1058
- let mockResponse2;
1059
- if (typeof mockDef.response === "function") {
1060
- mockResponse2 = await mockDef.response(request);
1061
- } else {
1062
- mockResponse2 = mockDef.response;
1063
- }
1064
- return {
1065
- type: "mock",
1066
- response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
1067
- };
1068
- }
1069
- for (const interceptor of this.request) {
1070
- const ctx = {
1071
- request: currentRequest,
1072
- meta
1073
- };
1074
- const result = await interceptor(ctx);
1075
- switch (result.action) {
1076
- case "passthrough":
1077
- break;
1078
- case "modify":
1079
- currentRequest = result.request;
1080
- break;
1081
- case "mock":
1082
- return {
1083
- type: "mock",
1084
- response: { ...result.response, id: request.id ?? result.response.id }
1085
- };
1086
- case "error":
1087
- return { type: "error", error: result.error };
1088
- }
1089
- }
1090
- return { type: "continue", request: currentRequest };
1059
+ withAuth(auth) {
1060
+ this.config.auth = { ...this.config.auth, ...auth };
1061
+ return this;
1091
1062
  }
1092
1063
  /**
1093
- * Process a response through the interceptor chain
1064
+ * Set the bearer token for authentication
1094
1065
  */
1095
- async processResponse(request, response, durationMs) {
1096
- let currentResponse = response;
1097
- for (const interceptor of this.response) {
1098
- const ctx = {
1099
- request,
1100
- response: currentResponse,
1101
- durationMs
1102
- };
1103
- const result = await interceptor(ctx);
1104
- switch (result.action) {
1105
- case "passthrough":
1106
- break;
1107
- case "modify":
1108
- currentResponse = result.response;
1109
- break;
1110
- }
1111
- }
1112
- return currentResponse;
1066
+ withToken(token) {
1067
+ this.config.auth = { ...this.config.auth, token };
1068
+ return this;
1113
1069
  }
1114
1070
  /**
1115
- * Clear all interceptors and mocks
1071
+ * Add custom headers to all requests
1116
1072
  */
1117
- clear() {
1118
- this.request = [];
1119
- this.response = [];
1120
- this.mocks.clear();
1073
+ withHeaders(headers) {
1074
+ this.config.auth = {
1075
+ ...this.config.auth,
1076
+ headers: { ...this.config.auth?.headers, ...headers }
1077
+ };
1078
+ return this;
1121
1079
  }
1122
- };
1123
- function sleep(ms) {
1124
- return new Promise((resolve) => setTimeout(resolve, ms));
1125
- }
1126
- var interceptors = {
1127
1080
  /**
1128
- * Create an interceptor that logs all requests
1081
+ * Set the transport type
1129
1082
  */
1130
- logger(logFn = console.log) {
1131
- return (ctx) => {
1132
- logFn(`[MCP Request] ${ctx.request.method}`, ctx.request.params);
1133
- return { action: "passthrough" };
1134
- };
1135
- },
1083
+ withTransport(transport) {
1084
+ this.config.transport = transport;
1085
+ return this;
1086
+ }
1136
1087
  /**
1137
- * Create an interceptor that adds latency to all requests
1088
+ * Set the request timeout in milliseconds
1138
1089
  */
1139
- delay(ms) {
1140
- return async () => {
1141
- await sleep(ms);
1142
- return { action: "passthrough" };
1143
- };
1144
- },
1090
+ withTimeout(timeoutMs) {
1091
+ this.config.timeout = timeoutMs;
1092
+ return this;
1093
+ }
1145
1094
  /**
1146
- * Create an interceptor that fails requests matching a condition
1095
+ * Enable debug logging
1147
1096
  */
1148
- failWhen(condition, error) {
1149
- return (ctx) => {
1150
- if (condition(ctx)) {
1151
- const err = typeof error === "string" ? new Error(error) : error;
1152
- return { action: "error", error: err };
1153
- }
1154
- return { action: "passthrough" };
1155
- };
1156
- },
1097
+ withDebug(enabled = true) {
1098
+ this.config.debug = enabled;
1099
+ return this;
1100
+ }
1157
1101
  /**
1158
- * Create an interceptor that modifies specific methods
1102
+ * Enable public mode - skip authentication entirely.
1103
+ * When true, no Authorization header is sent and anonymous token is not requested.
1104
+ * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
1159
1105
  */
1160
- modifyMethod(method, modifier) {
1161
- return (ctx) => {
1162
- if (ctx.request.method === method) {
1163
- return { action: "modify", request: modifier(ctx.request) };
1164
- }
1165
- return { action: "passthrough" };
1166
- };
1167
- },
1106
+ withPublicMode(enabled = true) {
1107
+ this.config.publicMode = enabled;
1108
+ return this;
1109
+ }
1168
1110
  /**
1169
- * Create a response interceptor that logs responses
1111
+ * Set the MCP protocol version to request
1170
1112
  */
1171
- responseLogger(logFn = console.log) {
1172
- return (ctx) => {
1173
- const status = ctx.response.error ? "ERROR" : "OK";
1174
- logFn(`[MCP Response] ${ctx.request.method} ${status} (${ctx.durationMs}ms)`, ctx.response);
1175
- return { action: "passthrough" };
1176
- };
1177
- },
1113
+ withProtocolVersion(version) {
1114
+ this.config.protocolVersion = version;
1115
+ return this;
1116
+ }
1178
1117
  /**
1179
- * Create a response interceptor that modifies specific responses
1118
+ * Set the client info sent during initialization
1180
1119
  */
1181
- modifyResponse(method, modifier) {
1182
- return (ctx) => {
1183
- if (ctx.request.method === method) {
1184
- return { action: "modify", response: modifier(ctx.response) };
1185
- }
1186
- return { action: "passthrough" };
1187
- };
1120
+ withClientInfo(info) {
1121
+ this.config.clientInfo = info;
1122
+ return this;
1123
+ }
1124
+ /**
1125
+ * Set the platform type for testing platform-specific meta keys.
1126
+ * Automatically configures clientInfo and capabilities for platform detection.
1127
+ *
1128
+ * Platform-specific behavior:
1129
+ * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
1130
+ * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
1131
+ * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
1132
+ * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
1133
+ * - Other platforms follow similar patterns
1134
+ *
1135
+ * @example
1136
+ * ```typescript
1137
+ * const client = await McpTestClient.create({ baseUrl })
1138
+ * .withPlatform('openai')
1139
+ * .buildAndConnect();
1140
+ *
1141
+ * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
1142
+ * const extAppsClient = await McpTestClient.create({ baseUrl })
1143
+ * .withPlatform('ext-apps')
1144
+ * .buildAndConnect();
1145
+ * ```
1146
+ */
1147
+ withPlatform(platform) {
1148
+ this.config.platform = platform;
1149
+ this.config.clientInfo = getPlatformClientInfo(platform);
1150
+ this.config.capabilities = getPlatformCapabilities(platform);
1151
+ return this;
1152
+ }
1153
+ /**
1154
+ * Set custom client capabilities for MCP initialization.
1155
+ * Use this for fine-grained control over capabilities sent during initialization.
1156
+ *
1157
+ * @example
1158
+ * ```typescript
1159
+ * const client = await McpTestClient.create({ baseUrl })
1160
+ * .withCapabilities({
1161
+ * sampling: {},
1162
+ * experimental: {
1163
+ * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
1164
+ * }
1165
+ * })
1166
+ * .buildAndConnect();
1167
+ * ```
1168
+ */
1169
+ withCapabilities(capabilities) {
1170
+ this.config.capabilities = capabilities;
1171
+ return this;
1172
+ }
1173
+ /**
1174
+ * Set query parameters to append to the connection URL.
1175
+ * Useful for testing mode switches like `?mode=skills_only`.
1176
+ *
1177
+ * @example
1178
+ * ```typescript
1179
+ * const client = await McpTestClient.create({ baseUrl })
1180
+ * .withQueryParams({ mode: 'skills_only' })
1181
+ * .buildAndConnect();
1182
+ * ```
1183
+ */
1184
+ withQueryParams(params) {
1185
+ this.config.queryParams = { ...this.config.queryParams, ...params };
1186
+ return this;
1187
+ }
1188
+ /**
1189
+ * Build the McpTestClient instance (does not connect)
1190
+ */
1191
+ build() {
1192
+ return new McpTestClient(this.config);
1193
+ }
1194
+ /**
1195
+ * Build the McpTestClient and connect to the server
1196
+ */
1197
+ async buildAndConnect() {
1198
+ const client = this.build();
1199
+ await client.connect();
1200
+ return client;
1188
1201
  }
1189
1202
  };
1190
1203
 
@@ -1195,6 +1208,21 @@ var DEFAULT_CLIENT_INFO = {
1195
1208
  name: "@frontmcp/testing",
1196
1209
  version: "0.4.0"
1197
1210
  };
1211
+ function extractEntryPaths(data) {
1212
+ let payload = data;
1213
+ if (typeof payload === "string") {
1214
+ try {
1215
+ payload = JSON.parse(payload);
1216
+ } catch {
1217
+ return void 0;
1218
+ }
1219
+ }
1220
+ if (!payload || typeof payload !== "object") return void 0;
1221
+ const paths = payload.entryPaths;
1222
+ if (!Array.isArray(paths)) return void 0;
1223
+ const strings = paths.filter((p) => typeof p === "string");
1224
+ return strings.length > 0 ? strings : void 0;
1225
+ }
1198
1226
  var McpTestClient = class {
1199
1227
  // Platform, capabilities, and queryParams are optional - only set when needed
1200
1228
  config;
@@ -1203,6 +1231,8 @@ var McpTestClient = class {
1203
1231
  requestIdCounter = 0;
1204
1232
  _lastRequestId = 0;
1205
1233
  _sessionId;
1234
+ /** #543 — discovery is attempted at most once per connect. */
1235
+ triedDiscoveredEntryPath = false;
1206
1236
  _sessionInfo = null;
1207
1237
  _authState = { isAnonymous: true, scopes: [] };
1208
1238
  // Logging and tracing
@@ -1220,6 +1250,7 @@ var McpTestClient = class {
1220
1250
  constructor(config) {
1221
1251
  this.config = {
1222
1252
  baseUrl: config.baseUrl,
1253
+ entryPath: config.entryPath,
1223
1254
  transport: config.transport ?? "streamable-http",
1224
1255
  auth: config.auth ?? {},
1225
1256
  publicMode: config.publicMode ?? false,
@@ -1255,11 +1286,13 @@ var McpTestClient = class {
1255
1286
  */
1256
1287
  async connect() {
1257
1288
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1289
+ this.triedDiscoveredEntryPath = false;
1258
1290
  this.transport = this.createTransport();
1259
1291
  await this.transport.connect();
1260
1292
  const maxRetries = 3;
1261
1293
  const retryDelayMs = 500;
1262
1294
  let lastError;
1295
+ let reportedEntryPaths;
1263
1296
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1264
1297
  const initResponse = await this.initialize();
1265
1298
  if (initResponse.success && initResponse.data) {
@@ -1267,13 +1300,27 @@ var McpTestClient = class {
1267
1300
  break;
1268
1301
  }
1269
1302
  lastError = initResponse.error?.message ?? "Unknown error";
1303
+ reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1304
+ if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1305
+ this.triedDiscoveredEntryPath = true;
1306
+ const discovered = reportedEntryPaths[0];
1307
+ this.log("info", `Server serves MCP at ${discovered}; reconnecting there (configured: ${this.mcpPathLabel()})`);
1308
+ this.config.entryPath = discovered;
1309
+ await this.transport.close();
1310
+ this.transport = this.createTransport();
1311
+ await this.transport.connect();
1312
+ continue;
1313
+ }
1270
1314
  if (attempt < maxRetries) {
1271
1315
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1272
1316
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
1273
1317
  }
1274
1318
  }
1275
1319
  if (!this.initResult) {
1276
- throw new Error(`Failed to initialize MCP connection after ${maxRetries} attempts: ${lastError}`);
1320
+ 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.` : "";
1321
+ throw new Error(
1322
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1323
+ );
1277
1324
  }
1278
1325
  this._sessionId = this.transport.getSessionId();
1279
1326
  this._sessionInfo = {
@@ -1871,6 +1918,11 @@ var McpTestClient = class {
1871
1918
  // ═══════════════════════════════════════════════════════════════════
1872
1919
  // PRIVATE: TRANSPORT & REQUEST HELPERS
1873
1920
  // ═══════════════════════════════════════════════════════════════════
1921
+ /** The MCP path this client is currently pointed at, for log and error text. */
1922
+ mcpPathLabel() {
1923
+ const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
1924
+ return entry ? `/${entry}` : "/";
1925
+ }
1874
1926
  createTransport() {
1875
1927
  let baseUrl = this.config.baseUrl;
1876
1928
  if (this.config.queryParams && Object.keys(this.config.queryParams).length > 0) {
@@ -1884,6 +1936,7 @@ var McpTestClient = class {
1884
1936
  case "streamable-http":
1885
1937
  return new StreamableHttpTransport({
1886
1938
  baseUrl,
1939
+ entryPath: this.config.entryPath,
1887
1940
  timeout: this.config.timeout,
1888
1941
  auth: this.config.auth,
1889
1942
  publicMode: this.config.publicMode,
@@ -4770,13 +4823,17 @@ var currentConfig = {};
4770
4823
  var serverInstance = null;
4771
4824
  var tokenFactory = null;
4772
4825
  var serverStartedByUs = false;
4826
+ var skipScopes = [{ skipped: false }];
4827
+ function currentSkipScope() {
4828
+ return skipScopes[skipScopes.length - 1];
4829
+ }
4773
4830
  async function initializeSharedResources() {
4774
4831
  if (!tokenFactory) {
4775
4832
  const sharedSecret = currentConfig.env?.["JWT_SECRET"];
4776
4833
  tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret } : {});
4777
4834
  }
4778
4835
  if (!serverInstance) {
4779
- if (currentConfig.baseUrl) {
4836
+ if (currentConfig.baseUrl && !currentConfig.server) {
4780
4837
  serverInstance = TestServer.connect(currentConfig.baseUrl);
4781
4838
  serverStartedByUs = false;
4782
4839
  } else if (currentConfig.server) {
@@ -4826,7 +4883,8 @@ async function createTestFixtures() {
4826
4883
  throw new Error("Token factory not initialized");
4827
4884
  }
4828
4885
  const clientInstance = McpTestClient.create({
4829
- baseUrl: serverInstance.info.baseUrl,
4886
+ baseUrl: resolveClientBaseUrl(serverInstance),
4887
+ entryPath: currentConfig.entryPath,
4830
4888
  transport: currentConfig.transport ?? "streamable-http",
4831
4889
  publicMode: currentConfig.publicMode
4832
4890
  }).build();
@@ -4875,6 +4933,9 @@ async function cleanupSharedResources() {
4875
4933
  tokenFactory = null;
4876
4934
  serverStartedByUs = false;
4877
4935
  }
4936
+ function resolveClientBaseUrl(server) {
4937
+ return currentConfig.baseUrl ?? server.info.baseUrl;
4938
+ }
4878
4939
  function createAuthFixture(factory) {
4879
4940
  const users = {
4880
4941
  admin: {
@@ -4924,7 +4985,8 @@ function createServerFixture(server) {
4924
4985
  info: server.info,
4925
4986
  createClient: async (opts) => {
4926
4987
  return McpTestClient.create({
4927
- baseUrl: server.info.baseUrl,
4988
+ baseUrl: resolveClientBaseUrl(server),
4989
+ entryPath: opts?.entryPath ?? currentConfig.entryPath,
4928
4990
  transport: opts?.transport ?? "streamable-http",
4929
4991
  auth: opts?.token ? { token: opts.token } : void 0,
4930
4992
  clientInfo: opts?.clientInfo,
@@ -4933,7 +4995,8 @@ function createServerFixture(server) {
4933
4995
  },
4934
4996
  createClientBuilder: () => {
4935
4997
  const builder = new McpTestClientBuilder({
4936
- baseUrl: server.info.baseUrl,
4998
+ baseUrl: resolveClientBaseUrl(server),
4999
+ entryPath: currentConfig.entryPath,
4937
5000
  publicMode: currentConfig.publicMode
4938
5001
  });
4939
5002
  return builder;
@@ -4949,8 +5012,8 @@ function resolveServerCommand(server) {
4949
5012
  }
4950
5013
  return `npx tsx ${server}`;
4951
5014
  }
4952
- function testWithFixtures(name, fn) {
4953
- it2(name, async () => {
5015
+ function runWithFixtures(fn) {
5016
+ return async () => {
4954
5017
  const fixtures = await createTestFixtures();
4955
5018
  let testFailed = false;
4956
5019
  try {
@@ -4961,7 +5024,15 @@ function testWithFixtures(name, fn) {
4961
5024
  } finally {
4962
5025
  await cleanupTestFixtures(fixtures, testFailed);
4963
5026
  }
4964
- });
5027
+ };
5028
+ }
5029
+ function testWithFixtures(name, fn) {
5030
+ const scope = currentSkipScope();
5031
+ if (scope.skipped) {
5032
+ it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5033
+ return;
5034
+ }
5035
+ it2(name, runWithFixtures(fn));
4965
5036
  }
4966
5037
  function use(config) {
4967
5038
  currentConfig = { ...currentConfig, ...config };
@@ -4969,40 +5040,60 @@ function use(config) {
4969
5040
  await cleanupSharedResources();
4970
5041
  });
4971
5042
  }
4972
- function skip(name, fn) {
4973
- it2.skip(name, async () => {
4974
- const fixtures = await createTestFixtures();
4975
- let testFailed = false;
4976
- try {
4977
- await fn(fixtures);
4978
- } catch (error) {
4979
- testFailed = true;
4980
- throw error;
4981
- } finally {
4982
- await cleanupTestFixtures(fixtures, testFailed);
5043
+ function skip(nameOrCondition, fnOrReason) {
5044
+ if (typeof nameOrCondition === "boolean") {
5045
+ if (nameOrCondition) {
5046
+ const scope = currentSkipScope();
5047
+ scope.skipped = true;
5048
+ scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
4983
5049
  }
4984
- });
5050
+ return;
5051
+ }
5052
+ if (typeof fnOrReason !== "function") {
5053
+ throw new TypeError(
5054
+ `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
5055
+ );
5056
+ }
5057
+ it2.skip(nameOrCondition, runWithFixtures(fnOrReason));
4985
5058
  }
4986
5059
  function only(name, fn) {
4987
- it2.only(name, async () => {
4988
- const fixtures = await createTestFixtures();
4989
- let testFailed = false;
4990
- try {
4991
- await fn(fixtures);
4992
- } catch (error) {
4993
- testFailed = true;
4994
- throw error;
4995
- } finally {
4996
- await cleanupTestFixtures(fixtures, testFailed);
4997
- }
4998
- });
5060
+ const scope = currentSkipScope();
5061
+ if (scope.skipped) {
5062
+ it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5063
+ return;
5064
+ }
5065
+ it2.only(name, runWithFixtures(fn));
4999
5066
  }
5000
5067
  function todo(name) {
5001
5068
  it2.todo(name);
5002
5069
  }
5070
+ function withSkipScope(register) {
5071
+ return (name, fn) => {
5072
+ register(name, () => {
5073
+ const parent = currentSkipScope();
5074
+ skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
5075
+ try {
5076
+ fn();
5077
+ } finally {
5078
+ skipScopes.pop();
5079
+ }
5080
+ });
5081
+ };
5082
+ }
5083
+ function eachWithSkipScope(each) {
5084
+ return ((...eachArgs) => {
5085
+ const register = each(...eachArgs);
5086
+ return withSkipScope(register);
5087
+ });
5088
+ }
5089
+ var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
5090
+ only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
5091
+ skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
5092
+ each: eachWithSkipScope(describe2.each)
5093
+ });
5003
5094
  var test = testWithFixtures;
5004
5095
  test.use = use;
5005
- test.describe = describe2;
5096
+ test.describe = describeWithSkipScope;
5006
5097
  test.beforeAll = beforeAll2;
5007
5098
  test.beforeEach = beforeEach2;
5008
5099
  test.afterEach = afterEach2;