@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.
- package/dist/adapters/in-memory-state-manager.d.ts +1 -0
- package/dist/adapters/in-memory-state-manager.d.ts.map +1 -1
- package/dist/adapters/in-memory-state-manager.js +14 -0
- package/dist/domain/deep-equals.d.ts +12 -0
- package/dist/domain/deep-equals.d.ts.map +1 -0
- package/dist/domain/deep-equals.js +62 -0
- package/dist/domain/index.d.ts +3 -0
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +3 -0
- package/dist/domain/response-selector.d.ts.map +1 -1
- package/dist/domain/response-selector.js +85 -9
- package/dist/domain/state-condition-evaluator.d.ts +28 -0
- package/dist/domain/state-condition-evaluator.d.ts.map +1 -0
- package/dist/domain/state-condition-evaluator.js +41 -0
- package/dist/domain/state-response-resolver.d.ts +36 -0
- package/dist/domain/state-response-resolver.d.ts.map +1 -0
- package/dist/domain/state-response-resolver.js +16 -0
- package/dist/ports/driven/state-manager.d.ts +10 -0
- package/dist/ports/driven/state-manager.d.ts.map +1 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/response.d.ts +15 -0
- package/dist/schemas/response.d.ts.map +1 -0
- package/dist/schemas/response.js +13 -0
- package/dist/schemas/scenario-definition.d.ts +50 -7
- package/dist/schemas/scenario-definition.d.ts.map +1 -1
- package/dist/schemas/scenario-definition.js +23 -7
- package/dist/schemas/scenarios-object.d.ts +21 -0
- package/dist/schemas/scenarios-object.d.ts.map +1 -1
- package/dist/schemas/state-aware-mocking.d.ts +93 -0
- package/dist/schemas/state-aware-mocking.d.ts.map +1 -0
- package/dist/schemas/state-aware-mocking.js +79 -0
- 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
|
+
};
|
package/dist/domain/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/domain/index.js
CHANGED
|
@@ -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;
|
|
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 (
|
|
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 (
|
|
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
|
-
* @
|
|
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
|
-
//
|
|
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;
|
|
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"}
|
package/dist/schemas/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/schemas/index.js
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
|
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.
|
|
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/
|
|
60
|
-
"@scenarist/
|
|
59
|
+
"@scenarist/typescript-config": "0.0.0",
|
|
60
|
+
"@scenarist/eslint-config": "0.0.0"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsc",
|