@scenarist/core 0.2.1 → 0.3.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 (55) hide show
  1. package/dist/adapters/console-logger.d.ts +40 -0
  2. package/dist/adapters/console-logger.d.ts.map +1 -0
  3. package/dist/adapters/console-logger.js +193 -0
  4. package/dist/adapters/in-memory-state-manager.d.ts.map +1 -1
  5. package/dist/adapters/in-memory-state-manager.js +3 -0
  6. package/dist/adapters/index.d.ts +2 -0
  7. package/dist/adapters/index.d.ts.map +1 -1
  8. package/dist/adapters/index.js +2 -0
  9. package/dist/adapters/noop-logger.d.ts +26 -0
  10. package/dist/adapters/noop-logger.d.ts.map +1 -0
  11. package/dist/adapters/noop-logger.js +26 -0
  12. package/dist/contracts/framework-adapter.d.ts +18 -0
  13. package/dist/contracts/framework-adapter.d.ts.map +1 -1
  14. package/dist/domain/config-builder.d.ts.map +1 -1
  15. package/dist/domain/config-builder.js +13 -0
  16. package/dist/domain/deep-equals.d.ts.map +1 -1
  17. package/dist/domain/deep-equals.js +2 -0
  18. package/dist/domain/index.d.ts +1 -0
  19. package/dist/domain/index.d.ts.map +1 -1
  20. package/dist/domain/index.js +1 -0
  21. package/dist/domain/log-events.d.ts +67 -0
  22. package/dist/domain/log-events.d.ts.map +1 -0
  23. package/dist/domain/log-events.js +70 -0
  24. package/dist/domain/path-extraction.js +1 -0
  25. package/dist/domain/regex-matching.d.ts.map +1 -1
  26. package/dist/domain/regex-matching.js +1 -0
  27. package/dist/domain/response-selector.d.ts +2 -1
  28. package/dist/domain/response-selector.d.ts.map +1 -1
  29. package/dist/domain/response-selector.js +89 -8
  30. package/dist/domain/scenario-manager.d.ts +4 -2
  31. package/dist/domain/scenario-manager.d.ts.map +1 -1
  32. package/dist/domain/scenario-manager.js +42 -24
  33. package/dist/domain/state-condition-evaluator.js +1 -0
  34. package/dist/domain/template-replacement.d.ts.map +1 -1
  35. package/dist/domain/template-replacement.js +3 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/ports/driven/logger.d.ts +119 -0
  40. package/dist/ports/driven/logger.d.ts.map +1 -0
  41. package/dist/ports/driven/logger.js +1 -0
  42. package/dist/ports/driven/response-selector.d.ts +3 -1
  43. package/dist/ports/driven/response-selector.d.ts.map +1 -1
  44. package/dist/ports/driven/response-selector.js +1 -0
  45. package/dist/ports/index.d.ts +1 -0
  46. package/dist/ports/index.d.ts.map +1 -1
  47. package/dist/types/config.d.ts +29 -0
  48. package/dist/types/config.d.ts.map +1 -1
  49. package/dist/types/errors.d.ts +49 -0
  50. package/dist/types/errors.d.ts.map +1 -0
  51. package/dist/types/errors.js +26 -0
  52. package/dist/types/index.d.ts +3 -1
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/dist/types/index.js +2 -1
  55. package/package.json +3 -3
@@ -1,9 +1,11 @@
1
- import { ResponseSelectionError } from "../ports/driven/response-selector.js";
1
+ import { ScenaristError, ErrorCodes } from "../types/errors.js";
2
2
  import { extractFromPath } from "./path-extraction.js";
3
3
  import { applyTemplates } from "./template-replacement.js";
4
4
  import { matchesRegex } from "./regex-matching.js";
5
5
  import { createStateResponseResolver } from "./state-response-resolver.js";
6
6
  import { deepEquals } from "./deep-equals.js";
