@scenarist/core 0.1.3 → 0.2.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 (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 +85 -9
  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 +3 -3
@@ -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,gBAuIF,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 +
@@ -73,8 +75,8 @@ export const createResponseSelector = (options = {}) => {
73
75
  if (bestMatch) {
74
76
  const { mockWithParams, mockIndex } = bestMatch;
75
77
  const mock = mockWithParams.mock;
76
- // Select response (either single or from sequence)
77
- const response = selectResponseFromMock(testId, scenarioId, mockIndex, mock, sequenceTracker);
78
+ // Select response (single, sequence, or stateResponse)
79
+ const response = selectResponseFromMock(testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager);
78
80
  if (!response) {
79
81
  return {
80
82
  success: false,
@@ -97,6 +99,10 @@ export const createResponseSelector = (options = {}) => {
97
99
  };
98
100
  finalResponse = applyTemplates(response, templateData);
99
101
  }
102
+ // Apply afterResponse.setState to mutate state for subsequent requests
103
+ if (mock.afterResponse?.setState && stateManager) {
104
+ stateManager.merge(testId, mock.afterResponse.setState);
105
+ }
100
106
  return { success: true, data: finalResponse };
101
107
  }
102
108
  // No mock matched
@@ -108,16 +114,17 @@ export const createResponseSelector = (options = {}) => {
108
114
  };
109
115
  };
110
116
  /**
111
- * Select a response from a mock (either single response or from sequence).
117
+ * Select a response from a mock (single response, sequence, or stateResponse).
112
118
  *
113
- * @param testId - Test ID for sequence tracking
119
+ * @param testId - Test ID for sequence/state tracking
114
120
  * @param scenarioId - Scenario ID for sequence tracking
115
121
  * @param mockIndex - Index of the mock in the mocks array
116
122
  * @param mock - The mock definition
117
123
  * @param sequenceTracker - Optional sequence tracker for Phase 2
118
- * @returns ScenaristResponse or null if mock has neither response nor sequence
124
+ * @param stateManager - Optional state manager for stateResponse
125
+ * @returns ScenaristResponse or null if mock has no response type
119
126
  */
120
- const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTracker) => {
127
+ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager) => {
121
128
  // Phase 2: If mock has a sequence, use sequence tracker
122
129
  if (mock.sequence) {
123
130
  if (!sequenceTracker) {
@@ -135,13 +142,39 @@ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTra
135
142
  sequenceTracker.advance(testId, scenarioId, mockIndex, mock.sequence.responses.length, repeatMode);
136
143
  return response;
137
144
  }
145
+ // State-aware response: evaluate conditions against current state
146
+ if (mock.stateResponse) {
147
+ return resolveStateResponse(testId, mock.stateResponse, stateManager);
148
+ }
138
149
  // Phase 1: Single response
139
150
  if (mock.response) {
140
151
  return mock.response;
141
152
  }
142
- // Neither response nor sequence defined
153
+ // No response type defined
143
154
  return null;
144
155
  };
156
+ /**
157
+ * Resolve a stateResponse configuration to a single response.
158
+ *
159
+ * Uses the StateResponseResolver to evaluate conditions against
160
+ * current test state and return the appropriate response.
161
+ *
162
+ * @param testId - Test ID for state isolation
163
+ * @param stateResponse - The stateResponse configuration
164
+ * @param stateManager - Optional state manager for state lookup
165
+ * @returns The resolved response (matching condition or default)
166
+ */
167
+ const resolveStateResponse = (testId, stateResponse, stateManager) => {
168
+ // Without stateManager, always return default
169
+ if (!stateManager) {
170
+ return stateResponse.default;
171
+ }
172
+ // Get current state for this test
173
+ const currentState = stateManager.getAll(testId);
174
+ // Create resolver and evaluate conditions
175
+ const resolver = createStateResponseResolver();
176
+ return resolver.resolveResponse(stateResponse, currentState);
177
+ };
145
178
  /**
146
179
  * Calculate specificity score for match criteria.
147
180
  * Higher score = more specific match.
@@ -151,11 +184,13 @@ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTra
151
184
  * - Each body field = +1 point
152
185
  * - Each header = +1 point
153
186
  * - Each query param = +1 point
187
+ * - Each state key = +1 point
154
188
  *
155
189
  * Example:
156
190
  * { body: { itemType: 'premium' } } = 1 point
157
191
  * { body: { itemType: 'premium', quantity: 5 }, headers: { 'x-tier': 'gold' } } = 3 points
158
192
  * { url: '/api/products', body: { itemType: 'premium' } } = 2 points
193
+ * { state: { step: 'reviewed', approved: true } } = 2 points
159
194
  */
160
195
  const calculateSpecificity = (criteria) => {
161
196
  let score = 0;
@@ -171,13 +206,21 @@ const calculateSpecificity = (criteria) => {
171
206
  if (criteria.query) {
172
207
  score += Object.keys(criteria.query).length;
173
208
  }
209
+ if (criteria.state) {
210
+ score += Object.keys(criteria.state).length;
211
+ }
174
212
  return score;
175
213
  };
176
214
  /**
177
215
  * Check if request context matches the specified criteria.
178
216
  * All specified criteria must match for the overall match to succeed.
217
+ *
218
+ * @param context - HTTP request context
219
+ * @param criteria - Match criteria from mock definition
220
+ * @param testId - Test ID for state isolation
221
+ * @param stateManager - Optional state manager for state-based matching
179
222
  */
180
- const matchesCriteria = (context, criteria) => {
223
+ const matchesCriteria = (context, criteria, testId, stateManager) => {
181
224
  // Check URL match (exact match or pattern)
182
225
  if (criteria.url) {
183
226
  if (!matchesValue(context.url, criteria.url)) {
@@ -202,9 +245,42 @@ const matchesCriteria = (context, criteria) => {
202
245
  return false;
203
246
  }
204
247
  }
248
+ // Check state match (partial match on current test state)
249
+ if (criteria.state) {
250
+ if (!matchesState(criteria.state, testId, stateManager)) {
251
+ return false;
252
+ }
253
+ }
205
254
  // All criteria matched
206
255
  return true;
207
256
  };
257
+ /**
258
+ * Check if current test state matches the specified state criteria.
259
+ * All keys in criteria must exist in state with equal values (partial match).
260
+ *
261
+ * @param stateCriteria - Required state key-value pairs
262
+ * @param testId - Test ID for state isolation
263
+ * @param stateManager - State manager to retrieve current state
264
+ * @returns true if all criteria keys match, false otherwise
265
+ */
266
+ const matchesState = (stateCriteria, testId, stateManager) => {
267
+ // Without stateManager, state matching always fails
268
+ if (!stateManager) {
269
+ return false;
270
+ }
271
+ const currentState = stateManager.getAll(testId);
272
+ // All keys in criteria must exist in state with equal values
273
+ for (const [key, expectedValue] of Object.entries(stateCriteria)) {
274
+ if (!(key in currentState)) {
275
+ return false;
276
+ }
277
+ // Deep equality check for values (handles primitives, null, objects)
278
+ if (!deepEquals(currentState[key], expectedValue)) {
279
+ return false;
280
+ }
281
+ }
282
+ return true;
283
+ };
208
284
  /**
209
285
  * Check if request body contains all required fields (partial match).
210
286
  * 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.3",
3
+ "version": "0.2.0",
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",
@@ -56,8 +56,8 @@
56
56
  "fast-check": "^4.3.0",
57
57
  "typescript": "^5.9.3",
58
58
  "vitest": "^4.0.14",
59
- "@scenarist/eslint-config": "0.0.0",
60
- "@scenarist/typescript-config": "0.0.0"
59
+ "@scenarist/typescript-config": "0.0.0",
60
+ "@scenarist/eslint-config": "0.0.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsc",