@scenarist/core 0.1.16 → 0.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.
Files changed (34) hide show
  1. package/dist/adapters/in-memory-state-manager.d.ts +1 -0
  2. package/dist/adapters/in-memory-state-manager.d.ts.map +1 -1
  3. package/dist/adapters/in-memory-state-manager.js +14 -0
  4. package/dist/domain/deep-equals.d.ts +12 -0
  5. package/dist/domain/deep-equals.d.ts.map +1 -0
  6. package/dist/domain/deep-equals.js +62 -0
  7. package/dist/domain/index.d.ts +3 -0
  8. package/dist/domain/index.d.ts.map +1 -1
  9. package/dist/domain/index.js +3 -0
  10. package/dist/domain/response-selector.d.ts.map +1 -1
  11. package/dist/domain/response-selector.js +89 -12
  12. package/dist/domain/state-condition-evaluator.d.ts +28 -0
  13. package/dist/domain/state-condition-evaluator.d.ts.map +1 -0
  14. package/dist/domain/state-condition-evaluator.js +41 -0
  15. package/dist/domain/state-response-resolver.d.ts +36 -0
  16. package/dist/domain/state-response-resolver.d.ts.map +1 -0
  17. package/dist/domain/state-response-resolver.js +16 -0
  18. package/dist/ports/driven/state-manager.d.ts +10 -0
  19. package/dist/ports/driven/state-manager.d.ts.map +1 -1
  20. package/dist/schemas/index.d.ts +1 -0
  21. package/dist/schemas/index.d.ts.map +1 -1
  22. package/dist/schemas/index.js +1 -0
  23. package/dist/schemas/response.d.ts +15 -0
  24. package/dist/schemas/response.d.ts.map +1 -0
  25. package/dist/schemas/response.js +13 -0
  26. package/dist/schemas/scenario-definition.d.ts +50 -7
  27. package/dist/schemas/scenario-definition.d.ts.map +1 -1
  28. package/dist/schemas/scenario-definition.js +23 -7
  29. package/dist/schemas/scenarios-object.d.ts +21 -0
  30. package/dist/schemas/scenarios-object.d.ts.map +1 -1
  31. package/dist/schemas/state-aware-mocking.d.ts +93 -0
  32. package/dist/schemas/state-aware-mocking.d.ts.map +1 -0
  33. package/dist/schemas/state-aware-mocking.js +79 -0
  34. package/package.json +1 -1
@@ -12,6 +12,7 @@ export declare class InMemoryStateManager implements StateManager {
12
12
  set(testId: string, key: string, value: unknown): void;
13
13
  getAll(testId: string): Record<string, unknown>;
14
14
  reset(testId: string): void;
15
+ merge(testId: string, partial: Record<string, unknown>): void;
15
16
  private getOrCreateTestState;
16
17
  private setNestedValue;
17
18
  private getNestedValue;
@@ -1 +1 @@
1
- {"version":3,"file":"in-memory-state-manager.d.ts","sourceRoot":"","sources":["../../src/adapters/in-memory-state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAMrE;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8C;IAEtE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAUzC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAyBtD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAI/C,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI3B,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,cAAc;IAwCtB,OAAO,CAAC,cAAc;CA4BvB;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAAO,YAE7C,CAAC"}
1
+ {"version":3,"file":"in-memory-state-manager.d.ts","sourceRoot":"","sources":["../../src/adapters/in-memory-state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAMrE;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8C;IAEtE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAUzC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAyBtD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAI/C,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAgB7D,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,cAAc;IAwCtB,OAAO,CAAC,cAAc;CA4BvB;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAAO,YAE7C,CAAC"}
@@ -43,6 +43,20 @@ export class InMemoryStateManager {
43
43
  reset(testId) {
44
44
  this.storage.delete(testId);
45
45
  }
46
+ merge(testId, partial) {
47
+ const currentState = this.getOrCreateTestState(testId);
48
+ // Filter out dangerous keys and shallow merge
49
+ for (const [key, value] of Object.entries(partial)) {
50
+ if (!isDangerousKey(key)) {
51
+ Object.defineProperty(currentState, key, {
52
+ value,
53
+ writable: true,
54
+ enumerable: true,
55
+ configurable: true,
56
+ });
57
+ }
58
+ }
59
+ }
46
60
  getOrCreateTestState(testId) {
47
61
  let testState = this.storage.get(testId);
48
62
  if (!testState) {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Deep equality comparison for values.
3
+ *
4
+ * Supports primitives, null, undefined, arrays, and objects.
5
+ * Used for state matching in stateResponse conditions and match.state criteria.
6
+ *
7
+ * @param a - First value to compare
8
+ * @param b - Second value to compare
9
+ * @returns true if values are deeply equal, false otherwise
10
+ */
11
+ export declare const deepEquals: (a: unknown, b: unknown) => boolean;
12
+ //# sourceMappingURL=deep-equals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-equals.d.ts","sourceRoot":"","sources":["../../src/domain/deep-equals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,OAAO,EAAE,GAAG,OAAO,KAAG,OAkEnD,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Deep equality comparison for values.
3
+ *
4
+ * Supports primitives, null, undefined, arrays, and objects.
5
+ * Used for state matching in stateResponse conditions and match.state criteria.
6
+ *
7
+ * @param a - First value to compare
8
+ * @param b - Second value to compare
9
+ * @returns true if values are deeply equal, false otherwise
10
+ */
11
+ export const deepEquals = (a, b) => {
12
+ // Handle primitives and reference equality
13
+ if (a === b) {
14
+ return true;
15
+ }
16
+ // Handle null (after === check, if either is null, they're not equal)
17
+ if (a === null || b === null) {
18
+ return false;
19
+ }
20
+ // Handle undefined
21
+ if (a === undefined || b === undefined) {
22
+ return false;
23
+ }
24
+ // Handle different types
25
+ if (typeof a !== typeof b) {
26
+ return false;
27
+ }
28
+ // Handle arrays
29
+ if (Array.isArray(a) && Array.isArray(b)) {
30
+ if (a.length !== b.length) {
31
+ return false;
32
+ }
33
+ for (let i = 0; i < a.length; i++) {
34
+ if (!deepEquals(a[i], b[i])) {
35
+ return false;
36
+ }
37
+ }
38
+ return true;
39
+ }
40
+ // Handle array vs non-array (one is array, other is object)
41
+ if (Array.isArray(a) !== Array.isArray(b)) {
42
+ return false;
43
+ }
44
+ // Handle objects
45
+ if (typeof a === "object" && typeof b === "object") {
46
+ const aKeys = Object.keys(a);
47
+ const bKeys = Object.keys(b);
48
+ if (aKeys.length !== bKeys.length) {
49
+ return false;
50
+ }
51
+ for (const key of aKeys) {
52
+ if (!(key in b)) {
53
+ return false;
54
+ }
55
+ if (!deepEquals(a[key], b[key])) {
56
+ return false;
57
+ }
58
+ }
59
+ return true;
60
+ }
61
+ return false;
62
+ };
@@ -2,4 +2,7 @@ export { createScenarioManager } from "./scenario-manager.js";
2
2
  export { buildConfig } from "./config-builder.js";
3
3
  export { createResponseSelector } from "./response-selector.js";
4
4
  export { matchesRegex } from "./regex-matching.js";
5
+ export { createStateConditionEvaluator, type StateConditionEvaluator, } from "./state-condition-evaluator.js";
6
+ export { createStateResponseResolver, type StateResponseResolver, } from "./state-response-resolver.js";
7
+ export { deepEquals } from "./deep-equals.js";
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/domain/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/domain/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EACL,6BAA6B,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,2BAA2B,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
@@ -2,3 +2,6 @@ export { createScenarioManager } from "./scenario-manager.js";
2
2
  export { buildConfig } from "./config-builder.js";
3
3
  export { createResponseSelector } from "./response-selector.js";
4
4
  export { matchesRegex } from "./regex-matching.js";
5
+ export { createStateConditionEvaluator, } from "./state-condition-evaluator.js";
6
+ export { createStateResponseResolver, } from "./state-response-resolver.js";
7
+ export { deepEquals } from "./deep-equals.js";
@@ -1 +1 @@
1
- {"version":3,"file":"response-selector.d.ts","sourceRoot":"","sources":["../../src/domain/response-selector.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,YAAY,EACb,MAAM,mBAAmB,CAAC;AAa3B;;GAEG;AACH,KAAK,6BAA6B,GAAG;IACnC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GACjC,UAAS,6BAAkC,KAC1C,gBAiIF,CAAC"}
1
+ {"version":3,"file":"response-selector.d.ts","sourceRoot":"","sources":["../../src/domain/response-selector.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,YAAY,EACb,MAAM,mBAAmB,CAAC;AAe3B;;GAEG;AACH,KAAK,6BAA6B,GAAG;IACnC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GACjC,UAAS,6BAAkC,KAC1C,gBAyIF,CAAC"}
@@ -2,6 +2,8 @@ import { ResponseSelectionError } from "../ports/driven/response-selector.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
+ import { createStateResponseResolver } from "./state-response-resolver.js";
6
+ import { deepEquals } from "./deep-equals.js";
5
7
  const SPECIFICITY_RANGES = {
6
8
  MATCH_CRITERIA_BASE: 100,
7
9
  SEQUENCE_FALLBACK: 1,
@@ -38,7 +40,7 @@ export const createResponseSelector = (options = {}) => {
38
40
  // Check if this mock has match criteria
39
41
  if (mock.match) {
40
42
  // If match criteria exists, check if it matches the request
41
- if (matchesCriteria(context, mock.match)) {
43
+ if (matchesCriteria(context, mock.match, testId, stateManager)) {
42
44
  // Match criteria always have higher priority than fallbacks
43
45
  // Base specificity ensures even 1 field beats any fallback
44
46
  const specificity = SPECIFICITY_RANGES.MATCH_CRITERIA_BASE +
@@ -53,9 +55,10 @@ export const createResponseSelector = (options = {}) => {
53
55
  continue;
54
56
  }
55
57
  // No match criteria = fallback mock (always matches)
56
- // Sequences get higher priority than simple responses
57
- // This ensures sequences are selected over simple fallback responses
58
- const fallbackSpecificity = mock.sequence
58
+ // Dynamic response types (sequence, stateResponse) get higher priority than simple responses
59
+ // This ensures they are selected over simple fallback responses
60
+ // Both sequence and stateResponse get the same specificity (Issue #316 fix)
61
+ const fallbackSpecificity = mock.sequence || mock.stateResponse
59
62
  ? SPECIFICITY_RANGES.SEQUENCE_FALLBACK
60
63
  : SPECIFICITY_RANGES.SIMPLE_FALLBACK;
61
64
  if (!bestMatch || fallbackSpecificity >= bestMatch.specificity) {
@@ -73,8 +76,8 @@ export const createResponseSelector = (options = {}) => {
73
76
  if (bestMatch) {
74
77
  const { mockWithParams, mockIndex } = bestMatch;
75
78
  const mock = mockWithParams.mock;
76
- // Select response (either single or from sequence)
77
- const response = selectResponseFromMock(testId, scenarioId, mockIndex, mock, sequenceTracker);
79
+ // Select response (single, sequence, or stateResponse)
80
+ const response = selectResponseFromMock(testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager);
78
81
  if (!response) {
79
82
  return {
80
83
  success: false,
@@ -97,6 +100,10 @@ export const createResponseSelector = (options = {}) => {
97
100
  };
98
101
  finalResponse = applyTemplates(response, templateData);
99
102
  }
103
+ // Apply afterResponse.setState to mutate state for subsequent requests
104
+ if (mock.afterResponse?.setState && stateManager) {
105
+ stateManager.merge(testId, mock.afterResponse.setState);
106
+ }
100
107
  return { success: true, data: finalResponse };
101
108
  }
102
109
  // No mock matched
@@ -108,16 +115,17 @@ export const createResponseSelector = (options = {}) => {
108
115
  };
109
116
  };
110
117
  /**
111
- * Select a response from a mock (either single response or from sequence).
118
+ * Select a response from a mock (single response, sequence, or stateResponse).
112
119
  *
113
- * @param testId - Test ID for sequence tracking
120
+ * @param testId - Test ID for sequence/state tracking
114
121
  * @param scenarioId - Scenario ID for sequence tracking
115
122
  * @param mockIndex - Index of the mock in the mocks array
116
123
  * @param mock - The mock definition
117
124
  * @param sequenceTracker - Optional sequence tracker for Phase 2
118
- * @returns ScenaristResponse or null if mock has neither response nor sequence
125
+ * @param stateManager - Optional state manager for stateResponse
126
+ * @returns ScenaristResponse or null if mock has no response type
119
127
  */
120
- const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTracker) => {
128
+ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager) => {
121
129
  // Phase 2: If mock has a sequence, use sequence tracker
122
130
  if (mock.sequence) {
123
131
  if (!sequenceTracker) {
@@ -135,13 +143,39 @@ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTra
135
143
  sequenceTracker.advance(testId, scenarioId, mockIndex, mock.sequence.responses.length, repeatMode);
136
144
  return response;
137
145
  }
146
+ // State-aware response: evaluate conditions against current state
147
+ if (mock.stateResponse) {
148
+ return resolveStateResponse(testId, mock.stateResponse, stateManager);
149
+ }
138
150
  // Phase 1: Single response
139
151
  if (mock.response) {
140
152
  return mock.response;
141
153
  }
142
- // Neither response nor sequence defined
154
+ // No response type defined
143
155
  return null;
144
156
  };
157
+ /**
158
+ * Resolve a stateResponse configuration to a single response.
159
+ *
160
+ * Uses the StateResponseResolver to evaluate conditions against
161
+ * current test state and return the appropriate response.
162
+ *
163
+ * @param testId - Test ID for state isolation
164
+ * @param stateResponse - The stateResponse configuration
165
+ * @param stateManager - Optional state manager for state lookup
166
+ * @returns The resolved response (matching condition or default)
167
+ */
168
+ const resolveStateResponse = (testId, stateResponse, stateManager) => {
169
+ // Without stateManager, always return default
170
+ if (!stateManager) {
171
+ return stateResponse.default;
172
+ }
173
+ // Get current state for this test
174
+ const currentState = stateManager.getAll(testId);
175
+ // Create resolver and evaluate conditions
176
+ const resolver = createStateResponseResolver();
177
+ return resolver.resolveResponse(stateResponse, currentState);
178
+ };
145
179
  /**
146
180
  * Calculate specificity score for match criteria.
147
181
  * Higher score = more specific match.
@@ -151,11 +185,13 @@ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTra
151
185
  * - Each body field = +1 point
152
186
  * - Each header = +1 point
153
187
  * - Each query param = +1 point
188
+ * - Each state key = +1 point
154
189
  *
155
190
  * Example:
156
191
  * { body: { itemType: 'premium' } } = 1 point
157
192
  * { body: { itemType: 'premium', quantity: 5 }, headers: { 'x-tier': 'gold' } } = 3 points
158
193
  * { url: '/api/products', body: { itemType: 'premium' } } = 2 points
194
+ * { state: { step: 'reviewed', approved: true } } = 2 points
159
195
  */
160
196
  const calculateSpecificity = (criteria) => {
161
197
  let score = 0;
@@ -171,13 +207,21 @@ const calculateSpecificity = (criteria) => {
171
207
  if (criteria.query) {
172
208
  score += Object.keys(criteria.query).length;
173
209
  }
210
+ if (criteria.state) {
211
+ score += Object.keys(criteria.state).length;
212
+ }
174
213
  return score;
175
214
  };
176
215
  /**
177
216
  * Check if request context matches the specified criteria.
178
217
  * All specified criteria must match for the overall match to succeed.
218
+ *
219
+ * @param context - HTTP request context
220
+ * @param criteria - Match criteria from mock definition
221
+ * @param testId - Test ID for state isolation
222
+ * @param stateManager - Optional state manager for state-based matching
179
223
  */
180
- const matchesCriteria = (context, criteria) => {
224
+ const matchesCriteria = (context, criteria, testId, stateManager) => {
181
225
  // Check URL match (exact match or pattern)
182
226
  if (criteria.url) {
183
227
  if (!matchesValue(context.url, criteria.url)) {
@@ -202,9 +246,42 @@ const matchesCriteria = (context, criteria) => {
202
246
  return false;
203
247
  }
204
248
  }
249
+ // Check state match (partial match on current test state)
250
+ if (criteria.state) {
251
+ if (!matchesState(criteria.state, testId, stateManager)) {
252
+ return false;
253
+ }
254
+ }
205
255
  // All criteria matched
206
256
  return true;
207
257
  };
258
+ /**
259
+ * Check if current test state matches the specified state criteria.
260
+ * All keys in criteria must exist in state with equal values (partial match).
261
+ *
262
+ * @param stateCriteria - Required state key-value pairs
263
+ * @param testId - Test ID for state isolation
264
+ * @param stateManager - State manager to retrieve current state
265
+ * @returns true if all criteria keys match, false otherwise
266
+ */
267
+ const matchesState = (stateCriteria, testId, stateManager) => {
268
+ // Without stateManager, state matching always fails
269
+ if (!stateManager) {
270
+ return false;
271
+ }
272
+ const currentState = stateManager.getAll(testId);
273
+ // All keys in criteria must exist in state with equal values
274
+ for (const [key, expectedValue] of Object.entries(stateCriteria)) {
275
+ if (!(key in currentState)) {
276
+ return false;
277
+ }
278
+ // Deep equality check for values (handles primitives, null, objects)
279
+ if (!deepEquals(currentState[key], expectedValue)) {
280
+ return false;
281
+ }
282
+ }
283
+ return true;
284
+ };
208
285
  /**
209
286
  * Check if request body contains all required fields (partial match).
210
287
  * Request can have additional fields beyond what's specified in criteria.
@@ -0,0 +1,28 @@
1
+ import type { StateCondition } from "../schemas/state-aware-mocking.js";
2
+ /**
3
+ * StateConditionEvaluator port for evaluating stateResponse conditions.
4
+ *
5
+ * Evaluates conditions against current test state using:
6
+ * - Partial matching: condition keys must exist in state with matching values
7
+ * - Specificity-based selection: most specific matching condition wins
8
+ * - Deep equality: objects/arrays compared structurally
9
+ */
10
+ export type StateConditionEvaluator = {
11
+ /**
12
+ * Find the matching condition based on current state.
13
+ *
14
+ * Applies specificity-based selection:
15
+ * - More specific conditions (more keys) take precedence
16
+ * - On equal specificity, first matching condition wins
17
+ *
18
+ * @param conditions - Array of conditions to evaluate
19
+ * @param currentState - Current test state to match against
20
+ * @returns Matching condition or undefined if no match
21
+ */
22
+ findMatchingCondition(conditions: ReadonlyArray<StateCondition>, currentState: Readonly<Record<string, unknown>>): StateCondition | undefined;
23
+ };
24
+ /**
25
+ * Factory function for creating StateConditionEvaluator.
26
+ */
27
+ export declare const createStateConditionEvaluator: () => StateConditionEvaluator;
28
+ //# sourceMappingURL=state-condition-evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-condition-evaluator.d.ts","sourceRoot":"","sources":["../../src/domain/state-condition-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGxE;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;;;;;;;OAUG;IACH,qBAAqB,CACnB,UAAU,EAAE,aAAa,CAAC,cAAc,CAAC,EACzC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC9C,cAAc,GAAG,SAAS,CAAC;CAC/B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,6BAA6B,QAAO,uBA4BhD,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { deepEquals } from "./deep-equals.js";
2
+ /**
3
+ * Factory function for creating StateConditionEvaluator.
4
+ */
5
+ export const createStateConditionEvaluator = () => {
6
+ return {
7
+ findMatchingCondition(conditions, currentState) {
8
+ let bestMatch;
9
+ let bestSpecificity = -1;
10
+ for (const condition of conditions) {
11
+ const matches = stateMatchesCondition(currentState, condition.when);
12
+ if (!matches) {
13
+ continue;
14
+ }
15
+ const specificity = Object.keys(condition.when).length;
16
+ // Keep if more specific than current best
17
+ if (specificity > bestSpecificity) {
18
+ bestMatch = condition;
19
+ bestSpecificity = specificity;
20
+ }
21
+ }
22
+ return bestMatch;
23
+ },
24
+ };
25
+ };
26
+ /**
27
+ * Check if current state matches a condition's 'when' clause.
28
+ * All keys in the condition must exist in state with matching values.
29
+ */
30
+ const stateMatchesCondition = (state, when) => {
31
+ for (const [key, expectedValue] of Object.entries(when)) {
32
+ if (!(key in state)) {
33
+ return false;
34
+ }
35
+ const actualValue = state[key];
36
+ if (!deepEquals(actualValue, expectedValue)) {
37
+ return false;
38
+ }
39
+ }
40
+ return true;
41
+ };
@@ -0,0 +1,36 @@
1
+ import type { StatefulMockResponse } from "../schemas/state-aware-mocking.js";
2
+ import type { ScenaristResponse } from "../schemas/scenario-definition.js";
3
+ import type { StateConditionEvaluator } from "./state-condition-evaluator.js";
4
+ /**
5
+ * StateResponseResolver port for resolving stateResponse configurations.
6
+ *
7
+ * Resolves which response to return based on current test state:
8
+ * - Evaluates conditions using StateConditionEvaluator
9
+ * - Returns matching condition's response or default
10
+ */
11
+ export type StateResponseResolver = {
12
+ /**
13
+ * Resolve the response from a stateResponse configuration.
14
+ *
15
+ * @param stateResponse - The stateResponse configuration
16
+ * @param currentState - Current test state
17
+ * @returns The resolved response (matching condition or default)
18
+ */
19
+ resolveResponse(stateResponse: StatefulMockResponse, currentState: Readonly<Record<string, unknown>>): ScenaristResponse;
20
+ };
21
+ /**
22
+ * Options for creating a StateResponseResolver.
23
+ */
24
+ type CreateStateResponseResolverOptions = {
25
+ /**
26
+ * Optional custom evaluator for testing.
27
+ * If not provided, creates a default evaluator.
28
+ */
29
+ evaluator?: StateConditionEvaluator;
30
+ };
31
+ /**
32
+ * Factory function for creating StateResponseResolver.
33
+ */
34
+ export declare const createStateResponseResolver: (options?: CreateStateResponseResolverOptions) => StateResponseResolver;
35
+ export {};
36
+ //# sourceMappingURL=state-response-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-response-resolver.d.ts","sourceRoot":"","sources":["../../src/domain/state-response-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAE3E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;;;OAMG;IACH,eAAe,CACb,aAAa,EAAE,oBAAoB,EACnC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC9C,iBAAiB,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,KAAK,kCAAkC,GAAG;IACxC;;;OAGG;IACH,SAAS,CAAC,EAAE,uBAAuB,CAAC;CACrC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,2BAA2B,GACtC,UAAS,kCAAuC,KAC/C,qBAoBF,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { createStateConditionEvaluator } from "./state-condition-evaluator.js";
2
+ /**
3
+ * Factory function for creating StateResponseResolver.
4
+ */
5
+ export const createStateResponseResolver = (options = {}) => {
6
+ const evaluator = options.evaluator ?? createStateConditionEvaluator();
7
+ return {
8
+ resolveResponse(stateResponse, currentState) {
9
+ const matchingCondition = evaluator.findMatchingCondition(stateResponse.conditions, currentState);
10
+ if (matchingCondition) {
11
+ return matchingCondition.then;
12
+ }
13
+ return stateResponse.default;
14
+ },
15
+ };
16
+ };
@@ -52,5 +52,15 @@ export interface StateManager {
52
52
  * @param testId - Test identifier
53
53
  */
54
54
  reset(testId: string): void;
55
+ /**
56
+ * Merge partial state into existing state for a test ID (ADR-0019).
57
+ *
58
+ * Performs a shallow merge: `{ ...currentState, ...partial }`.
59
+ * Used by afterResponse.setState to update test state after mock responses.
60
+ *
61
+ * @param testId - Test identifier for state isolation
62
+ * @param partial - Partial state to merge into existing state
63
+ */
64
+ merge(testId: string, partial: Record<string, unknown>): void;
55
65
  }
56
66
  //# sourceMappingURL=state-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../../src/ports/driven/state-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAE1C;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAEvD;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEhD;;;;;;;;;;OAUG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B"}
1
+ {"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../../src/ports/driven/state-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAE1C;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAEvD;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEhD;;;;;;;;;;OAUG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;;;;;;OAQG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC/D"}
@@ -15,4 +15,5 @@ export { ScenarioRequestSchema, type ScenarioRequest, } from "./scenario-request
15
15
  export { ScenariosObjectSchema } from "./scenarios-object.js";
16
16
  export { HttpMethodSchema, ScenaristResponseSchema, MatchValueSchema, ScenaristMatchSchema, RepeatModeSchema, ScenaristSequenceSchema, ScenaristCaptureConfigSchema, ScenaristUrlPatternSchema, ScenaristMockSchema, ScenaristScenarioSchema, type HttpMethod, type ScenaristResponse, type MatchValue, type ScenaristMatch, type RepeatMode, type ScenaristSequence, type ScenaristCaptureConfig, type ScenaristUrlPattern, type ScenaristMock, type ScenaristScenario, } from "./scenario-definition.js";
17
17
  export { SerializedRegexSchema, type SerializedRegex, } from "./match-criteria.js";
18
+ export { StateConditionSchema, StatefulMockResponseSchema, StateAfterResponseSchema, StateMatchCriteriaSchema, type StateCondition, type StatefulMockResponse, type StateAfterResponse, type StateMatchCriteria, } from "./state-aware-mocking.js";
18
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAC5B,yBAAyB,EACzB,mBAAmB,EACnB,uBAAuB,EAEvB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAC5B,yBAAyB,EACzB,mBAAmB,EACnB,uBAAuB,EAEvB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,wBAAwB,EACxB,wBAAwB,EACxB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,0BAA0B,CAAC"}
@@ -15,3 +15,4 @@ export { ScenarioRequestSchema, } from "./scenario-requests.js";
15
15
  export { ScenariosObjectSchema } from "./scenarios-object.js";
16
16
  export { HttpMethodSchema, ScenaristResponseSchema, MatchValueSchema, ScenaristMatchSchema, RepeatModeSchema, ScenaristSequenceSchema, ScenaristCaptureConfigSchema, ScenaristUrlPatternSchema, ScenaristMockSchema, ScenaristScenarioSchema, } from "./scenario-definition.js";
17
17
  export { SerializedRegexSchema, } from "./match-criteria.js";
18
+ export { StateConditionSchema, StatefulMockResponseSchema, StateAfterResponseSchema, StateMatchCriteriaSchema, } from "./state-aware-mocking.js";
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schema for a scenarist response.
4
+ *
5
+ * Extracted to avoid circular dependencies between
6
+ * scenario-definition.ts and state-aware-mocking.ts.
7
+ */
8
+ export declare const ScenaristResponseSchema: z.ZodObject<{
9
+ status: z.ZodNumber;
10
+ body: z.ZodOptional<z.ZodUnknown>;
11
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
12
+ delay: z.ZodOptional<z.ZodNumber>;
13
+ }, z.core.$strip>;
14
+ export type ScenaristResponse = z.infer<typeof ScenaristResponseSchema>;
15
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/schemas/response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;;;iBAKlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schema for a scenarist response.
4
+ *
5
+ * Extracted to avoid circular dependencies between
6
+ * scenario-definition.ts and state-aware-mocking.ts.
7
+ */
8
+ export const ScenaristResponseSchema = z.object({
9
+ status: z.number().int().min(100).max(599),
10
+ body: z.unknown().optional(),
11
+ headers: z.record(z.string(), z.string()).optional(),
12
+ delay: z.number().nonnegative().optional(),
13
+ });
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ export { ScenaristResponseSchema, type ScenaristResponse } from "./response.js";
2
3
  /**
3
4
  * Zod schemas for scenario definitions.
4
5
  * These schemas validate the structure of scenario data at trust boundaries.
@@ -16,13 +17,6 @@ export declare const HttpMethodSchema: z.ZodEnum<{
16
17
  HEAD: "HEAD";
17
18
  }>;
18
19
  export type HttpMethod = z.infer<typeof HttpMethodSchema>;
19
- export declare const ScenaristResponseSchema: z.ZodObject<{
20
- status: z.ZodNumber;
21
- body: z.ZodOptional<z.ZodUnknown>;
22
- headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
23
- delay: z.ZodOptional<z.ZodNumber>;
24
- }, z.core.$strip>;
25
- export type ScenaristResponse = z.infer<typeof ScenaristResponseSchema>;
26
20
  /**
27
21
  * Match value supports 6 matching strategies:
28
22
  * - Plain string: exact match (backward compatible)
@@ -87,6 +81,7 @@ export declare const ScenaristMatchSchema: z.ZodObject<{
87
81
  flags: z.ZodOptional<z.ZodString>;
88
82
  }, z.core.$strip>>;
89
83
  }, z.core.$strip>]>>>;
84
+ state: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
90
85
  }, z.core.$strip>;
91
86
  export type ScenaristMatch = z.infer<typeof ScenaristMatchSchema>;
92
87
  export declare const RepeatModeSchema: z.ZodEnum<{
@@ -118,6 +113,12 @@ export type ScenaristCaptureConfig = z.infer<typeof ScenaristCaptureConfigSchema
118
113
  */
119
114
  export declare const ScenaristUrlPatternSchema: z.ZodUnion<readonly [z.ZodString, z.ZodCustom<RegExp, RegExp>]>;
120
115
  export type ScenaristUrlPattern = z.infer<typeof ScenaristUrlPatternSchema>;
116
+ /**
117
+ * ScenaristMock schema with mutual exclusion constraint.
118
+ *
119
+ * A mock can have exactly ONE of: response, sequence, or stateResponse.
120
+ * The afterResponse field can be combined with any of these.
121
+ */
121
122
  export declare const ScenaristMockSchema: z.ZodObject<{
122
123
  method: z.ZodEnum<{
123
124
  GET: "GET";
@@ -170,6 +171,7 @@ export declare const ScenaristMockSchema: z.ZodObject<{
170
171
  flags: z.ZodOptional<z.ZodString>;
171
172
  }, z.core.$strip>>;
172
173
  }, z.core.$strip>]>>>;
174
+ state: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
173
175
  }, z.core.$strip>>;
174
176
  response: z.ZodOptional<z.ZodObject<{
175
177
  status: z.ZodNumber;
@@ -190,7 +192,27 @@ export declare const ScenaristMockSchema: z.ZodObject<{
190
192
  none: "none";
191
193
  }>>;
192
194
  }, z.core.$strip>>;
195
+ stateResponse: z.ZodOptional<z.ZodObject<{
196
+ default: z.ZodObject<{
197
+ status: z.ZodNumber;
198
+ body: z.ZodOptional<z.ZodUnknown>;
199
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
200
+ delay: z.ZodOptional<z.ZodNumber>;
201
+ }, z.core.$strip>;
202
+ conditions: z.ZodArray<z.ZodObject<{
203
+ when: z.ZodRecord<z.ZodString, z.ZodUnknown>;
204
+ then: z.ZodObject<{
205
+ status: z.ZodNumber;
206
+ body: z.ZodOptional<z.ZodUnknown>;
207
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
208
+ delay: z.ZodOptional<z.ZodNumber>;
209
+ }, z.core.$strip>;
210
+ }, z.core.$strip>>;
211
+ }, z.core.$strip>>;
193
212
  captureState: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
213
+ afterResponse: z.ZodOptional<z.ZodObject<{
214
+ setState: z.ZodRecord<z.ZodString, z.ZodUnknown>;
215
+ }, z.core.$strip>>;
194
216
  }, z.core.$strip>;
195
217
  export type ScenaristMock = z.infer<typeof ScenaristMockSchema>;
196
218
  export declare const ScenaristScenarioSchema: z.ZodObject<{
@@ -249,6 +271,7 @@ export declare const ScenaristScenarioSchema: z.ZodObject<{
249
271
  flags: z.ZodOptional<z.ZodString>;
250
272
  }, z.core.$strip>>;
251
273
  }, z.core.$strip>]>>>;
274
+ state: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
252
275
  }, z.core.$strip>>;
253
276
  response: z.ZodOptional<z.ZodObject<{
254
277
  status: z.ZodNumber;
@@ -269,7 +292,27 @@ export declare const ScenaristScenarioSchema: z.ZodObject<{
269
292
  none: "none";
270
293
  }>>;
271
294
  }, z.core.$strip>>;
295
+ stateResponse: z.ZodOptional<z.ZodObject<{
296
+ default: z.ZodObject<{
297
+ status: z.ZodNumber;
298
+ body: z.ZodOptional<z.ZodUnknown>;
299
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
300
+ delay: z.ZodOptional<z.ZodNumber>;
301
+ }, z.core.$strip>;
302
+ conditions: z.ZodArray<z.ZodObject<{
303
+ when: z.ZodRecord<z.ZodString, z.ZodUnknown>;
304
+ then: z.ZodObject<{
305
+ status: z.ZodNumber;
306
+ body: z.ZodOptional<z.ZodUnknown>;
307
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
308
+ delay: z.ZodOptional<z.ZodNumber>;
309
+ }, z.core.$strip>;
310
+ }, z.core.$strip>>;
311
+ }, z.core.$strip>>;
272
312
  captureState: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
313
+ afterResponse: z.ZodOptional<z.ZodObject<{
314
+ setState: z.ZodRecord<z.ZodString, z.ZodUnknown>;
315
+ }, z.core.$strip>>;
273
316
  }, z.core.$strip>>;
274
317
  }, z.core.$strip>;
275
318
  export type ScenaristScenario = z.infer<typeof ScenaristScenarioSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"scenario-definition.d.ts","sourceRoot":"","sources":["../../src/schemas/scenario-definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;GAMG;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;EAQ3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,uBAAuB;;;;;iBAKlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;mBA4B3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,gBAAgB;;;;EAAoC,CAAC;AAClE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,uBAAuB;;;;;;;;;;;;iBAGlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,4BAA4B,uCAAmC,CAAC;AAC7E,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAC1C,OAAO,4BAA4B,CACpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,iEAGpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
1
+ {"version":3,"file":"scenario-definition.d.ts","sourceRoot":"","sources":["../../src/schemas/scenario-definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,OAAO,EAAE,uBAAuB,EAAE,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEhF;;;;;;GAMG;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;EAQ3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;mBA4B3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAM/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,gBAAgB;;;;EAAoC,CAAC;AAClE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,uBAAuB;;;;;;;;;;;;iBAGlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,4BAA4B,uCAAmC,CAAC;AAC7E,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAC1C,OAAO,4BAA4B,CACpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,iEAGpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsB7B,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
@@ -1,5 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { SerializedRegexSchema } from "./match-criteria.js";
3
+ import { ScenaristResponseSchema } from "./response.js";
4
+ import { StatefulMockResponseSchema, StateAfterResponseSchema, } from "./state-aware-mocking.js";
5
+ // Re-export from response.ts (no backward compatibility concerns - no consumers yet)
6
+ export { ScenaristResponseSchema } from "./response.js";
3
7
  /**
4
8
  * Zod schemas for scenario definitions.
5
9
  * These schemas validate the structure of scenario data at trust boundaries.
@@ -16,12 +20,6 @@ export const HttpMethodSchema = z.enum([
16
20
  "OPTIONS",
17
21
  "HEAD",
18
22
  ]);
19
- export const ScenaristResponseSchema = z.object({
20
- status: z.number().int().min(100).max(599),
21
- body: z.unknown().optional(),
22
- headers: z.record(z.string(), z.string()).optional(),
23
- delay: z.number().nonnegative().optional(),
24
- });
25
23
  /**
26
24
  * Match value supports 6 matching strategies:
27
25
  * - Plain string: exact match (backward compatible)
@@ -64,6 +62,7 @@ export const ScenaristMatchSchema = z.object({
64
62
  body: z.record(z.string(), MatchValueSchema).optional(),
65
63
  headers: z.record(z.string(), MatchValueSchema).optional(),
66
64
  query: z.record(z.string(), MatchValueSchema).optional(),
65
+ state: z.record(z.string(), z.unknown()).optional(),
67
66
  });
68
67
  export const RepeatModeSchema = z.enum(["last", "cycle", "none"]);
69
68
  export const ScenaristSequenceSchema = z.object({
@@ -80,13 +79,30 @@ export const ScenaristUrlPatternSchema = z.union([
80
79
  z.string().min(1),
81
80
  z.instanceof(RegExp),
82
81
  ]);
83
- export const ScenaristMockSchema = z.object({
82
+ /**
83
+ * ScenaristMock schema with mutual exclusion constraint.
84
+ *
85
+ * A mock can have exactly ONE of: response, sequence, or stateResponse.
86
+ * The afterResponse field can be combined with any of these.
87
+ */
88
+ export const ScenaristMockSchema = z
89
+ .object({
84
90
  method: HttpMethodSchema,
85
91
  url: ScenaristUrlPatternSchema,
86
92
  match: ScenaristMatchSchema.optional(),
87
93
  response: ScenaristResponseSchema.optional(),
88
94
  sequence: ScenaristSequenceSchema.optional(),
95
+ stateResponse: StatefulMockResponseSchema.optional(),
89
96
  captureState: ScenaristCaptureConfigSchema.optional(),
97
+ afterResponse: StateAfterResponseSchema.optional(),
98
+ })
99
+ .refine((mock) => {
100
+ const responseTypes = [mock.response, mock.sequence, mock.stateResponse];
101
+ const definedCount = responseTypes.filter((r) => r !== undefined).length;
102
+ // Allow 0 (for fallback mocks) or exactly 1
103
+ return definedCount <= 1;
104
+ }, {
105
+ message: "A mock can have at most one of: response, sequence, or stateResponse",
90
106
  });
91
107
  export const ScenaristScenarioSchema = z.object({
92
108
  id: z.string().min(1),
@@ -65,6 +65,7 @@ export declare const ScenariosObjectSchema: z.ZodRecord<z.ZodString, z.ZodObject
65
65
  flags: z.ZodOptional<z.ZodString>;
66
66
  }, z.core.$strip>>;
67
67
  }, z.core.$strip>]>>>;
68
+ state: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
68
69
  }, z.core.$strip>>;
69
70
  response: z.ZodOptional<z.ZodObject<{
70
71
  status: z.ZodNumber;
@@ -85,7 +86,27 @@ export declare const ScenariosObjectSchema: z.ZodRecord<z.ZodString, z.ZodObject
85
86
  none: "none";
86
87
  }>>;
87
88
  }, z.core.$strip>>;
89
+ stateResponse: z.ZodOptional<z.ZodObject<{
90
+ default: z.ZodObject<{
91
+ status: z.ZodNumber;
92
+ body: z.ZodOptional<z.ZodUnknown>;
93
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
94
+ delay: z.ZodOptional<z.ZodNumber>;
95
+ }, z.core.$strip>;
96
+ conditions: z.ZodArray<z.ZodObject<{
97
+ when: z.ZodRecord<z.ZodString, z.ZodUnknown>;
98
+ then: z.ZodObject<{
99
+ status: z.ZodNumber;
100
+ body: z.ZodOptional<z.ZodUnknown>;
101
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
102
+ delay: z.ZodOptional<z.ZodNumber>;
103
+ }, z.core.$strip>;
104
+ }, z.core.$strip>>;
105
+ }, z.core.$strip>>;
88
106
  captureState: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
107
+ afterResponse: z.ZodOptional<z.ZodObject<{
108
+ setState: z.ZodRecord<z.ZodString, z.ZodUnknown>;
109
+ }, z.core.$strip>>;
89
110
  }, z.core.$strip>>;
90
111
  }, z.core.$strip>>;
91
112
  //# sourceMappingURL=scenarios-object.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scenarios-object.d.ts","sourceRoot":"","sources":["../../src/schemas/scenarios-object.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC"}
1
+ {"version":3,"file":"scenarios-object.d.ts","sourceRoot":"","sources":["../../src/schemas/scenarios-object.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC"}
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Zod schemas for state-aware mocking (ADR-0019).
4
+ *
5
+ * These schemas define the structure of:
6
+ * - stateResponse: Conditional responses based on test state
7
+ * - afterResponse.setState: State mutation after response
8
+ * - match.state: Mock selection based on current state
9
+ */
10
+ /**
11
+ * Schema for a single state condition.
12
+ *
13
+ * The `when` clause is a partial match object - all keys must match
14
+ * the current state for the condition to apply.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * {
19
+ * when: { checked: true, step: 'reviewed' },
20
+ * then: { status: 200, body: { state: 'approved' } }
21
+ * }
22
+ * ```
23
+ */
24
+ export declare const StateConditionSchema: z.ZodObject<{
25
+ when: z.ZodRecord<z.ZodString, z.ZodUnknown>;
26
+ then: z.ZodObject<{
27
+ status: z.ZodNumber;
28
+ body: z.ZodOptional<z.ZodUnknown>;
29
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
30
+ delay: z.ZodOptional<z.ZodNumber>;
31
+ }, z.core.$strip>;
32
+ }, z.core.$strip>;
33
+ export type StateCondition = z.infer<typeof StateConditionSchema>;
34
+ /**
35
+ * Schema for stateful mock response (stateResponse).
36
+ *
37
+ * Returns different responses based on current test state.
38
+ * Uses specificity-based selection: most specific matching condition wins.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * {
43
+ * default: { status: 200, body: { state: 'appStarted' } },
44
+ * conditions: [
45
+ * { when: { checked: true }, then: { status: 200, body: { state: 'quoteDecline' } } }
46
+ * ]
47
+ * }
48
+ * ```
49
+ */
50
+ export declare const StatefulMockResponseSchema: z.ZodObject<{
51
+ default: z.ZodObject<{
52
+ status: z.ZodNumber;
53
+ body: z.ZodOptional<z.ZodUnknown>;
54
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
55
+ delay: z.ZodOptional<z.ZodNumber>;
56
+ }, z.core.$strip>;
57
+ conditions: z.ZodArray<z.ZodObject<{
58
+ when: z.ZodRecord<z.ZodString, z.ZodUnknown>;
59
+ then: z.ZodObject<{
60
+ status: z.ZodNumber;
61
+ body: z.ZodOptional<z.ZodUnknown>;
62
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
63
+ delay: z.ZodOptional<z.ZodNumber>;
64
+ }, z.core.$strip>;
65
+ }, z.core.$strip>>;
66
+ }, z.core.$strip>;
67
+ export type StatefulMockResponse = z.infer<typeof StatefulMockResponseSchema>;
68
+ /**
69
+ * Schema for afterResponse configuration.
70
+ *
71
+ * The setState object is merged into the current test state after
72
+ * the response is returned.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * {
77
+ * setState: { checked: true, step: 'completed' }
78
+ * }
79
+ * ```
80
+ */
81
+ export declare const StateAfterResponseSchema: z.ZodObject<{
82
+ setState: z.ZodRecord<z.ZodString, z.ZodUnknown>;
83
+ }, z.core.$strip>;
84
+ export type StateAfterResponse = z.infer<typeof StateAfterResponseSchema>;
85
+ /**
86
+ * Schema for state matching criteria (used in match.state).
87
+ *
88
+ * Partial match - all keys in the match criteria must match
89
+ * the current state, but state can have additional keys.
90
+ */
91
+ export declare const StateMatchCriteriaSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
92
+ export type StateMatchCriteria = z.infer<typeof StateMatchCriteriaSchema>;
93
+ //# sourceMappingURL=state-aware-mocking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-aware-mocking.d.ts","sourceRoot":"","sources":["../../src/schemas/state-aware-mocking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;GAOG;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;iBAO/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;iBAGrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB;;iBAMnC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,wCAAoC,CAAC;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { ScenaristResponseSchema } from "./response.js";
3
+ /**
4
+ * Zod schemas for state-aware mocking (ADR-0019).
5
+ *
6
+ * These schemas define the structure of:
7
+ * - stateResponse: Conditional responses based on test state
8
+ * - afterResponse.setState: State mutation after response
9
+ * - match.state: Mock selection based on current state
10
+ */
11
+ /**
12
+ * Schema for a single state condition.
13
+ *
14
+ * The `when` clause is a partial match object - all keys must match
15
+ * the current state for the condition to apply.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * {
20
+ * when: { checked: true, step: 'reviewed' },
21
+ * then: { status: 200, body: { state: 'approved' } }
22
+ * }
23
+ * ```
24
+ */
25
+ export const StateConditionSchema = z.object({
26
+ when: z
27
+ .record(z.string(), z.unknown())
28
+ .refine((obj) => Object.keys(obj).length > 0, {
29
+ message: "when clause must have at least one key",
30
+ }),
31
+ then: ScenaristResponseSchema,
32
+ });
33
+ /**
34
+ * Schema for stateful mock response (stateResponse).
35
+ *
36
+ * Returns different responses based on current test state.
37
+ * Uses specificity-based selection: most specific matching condition wins.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * {
42
+ * default: { status: 200, body: { state: 'appStarted' } },
43
+ * conditions: [
44
+ * { when: { checked: true }, then: { status: 200, body: { state: 'quoteDecline' } } }
45
+ * ]
46
+ * }
47
+ * ```
48
+ */
49
+ export const StatefulMockResponseSchema = z.object({
50
+ default: ScenaristResponseSchema,
51
+ conditions: z.array(StateConditionSchema),
52
+ });
53
+ /**
54
+ * Schema for afterResponse configuration.
55
+ *
56
+ * The setState object is merged into the current test state after
57
+ * the response is returned.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * {
62
+ * setState: { checked: true, step: 'completed' }
63
+ * }
64
+ * ```
65
+ */
66
+ export const StateAfterResponseSchema = z.object({
67
+ setState: z
68
+ .record(z.string(), z.unknown())
69
+ .refine((obj) => Object.keys(obj).length > 0, {
70
+ message: "setState must have at least one key",
71
+ }),
72
+ });
73
+ /**
74
+ * Schema for state matching criteria (used in match.state).
75
+ *
76
+ * Partial match - all keys in the match criteria must match
77
+ * the current state, but state can have additional keys.
78
+ */
79
+ export const StateMatchCriteriaSchema = z.record(z.string(), z.unknown());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenarist/core",
3
- "version": "0.1.16",
3
+ "version": "0.2.1",
4
4
  "description": "Internal: Hexagonal architecture core for scenario-based testing with MSW",
5
5
  "author": "Paul Hammond (citypaul) <paul@packsoftware.co.uk>",
6
6
  "license": "MIT",