7
+ import { noOpLogger } from "../adapters/index.js";
8
+ import { LogCategories, LogEvents } from "./log-events.js";
7
9
  const SPECIFICITY_RANGES = {
8
10
  MATCH_CRITERIA_BASE: 100,
9
11
  SEQUENCE_FALLBACK: 1,
@@ -21,26 +23,41 @@ const SPECIFICITY_RANGES = {
21
23
  * @param options.stateManager - Optional state manager for capture/injection (Phase 3)
22
24
  */
23
25
  export const createResponseSelector = (options = {}) => {
24
- const { sequenceTracker, stateManager } = options;
26
+ const { sequenceTracker, stateManager, logger = noOpLogger } = options;
25
27
  return {
26
28
  selectResponse(testId, scenarioId, context, mocks) {
29
+ const logContext = { testId, scenarioId };
30
+ // Log the number of candidate mocks
31
+ logger.debug(LogCategories.MATCHING, LogEvents.MOCK_CANDIDATES_FOUND, logContext, {
32
+ count: mocks.length,
33
+ });
27
34
  let bestMatch = null;
35
+ // Track if we skipped any exhausted sequences (for better error messages)
36
+ let skippedExhaustedSequences = false;
28
37
  // Find all matching mocks and score them by specificity
29
38
  for (let mockIndex = 0; mockIndex < mocks.length; mockIndex++) {
30
- // Index is guaranteed in bounds by loop condition (0 <= mockIndex < length)
39
+ // eslint-disable-next-line security/detect-object-injection -- Index bounded by loop (0 <= i < length)
31
40
  const mockWithParams = mocks[mockIndex];
32
41
  const mock = mockWithParams.mock;
33
42
  // Skip exhausted sequences (repeat: 'none' that have been exhausted)
34
43
  if (mock.sequence && sequenceTracker) {
35
44
  const { exhausted } = sequenceTracker.getPosition(testId, scenarioId, mockIndex);
36
45
  if (exhausted) {
46
+ skippedExhaustedSequences = true;
37
47
  continue; // Skip to next mock, allowing fallback to be selected
38
48
  }
39
49
  }
40
50
  // Check if this mock has match criteria
41
51
  if (mock.match) {
42
52
  // If match criteria exists, check if it matches the request
43
- if (matchesCriteria(context, mock.match, testId, stateManager)) {
53
+ const matched = matchesCriteria(context, mock.match, testId, stateManager);
54
+ // Log the evaluation result
55
+ logger.debug(LogCategories.MATCHING, LogEvents.MOCK_MATCH_EVALUATED, logContext, {
56
+ mockIndex,
57
+ matched,
58
+ hasCriteria: true,
59
+ });
60
+ if (matched) {
44
61
  // Match criteria always have higher priority than fallbacks
45
62
  // Base specificity ensures even 1 field beats any fallback
46
63
  const specificity = SPECIFICITY_RANGES.MATCH_CRITERIA_BASE +
@@ -55,6 +72,12 @@ export const createResponseSelector = (options = {}) => {
55
72
  continue;
56
73
  }
57
74
  // No match criteria = fallback mock (always matches)
75
+ // Log fallback evaluation
76
+ logger.debug(LogCategories.MATCHING, LogEvents.MOCK_MATCH_EVALUATED, logContext, {
77
+ mockIndex,
78
+ matched: true,
79
+ hasCriteria: false,
80
+ });
58
81
  // Dynamic response types (sequence, stateResponse) get higher priority than simple responses
59
82
  // This ensures they are selected over simple fallback responses
60
83
  // Both sequence and stateResponse get the same specificity (Issue #316 fix)
@@ -74,14 +97,29 @@ export const createResponseSelector = (options = {}) => {
74
97
  }
75
98
  // Return the best matching mock
76
99
  if (bestMatch) {
77
- const { mockWithParams, mockIndex } = bestMatch;
100
+ const { mockWithParams, mockIndex, specificity } = bestMatch;
78
101
  const mock = mockWithParams.mock;
102
+ // Log successful selection
103
+ logger.info(LogCategories.MATCHING, LogEvents.MOCK_SELECTED, logContext, {
104
+ mockIndex,
105
+ specificity,
106
+ });
79
107
  // Select response (single, sequence, or stateResponse)
80
108
  const response = selectResponseFromMock(testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager);
81
109
  if (!response) {
82
110
  return {
83
111
  success: false,
84
- error: new ResponseSelectionError(`Mock has neither response nor sequence field`),
112
+ error: new ScenaristError(`Mock has neither response nor sequence field`, {
113
+ code: ErrorCodes.VALIDATION_ERROR,
114
+ context: {
115
+ testId,
116
+ scenarioId,
117
+ mockInfo: {
118
+ index: mockIndex,
119
+ },
120
+ hint: "Each mock must have a 'response', 'sequence', or 'stateResponse' field.",
121
+ },
122
+ }),
85
123
  };
86
124
  }
87
125
  // Phase 3: Capture state from request if configured
@@ -106,10 +144,48 @@ export const createResponseSelector = (options = {}) => {
106
144
  }
107
145
  return { success: true, data: finalResponse };
108
146
  }
109
- // No mock matched
147
+ // No mock matched - determine specific error type
148
+ if (skippedExhaustedSequences) {
149
+ // All matching mocks were exhausted sequences
150
+ logger.warn(LogCategories.SEQUENCE, LogEvents.SEQUENCE_EXHAUSTED, logContext, {
151
+ url: context.url,
152
+ method: context.method,
153
+ });
154
+ return {
155
+ success: false,
156
+ error: new ScenaristError(`Sequence exhausted for ${context.method} ${context.url}. All responses have been consumed and repeat mode is 'none'.`, {
157
+ code: ErrorCodes.SEQUENCE_EXHAUSTED,
158
+ context: {
159
+ testId,
160
+ scenarioId,
161
+ requestInfo: {
162
+ method: context.method,
163
+ url: context.url,
164
+ },
165
+ hint: "Add a fallback mock to handle requests after sequence exhaustion, or use repeat: 'last' or 'cycle' instead of 'none'.",
166
+ },
167
+ }),
168
+ };
169
+ }
170
+ logger.warn(LogCategories.MATCHING, LogEvents.MOCK_NO_MATCH, logContext, {
171
+ url: context.url,
172
+ method: context.method,
173
+ candidateCount: 0,
174
+ });
110
175
  return {
111
176
  success: false,
112
- error: new ResponseSelectionError(`No mock matched for ${context.method} ${context.url}`),
177
+ error: new ScenaristError(`No mock matched for ${context.method} ${context.url}`, {
178
+ code: ErrorCodes.NO_MOCK_FOUND,
179
+ context: {
180
+ testId,
181
+ scenarioId,
182
+ requestInfo: {
183
+ method: context.method,
184
+ url: context.url,
185
+ },
186
+ hint: "Add a fallback mock (without match criteria) to handle unmatched requests, or add a mock with matching criteria.",
187
+ },
188
+ }),
113
189
  };
114
190
  },
115
191
  };
@@ -137,6 +213,7 @@ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTra
137
213
  // Get response at current position
138
214
  // Note: Exhausted sequences are skipped during matching phase,
139
215
  // so position should always be valid here
216
+ // eslint-disable-next-line security/detect-object-injection -- Position bounded by sequence tracker
140
217
  const response = mock.sequence.responses[position];
141
218
  // Advance position for next call
142
219
  const repeatMode = mock.sequence.repeat || "last";
@@ -276,6 +353,7 @@ const matchesState = (stateCriteria, testId, stateManager) => {
276
353
  return false;
277
354
  }
278
355
  // Deep equality check for values (handles primitives, null, objects)
356
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
279
357
  if (!deepEquals(currentState[key], expectedValue)) {
280
358
  return false;
281
359
  }
@@ -296,6 +374,7 @@ const matchesBody = (requestBody, criteriaBody) => {
296
374
  const body = requestBody;
297
375
  // Check all required fields exist in request body with matching values
298
376
  for (const [key, criteriaValue] of Object.entries(criteriaBody)) {
377
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
299
378
  const requestValue = body[key];
300
379
  // Convert to string for matching (type coercion like headers/query)
301
380
  const stringValue = requestValue == null ? "" : String(requestValue);
@@ -378,6 +457,7 @@ const matchesHeaders = (requestHeaders, criteriaHeaders) => {
378
457
  const normalizedRequest = createNormalizedHeaderMap(requestHeaders);
379
458
  for (const [key, value] of Object.entries(criteriaHeaders)) {
380
459
  const normalizedKey = normalizeHeaderName(key);
460
+ // eslint-disable-next-line security/detect-object-injection -- Key normalized from Object.entries iteration
381
461
  const requestValue = normalizedRequest[normalizedKey];
382
462
  if (!requestValue || !matchesValue(requestValue, value)) {
383
463
  return false;
@@ -392,6 +472,7 @@ const matchesHeaders = (requestHeaders, criteriaHeaders) => {
392
472
  const matchesQuery = (requestQuery, criteriaQuery) => {
393
473
  // Check all required query params exist with exact matching values
394
474
  for (const [key, value] of Object.entries(criteriaQuery)) {
475
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
395
476
  const requestValue = requestQuery[key];
396
477
  if (!requestValue || !matchesValue(requestValue, value)) {
397
478
  return false;
@@ -1,4 +1,4 @@
1
- import type { ScenarioManager, ScenarioRegistry, ScenarioStore, SequenceTracker, StateManager } from "../ports/index.js";
1
+ import type { ScenarioManager, ScenarioRegistry, ScenarioStore, SequenceTracker, StateManager, Logger } from "../ports/index.js";
2
2
  /**
3
3
  * Factory function to create a ScenarioManager implementation.
4
4
  *
@@ -8,13 +8,15 @@ import type { ScenarioManager, ScenarioRegistry, ScenarioStore, SequenceTracker,
8
8
  * - Any registry implementation (in-memory, Redis, files, remote)
9
9
  * - Any store implementation (in-memory, Redis, database)
10
10
  * - Any state manager implementation (in-memory, Redis, database)
11
+ * - Any logger implementation (console, file, remote)
11
12
  * - Proper testing with mock dependencies
12
13
  * - True hexagonal architecture
13
14
  */
14
- export declare const createScenarioManager: ({ registry, store, stateManager, sequenceTracker, }: {
15
+ export declare const createScenarioManager: ({ registry, store, stateManager, sequenceTracker, logger, }: {
15
16
  registry: ScenarioRegistry;
16
17
  store: ScenarioStore;
17
18
  stateManager?: StateManager;
18
19
  sequenceTracker?: SequenceTracker;
20
+ logger?: Logger;
19
21
  }) => ScenarioManager;
20
22
  //# sourceMappingURL=scenario-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scenario-manager.d.ts","sourceRoot":"","sources":["../../src/domain/scenario-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,YAAY,EACb,MAAM,mBAAmB,CAAC;AAkC3B;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GAAI,qDAKnC;IACD,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,KAAG,eAgFH,CAAC"}
1
+ {"version":3,"file":"scenario-manager.d.ts","sourceRoot":"","sources":["../../src/domain/scenario-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,YAAY,EACZ,MAAM,EACP,MAAM,mBAAmB,CAAC;AAwC3B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GAAI,6DAMnC;IACD,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,KAAG,eAwHH,CAAC"}
@@ -1,24 +1,25 @@
1
+ import { noOpLogger } from "../adapters/index.js";
1
2
  import { ScenaristScenarioSchema } from "../schemas/index.js";
2
- class ScenarioNotFoundError extends Error {
3
- constructor(scenarioId) {
4
- super(`Scenario '${scenarioId}' not found. Did you forget to register it?`);
5
- this.name = "ScenarioNotFoundError";
6
- }
7
- }
8
- class DuplicateScenarioError extends Error {
9
- constructor(scenarioId) {
10
- super(`Scenario '${scenarioId}' is already registered. Each scenario must have a unique ID.`);
11
- this.name = "DuplicateScenarioError";
12
- }
13
- }
14
- class ScenarioValidationError extends Error {
15
- validationErrors;
16
- constructor(message, validationErrors) {
17
- super(message);
18
- this.validationErrors = validationErrors;
19
- this.name = "ScenarioValidationError";
20
- }
21
- }
3
+ import { ScenaristError, ErrorCodes } from "../types/errors.js";
4
+ import { LogCategories, LogEvents } from "./log-events.js";
5
+ const createDuplicateScenarioError = (scenarioId) => {
6
+ return new ScenaristError(`Scenario '${scenarioId}' is already registered. Each scenario must have a unique ID.`, {
7
+ code: ErrorCodes.DUPLICATE_SCENARIO,
8
+ context: {
9
+ scenarioId,
10
+ hint: "Use a different scenario ID, or remove the existing scenario before registering a new one.",
11
+ },
12
+ });
13
+ };
14
+ const createScenarioValidationError = (scenarioId, validationErrors) => {
15
+ return new ScenaristError(`Invalid scenario definition for '${scenarioId}': ${validationErrors.join(", ")}`, {
16
+ code: ErrorCodes.VALIDATION_ERROR,
17
+ context: {
18
+ scenarioId,
19
+ hint: `Check your scenario definition. Validation errors: ${validationErrors.join("; ")}`,
20
+ },
21
+ });
22
+ };
22
23
  /**
23
24
  * Factory function to create a ScenarioManager implementation.
24
25
  *
@@ -28,10 +29,11 @@ class ScenarioValidationError extends Error {
28
29
  * - Any registry implementation (in-memory, Redis, files, remote)
29
30
  * - Any store implementation (in-memory, Redis, database)
30
31
  * - Any state manager implementation (in-memory, Redis, database)
32
+ * - Any logger implementation (console, file, remote)
31
33
  * - Proper testing with mock dependencies
32
34
  * - True hexagonal architecture
33
35
  */
34
- export const createScenarioManager = ({ registry, store, stateManager, sequenceTracker, }) => {
36
+ export const createScenarioManager = ({ registry, store, stateManager, sequenceTracker, logger = noOpLogger, }) => {
35
37
  return {
36
38
  registerScenario(definition) {
37
39
  // Validate scenario definition at trust boundary
@@ -39,7 +41,7 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
39
41
  if (!validationResult.success) {
40
42
  const errorMessages = validationResult.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`);
41
43
  const scenarioId = definition?.id || "<unknown>";
42
- throw new ScenarioValidationError(`Invalid scenario definition for '${scenarioId}': ${errorMessages.join(", ")}`, errorMessages);
44
+ throw createScenarioValidationError(scenarioId, errorMessages);
43
45
  }
44
46
  const existing = registry.get(definition.id);
45
47
  // Allow re-registering the exact same scenario object (idempotent)
@@ -48,16 +50,30 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
48
50
  }
49
51
  // Prevent registering a different scenario with the same ID
50
52
  if (existing) {
51
- throw new DuplicateScenarioError(definition.id);
53
+ throw createDuplicateScenarioError(definition.id);
52
54
  }
53
55
  registry.register(definition);
56
+ logger.debug(LogCategories.SCENARIO, LogEvents.SCENARIO_REGISTERED, {}, {
57
+ scenarioId: definition.id,
58
+ mockCount: definition.mocks.length, // mocks is required by ScenaristScenarioSchema
59
+ });
54
60
  },
55
61
  switchScenario(testId, scenarioId) {
56
62
  const definition = registry.get(scenarioId);
57
63
  if (!definition) {
64
+ logger.error(LogCategories.SCENARIO, LogEvents.SCENARIO_NOT_FOUND, { testId }, {
65
+ requestedScenarioId: scenarioId,
66
+ });
58
67
  return {
59
68
  success: false,
60
- error: new ScenarioNotFoundError(scenarioId),
69
+ error: new ScenaristError(`Scenario '${scenarioId}' not found. Did you forget to register it?`, {
70
+ code: ErrorCodes.SCENARIO_NOT_FOUND,
71
+ context: {
72
+ testId,
73
+ scenarioId,
74
+ hint: "Make sure to register the scenario before switching to it. Use manager.registerScenario(definition) first.",
75
+ },
76
+ }),
61
77
  };
62
78
  }
63
79
  const activeScenario = {
@@ -72,6 +88,7 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
72
88
  if (stateManager) {
73
89
  stateManager.reset(testId);
74
90
  }
91
+ logger.info(LogCategories.SCENARIO, LogEvents.SCENARIO_SWITCHED, { testId, scenarioId }, {});
75
92
  return { success: true, data: undefined };
76
93
  },
77
94
  getActiveScenario(testId) {
@@ -82,6 +99,7 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
82
99
  },
83
100
  clearScenario(testId) {
84
101
  store.delete(testId);
102
+ logger.debug(LogCategories.SCENARIO, LogEvents.SCENARIO_CLEARED, { testId }, {});
85
103
  },
86
104
  getScenarioById(id) {
87
105
  return registry.get(id);
@@ -32,6 +32,7 @@ const stateMatchesCondition = (state, when) => {
32
32
  if (!(key in state)) {
33
33
  return false;
34
34
  }
35
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
35
36
  const actualValue = state[key];
36
37
  if (!deepEquals(actualValue, expectedValue)) {
37
38
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"template-replacement.d.ts","sourceRoot":"","sources":["../../src/domain/template-replacement.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,OAAO,EACd,cAAc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACpC,OA8DF,CAAC"}
1
+ {"version":3,"file":"template-replacement.d.ts","sourceRoot":"","sources":["../../src/domain/template-replacement.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,OAAO,EACd,cAAc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACpC,OA+DF,CAAC"}
@@ -49,6 +49,7 @@ export const applyTemplates = (value, templateData) => {
49
49
  if (typeof value === "object" && value !== null) {
50
50
  const result = {};
51
51
  for (const [key, val] of Object.entries(value)) {
52
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
52
53
  result[key] = applyTemplates(val, normalizedData);
53
54
  }
54
55
  return result;
@@ -67,6 +68,7 @@ export const applyTemplates = (value, templateData) => {
67
68
  */
68
69
  const resolveTemplatePath = (templateData, prefix, path) => {
69
70
  // Get the root object (state or params)
71
+ // eslint-disable-next-line security/detect-object-injection -- Prefix validated as 'state' or 'params' by regex
70
72
  const root = templateData[prefix];
71
73
  // Guard: Prefix doesn't exist (e.g., no params provided)
72
74
  if (root === undefined || typeof root !== "object" || root === null) {
@@ -86,6 +88,7 @@ const resolveTemplatePath = (templateData, prefix, path) => {
86
88
  }
87
89
  // Traverse object
88
90
  const record = current;
91
+ // eslint-disable-next-line security/detect-object-injection -- Segment from split() iteration
89
92
  current = record[segment];
90
93
  // Guard: Return undefined if property doesn't exist
91
94
  if (current === undefined) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type * from "./types/index.js";
2
+ export { ScenaristError, ErrorCodes } from "./types/errors.js";
2
3
  export * from "./schemas/index.js";
3
4
  export * from "./constants/index.js";
4
5
  export type * from "./ports/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,kBAAkB,CAAC;AAGtC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,sBAAsB,CAAC;AAGrC,mBAAmB,kBAAkB,CAAC;AAGtC,mBAAmB,sBAAsB,CAAC;AAG1C,cAAc,mBAAmB,CAAC;AAGlC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,kBAAkB,CAAC;AAGtC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG/D,cAAc,oBAAoB,CAAC;AAGnC,cAAc,sBAAsB,CAAC;AAGrC,mBAAmB,kBAAkB,CAAC;AAGtC,mBAAmB,sBAAsB,CAAC;AAG1C,cAAc,mBAAmB,CAAC;AAGlC,cAAc,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ // Error classes and constants (runtime values, not just types)
2
+ export { ScenaristError, ErrorCodes } from "./types/errors.js";
1
3
  // Schemas (runtime validation)
2
4
  export * from "./schemas/index.js";
3
5
  // Constants
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Log levels ordered by verbosity (ascending).
3
+ * Each level includes all less verbose levels.
4
+ *
5
+ * - silent: No output (production default)
6
+ * - error: Critical failures preventing operation
7
+ * - warn: Potential issues that may cause problems
8
+ * - info: Operation flow and key events
9
+ * - debug: Detailed decision logic
10
+ * - trace: Request/response bodies, verbose details
11
+ */
12
+ export type LogLevel = "silent" | "error" | "warn" | "info" | "debug" | "trace";
13
+ /**
14
+ * Log categories for filtering specific areas of concern.
15
+ * Users can enable/disable categories independently.
16
+ */
17
+ export type LogCategory = "lifecycle" | "scenario" | "matching" | "sequence" | "state" | "template" | "request";
18
+ /**
19
+ * Base context included in all log events.
20
+ * Enables filtering and correlation by test ID.
21
+ */
22
+ export type LogContext = {
23
+ readonly testId?: string;
24
+ readonly scenarioId?: string;
25
+ readonly requestUrl?: string;
26
+ readonly requestMethod?: string;
27
+ };
28
+ /**
29
+ * Structured log entry for capture and serialization.
30
+ */
31
+ export type LogEntry = {
32
+ readonly level: Exclude<LogLevel, "silent">;
33
+ readonly category: LogCategory;
34
+ readonly message: string;
35
+ readonly context: LogContext;
36
+ readonly data?: Record<string, unknown>;
37
+ readonly timestamp: number;
38
+ };
39
+ /**
40
+ * Logger port for structured logging.
41
+ *
42
+ * This is a driven (secondary) port - domain logic calls out to it,
43
+ * implementations are injected via dependency injection.
44
+ *
45
+ * Implementations must handle:
46
+ * - Level filtering (only emit logs at or above configured level)
47
+ * - Category filtering (optionally filter by category)
48
+ * - Output formatting (console, JSON, custom)
49
+ *
50
+ * This port enables:
51
+ * - NoOpLogger: Zero overhead when disabled (production, silent mode)
52
+ * - ConsoleLogger: Human-readable or JSON output (development)
53
+ * - TestLogger: Capture logs for assertion in tests
54
+ * - Custom loggers: User-provided implementations (Winston, Pino, etc.)
55
+ *
56
+ * All methods return void - logging is fire-and-forget.
57
+ * Implementations should never throw.
58
+ */
59
+ export interface Logger {
60
+ /**
61
+ * Log at error level - critical failures preventing operation.
62
+ *
63
+ * @param category - Area of concern for filtering
64
+ * @param message - Human-readable event description
65
+ * @param context - Request context for correlation (testId, scenarioId, etc.)
66
+ * @param data - Optional structured data for the event
67
+ */
68
+ error(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
69
+ /**
70
+ * Log at warn level - potential issues that may cause problems.
71
+ *
72
+ * @param category - Area of concern for filtering
73
+ * @param message - Human-readable event description
74
+ * @param context - Request context for correlation
75
+ * @param data - Optional structured data for the event
76
+ */
77
+ warn(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
78
+ /**
79
+ * Log at info level - operation flow and key events.
80
+ *
81
+ * @param category - Area of concern for filtering
82
+ * @param message - Human-readable event description
83
+ * @param context - Request context for correlation
84
+ * @param data - Optional structured data for the event
85
+ */
86
+ info(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
87
+ /**
88
+ * Log at debug level - detailed decision logic.
89
+ *
90
+ * @param category - Area of concern for filtering
91
+ * @param message - Human-readable event description
92
+ * @param context - Request context for correlation
93
+ * @param data - Optional structured data for the event
94
+ */
95
+ debug(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
96
+ /**
97
+ * Log at trace level - request/response bodies, verbose details.
98
+ *
99
+ * @param category - Area of concern for filtering
100
+ * @param message - Human-readable event description
101
+ * @param context - Request context for correlation
102
+ * @param data - Optional structured data for the event
103
+ */
104
+ trace(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
105
+ /**
106
+ * Check if a specific level would be logged.
107
+ * Enables conditional expensive operations before logging.
108
+ *
109
+ * @example
110
+ * if (logger.isEnabled('trace')) {
111
+ * logger.trace('request', 'body', ctx, { body: JSON.stringify(large) });
112
+ * }
113
+ *
114
+ * @param level - The log level to check
115
+ * @returns true if logging at this level is enabled
116
+ */
117
+ isEnabled(level: Exclude<LogLevel, "silent">): boolean;
118
+ }
119
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/ports/driven/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAEhF;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,UAAU,GACV,UAAU,GACV,UAAU,GACV,OAAO,GACP,UAAU,GACV,SAAS,CAAC;AAEd;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,MAAM;IACrB;;;;;;;OAOG;IACH,KAAK,CACH,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;OAOG;IACH,IAAI,CACF,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;OAOG;IACH,IAAI,CACF,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;OAOG;IACH,KAAK,CACH,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;OAOG;IACH,KAAK,CACH,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC;CACxD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,8 @@
1
1
  import type { ScenaristMockWithParams, ScenaristResponse, HttpRequestContext, ScenaristResult } from "../../types/index.js";
2
+ import { ScenaristError } from "../../types/errors.js";
2
3
  /**
3
4
  * Error type for response selection failures.
5
+ * @deprecated Use ScenaristError with ErrorCodes.NO_MOCK_FOUND instead
4
6
  */
5
7
  export declare class ResponseSelectionError extends Error {
6
8
  constructor(message: string);
@@ -29,6 +31,6 @@ export interface ResponseSelector {
29
31
  * @param mocks - Candidate mocks with extracted params (already filtered by URL/method)
30
32
  * @returns ScenaristResult with selected ScenaristResponse or error if no match found
31
33
  */
32
- selectResponse(testId: string, scenarioId: string, context: HttpRequestContext, mocks: ReadonlyArray<ScenaristMockWithParams>): ScenaristResult<ScenaristResponse, ResponseSelectionError>;
34
+ selectResponse(testId: string, scenarioId: string, context: HttpRequestContext, mocks: ReadonlyArray<ScenaristMockWithParams>): ScenaristResult<ScenaristResponse, ScenaristError>;
33
35
  }
34
36
  //# sourceMappingURL=response-selector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"response-selector.d.ts","sourceRoot":"","sources":["../../../src/ports/driven/response-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAE9B;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;OAQG;IACH,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,aAAa,CAAC,uBAAuB,CAAC,GAC5C,eAAe,CAAC,iBAAiB,EAAE,sBAAsB,CAAC,CAAC;CAC/D"}
1
+ {"version":3,"file":"response-selector.d.ts","sourceRoot":"","sources":["../../../src/ports/driven/response-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;OAQG;IACH,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,aAAa,CAAC,uBAAuB,CAAC,GAC5C,eAAe,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;CACvD"}
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Error type for response selection failures.
3
+ * @deprecated Use ScenaristError with ErrorCodes.NO_MOCK_FOUND instead
3
4
  */
4
5
  export class ResponseSelectionError extends Error {
5
6
  constructor(message) {
@@ -5,4 +5,5 @@ export type { RequestContext } from "./driven/request-context.js";
5
5
  export type { ResponseSelector } from "./driven/response-selector.js";
6
6
  export type { SequenceTracker, SequencePosition, } from "./driven/sequence-tracker.js";
7
7
  export type { StateManager } from "./driven/state-manager.js";
8
+ export type { Logger, LogLevel, LogCategory, LogContext, LogEntry, } from "./driven/logger.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9D,YAAY,EACV,MAAM,EACN,QAAQ,EACR,WAAW,EACX,UAAU,EACV,QAAQ,GACT,MAAM,oBAAoB,CAAC"}
@@ -1,4 +1,24 @@
1
1
  import type { ScenaristScenarios } from "./scenario.js";
2
+ /**
3
+ * How errors should be handled when they occur.
4
+ *
5
+ * - `throw`: Throw ScenaristError (strict - test fails with clear message)
6
+ * - `warn`: Log at warn level, return undefined (let strictMode decide next step)
7
+ * - `ignore`: Return undefined silently (let strictMode decide next step)
8
+ */
9
+ export type ErrorBehavior = "throw" | "warn" | "ignore";
10
+ /**
11
+ * Configuration for how different error types should be handled.
12
+ * Default is 'throw' for all (strict by default).
13
+ */
14
+ export type ErrorBehaviors = {
15
+ /** How to handle when no mock matches a request. Default: 'throw' */
16
+ readonly onNoMockFound: ErrorBehavior;
17
+ /** How to handle when a sequence is exhausted. Default: 'throw' */
18
+ readonly onSequenceExhausted: ErrorBehavior;
19
+ /** How to handle when x-scenarist-test-id header is missing. Default: 'throw' */
20
+ readonly onMissingTestId: ErrorBehavior;
21
+ };
2
22
  /**
3
23
  * Configuration for the scenario management system.
4
24
  * All properties are readonly for immutability.
@@ -35,6 +55,11 @@ export type ScenaristConfig = {
35
55
  * The default test ID to use when no x-scenarist-test-id header is present.
36
56
  */
37
57
  readonly defaultTestId: string;
58
+ /**
59
+ * How different error types should be handled.
60
+ * Default is 'throw' for all (strict by default).
61
+ */
62
+ readonly errorBehaviors: ErrorBehaviors;
38
63
  };
39
64
  /**
40
65
  * Partial config for user input - missing values will use defaults.
@@ -66,5 +91,9 @@ export type ScenaristConfigInput<T extends ScenaristScenarios = ScenaristScenari
66
91
  */
67
92
  readonly scenarios: T;
68
93
  readonly defaultTestId?: string;
94
+ /**
95
+ * Optional error behavior overrides. Missing values use 'throw' as default.
96
+ */
97
+ readonly errorBehaviors?: Partial<ErrorBehaviors>;
69
98
  };
70
99
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE;QAClB,kEAAkE;QAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,kEAAkE;QAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;KAC9B,CAAC;IAEF;;OAEG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,CAC9B,CAAC,SAAS,kBAAkB,GAAG,kBAAkB,IAC/C;IACF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3D;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;IACtB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,mEAAmE;IACnE,QAAQ,CAAC,mBAAmB,EAAE,aAAa,CAAC;IAC5C,iFAAiF;IACjF,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;CACzC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE;QAClB,kEAAkE;QAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,kEAAkE;QAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;KAC9B,CAAC;IAEF;;OAEG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,CAC9B,CAAC,SAAS,kBAAkB,GAAG,kBAAkB,IAC/C;IACF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3D;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;IACtB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CACnD,CAAC"}