@zze/mock-server 0.2.6 → 0.6.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.
Files changed (52) hide show
  1. package/README.md +117 -28
  2. package/dist/__tests__/mock-server.test.js +58 -1
  3. package/dist/admin/__tests__/admin-router.test.js +2 -151
  4. package/dist/admin/admin-router.d.ts +4 -4
  5. package/dist/admin/admin-router.d.ts.map +1 -1
  6. package/dist/admin/admin-router.js +1 -205
  7. package/dist/admin/types.d.ts +1 -84
  8. package/dist/admin/types.d.ts.map +1 -1
  9. package/dist/admin/types.js +1 -5
  10. package/dist/config/__tests__/config-loader.test.js +18 -0
  11. package/dist/config/config-loader.d.ts +3 -3
  12. package/dist/config/config-loader.d.ts.map +1 -1
  13. package/dist/config/config-loader.js +4 -1
  14. package/dist/config/config-writer.d.ts +1 -35
  15. package/dist/config/config-writer.d.ts.map +1 -1
  16. package/dist/config/config-writer.js +0 -103
  17. package/dist/config/index.d.ts +1 -1
  18. package/dist/config/index.d.ts.map +1 -1
  19. package/dist/config/types.d.ts +9 -1
  20. package/dist/config/types.d.ts.map +1 -1
  21. package/dist/mock-server.d.ts +9 -0
  22. package/dist/mock-server.d.ts.map +1 -1
  23. package/dist/mock-server.js +3 -0
  24. package/dist/resolver/__tests__/response-resolver.test.js +134 -0
  25. package/dist/resolver/response-resolver.d.ts +18 -2
  26. package/dist/resolver/response-resolver.d.ts.map +1 -1
  27. package/dist/resolver/response-resolver.js +62 -5
  28. package/dist/resolver/types.d.ts +8 -0
  29. package/dist/resolver/types.d.ts.map +1 -1
  30. package/dist/resolver/types.js +7 -0
  31. package/dist/schemas/__tests__/endpoint.schema.test.js +48 -0
  32. package/dist/schemas/endpoint.schema.d.ts +31 -8
  33. package/dist/schemas/endpoint.schema.d.ts.map +1 -1
  34. package/dist/schemas/scenario.schema.d.ts +28 -4
  35. package/dist/schemas/scenario.schema.d.ts.map +1 -1
  36. package/dist/schemas/scenario.schema.js +23 -3
  37. package/dist/server/__tests__/mock-server-app.test.js +40 -4
  38. package/dist/server/mock-server-app.d.ts +4 -4
  39. package/dist/server/mock-server-app.d.ts.map +1 -1
  40. package/dist/server/mock-server-app.js +5 -3
  41. package/dist/server/types.d.ts +8 -0
  42. package/dist/server/types.d.ts.map +1 -1
  43. package/dist/server/types.js +1 -0
  44. package/dist/state/__tests__/state-manager.test.js +33 -0
  45. package/dist/state/state-manager.d.ts.map +1 -1
  46. package/dist/state/state-manager.js +9 -6
  47. package/dist/ui/assets/index-C2lhc6Pd.css +1 -0
  48. package/dist/ui/assets/index-D8nVOwBC.js +9 -0
  49. package/dist/ui/index.html +2 -2
  50. package/package.json +5 -3
  51. package/dist/ui/assets/index-SADMOgPE.js +0 -10
  52. package/dist/ui/assets/index-rj7m-XdG.css +0 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ScenarioConfigSchema = exports.DelaySchema = exports.ResponseBodySchema = exports.NameSchema = exports.IdSchema = void 0;
3
+ exports.ScenarioConfigSchema = exports.FilePathSchema = exports.DelaySchema = exports.ResponseBodySchema = exports.NameSchema = exports.IdSchema = void 0;
4
4
  exports.validateUniqueScenarioIds = validateUniqueScenarioIds;
5
5
  const zod_1 = require("zod");
6
6
  const http_schema_js_1 = require("./http.schema.js");
@@ -43,17 +43,37 @@ exports.DelaySchema = zod_1.z
43
43
  .max(30000)
44
44
  .optional()
45
45
  .describe('Response delay in milliseconds');
46
+ /**
47
+ * File path for file download responses
48
+ * Relative to the config directory
49
+ */
50
+ exports.FilePathSchema = zod_1.z
51
+ .string()
52
+ .min(1)
53
+ .max(1000)
54
+ .describe('File path relative to the config directory (e.g., "./assets/report.pdf")');
46
55
  /**
47
56
  * Scenario configuration schema
48
57
  * Defines a single response scenario for an endpoint
49
58
  */
50
- exports.ScenarioConfigSchema = zod_1.z.object({
59
+ exports.ScenarioConfigSchema = zod_1.z
60
+ .object({
51
61
  id: exports.IdSchema.describe('Unique scenario identifier'),
52
62
  name: exports.NameSchema.describe('Human-readable scenario name'),
53
63
  status: http_schema_js_1.HttpStatusCodeSchema,
54
64
  headers: http_schema_js_1.HttpHeadersSchema,
55
- body: exports.ResponseBodySchema.describe('Response body'),
65
+ body: exports.ResponseBodySchema.describe('Response body').optional(),
66
+ file: exports.FilePathSchema.optional(),
56
67
  delay: exports.DelaySchema,
68
+ })
69
+ .superRefine((data, ctx) => {
70
+ if (data.body !== undefined && data.file !== undefined) {
71
+ ctx.addIssue({
72
+ code: zod_1.z.ZodIssueCode.custom,
73
+ message: `Scenario "${data.id}": cannot specify both 'body' and 'file' — use one or the other`,
74
+ path: ['file'],
75
+ });
76
+ }
57
77
  });
58
78
  /**
59
79
  * Validates that scenario IDs are unique within an array
@@ -267,14 +267,13 @@ const mockFlows = [
267
267
  .expect(200);
268
268
  (0, vitest_1.expect)(res.body).toEqual({ new: true });
269
269
  });
270
- (0, vitest_1.it)('should reset state on reload', async () => {
270
+ (0, vitest_1.it)('should preserve valid scenario overrides on reload', async () => {
271
271
  serverApp.getStateManager().activateScenario('get-users', 'error');
272
272
  serverApp.reloadConfig(mockEndpoints, mockFlows);
273
- // State should be reset, so default scenario should be active
274
273
  const res = await (0, supertest_1.default)(serverApp.getApp())
275
274
  .get('/api/users')
276
- .expect(200);
277
- (0, vitest_1.expect)(res.body.length).toBe(2);
275
+ .expect(500);
276
+ (0, vitest_1.expect)(res.body).toEqual({ error: 'Internal Server Error' });
278
277
  });
279
278
  (0, vitest_1.it)('should remove old routes on reload', async () => {
280
279
  serverApp.reloadConfig([], []);
@@ -294,3 +293,40 @@ const mockFlows = [
294
293
  (0, vitest_1.expect)(app.getOptions().port).toBe(5000);
295
294
  });
296
295
  });
296
+ (0, vitest_1.describe)('multipart upload support', () => {
297
+ let serverApp;
298
+ const uploadEndpoint = {
299
+ id: 'upload-avatar',
300
+ path: '/users/:id/avatar',
301
+ method: 'POST',
302
+ scenarios: [
303
+ { id: 'success', name: 'Uploaded', status: 200, body: { url: '/cdn/avatar.jpg' } },
304
+ { id: 'too-large', name: 'Too Large', status: 413, body: { error: 'File too large' } },
305
+ ],
306
+ defaultScenarioId: 'success',
307
+ };
308
+ (0, vitest_1.beforeEach)(() => {
309
+ serverApp = new mock_server_app_js_1.MockServerApp({ port: 0 });
310
+ serverApp.registerConfig([uploadEndpoint], []);
311
+ });
312
+ (0, vitest_1.afterEach)(async () => {
313
+ if (serverApp.isRunning()) {
314
+ await serverApp.stop();
315
+ }
316
+ });
317
+ (0, vitest_1.it)('should accept multipart/form-data and return active scenario', async () => {
318
+ const res = await (0, supertest_1.default)(serverApp.getApp())
319
+ .post('/api/users/123/avatar')
320
+ .attach('file', Buffer.from('fake image data'), { filename: 'avatar.png', contentType: 'image/png' })
321
+ .expect(200);
322
+ (0, vitest_1.expect)(res.body).toEqual({ url: '/cdn/avatar.jpg' });
323
+ });
324
+ (0, vitest_1.it)('should return the active scenario regardless of uploaded file', async () => {
325
+ serverApp.getStateManager().activateScenario('upload-avatar', 'too-large');
326
+ const res = await (0, supertest_1.default)(serverApp.getApp())
327
+ .post('/api/users/123/avatar')
328
+ .attach('file', Buffer.from('big file'), { filename: 'big.png', contentType: 'image/png' })
329
+ .expect(413);
330
+ (0, vitest_1.expect)(res.body).toEqual({ error: 'File too large' });
331
+ });
332
+ });
@@ -1,9 +1,9 @@
1
1
  import { Express } from 'express';
2
2
  import type { Server } from 'http';
3
- import type { EndpointConfig, FlowConfig } from '../schemas/index.js';
3
+ import type { FlowConfig } from '../schemas/index.js';
4
4
  import { StateManager } from '../state/index.js';
5
5
  import { ResponseResolver } from '../resolver/index.js';
6
- import type { ConfigWriter } from '../config/index.js';
6
+ import type { ConfigWriter, RuntimeEndpointConfig } from '../config/index.js';
7
7
  import type { ServerOptions, ServerState, RegisteredRoute } from './types.js';
8
8
  export declare const ADMIN_UI_PATH = "/mock-admin";
9
9
  /**
@@ -70,7 +70,7 @@ export declare class MockServerApp {
70
70
  /**
71
71
  * Register endpoints and flows
72
72
  */
73
- registerConfig(endpoints: EndpointConfig[], flows: FlowConfig[]): void;
73
+ registerConfig(endpoints: RuntimeEndpointConfig[], flows: FlowConfig[]): void;
74
74
  /**
75
75
  * Register routes for all endpoints
76
76
  */
@@ -110,7 +110,7 @@ export declare class MockServerApp {
110
110
  /**
111
111
  * Reload configuration (for hot reload support)
112
112
  */
113
- reloadConfig(endpoints: EndpointConfig[], flows: FlowConfig[]): void;
113
+ reloadConfig(endpoints: RuntimeEndpointConfig[], flows: FlowConfig[]): void;
114
114
  }
115
115
  /**
116
116
  * Create a new MockServerApp instance
@@ -1 +1 @@
1
- {"version":3,"file":"mock-server-app.d.ts","sourceRoot":"","sources":["../../src/server/mock-server-app.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,OAAO,EAA2C,MAAM,SAAS,CAAC;AACpF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAsB,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAA0B,MAAM,sBAAsB,CAAC;AAEhF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI9E,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAE3C;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAU;IACrB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,cAAc,CAAC,CAAsB;gBAEjC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM;IAahD;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAiCpB;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;OAEG;IACH,eAAe,IAAI,YAAY;IAI/B;;OAEG;IACH,WAAW,IAAI,gBAAgB;IAI/B;;OAEG;IACH,UAAU,IAAI,aAAa;IAI3B;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAIxC;;OAEG;IACH,eAAe,CAAC,YAAY,EAAE,YAAY,EAAE,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAKvF;;OAEG;IACH,QAAQ,IAAI,WAAW;IASvB;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI;IAetE;;OAEG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAwBxB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAgB7B;;OAEG;IACH,GAAG,CACD,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAClC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAI7B;;OAEG;IACG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyB3C;;OAEG;IACH,iBAAiB,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAexE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;IAI9B;;OAEG;IACH,YAAY,CAAC,SAAS,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI;CAKrE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GACnC,aAAa,CAEf"}
1
+ {"version":3,"file":"mock-server-app.d.ts","sourceRoot":"","sources":["../../src/server/mock-server-app.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,OAAO,EAA2C,MAAM,SAAS,CAAC;AACpF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,OAAO,KAAK,EAAkB,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAsB,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAA0B,MAAM,sBAAsB,CAAC;AAEhF,OAAO,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI9E,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAE3C;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAU;IACrB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,cAAc,CAAC,CAAsB;gBAEjC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM;IAahD;;OAEG;IACH,OAAO,CAAC,eAAe;IASvB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAiCpB;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;OAEG;IACH,eAAe,IAAI,YAAY;IAI/B;;OAEG;IACH,WAAW,IAAI,gBAAgB;IAI/B;;OAEG;IACH,UAAU,IAAI,aAAa;IAI3B;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAIxC;;OAEG;IACH,eAAe,CAAC,YAAY,EAAE,YAAY,EAAE,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAKvF;;OAEG;IACH,QAAQ,IAAI,WAAW;IASvB;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,qBAAqB,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI;IAgB7E;;OAEG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAwBxB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAgB7B;;OAEG;IACH,GAAG,CACD,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAClC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAI7B;;OAEG;IACG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyB3C;;OAEG;IACH,iBAAiB,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAexE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;IAI9B;;OAEG;IACH,YAAY,CAAC,SAAS,EAAE,qBAAqB,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI;CAG5E;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GACnC,aAAa,CAEf"}
@@ -7,6 +7,7 @@ exports.MockServerApp = exports.ADMIN_UI_PATH = void 0;
7
7
  exports.createMockServerApp = createMockServerApp;
8
8
  const express_1 = __importDefault(require("express"));
9
9
  const path_1 = __importDefault(require("path"));
10
+ const multer_1 = __importDefault(require("multer"));
10
11
  const index_js_1 = require("../state/index.js");
11
12
  const index_js_2 = require("../resolver/index.js");
12
13
  const index_js_3 = require("../admin/index.js");
@@ -25,7 +26,7 @@ class MockServerApp {
25
26
  this.options = { ...types_js_1.DEFAULT_SERVER_OPTIONS, ...options };
26
27
  this.app = (0, express_1.default)();
27
28
  this.stateManager = (0, index_js_1.createStateManager)();
28
- this.resolver = (0, index_js_2.createResponseResolver)(this.stateManager);
29
+ this.resolver = (0, index_js_2.createResponseResolver)(this.stateManager, this.options.configPath, this.options.globalDelay);
29
30
  this.mockRouter = express_1.default.Router();
30
31
  this.setupMiddleware();
31
32
  this.mountAdminRouter();
@@ -40,6 +41,8 @@ class MockServerApp {
40
41
  this.app.use(express_1.default.json());
41
42
  // Parse URL-encoded bodies
42
43
  this.app.use(express_1.default.urlencoded({ extended: true }));
44
+ // Parse multipart/form-data (file uploads) — files are discarded, fields are parsed
45
+ this.app.use((0, multer_1.default)({ storage: multer_1.default.memoryStorage() }).any());
43
46
  }
44
47
  /**
45
48
  * Mount the admin API router
@@ -153,6 +156,7 @@ class MockServerApp {
153
156
  this.stateManager.setFlows(flows);
154
157
  // Update resolver
155
158
  this.resolver.setEndpoints(endpoints);
159
+ this.resolver.setConfigPath(this.options.configPath);
156
160
  // Register routes
157
161
  this.registerRoutes(endpoints);
158
162
  }
@@ -290,8 +294,6 @@ class MockServerApp {
290
294
  * Reload configuration (for hot reload support)
291
295
  */
292
296
  reloadConfig(endpoints, flows) {
293
- // Reset state before reloading
294
- this.stateManager.reset();
295
297
  this.registerConfig(endpoints, flows);
296
298
  }
297
299
  }
@@ -8,6 +8,14 @@ export interface ServerOptions {
8
8
  apiPrefix: string;
9
9
  /** Enable hot reload (default: true) */
10
10
  hotReload: boolean;
11
+ /** Base directory for resolving relative file paths in scenarios (default: '') */
12
+ configPath: string;
13
+ /**
14
+ * Global delay applied to every response when the scenario has no explicit `delay`.
15
+ * A number is a fixed ms value; a tuple [min, max] produces a random ms value in that range.
16
+ * Scenario-level delay always takes precedence.
17
+ */
18
+ globalDelay?: number | [number, number];
11
19
  }
12
20
  /**
13
21
  * Default server options
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAIpC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAKpC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -8,4 +8,5 @@ exports.DEFAULT_SERVER_OPTIONS = {
8
8
  port: 3001,
9
9
  apiPrefix: '/api',
10
10
  hotReload: true,
11
+ configPath: '',
11
12
  };
@@ -67,6 +67,39 @@ const mockFlows = [
67
67
  (0, vitest_1.expect)(state.activeScenarios).toEqual({ 'get-users': 'error' });
68
68
  });
69
69
  });
70
+ (0, vitest_1.describe)('configuration reconciliation', () => {
71
+ (0, vitest_1.it)('should preserve a valid scenario override when endpoints are replaced', () => {
72
+ stateManager.activateScenario('get-users', 'error');
73
+ stateManager.setEndpoints(mockEndpoints);
74
+ (0, vitest_1.expect)(stateManager.getActiveScenarioId('get-users')).toBe('error');
75
+ });
76
+ (0, vitest_1.it)('should remove an override when its endpoint is removed', () => {
77
+ stateManager.activateScenario('get-users', 'error');
78
+ stateManager.setEndpoints([mockEndpoints[1]]);
79
+ (0, vitest_1.expect)(stateManager.getActiveScenarioId('get-users')).toBeUndefined();
80
+ });
81
+ (0, vitest_1.it)('should remove an override when its scenario is removed', () => {
82
+ stateManager.activateScenario('get-users', 'error');
83
+ const endpointsWithoutError = mockEndpoints.map((endpoint) => endpoint.id === 'get-users'
84
+ ? {
85
+ ...endpoint,
86
+ scenarios: endpoint.scenarios.filter((scenario) => scenario.id !== 'error'),
87
+ }
88
+ : endpoint);
89
+ stateManager.setEndpoints(endpointsWithoutError);
90
+ (0, vitest_1.expect)(stateManager.getActiveScenarioId('get-users')).toBeUndefined();
91
+ });
92
+ (0, vitest_1.it)('should preserve the active flow when flows are replaced', () => {
93
+ stateManager.activateFlow('admin-flow');
94
+ stateManager.setFlows(mockFlows);
95
+ (0, vitest_1.expect)(stateManager.getActiveFlowId()).toBe('admin-flow');
96
+ });
97
+ (0, vitest_1.it)('should clear the active flow when it is removed', () => {
98
+ stateManager.activateFlow('admin-flow');
99
+ stateManager.setFlows([mockFlows[1]]);
100
+ (0, vitest_1.expect)(stateManager.getActiveFlowId()).toBeNull();
101
+ });
102
+ });
70
103
  (0, vitest_1.describe)('activateScenario', () => {
71
104
  (0, vitest_1.it)('should activate a valid scenario', () => {
72
105
  stateManager.activateScenario('get-users', 'error');
@@ -1 +1 @@
1
- {"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EACV,oBAAoB,EAEpB,mBAAmB,EAEpB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,YAAY;IACvB,6DAA6D;IAC7D,OAAO,CAAC,eAAe,CAAkC;IAEzD,+BAA+B;IAC/B,OAAO,CAAC,YAAY,CAAuB;IAE3C,0CAA0C;IAC1C,OAAO,CAAC,SAAS,CAA0C;IAE3D,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAsC;IAEnD,6BAA6B;IAC7B,OAAO,CAAC,SAAS,CAAuC;IAExD;;OAEG;IACH,YAAY,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI;IAO/C;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI;IAOnC;;OAEG;IACH,QAAQ,IAAI,oBAAoB;IAOhC;;OAEG;IACH,eAAe,IAAI,MAAM,GAAG,IAAI;IAIhC;;OAEG;IACH,aAAa,IAAI,UAAU,GAAG,IAAI;IAKlC;;;OAGG;IACH,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI3D;;;OAGG;IACH,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAsBpD;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI;IAU/E;;;OAGG;IACH,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAkB9D;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIvC;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAalC;;OAEG;IACH,cAAc,IAAI,IAAI;IAQtB;;;OAGG;IACH,KAAK,IAAI,IAAI;IAMb;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAIhD;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAInD;;OAEG;IACH,OAAO,CAAC,IAAI;CAmBb;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,YAAY,CAEjD"}
1
+ {"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EACV,oBAAoB,EAEpB,mBAAmB,EAEpB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,YAAY;IACvB,6DAA6D;IAC7D,OAAO,CAAC,eAAe,CAAkC;IAEzD,+BAA+B;IAC/B,OAAO,CAAC,YAAY,CAAuB;IAE3C,0CAA0C;IAC1C,OAAO,CAAC,SAAS,CAA0C;IAE3D,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAsC;IAEnD,6BAA6B;IAC7B,OAAO,CAAC,SAAS,CAAuC;IAExD;;OAEG;IACH,YAAY,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI;IAW/C;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI;IAQnC;;OAEG;IACH,QAAQ,IAAI,oBAAoB;IAOhC;;OAEG;IACH,eAAe,IAAI,MAAM,GAAG,IAAI;IAIhC;;OAEG;IACH,aAAa,IAAI,UAAU,GAAG,IAAI;IAKlC;;;OAGG;IACH,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI3D;;;OAGG;IACH,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAsBpD;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI;IAU/E;;;OAGG;IACH,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAkB9D;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIvC;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAalC;;OAEG;IACH,cAAc,IAAI,IAAI;IAQtB;;;OAGG;IACH,KAAK,IAAI,IAAI;IAMb;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAIhD;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAInD;;OAEG;IACH,OAAO,CAAC,IAAI;CAmBb;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,YAAY,CAEjD"}
@@ -23,18 +23,21 @@ class StateManager {
23
23
  * Register endpoints for scenario validation
24
24
  */
25
25
  setEndpoints(endpoints) {
26
- this.endpoints.clear();
27
- for (const endpoint of endpoints) {
28
- this.endpoints.set(endpoint.id, endpoint);
26
+ this.endpoints = new Map(endpoints.map((endpoint) => [endpoint.id, endpoint]));
27
+ for (const [endpointId, scenarioId] of this.activeScenarios) {
28
+ const endpoint = this.endpoints.get(endpointId);
29
+ if (!endpoint?.scenarios.some((scenario) => scenario.id === scenarioId)) {
30
+ this.activeScenarios.delete(endpointId);
31
+ }
29
32
  }
30
33
  }
31
34
  /**
32
35
  * Register flows for validation
33
36
  */
34
37
  setFlows(flows) {
35
- this.flows.clear();
36
- for (const flow of flows) {
37
- this.flows.set(flow.id, flow);
38
+ this.flows = new Map(flows.map((flow) => [flow.id, flow]));
39
+ if (this.activeFlowId && !this.flows.has(this.activeFlowId)) {
40
+ this.activeFlowId = null;
38
41
  }
39
42
  }
40
43
  /**
@@ -0,0 +1 @@
1
+ :root{--primary: #4f46e5;--primary-hover: #4338ca;--success: #10b981;--warning: #f59e0b;--error: #ef4444;--bg: #f5f5f5;--card-bg: #ffffff;--border: #e5e7eb;--text: #1f2937;--text-muted: #6b7280}*{box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif;background:var(--bg);color:var(--text);margin:0;padding:0}.app{max-width:1200px;margin:0 auto;padding:20px}.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}.header h1{font-size:24px;font-weight:600;color:var(--text)}.header-actions{display:flex;gap:12px}.btn{padding:8px 16px;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:all .15s ease}.btn-primary{background:var(--primary);color:#fff}.btn-primary:hover{background:var(--primary-hover)}.btn-secondary{background:var(--card-bg);color:var(--text);border:1px solid var(--border)}.btn-secondary:hover{background:var(--bg)}.btn-danger{background:var(--error);color:#fff}.btn-danger:hover{background:#dc2626}.btn-sm{padding:4px 10px;font-size:12px}.grid{display:grid;grid-template-columns:1fr 300px;gap:24px}.main-content{display:flex;flex-direction:column;gap:24px}.card{background:var(--card-bg);border-radius:8px;border:1px solid var(--border);overflow:hidden}.card-header{padding:16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}.card-header h2{font-size:16px;font-weight:600}.card-body{padding:16px}.endpoint-groups{display:flex;flex-direction:column;gap:12px}.endpoints-search{padding:10px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;position:relative}.endpoints-search-input{flex:1;min-width:0;padding:8px 32px 8px 10px;border:1px solid var(--border);border-radius:4px;font-size:13px}.endpoints-search-input:focus{outline:none;border-color:var(--primary)}.endpoints-search-clear{position:absolute;right:146px;border:none;background:none;color:var(--text-muted);cursor:pointer;font-size:18px;line-height:1}.endpoint-filter-toggle{display:flex;align-items:center;gap:6px;white-space:nowrap;color:var(--text-muted);font-size:12px;cursor:pointer}.endpoint-filter-toggle input{accent-color:var(--primary)}.endpoint-group{border:1px solid var(--border);border-radius:6px;overflow:hidden}.endpoint-group-header{display:flex;align-items:center;gap:8px;padding:10px 12px;background:var(--bg-secondary);cursor:pointer;-webkit-user-select:none;user-select:none}.endpoint-group-header:hover{background:#e5e7eb}.endpoint-group-toggle{width:12px;color:var(--text-muted);font-size:10px}.endpoint-group-name{flex:1;min-width:0;overflow-wrap:anywhere;font-family:SF Mono,Monaco,monospace;font-size:13px;font-weight:600}.endpoint-group-count{padding:2px 6px;border-radius:10px;background:var(--bg-primary);color:var(--text-muted);font-size:11px;font-weight:500}.endpoint-group-overrides{color:#b45309;font-size:11px;font-weight:600}.endpoint-group-items{display:flex;flex-direction:column;gap:8px;padding:8px}.endpoint-item{border:1px solid var(--border);border-radius:6px;overflow:hidden}.endpoint-item.has-override{border-color:var(--warning)}.endpoint-item.has-override .endpoint-header{background:#fffbeb}.endpoint-header{padding:12px 16px;background:var(--bg);display:flex;justify-content:space-between;align-items:center;cursor:pointer}.endpoint-header:hover{background:#e5e7eb}.endpoint-info{display:flex;align-items:center;gap:12px}.method-badge{padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;text-transform:uppercase}.method-GET{background:#dbeafe;color:#1d4ed8}.method-POST{background:#dcfce7;color:#16a34a}.method-PUT{background:#fef3c7;color:#d97706}.method-DELETE{background:#fee2e2;color:#dc2626}.method-PATCH{background:#f3e8ff;color:#9333ea}.endpoint-path{font-family:SF Mono,Monaco,monospace;font-size:13px}.endpoint-id{font-size:12px;color:var(--text-muted)}.scenarios-list{padding:12px 16px;border-top:1px solid var(--border)}.scenario-item{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border)}.scenario-item:last-child{border-bottom:none}.scenario-info{display:flex;align-items:center;gap:8px}.scenario-name{font-size:14px}.badge{padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase}.badge-default{background:#dbeafe;color:#1d4ed8}.badge-active{background:#dcfce7;color:#16a34a}.badge-override{background:#fef3c7;color:#b45309}.sidebar{display:flex;flex-direction:column;gap:24px}.state-panel .state-item{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border)}.state-panel .state-item:last-child{border-bottom:none}.state-label{font-size:13px;color:var(--text-muted)}.state-value{font-size:13px;font-weight:500}.override-list{display:flex;flex-direction:column;gap:6px;padding-top:10px}.override-empty{color:var(--text-muted);font-size:12px;line-height:1.4}.override-item{width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--card-bg);color:var(--text);text-align:left;cursor:pointer}.override-item:hover{border-color:var(--warning);background:#fffbeb}.override-summary,.override-ids{display:block;overflow-wrap:anywhere}.override-summary{display:flex;justify-content:space-between;gap:8px;font-size:12px;font-weight:600}.override-scenario{color:#b45309;text-align:right}.override-ids{margin-top:3px;color:var(--text-muted);font-family:SF Mono,Monaco,monospace;font-size:10px}.flow-list{display:flex;flex-direction:column;gap:8px}.flow-item{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border:1px solid var(--border);border-radius:6px;cursor:pointer;transition:all .15s ease}.flow-item:hover{border-color:var(--primary)}.flow-item.active{border-color:var(--success);background:#f0fdf4}.flow-name{font-size:14px;font-weight:500}.flow-description{font-size:12px;color:var(--text-muted);margin-top:2px}.flows-card{display:flex;flex-direction:column;max-height:600px}.flows-body{flex:1;overflow-y:auto;min-height:0}.active-flow-banner{padding:8px 12px;background:#f0fdf4;border-bottom:1px solid #bbf7d0;display:flex;align-items:center;gap:8px;font-size:13px}.active-flow-label{color:var(--success);font-weight:600}.active-flow-name{font-weight:500}.active-flow-category{color:var(--text-muted)}.flows-search{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;position:relative}.flows-search-input{flex:1;padding:6px 28px 6px 10px;border:1px solid var(--border);border-radius:4px;font-size:13px}.flows-search-input:focus{outline:none;border-color:var(--primary)}.flows-search-clear{position:absolute;right:20px;background:none;border:none;font-size:18px;color:var(--text-muted);cursor:pointer;padding:0 4px;line-height:1}.flows-search-clear:hover{color:var(--text)}.flow-groups{display:flex;flex-direction:column}.flow-group{border-bottom:1px solid var(--border)}.flow-group:last-child{border-bottom:none}.flow-group-header{display:flex;align-items:center;gap:8px;padding:10px 12px;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--bg-primary);transition:background .15s ease}.flow-group-header:hover{background:var(--bg-secondary)}.flow-group-header.has-active{background:#f0fdf4}.flow-group-toggle{font-size:10px;color:var(--text-muted);width:12px}.flow-group-name{flex:1;font-weight:600;font-size:13px}.flow-group-count{background:var(--bg-secondary);color:var(--text-muted);font-size:11px;padding:2px 6px;border-radius:10px;font-weight:500}.flow-group-items{display:flex;flex-direction:column;gap:4px;padding:8px 12px;background:var(--bg-secondary)}.flow-group-items .flow-item{background:var(--bg-primary);padding:8px 10px}.flow-item-content{flex:1;min-width:0}.flow-meta{display:flex;gap:8px;margin-top:4px}.flow-endpoints{font-size:11px;color:var(--text-muted)}.loading{display:flex;justify-content:center;align-items:center;padding:40px;color:var(--text-muted)}.error{padding:16px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:var(--error)}.empty{padding:24px;text-align:center;color:var(--text-muted)}@media(max-width:900px){.grid{grid-template-columns:1fr}.sidebar{order:-1}.endpoints-search{flex-wrap:wrap}.endpoints-search-clear{right:24px;top:16px}.endpoint-filter-toggle{width:100%}}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-success{background:var(--success);color:#fff}.btn-success:hover:not(:disabled){background:#059669}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;justify-content:center;align-items:center;z-index:1000;padding:20px}.modal{background:var(--card-bg);border-radius:12px;box-shadow:0 20px 40px #0003;width:100%;max-width:700px;max-height:90vh;display:flex;flex-direction:column;overflow:hidden}.modal-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}.modal-header h2{font-size:18px;font-weight:600;margin:0}.modal-close{background:none;border:none;font-size:24px;cursor:pointer;color:var(--text-muted);padding:0;line-height:1}.modal-close:hover{color:var(--text)}.modal-body{padding:20px;overflow-y:auto;flex:1}.modal-footer{padding:16px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:12px}.modal-error{margin-top:12px;padding:10px 12px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:var(--error);font-size:13px}.endpoint-actions{display:flex;gap:6px;margin-left:auto}.btn-icon{padding:4px 8px;font-size:12px;background:transparent;border:1px solid var(--border);border-radius:4px;cursor:pointer;color:var(--text-muted)}.btn-icon:hover{background:var(--bg);color:var(--text)}.btn-icon.btn-danger{border-color:transparent}.btn-icon.btn-danger:hover{background:#fef2f2;color:var(--error);border-color:#fecaca}.confirm-dialog{text-align:center}.confirm-dialog p{margin:0 0 20px;font-size:15px;color:var(--text)}.confirm-dialog .btn-group{display:flex;justify-content:center;gap:12px}.flow-item.has-actions{cursor:default}.flow-item.has-actions .flow-item-content{cursor:pointer}.flow-item-actions{display:flex;align-items:center;gap:8px}.flow-action-buttons{display:flex;gap:4px;opacity:0;transition:opacity .15s ease}.flow-item:hover .flow-action-buttons{opacity:1}.btn-danger-icon:hover{color:var(--error)}.card-header-actions{display:flex;gap:8px}.modal-large{max-width:900px}.flow-editor-modal .modal-body{display:flex;flex-direction:column;gap:20px}.flow-editor-fields{display:flex;flex-direction:column;gap:16px}.form-row{display:flex;gap:16px}.form-group{flex:1;display:flex;flex-direction:column;gap:6px}.form-group-wide{flex:2}.form-group label{font-size:13px;font-weight:500;color:var(--text)}.form-group input{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:14px;transition:border-color .15s ease;box-sizing:border-box}.form-group input:focus{outline:none;border-color:var(--primary)}.form-group input.disabled{background:var(--bg-secondary);color:var(--text-muted);cursor:not-allowed}.form-hint{font-size:11px;color:var(--text-muted)}.required{color:var(--error)}.category-input,.category-input-wrapper{position:relative}.category-input-wrapper input{width:100%}.category-suggestions{position:absolute;top:100%;left:0;right:0;background:var(--card-bg);border:1px solid var(--border);border-radius:6px;box-shadow:0 4px 12px #0000001a;z-index:10;max-height:150px;overflow-y:auto;margin-top:4px}.category-suggestion{display:block;width:100%;text-align:left;padding:8px 12px;background:none;border:none;font-size:13px;cursor:pointer}.category-suggestion:hover{background:var(--bg-secondary)}.flow-editor-picker{border-top:1px solid var(--border);padding-top:20px}.flow-editor-picker h4{margin:0 0 4px;font-size:15px;font-weight:600}.picker-description{font-size:13px;color:var(--text-muted);margin:0 0 12px}.endpoint-scenario-picker{border:1px solid var(--border);border-radius:8px;overflow:hidden}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--bg-secondary);border-bottom:1px solid var(--border)}.picker-stats{font-size:13px;color:var(--text-muted)}.picker-actions{display:flex;gap:8px}.picker-search{padding:8px 12px;border-bottom:1px solid var(--border);position:relative}.picker-search-input{width:100%;padding:6px 28px 6px 10px;border:1px solid var(--border);border-radius:4px;font-size:13px}.picker-search-input:focus{outline:none;border-color:var(--primary)}.picker-search-clear{position:absolute;right:20px;top:50%;transform:translateY(-50%);background:none;border:none;font-size:18px;color:var(--text-muted);cursor:pointer;padding:0 4px;line-height:1}.picker-list{max-height:300px;overflow-y:auto}.picker-empty{padding:24px;text-align:center;color:var(--text-muted)}.picker-endpoint{border-bottom:1px solid var(--border)}.picker-endpoint:last-child{border-bottom:none}.picker-endpoint.has-selection{background:#f0fdf4}.picker-endpoint-header{display:flex;align-items:center;gap:8px;padding:10px 12px;cursor:pointer;-webkit-user-select:none;user-select:none}.picker-endpoint-header:hover{background:var(--bg-secondary)}.picker-toggle{font-size:10px;color:var(--text-muted);width:12px}.picker-endpoint-path{font-family:SF Mono,Monaco,monospace;font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.picker-selected-badge{font-size:11px;padding:2px 6px;background:var(--success);color:#fff;border-radius:4px}.picker-scenarios{padding:8px 12px 8px 32px;background:var(--bg-secondary);display:flex;flex-direction:column;gap:6px}.picker-scenario{display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--card-bg);border:1px solid var(--border);border-radius:4px;cursor:pointer;transition:all .15s ease}.picker-scenario:hover{border-color:var(--primary)}.picker-scenario.selected{border-color:var(--success);background:#f0fdf4}.picker-scenario input[type=radio]{margin:0}.picker-scenario-name{font-size:13px;font-weight:500}.picker-scenario-id{font-size:11px;color:var(--text-muted);font-family:SF Mono,Monaco,monospace}.picker-scenario-default{font-size:10px;padding:1px 4px;background:var(--primary);color:#fff;border-radius:3px;margin-left:auto}.picker-remove{padding:4px 8px;color:var(--text-muted);font-size:12px}.btn-link{background:none;border:none;color:var(--primary);cursor:pointer;text-decoration:none}.btn-link:hover{text-decoration:underline}