@schmock/core 1.0.4 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/builder.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { isStatusTuple } from "./constants.js";
1
2
  import {
2
3
  PluginError,
3
4
  RouteDefinitionError,
@@ -5,19 +6,10 @@ import {
5
6
  SchmockError,
6
7
  } from "./errors.js";
7
8
  import { parseRouteKey } from "./parser.js";
8
- import type {
9
- Generator,
10
- GeneratorFunction,
11
- GlobalConfig,
12
- HttpMethod,
13
- Plugin,
14
- PluginContext,
15
- RequestContext,
16
- RequestOptions,
17
- Response,
18
- RouteConfig,
19
- RouteKey,
20
- } from "./types.js";
9
+
10
+ function errorMessage(error: unknown): string {
11
+ return error instanceof Error ? error.message : "Unknown error";
12
+ }
21
13
 
22
14
  /**
23
15
  * Debug logger that respects debug mode configuration
@@ -25,7 +17,7 @@ import type {
25
17
  class DebugLogger {
26
18
  constructor(private enabled = false) {}
27
19
 
28
- log(category: string, message: string, data?: any) {
20
+ log(category: string, message: string, data?: unknown) {
29
21
  if (!this.enabled) return;
30
22
 
31
23
  const timestamp = new Date().toISOString();
@@ -55,10 +47,29 @@ class DebugLogger {
55
47
  interface CompiledCallableRoute {
56
48
  pattern: RegExp;
57
49
  params: string[];
58
- method: HttpMethod;
50
+ method: Schmock.HttpMethod;
59
51
  path: string;
60
- generator: Generator;
61
- config: RouteConfig;
52
+ generator: Schmock.Generator;
53
+ config: Schmock.RouteConfig;
54
+ }
55
+
56
+ function isGeneratorFunction(
57
+ gen: Schmock.Generator,
58
+ ): gen is Schmock.GeneratorFunction {
59
+ return typeof gen === "function";
60
+ }
61
+
62
+ function isResponseObject(value: unknown): value is {
63
+ status: number;
64
+ body: unknown;
65
+ headers?: Record<string, string>;
66
+ } {
67
+ return (
68
+ typeof value === "object" &&
69
+ value !== null &&
70
+ "status" in value &&
71
+ "body" in value
72
+ );
62
73
  }
63
74
 
64
75
  /**
@@ -68,10 +79,13 @@ interface CompiledCallableRoute {
68
79
  */
69
80
  export class CallableMockInstance {
70
81
  private routes: CompiledCallableRoute[] = [];
71
- private plugins: Plugin[] = [];
82
+ private staticRoutes = new Map<string, CompiledCallableRoute>();
83
+ private plugins: Schmock.Plugin[] = [];
72
84
  private logger: DebugLogger;
85
+ private requestHistory: Schmock.RequestRecord[] = [];
86
+ private callableRef: Schmock.CallableMockInstance | undefined;
73
87
 
74
- constructor(private globalConfig: GlobalConfig = {}) {
88
+ constructor(private globalConfig: Schmock.GlobalConfig = {}) {
75
89
  this.logger = new DebugLogger(globalConfig.debug || false);
76
90
  if (globalConfig.debug) {
77
91
  this.logger.log("config", "Debug mode enabled");
@@ -85,9 +99,9 @@ export class CallableMockInstance {
85
99
 
86
100
  // Method for defining routes (called when instance is invoked)
87
101
  defineRoute(
88
- route: RouteKey,
89
- generator: Generator,
90
- config: RouteConfig,
102
+ route: Schmock.RouteKey,
103
+ generator: Schmock.Generator,
104
+ config: Schmock.RouteConfig,
91
105
  ): this {
92
106
  // Auto-detect contentType if not provided
93
107
  if (!config.contentType) {
@@ -150,6 +164,20 @@ export class CallableMockInstance {
150
164
  };
151
165
 
152
166
  this.routes.push(compiledRoute);
167
+
168
+ // Store static routes (no params) in Map for O(1) lookup
169
+ // Only store the first registration — "first registration wins" semantics
170
+ if (parsed.params.length === 0) {
171
+ const normalizedPath =
172
+ parsed.path.endsWith("/") && parsed.path !== "/"
173
+ ? parsed.path.slice(0, -1)
174
+ : parsed.path;
175
+ const key = `${parsed.method} ${normalizedPath}`;
176
+ if (!this.staticRoutes.has(key)) {
177
+ this.staticRoutes.set(key, compiledRoute);
178
+ }
179
+ }
180
+
153
181
  this.logger.log("route", `Route defined: ${route}`, {
154
182
  contentType: config.contentType,
155
183
  generatorType: typeof generator,
@@ -159,7 +187,11 @@ export class CallableMockInstance {
159
187
  return this;
160
188
  }
161
189
 
162
- pipe(plugin: Plugin): this {
190
+ setCallableRef(ref: Schmock.CallableMockInstance): void {
191
+ this.callableRef = ref;
192
+ }
193
+
194
+ pipe(plugin: Schmock.Plugin): this {
163
195
  this.plugins.push(plugin);
164
196
  this.logger.log(
165
197
  "plugin",
@@ -171,15 +203,89 @@ export class CallableMockInstance {
171
203
  hasOnError: typeof plugin.onError === "function",
172
204
  },
173
205
  );
206
+ if (plugin.install && this.callableRef) {
207
+ plugin.install(this.callableRef);
208
+ }
174
209
  return this;
175
210
  }
176
211
 
212
+ // ===== Request Spy / History API =====
213
+
214
+ history(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord[] {
215
+ if (method && path) {
216
+ return this.requestHistory.filter(
217
+ (r) => r.method === method && r.path === path,
218
+ );
219
+ }
220
+ return [...this.requestHistory];
221
+ }
222
+
223
+ called(method?: Schmock.HttpMethod, path?: string): boolean {
224
+ if (method && path) {
225
+ return this.requestHistory.some(
226
+ (r) => r.method === method && r.path === path,
227
+ );
228
+ }
229
+ return this.requestHistory.length > 0;
230
+ }
231
+
232
+ callCount(method?: Schmock.HttpMethod, path?: string): number {
233
+ if (method && path) {
234
+ return this.requestHistory.filter(
235
+ (r) => r.method === method && r.path === path,
236
+ ).length;
237
+ }
238
+ return this.requestHistory.length;
239
+ }
240
+
241
+ lastRequest(
242
+ method?: Schmock.HttpMethod,
243
+ path?: string,
244
+ ): Schmock.RequestRecord | undefined {
245
+ if (method && path) {
246
+ const filtered = this.requestHistory.filter(
247
+ (r) => r.method === method && r.path === path,
248
+ );
249
+ return filtered[filtered.length - 1];
250
+ }
251
+ return this.requestHistory[this.requestHistory.length - 1];
252
+ }
253
+
254
+ // ===== Reset / Lifecycle =====
255
+
256
+ reset(): void {
257
+ this.routes = [];
258
+ this.staticRoutes.clear();
259
+ this.plugins = [];
260
+ this.requestHistory = [];
261
+ if (this.globalConfig.state) {
262
+ for (const key of Object.keys(this.globalConfig.state)) {
263
+ delete this.globalConfig.state[key];
264
+ }
265
+ }
266
+ this.logger.log("lifecycle", "Mock fully reset");
267
+ }
268
+
269
+ resetHistory(): void {
270
+ this.requestHistory = [];
271
+ this.logger.log("lifecycle", "Request history cleared");
272
+ }
273
+
274
+ resetState(): void {
275
+ if (this.globalConfig.state) {
276
+ for (const key of Object.keys(this.globalConfig.state)) {
277
+ delete this.globalConfig.state[key];
278
+ }
279
+ }
280
+ this.logger.log("lifecycle", "State cleared");
281
+ }
282
+
177
283
  async handle(
178
- method: HttpMethod,
284
+ method: Schmock.HttpMethod,
179
285
  path: string,
180
- options?: RequestOptions,
181
- ): Promise<Response> {
182
- const requestId = Math.random().toString(36).substring(2, 10) || "00000000";
286
+ options?: Schmock.RequestOptions,
287
+ ): Promise<Schmock.Response> {
288
+ const requestId = crypto.randomUUID();
183
289
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
184
290
  headers: options?.headers,
185
291
  query: options?.query,
@@ -190,46 +296,40 @@ export class CallableMockInstance {
190
296
  try {
191
297
  // Apply namespace if configured
192
298
  let requestPath = path;
193
- if (this.globalConfig.namespace) {
194
- // Normalize namespace to handle edge cases
195
- const namespace = this.globalConfig.namespace;
196
- if (namespace === "/") {
197
- // Root namespace means no transformation needed
198
- requestPath = path;
199
- } else {
200
- // Handle namespace without leading slash by normalizing both namespace and path
201
- const normalizedNamespace = namespace.startsWith("/")
202
- ? namespace
203
- : `/${namespace}`;
204
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
205
-
206
- // Remove trailing slash from namespace unless it's root
207
- const finalNamespace =
208
- normalizedNamespace.endsWith("/") && normalizedNamespace !== "/"
209
- ? normalizedNamespace.slice(0, -1)
210
- : normalizedNamespace;
211
-
212
- if (!normalizedPath.startsWith(finalNamespace)) {
213
- this.logger.log(
214
- "route",
215
- `[${requestId}] Path doesn't match namespace ${namespace}`,
216
- );
217
- const error = new RouteNotFoundError(method, path);
218
- const response = {
219
- status: 404,
220
- body: { error: error.message, code: error.code },
221
- headers: {},
222
- };
223
- this.logger.timeEnd(`request-${requestId}`);
224
- return response;
225
- }
299
+ if (this.globalConfig.namespace && this.globalConfig.namespace !== "/") {
300
+ const namespace = this.globalConfig.namespace.startsWith("/")
301
+ ? this.globalConfig.namespace
302
+ : `/${this.globalConfig.namespace}`;
303
+
304
+ const pathToCheck = path.startsWith("/") ? path : `/${path}`;
305
+
306
+ // Check if path starts with namespace
307
+ // handle both "/api/users" (starts with /api) and "/api" (exact match)
308
+ // but NOT "/apiv2" (prefix match but wrong segment)
309
+ const isMatch =
310
+ pathToCheck === namespace ||
311
+ pathToCheck.startsWith(
312
+ namespace.endsWith("/") ? namespace : `${namespace}/`,
313
+ );
226
314
 
227
- // Remove namespace prefix, ensuring we always start with /
228
- requestPath = normalizedPath.substring(finalNamespace.length);
229
- if (!requestPath.startsWith("/")) {
230
- requestPath = `/${requestPath}`;
231
- }
315
+ if (!isMatch) {
316
+ this.logger.log(
317
+ "route",
318
+ `[${requestId}] Path doesn't match namespace ${namespace}`,
319
+ );
320
+ const error = new RouteNotFoundError(method, path);
321
+ const response = {
322
+ status: 404,
323
+ body: { error: error.message, code: error.code },
324
+ headers: {},
325
+ };
326
+ this.logger.timeEnd(`request-${requestId}`);
327
+ return response;
232
328
  }
329
+
330
+ // Remove namespace prefix, ensuring we always start with /
331
+ const stripped = pathToCheck.slice(namespace.length);
332
+ requestPath = stripped.startsWith("/") ? stripped : `/${stripped}`;
233
333
  }
234
334
 
235
335
  // Find matching route
@@ -259,7 +359,7 @@ export class CallableMockInstance {
259
359
  const params = this.extractParams(matchedRoute, requestPath);
260
360
 
261
361
  // Generate initial response from route handler
262
- const context: RequestContext = {
362
+ const context: Schmock.RequestContext = {
263
363
  method,
264
364
  path: requestPath,
265
365
  params,
@@ -269,15 +369,15 @@ export class CallableMockInstance {
269
369
  state: this.globalConfig.state || {},
270
370
  };
271
371
 
272
- let result: any;
273
- if (typeof matchedRoute.generator === "function") {
274
- result = await (matchedRoute.generator as GeneratorFunction)(context);
372
+ let result: unknown;
373
+ if (isGeneratorFunction(matchedRoute.generator)) {
374
+ result = await matchedRoute.generator(context);
275
375
  } else {
276
376
  result = matchedRoute.generator;
277
377
  }
278
378
 
279
379
  // Build plugin context
280
- let pluginContext: PluginContext = {
380
+ let pluginContext: Schmock.PluginContext = {
281
381
  path: requestPath,
282
382
  route: matchedRoute.config,
283
383
  method,
@@ -302,7 +402,7 @@ export class CallableMockInstance {
302
402
  } catch (error) {
303
403
  this.logger.log(
304
404
  "error",
305
- `[${requestId}] Plugin pipeline error: ${(error as Error).message}`,
405
+ `[${requestId}] Plugin pipeline error: ${errorMessage(error)}`,
306
406
  );
307
407
  throw error;
308
408
  }
@@ -313,6 +413,18 @@ export class CallableMockInstance {
313
413
  // Apply global delay if configured
314
414
  await this.applyDelay();
315
415
 
416
+ // Record request in history
417
+ this.requestHistory.push({
418
+ method,
419
+ path: requestPath,
420
+ params,
421
+ query: options?.query || {},
422
+ headers: options?.headers || {},
423
+ body: options?.body,
424
+ timestamp: Date.now(),
425
+ response: { status: response.status, body: response.body },
426
+ });
427
+
316
428
  // Log successful response
317
429
  this.logger.log(
318
430
  "response",
@@ -329,7 +441,7 @@ export class CallableMockInstance {
329
441
  } catch (error) {
330
442
  this.logger.log(
331
443
  "error",
332
- `[${requestId}] Error processing request: ${(error as Error).message}`,
444
+ `[${requestId}] Error processing request: ${errorMessage(error)}`,
333
445
  error,
334
446
  );
335
447
 
@@ -337,11 +449,8 @@ export class CallableMockInstance {
337
449
  const errorResponse = {
338
450
  status: 500,
339
451
  body: {
340
- error: (error as Error).message,
341
- code:
342
- error instanceof SchmockError
343
- ? (error as SchmockError).code
344
- : "INTERNAL_ERROR",
452
+ error: errorMessage(error),
453
+ code: error instanceof SchmockError ? error.code : "INTERNAL_ERROR",
345
454
  },
346
455
  headers: {},
347
456
  };
@@ -382,20 +491,18 @@ export class CallableMockInstance {
382
491
  * @returns Normalized Response object with status, body, and headers
383
492
  * @private
384
493
  */
385
- private parseResponse(result: any, routeConfig: RouteConfig): Response {
494
+ private parseResponse(
495
+ result: unknown,
496
+ routeConfig: Schmock.RouteConfig,
497
+ ): Schmock.Response {
386
498
  let status = 200;
387
- let body = result;
499
+ let body: unknown = result;
388
500
  let headers: Record<string, string> = {};
389
501
 
390
502
  let tupleFormat = false;
391
503
 
392
504
  // Handle already-formed response objects (from plugin error recovery)
393
- if (
394
- result &&
395
- typeof result === "object" &&
396
- "status" in result &&
397
- "body" in result
398
- ) {
505
+ if (isResponseObject(result)) {
399
506
  return {
400
507
  status: result.status,
401
508
  body: result.body,
@@ -404,7 +511,7 @@ export class CallableMockInstance {
404
511
  }
405
512
 
406
513
  // Handle tuple response format [status, body, headers?]
407
- if (Array.isArray(result) && typeof result[0] === "number") {
514
+ if (isStatusTuple(result)) {
408
515
  [status, body, headers = {}] = result;
409
516
  tupleFormat = true;
410
517
  }
@@ -452,13 +559,13 @@ export class CallableMockInstance {
452
559
  * @private
453
560
  */
454
561
  private async runPluginPipeline(
455
- context: PluginContext,
456
- initialResponse?: any,
457
- _routeConfig?: RouteConfig,
562
+ context: Schmock.PluginContext,
563
+ initialResponse?: unknown,
564
+ _routeConfig?: Schmock.RouteConfig,
458
565
  _requestId?: string,
459
- ): Promise<{ context: PluginContext; response?: any }> {
566
+ ): Promise<{ context: Schmock.PluginContext; response?: unknown }> {
460
567
  let currentContext = context;
461
- let response: any = initialResponse;
568
+ let response: unknown = initialResponse;
462
569
 
463
570
  this.logger.log(
464
571
  "pipeline",
@@ -497,14 +604,16 @@ export class CallableMockInstance {
497
604
  } catch (error) {
498
605
  this.logger.log(
499
606
  "pipeline",
500
- `Plugin ${plugin.name} failed: ${(error as Error).message}`,
607
+ `Plugin ${plugin.name} failed: ${errorMessage(error)}`,
501
608
  );
502
609
 
503
610
  // Try error handling if plugin has onError hook
504
611
  if (plugin.onError) {
505
612
  try {
613
+ const pluginError =
614
+ error instanceof Error ? error : new Error(errorMessage(error));
506
615
  const errorResult = await plugin.onError(
507
- error as Error,
616
+ pluginError,
508
617
  currentContext,
509
618
  );
510
619
  if (errorResult) {
@@ -512,26 +621,38 @@ export class CallableMockInstance {
512
621
  "pipeline",
513
622
  `Plugin ${plugin.name} handled error`,
514
623
  );
515
- // If error handler returns response, use it and stop pipeline
624
+
625
+ // Error return → transform the thrown error
626
+ if (errorResult instanceof Error) {
627
+ throw new PluginError(plugin.name, errorResult);
628
+ }
629
+
630
+ // ResponseResult return → recover, stop pipeline
516
631
  if (
517
632
  typeof errorResult === "object" &&
518
633
  errorResult !== null &&
519
634
  "status" in errorResult
520
635
  ) {
521
- // Return the error response as the current response, stop pipeline
522
636
  response = errorResult;
523
637
  break;
524
638
  }
525
639
  }
640
+ // void/falsy return → propagate original error below
526
641
  } catch (hookError) {
642
+ // If the hook itself threw (including our PluginError above), re-throw it
643
+ if (hookError instanceof PluginError) {
644
+ throw hookError;
645
+ }
527
646
  this.logger.log(
528
647
  "pipeline",
529
- `Plugin ${plugin.name} error handler failed: ${(hookError as Error).message}`,
648
+ `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`,
530
649
  );
531
650
  }
532
651
  }
533
652
 
534
- throw new PluginError(plugin.name, error as Error);
653
+ const cause =
654
+ error instanceof Error ? error : new Error(errorMessage(error));
655
+ throw new PluginError(plugin.name, cause);
535
656
  }
536
657
  }
537
658
 
@@ -548,21 +669,18 @@ export class CallableMockInstance {
548
669
  * @private
549
670
  */
550
671
  private findRoute(
551
- method: HttpMethod,
672
+ method: Schmock.HttpMethod,
552
673
  path: string,
553
674
  ): CompiledCallableRoute | undefined {
554
- // First pass: Look for static routes (routes without parameters)
555
- for (const route of this.routes) {
556
- if (
557
- route.method === method &&
558
- route.params.length === 0 &&
559
- route.pattern.test(path)
560
- ) {
561
- return route;
562
- }
675
+ // O(1) lookup for static routes
676
+ const normalizedPath =
677
+ path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
678
+ const staticMatch = this.staticRoutes.get(`${method} ${normalizedPath}`);
679
+ if (staticMatch) {
680
+ return staticMatch;
563
681
  }
564
682
 
565
- // Second pass: Look for parameterized routes
683
+ // Fall through to parameterized route scan
566
684
  for (const route of this.routes) {
567
685
  if (
568
686
  route.method === method &&
package/src/constants.ts CHANGED
@@ -13,7 +13,7 @@ export const HTTP_METHODS: readonly HttpMethod[] = [
13
13
  ] as const;
14
14
 
15
15
  export function isHttpMethod(method: string): method is HttpMethod {
16
- return HTTP_METHODS.includes(method as HttpMethod);
16
+ return (HTTP_METHODS as readonly string[]).includes(method);
17
17
  }
18
18
 
19
19
  export function toHttpMethod(method: string): HttpMethod {
@@ -23,3 +23,19 @@ export function toHttpMethod(method: string): HttpMethod {
23
23
  }
24
24
  return upper;
25
25
  }
26
+
27
+ /**
28
+ * Check if a value is a status tuple: [status, body] or [status, body, headers]
29
+ * Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
30
+ */
31
+ export function isStatusTuple(
32
+ value: unknown,
33
+ ): value is [number, unknown] | [number, unknown, Record<string, string>] {
34
+ return (
35
+ Array.isArray(value) &&
36
+ (value.length === 2 || value.length === 3) &&
37
+ typeof value[0] === "number" &&
38
+ value[0] >= 100 &&
39
+ value[0] <= 599
40
+ );
41
+ }
package/src/index.ts CHANGED
@@ -1,12 +1,4 @@
1
- import { CallableMockInstance as CallableMockInstanceImpl } from "./builder.js";
2
- import type {
3
- CallableMockInstance,
4
- Generator,
5
- GlobalConfig,
6
- Plugin,
7
- RouteConfig,
8
- RouteKey,
9
- } from "./types.js";
1
+ import { CallableMockInstance } from "./builder.js";
10
2
 
11
3
  /**
12
4
  * Create a new Schmock mock instance with callable API.
@@ -31,34 +23,48 @@ import type {
31
23
  * @param config Optional global configuration
32
24
  * @returns A callable mock instance
33
25
  */
34
- export function schmock(config?: GlobalConfig): CallableMockInstance {
26
+ export function schmock(
27
+ config?: Schmock.GlobalConfig,
28
+ ): Schmock.CallableMockInstance {
35
29
  // Always use new callable API
36
- const instance = new CallableMockInstanceImpl(config || {});
30
+ const instance = new CallableMockInstance(config || {});
37
31
 
38
- // Create a callable function that wraps the instance
39
- const callableInstance = ((
40
- route: RouteKey,
41
- generator: Generator,
42
- routeConfig: RouteConfig = {},
43
- ) => {
44
- instance.defineRoute(route, generator, routeConfig);
45
- return callableInstance; // Return the callable function for chaining
46
- }) as any;
32
+ // Callable proxy: a function with attached methods
33
+ const callableInstance: Schmock.CallableMockInstance = Object.assign(
34
+ (
35
+ route: Schmock.RouteKey,
36
+ generator: Schmock.Generator,
37
+ routeConfig: Schmock.RouteConfig = {},
38
+ ) => {
39
+ instance.defineRoute(route, generator, routeConfig);
40
+ return callableInstance;
41
+ },
42
+ {
43
+ pipe: (plugin: Schmock.Plugin) => {
44
+ instance.pipe(plugin);
45
+ return callableInstance;
46
+ },
47
+ handle: instance.handle.bind(instance),
48
+ history: instance.history.bind(instance),
49
+ called: instance.called.bind(instance),
50
+ callCount: instance.callCount.bind(instance),
51
+ lastRequest: instance.lastRequest.bind(instance),
52
+ reset: instance.reset.bind(instance),
53
+ resetHistory: instance.resetHistory.bind(instance),
54
+ resetState: instance.resetState.bind(instance),
55
+ },
56
+ );
47
57
 
48
- // Manually bind all instance methods to the callable function with proper return values
49
- callableInstance.pipe = (plugin: Plugin) => {
50
- instance.pipe(plugin);
51
- return callableInstance; // Return callable function for chaining
52
- };
53
- callableInstance.handle = instance.handle.bind(instance);
58
+ instance.setCallableRef(callableInstance);
54
59
 
55
- return callableInstance as CallableMockInstance;
60
+ return callableInstance;
56
61
  }
57
62
 
58
63
  // Re-export constants and utilities
59
64
  export {
60
65
  HTTP_METHODS,
61
66
  isHttpMethod,
67
+ isStatusTuple,
62
68
  ROUTE_NOT_FOUND_CODE,
63
69
  toHttpMethod,
64
70
  } from "./constants.js";
@@ -74,7 +80,6 @@ export {
74
80
  SchemaValidationError,
75
81
  SchmockError,
76
82
  } from "./errors.js";
77
-
78
83
  // Re-export types
79
84
  export type {
80
85
  CallableMockInstance,
@@ -87,6 +92,7 @@ export type {
87
92
  PluginResult,
88
93
  RequestContext,
89
94
  RequestOptions,
95
+ RequestRecord,
90
96
  Response,
91
97
  ResponseBody,
92
98
  ResponseResult,
@@ -95,8 +95,9 @@ describe("namespace functionality", () => {
95
95
  const response2 = await mock.handle("GET", "/api//users");
96
96
 
97
97
  expect(response1.body).toBe("users");
98
- // This might not match depending on implementation
99
- expect(response2.status).toBe(404);
98
+ // New logic gracefully handles double slashes by stripping the full namespace
99
+ expect(response2.status).toBe(200);
100
+ expect(response2.body).toBe("users");
100
101
  });
101
102
 
102
103
  it("handles empty namespace", async () => {