@scenarist/core 0.4.8 → 0.4.10
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.map +1 -1
- package/dist/adapters/in-memory-state-manager.js +1 -9
- package/dist/domain/deep-equals.d.ts.map +1 -1
- package/dist/domain/deep-equals.js +1 -7
- package/dist/domain/path-extraction.d.ts.map +1 -1
- package/dist/domain/path-extraction.js +1 -9
- package/dist/domain/regex-matching.d.ts.map +1 -1
- package/dist/domain/regex-matching.js +1 -2
- package/dist/domain/response-selector.d.ts +0 -3
- package/dist/domain/response-selector.d.ts.map +1 -1
- package/dist/domain/response-selector.js +197 -210
- package/dist/domain/scenario-manager.d.ts.map +1 -1
- package/dist/domain/scenario-manager.js +9 -8
- package/dist/domain/template-replacement.d.ts +0 -13
- package/dist/domain/template-replacement.d.ts.map +1 -1
- package/dist/domain/template-replacement.js +24 -48
- package/dist/domain/type-guards.d.ts +3 -0
- package/dist/domain/type-guards.d.ts.map +1 -0
- package/dist/domain/type-guards.js +3 -0
- package/package.json +3 -3
|
@@ -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;
|
|
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;AAGrE;;;;;;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;IA6CtB,OAAO,CAAC,cAAc;CA8BvB;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAAO,YAE7C,CAAC"}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const isDangerousKey = (key) => DANGEROUS_KEYS.has(key);
|
|
3
|
-
/**
|
|
4
|
-
* Type guard to check if a value is a plain object (Record).
|
|
5
|
-
* Used to properly narrow types after typeof checks.
|
|
6
|
-
*/
|
|
7
|
-
const isRecord = (value) => {
|
|
8
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
-
};
|
|
1
|
+
import { isRecord, isDangerousKey } from "../domain/type-guards.js";
|
|
10
2
|
/**
|
|
11
3
|
* In-memory implementation of StateManager port.
|
|
12
4
|
* Fast, single-process state storage for stateful mocks.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deep-equals.d.ts","sourceRoot":"","sources":["../../src/domain/deep-equals.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deep-equals.d.ts","sourceRoot":"","sources":["../../src/domain/deep-equals.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,OAAO,EAAE,GAAG,OAAO,KAAG,OA+DnD,CAAC"}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Type guard to check if a value is a plain object (Record).
|
|
3
|
-
* Used to properly narrow types after typeof checks.
|
|
4
|
-
*/
|
|
5
|
-
const isRecord = (value) => {
|
|
6
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
-
};
|
|
1
|
+
import { isRecord } from "./type-guards.js";
|
|
8
2
|
/**
|
|
9
3
|
* Deep equality comparison for values.
|
|
10
4
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-extraction.d.ts","sourceRoot":"","sources":["../../src/domain/path-extraction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"path-extraction.d.ts","sourceRoot":"","sources":["../../src/domain/path-extraction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAS,kBAAkB,EAC3B,MAAM,MAAM,KACX,OAyBF,CAAC"}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const isDangerousKey = (key) => DANGEROUS_KEYS.has(key);
|
|
3
|
-
/**
|
|
4
|
-
* Type guard to check if a value is a plain object (Record).
|
|
5
|
-
* Used to properly narrow types after typeof checks.
|
|
6
|
-
*/
|
|
7
|
-
const isRecord = (value) => {
|
|
8
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
-
};
|
|
1
|
+
import { isRecord, isDangerousKey } from "./type-guards.js";
|
|
10
2
|
/**
|
|
11
3
|
* Extracts a value from HttpRequestContext based on a path expression.
|
|
12
4
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regex-matching.d.ts","sourceRoot":"","sources":["../../src/domain/regex-matching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GACvB,OAAO,MAAM,EACb,SAAS,eAAe,KACvB,
|
|
1
|
+
{"version":3,"file":"regex-matching.d.ts","sourceRoot":"","sources":["../../src/domain/regex-matching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GACvB,OAAO,MAAM,EACb,SAAS,eAAe,KACvB,OAQF,CAAC"}
|
|
@@ -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,EACZ,MAAM,EACP,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,EACZ,MAAM,EACP,MAAM,mBAAmB,CAAC;AA2C3B,KAAK,6BAA6B,GAAG;IACnC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAsEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GACjC,UAAS,6BAAkC,KAC1C,gBAwSF,CAAC"}
|
|
@@ -4,20 +4,49 @@ import { applyTemplates } from "./template-replacement.js";
|
|
|
4
4
|
import { matchesRegex } from "./regex-matching.js";
|
|
5
5
|
import { createStateResponseResolver } from "./state-response-resolver.js";
|
|
6
6
|
import { deepEquals } from "./deep-equals.js";
|
|
7
|
+
import { isRecord } from "./type-guards.js";
|
|
7
8
|
import { noOpLogger } from "../adapters/index.js";
|
|
8
9
|
import { LogCategories, LogEvents } from "./log-events.js";
|
|
9
|
-
/**
|
|
10
|
-
* Type guard to check if a value is a plain object (Record).
|
|
11
|
-
* Used to properly narrow types after typeof checks.
|
|
12
|
-
*/
|
|
13
|
-
const isRecord = (value) => {
|
|
14
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
-
};
|
|
16
10
|
const SPECIFICITY_RANGES = {
|
|
17
11
|
MATCH_CRITERIA_BASE: 100,
|
|
18
12
|
SEQUENCE_FALLBACK: 1,
|
|
19
13
|
SIMPLE_FALLBACK: 0,
|
|
20
14
|
};
|
|
15
|
+
const createSequenceExhaustedError = ({ testId, scenarioId, context, }) => new ScenaristError(`Sequence exhausted for ${context.method} ${context.url}. All responses have been consumed and repeat mode is 'none'.`, {
|
|
16
|
+
code: ErrorCodes.SEQUENCE_EXHAUSTED,
|
|
17
|
+
context: {
|
|
18
|
+
testId,
|
|
19
|
+
scenarioId,
|
|
20
|
+
requestInfo: {
|
|
21
|
+
method: context.method,
|
|
22
|
+
url: context.url,
|
|
23
|
+
},
|
|
24
|
+
hint: "Add a fallback mock to handle requests after sequence exhaustion, or use repeat: 'last' or 'cycle' instead of 'none'.",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const createNoMockFoundError = ({ testId, scenarioId, context, }) => new ScenaristError(`No mock matched for ${context.method} ${context.url}`, {
|
|
28
|
+
code: ErrorCodes.NO_MOCK_FOUND,
|
|
29
|
+
context: {
|
|
30
|
+
testId,
|
|
31
|
+
scenarioId,
|
|
32
|
+
requestInfo: {
|
|
33
|
+
method: context.method,
|
|
34
|
+
url: context.url,
|
|
35
|
+
},
|
|
36
|
+
hint: "Add a fallback mock (without match criteria) to handle unmatched requests, or add a mock with matching criteria.",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const createNoResponseTypeError = ({ testId, scenarioId, mockIndex, }) => new ScenaristError(`Mock has neither response nor sequence field`, {
|
|
40
|
+
code: ErrorCodes.VALIDATION_ERROR,
|
|
41
|
+
context: {
|
|
42
|
+
testId,
|
|
43
|
+
scenarioId,
|
|
44
|
+
mockInfo: {
|
|
45
|
+
index: mockIndex,
|
|
46
|
+
},
|
|
47
|
+
hint: "Each mock must have a 'response', 'sequence', or 'stateResponse' field.",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
21
50
|
/**
|
|
22
51
|
* Creates a response selector domain service.
|
|
23
52
|
*
|
|
@@ -31,159 +60,154 @@ const SPECIFICITY_RANGES = {
|
|
|
31
60
|
*/
|
|
32
61
|
export const createResponseSelector = (options = {}) => {
|
|
33
62
|
const { sequenceTracker, stateManager, logger = noOpLogger } = options;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
// If match criteria exists but doesn't match, skip to next mock
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
// No match criteria = fallback mock (always matches)
|
|
82
|
-
// Log fallback evaluation with response type info for debugging Issue #328
|
|
83
|
-
logger.debug(LogCategories.MATCHING, LogEvents.MOCK_MATCH_EVALUATED, logContext, {
|
|
63
|
+
const isExhaustedSequence = ({ testId, scenarioId, mockIndex, mock, }) => {
|
|
64
|
+
if (!mock.sequence || !sequenceTracker) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return sequenceTracker.getPosition(testId, scenarioId, mockIndex).exhausted;
|
|
68
|
+
};
|
|
69
|
+
const scoreCriteriaMatch = ({ context, criteria, mockIndex, logContext, }) => {
|
|
70
|
+
const matched = matchesCriteria({
|
|
71
|
+
context,
|
|
72
|
+
criteria,
|
|
73
|
+
testId: logContext.testId,
|
|
74
|
+
stateManager,
|
|
75
|
+
});
|
|
76
|
+
logger.debug(LogCategories.MATCHING, LogEvents.MOCK_MATCH_EVALUATED, logContext, { mockIndex, matched, hasCriteria: true });
|
|
77
|
+
if (!matched) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return (SPECIFICITY_RANGES.MATCH_CRITERIA_BASE + calculateSpecificity(criteria));
|
|
81
|
+
};
|
|
82
|
+
const scoreFallback = ({ mock, mockIndex, logContext, }) => {
|
|
83
|
+
logger.debug(LogCategories.MATCHING, LogEvents.MOCK_MATCH_EVALUATED, logContext, {
|
|
84
|
+
mockIndex,
|
|
85
|
+
matched: true,
|
|
86
|
+
hasCriteria: false,
|
|
87
|
+
hasSequence: !!mock.sequence,
|
|
88
|
+
hasStateResponse: !!mock.stateResponse,
|
|
89
|
+
hasResponse: !!mock.response,
|
|
90
|
+
});
|
|
91
|
+
return mock.sequence || mock.stateResponse
|
|
92
|
+
? SPECIFICITY_RANGES.SEQUENCE_FALLBACK
|
|
93
|
+
: SPECIFICITY_RANGES.SIMPLE_FALLBACK;
|
|
94
|
+
};
|
|
95
|
+
const findBestMatch = ({ testId, scenarioId, context, mocks, }) => {
|
|
96
|
+
const logContext = { testId, scenarioId };
|
|
97
|
+
return mocks.reduce((acc, mockWithParams, mockIndex) => {
|
|
98
|
+
const mock = mockWithParams.mock;
|
|
99
|
+
if (isExhaustedSequence({ testId, scenarioId, mockIndex, mock })) {
|
|
100
|
+
return { ...acc, skippedExhaustedSequences: true };
|
|
101
|
+
}
|
|
102
|
+
if (mock.match) {
|
|
103
|
+
const specificity = scoreCriteriaMatch({
|
|
104
|
+
context,
|
|
105
|
+
criteria: mock.match,
|
|
84
106
|
mockIndex,
|
|
85
|
-
|
|
86
|
-
hasCriteria: false,
|
|
87
|
-
hasSequence: !!mock.sequence,
|
|
88
|
-
hasStateResponse: !!mock.stateResponse,
|
|
89
|
-
hasResponse: !!mock.response,
|
|
107
|
+
logContext,
|
|
90
108
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
: SPECIFICITY_RANGES.SIMPLE_FALLBACK;
|
|
97
|
-
if (!bestMatch || fallbackSpecificity >= bestMatch.specificity) {
|
|
98
|
-
// For equal specificity fallbacks, last wins
|
|
99
|
-
// This allows active scenario mocks to override default mocks
|
|
100
|
-
// Applies to both simple fallbacks (0) and sequence fallbacks (1)
|
|
101
|
-
bestMatch = {
|
|
102
|
-
mockWithParams,
|
|
103
|
-
mockIndex,
|
|
104
|
-
specificity: fallbackSpecificity,
|
|
109
|
+
if (specificity !== null &&
|
|
110
|
+
(!acc.bestMatch || specificity > acc.bestMatch.specificity)) {
|
|
111
|
+
return {
|
|
112
|
+
...acc,
|
|
113
|
+
bestMatch: { mockWithParams, mockIndex, specificity },
|
|
105
114
|
};
|
|
106
115
|
}
|
|
116
|
+
return acc;
|
|
117
|
+
}
|
|
118
|
+
const specificity = scoreFallback({ mock, mockIndex, logContext });
|
|
119
|
+
if (!acc.bestMatch || specificity >= acc.bestMatch.specificity) {
|
|
120
|
+
return {
|
|
121
|
+
...acc,
|
|
122
|
+
bestMatch: { mockWithParams, mockIndex, specificity },
|
|
123
|
+
};
|
|
107
124
|
}
|
|
108
|
-
|
|
125
|
+
return acc;
|
|
126
|
+
}, { bestMatch: null, skippedExhaustedSequences: false });
|
|
127
|
+
};
|
|
128
|
+
const applyResponseTemplates = ({ testId, response, params, }) => {
|
|
129
|
+
if (!stateManager && !params) {
|
|
130
|
+
return response;
|
|
131
|
+
}
|
|
132
|
+
const templateData = {
|
|
133
|
+
state: stateManager ? stateManager.getAll(testId) : {},
|
|
134
|
+
params: params || {},
|
|
135
|
+
};
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- applyTemplates preserves structure; input ScenaristResponse → output ScenaristResponse
|
|
137
|
+
return applyTemplates(response, templateData);
|
|
138
|
+
};
|
|
139
|
+
const applyAfterResponseState = ({ testId, mock, responseResult, logContext, }) => {
|
|
140
|
+
const effectiveAfterResponse = resolveEffectiveAfterResponse(mock.afterResponse, responseResult.matchedCondition);
|
|
141
|
+
if (effectiveAfterResponse?.setState && stateManager) {
|
|
142
|
+
stateManager.merge(testId, effectiveAfterResponse.setState);
|
|
143
|
+
logger.debug(LogCategories.STATE, LogEvents.STATE_SET, logContext, {
|
|
144
|
+
setState: effectiveAfterResponse.setState,
|
|
145
|
+
source: responseResult.matchedCondition ? "condition" : "mock-level",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const processMatchedMock = ({ testId, scenarioId, context, match, }) => {
|
|
150
|
+
const logContext = { testId, scenarioId };
|
|
151
|
+
const { mockWithParams, mockIndex, specificity } = match;
|
|
152
|
+
const mock = mockWithParams.mock;
|
|
153
|
+
logger.info(LogCategories.MATCHING, LogEvents.MOCK_SELECTED, logContext, {
|
|
154
|
+
mockIndex,
|
|
155
|
+
specificity,
|
|
156
|
+
});
|
|
157
|
+
const responseResult = selectResponseFromMock({
|
|
158
|
+
testId,
|
|
159
|
+
scenarioId,
|
|
160
|
+
mockIndex,
|
|
161
|
+
mock,
|
|
162
|
+
sequenceTracker,
|
|
163
|
+
stateManager,
|
|
164
|
+
logger,
|
|
165
|
+
});
|
|
166
|
+
if (!responseResult) {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: createNoResponseTypeError({ testId, scenarioId, mockIndex }),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (mock.captureState && stateManager) {
|
|
173
|
+
captureState({
|
|
174
|
+
testId,
|
|
175
|
+
context,
|
|
176
|
+
captureConfig: mock.captureState,
|
|
177
|
+
stateManager,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const finalResponse = applyResponseTemplates({
|
|
181
|
+
testId,
|
|
182
|
+
response: responseResult.response,
|
|
183
|
+
params: mockWithParams.params,
|
|
184
|
+
});
|
|
185
|
+
applyAfterResponseState({ testId, mock, responseResult, logContext });
|
|
186
|
+
return { success: true, data: finalResponse };
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
selectResponse(testId, scenarioId, context, mocks) {
|
|
190
|
+
const logContext = { testId, scenarioId };
|
|
191
|
+
logger.debug(LogCategories.MATCHING, LogEvents.MOCK_CANDIDATES_FOUND, logContext, { count: mocks.length });
|
|
192
|
+
const { bestMatch, skippedExhaustedSequences } = findBestMatch({
|
|
193
|
+
testId,
|
|
194
|
+
scenarioId,
|
|
195
|
+
context,
|
|
196
|
+
mocks,
|
|
197
|
+
});
|
|
109
198
|
if (bestMatch) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
specificity,
|
|
199
|
+
return processMatchedMock({
|
|
200
|
+
testId,
|
|
201
|
+
scenarioId,
|
|
202
|
+
context,
|
|
203
|
+
match: bestMatch,
|
|
116
204
|
});
|
|
117
|
-
// Select response (single, sequence, or stateResponse)
|
|
118
|
-
const responseResult = selectResponseFromMock(testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager, logger);
|
|
119
|
-
if (!responseResult) {
|
|
120
|
-
return {
|
|
121
|
-
success: false,
|
|
122
|
-
error: new ScenaristError(`Mock has neither response nor sequence field`, {
|
|
123
|
-
code: ErrorCodes.VALIDATION_ERROR,
|
|
124
|
-
context: {
|
|
125
|
-
testId,
|
|
126
|
-
scenarioId,
|
|
127
|
-
mockInfo: {
|
|
128
|
-
index: mockIndex,
|
|
129
|
-
},
|
|
130
|
-
hint: "Each mock must have a 'response', 'sequence', or 'stateResponse' field.",
|
|
131
|
-
},
|
|
132
|
-
}),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
// Phase 3: Capture state from request if configured
|
|
136
|
-
if (mock.captureState && stateManager) {
|
|
137
|
-
captureState(testId, context, mock.captureState, stateManager);
|
|
138
|
-
}
|
|
139
|
-
// Apply templates to response (both state AND params)
|
|
140
|
-
let finalResponse = responseResult.response;
|
|
141
|
-
if (stateManager || mockWithParams.params) {
|
|
142
|
-
const currentState = stateManager ? stateManager.getAll(testId) : {};
|
|
143
|
-
// Merge state and params for template replacement
|
|
144
|
-
// params take precedence over state for the same key
|
|
145
|
-
const templateData = {
|
|
146
|
-
state: currentState,
|
|
147
|
-
params: mockWithParams.params || {},
|
|
148
|
-
};
|
|
149
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- applyTemplates preserves structure; input ScenaristResponse → output ScenaristResponse
|
|
150
|
-
finalResponse = applyTemplates(responseResult.response, templateData);
|
|
151
|
-
}
|
|
152
|
-
// Apply afterResponse.setState to mutate state for subsequent requests
|
|
153
|
-
// Uses condition-level afterResponse if available (#338)
|
|
154
|
-
const effectiveAfterResponse = resolveEffectiveAfterResponse(mock.afterResponse, responseResult.matchedCondition);
|
|
155
|
-
if (effectiveAfterResponse?.setState && stateManager) {
|
|
156
|
-
stateManager.merge(testId, effectiveAfterResponse.setState);
|
|
157
|
-
logger.debug(LogCategories.STATE, LogEvents.STATE_SET, logContext, {
|
|
158
|
-
setState: effectiveAfterResponse.setState,
|
|
159
|
-
source: responseResult.matchedCondition
|
|
160
|
-
? "condition"
|
|
161
|
-
: "mock-level",
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
return { success: true, data: finalResponse };
|
|
165
205
|
}
|
|
166
|
-
// No mock matched - determine specific error type
|
|
167
206
|
if (skippedExhaustedSequences) {
|
|
168
|
-
|
|
169
|
-
logger.warn(LogCategories.SEQUENCE, LogEvents.SEQUENCE_EXHAUSTED, logContext, {
|
|
170
|
-
url: context.url,
|
|
171
|
-
method: context.method,
|
|
172
|
-
});
|
|
207
|
+
logger.warn(LogCategories.SEQUENCE, LogEvents.SEQUENCE_EXHAUSTED, logContext, { url: context.url, method: context.method });
|
|
173
208
|
return {
|
|
174
209
|
success: false,
|
|
175
|
-
error:
|
|
176
|
-
code: ErrorCodes.SEQUENCE_EXHAUSTED,
|
|
177
|
-
context: {
|
|
178
|
-
testId,
|
|
179
|
-
scenarioId,
|
|
180
|
-
requestInfo: {
|
|
181
|
-
method: context.method,
|
|
182
|
-
url: context.url,
|
|
183
|
-
},
|
|
184
|
-
hint: "Add a fallback mock to handle requests after sequence exhaustion, or use repeat: 'last' or 'cycle' instead of 'none'.",
|
|
185
|
-
},
|
|
186
|
-
}),
|
|
210
|
+
error: createSequenceExhaustedError({ testId, scenarioId, context }),
|
|
187
211
|
};
|
|
188
212
|
}
|
|
189
213
|
logger.warn(LogCategories.MATCHING, LogEvents.MOCK_NO_MATCH, logContext, {
|
|
@@ -193,18 +217,7 @@ export const createResponseSelector = (options = {}) => {
|
|
|
193
217
|
});
|
|
194
218
|
return {
|
|
195
219
|
success: false,
|
|
196
|
-
error:
|
|
197
|
-
code: ErrorCodes.NO_MOCK_FOUND,
|
|
198
|
-
context: {
|
|
199
|
-
testId,
|
|
200
|
-
scenarioId,
|
|
201
|
-
requestInfo: {
|
|
202
|
-
method: context.method,
|
|
203
|
-
url: context.url,
|
|
204
|
-
},
|
|
205
|
-
hint: "Add a fallback mock (without match criteria) to handle unmatched requests, or add a mock with matching criteria.",
|
|
206
|
-
},
|
|
207
|
-
}),
|
|
220
|
+
error: createNoMockFoundError({ testId, scenarioId, context }),
|
|
208
221
|
};
|
|
209
222
|
},
|
|
210
223
|
};
|
|
@@ -240,40 +253,34 @@ const resolveEffectiveAfterResponse = (mockAfterResponse, matchedCondition) => {
|
|
|
240
253
|
* @param logger - Logger for debugging
|
|
241
254
|
* @returns MockResponseResult or null if mock has no response type
|
|
242
255
|
*/
|
|
243
|
-
const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager, logger) => {
|
|
256
|
+
const selectResponseFromMock = ({ testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager, logger, }) => {
|
|
244
257
|
const logContext = { testId, scenarioId };
|
|
245
|
-
// Phase 2: If mock has a sequence, use sequence tracker
|
|
246
258
|
if (mock.sequence) {
|
|
247
259
|
if (!sequenceTracker) {
|
|
248
|
-
// Sequence defined but no tracker provided - return first response
|
|
249
|
-
// Note: Schema validation ensures responses array has at least 1 element,
|
|
250
|
-
// but defensive check handles malformed data that bypasses validation
|
|
251
260
|
const firstResponse = mock.sequence.responses[0];
|
|
252
261
|
return firstResponse
|
|
253
262
|
? { response: firstResponse, matchedCondition: null }
|
|
254
263
|
: null;
|
|
255
264
|
}
|
|
256
|
-
// Get current position from tracker
|
|
257
265
|
const { position } = sequenceTracker.getPosition(testId, scenarioId, mockIndex);
|
|
258
|
-
// Get response at current position
|
|
259
|
-
// Note: Exhausted sequences are skipped during matching phase,
|
|
260
|
-
// so position should always be valid here
|
|
261
266
|
// eslint-disable-next-line security/detect-object-injection -- Position bounded by sequence tracker
|
|
262
267
|
const response = mock.sequence.responses[position];
|
|
263
|
-
// Advance position for next call
|
|
264
268
|
const repeatMode = mock.sequence.repeat || "last";
|
|
265
269
|
sequenceTracker.advance(testId, scenarioId, mockIndex, mock.sequence.responses.length, repeatMode);
|
|
266
270
|
return { response, matchedCondition: null };
|
|
267
271
|
}
|
|
268
|
-
// State-aware response: evaluate conditions against current state
|
|
269
272
|
if (mock.stateResponse) {
|
|
270
|
-
return resolveStateResponse(
|
|
273
|
+
return resolveStateResponse({
|
|
274
|
+
testId,
|
|
275
|
+
stateResponse: mock.stateResponse,
|
|
276
|
+
stateManager,
|
|
277
|
+
logger,
|
|
278
|
+
logContext,
|
|
279
|
+
});
|
|
271
280
|
}
|
|
272
|
-
// Phase 1: Single response
|
|
273
281
|
if (mock.response) {
|
|
274
282
|
return { response: mock.response, matchedCondition: null };
|
|
275
283
|
}
|
|
276
|
-
// No response type defined
|
|
277
284
|
return null;
|
|
278
285
|
};
|
|
279
286
|
/**
|
|
@@ -289,8 +296,7 @@ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTra
|
|
|
289
296
|
* @param logContext - Context for log messages
|
|
290
297
|
* @returns MockResponseResult with response and matched condition
|
|
291
298
|
*/
|
|
292
|
-
const resolveStateResponse = (testId, stateResponse, stateManager, logger, logContext) => {
|
|
293
|
-
// Without stateManager, always return default
|
|
299
|
+
const resolveStateResponse = ({ testId, stateResponse, stateManager, logger, logContext, }) => {
|
|
294
300
|
if (!stateManager) {
|
|
295
301
|
logger.debug(LogCategories.STATE, LogEvents.STATE_RESPONSE_RESOLVED, logContext, {
|
|
296
302
|
result: "default",
|
|
@@ -356,38 +362,23 @@ const calculateSpecificity = (criteria) => {
|
|
|
356
362
|
* @param testId - Test ID for state isolation
|
|
357
363
|
* @param stateManager - Optional state manager for state-based matching
|
|
358
364
|
*/
|
|
359
|
-
const matchesCriteria = (context, criteria, testId, stateManager) => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (!matchesValue(context.url, criteria.url)) {
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
+
const matchesCriteria = ({ context, criteria, testId, stateManager, }) => {
|
|
366
|
+
if (criteria.url && !matchesValue(context.url, criteria.url)) {
|
|
367
|
+
return false;
|
|
365
368
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (!matchesBody(context.body, criteria.body)) {
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
369
|
+
if (criteria.body && !matchesBody(context.body, criteria.body)) {
|
|
370
|
+
return false;
|
|
371
371
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (!matchesHeaders(context.headers, criteria.headers)) {
|
|
375
|
-
return false;
|
|
376
|
-
}
|
|
372
|
+
if (criteria.headers && !matchesHeaders(context.headers, criteria.headers)) {
|
|
373
|
+
return false;
|
|
377
374
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (!matchesQuery(context.query, criteria.query)) {
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
375
|
+
if (criteria.query && !matchesQuery(context.query, criteria.query)) {
|
|
376
|
+
return false;
|
|
383
377
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
378
|
+
if (criteria.state &&
|
|
379
|
+
!matchesState({ stateCriteria: criteria.state, testId, stateManager })) {
|
|
380
|
+
return false;
|
|
389
381
|
}
|
|
390
|
-
// All criteria matched
|
|
391
382
|
return true;
|
|
392
383
|
};
|
|
393
384
|
/**
|
|
@@ -399,18 +390,15 @@ const matchesCriteria = (context, criteria, testId, stateManager) => {
|
|
|
399
390
|
* @param stateManager - State manager to retrieve current state
|
|
400
391
|
* @returns true if all criteria keys match, false otherwise
|
|
401
392
|
*/
|
|
402
|
-
const matchesState = (stateCriteria, testId, stateManager) => {
|
|
403
|
-
// Without stateManager, state matching always fails
|
|
393
|
+
const matchesState = ({ stateCriteria, testId, stateManager, }) => {
|
|
404
394
|
if (!stateManager) {
|
|
405
395
|
return false;
|
|
406
396
|
}
|
|
407
397
|
const currentState = stateManager.getAll(testId);
|
|
408
|
-
// All keys in criteria must exist in state with equal values
|
|
409
398
|
for (const [key, expectedValue] of Object.entries(stateCriteria)) {
|
|
410
399
|
if (!(key in currentState)) {
|
|
411
400
|
return false;
|
|
412
401
|
}
|
|
413
|
-
// Deep equality check for values (handles primitives, null, objects)
|
|
414
402
|
// eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
|
|
415
403
|
if (!deepEquals(currentState[key], expectedValue)) {
|
|
416
404
|
return false;
|
|
@@ -554,10 +542,9 @@ const matchesQuery = (requestQuery, criteriaQuery) => {
|
|
|
554
542
|
* @param captureConfig - Capture configuration (state key -> path expression)
|
|
555
543
|
* @param stateManager - State manager to store captured values
|
|
556
544
|
*/
|
|
557
|
-
const captureState = (testId, context, captureConfig, stateManager) => {
|
|
545
|
+
const captureState = ({ testId, context, captureConfig, stateManager, }) => {
|
|
558
546
|
for (const [stateKey, pathExpression] of Object.entries(captureConfig)) {
|
|
559
547
|
const value = extractFromPath(context, pathExpression);
|
|
560
|
-
// Guard: Only capture if value exists
|
|
561
548
|
if (value === undefined) {
|
|
562
549
|
continue;
|
|
563
550
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenario-manager.d.ts","sourceRoot":"","sources":["../../src/domain/scenario-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,YAAY,EACZ,MAAM,EACP,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"scenario-manager.d.ts","sourceRoot":"","sources":["../../src/domain/scenario-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,YAAY,EACZ,MAAM,EACP,MAAM,mBAAmB,CAAC;AAwD3B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GAAI,6DAMnC;IACD,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,KAAG,eAyHH,CAAC"}
|
|
@@ -20,6 +20,14 @@ const createScenarioValidationError = (scenarioId, validationErrors) => {
|
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
};
|
|
23
|
+
const createScenarioNotFoundError = (scenarioId, testId) => new ScenaristError(`Scenario '${scenarioId}' not found. Did you forget to register it?`, {
|
|
24
|
+
code: ErrorCodes.SCENARIO_NOT_FOUND,
|
|
25
|
+
context: {
|
|
26
|
+
testId,
|
|
27
|
+
scenarioId,
|
|
28
|
+
hint: "Make sure to register the scenario before switching to it. Use manager.registerScenario(definition) first.",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
23
31
|
/**
|
|
24
32
|
* Factory function to create a ScenarioManager implementation.
|
|
25
33
|
*
|
|
@@ -72,14 +80,7 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
|
|
|
72
80
|
});
|
|
73
81
|
return {
|
|
74
82
|
success: false,
|
|
75
|
-
error:
|
|
76
|
-
code: ErrorCodes.SCENARIO_NOT_FOUND,
|
|
77
|
-
context: {
|
|
78
|
-
testId,
|
|
79
|
-
scenarioId,
|
|
80
|
-
hint: "Make sure to register the scenario before switching to it. Use manager.registerScenario(definition) first.",
|
|
81
|
-
},
|
|
82
|
-
}),
|
|
83
|
+
error: createScenarioNotFoundError(scenarioId, testId),
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
const activeScenario = {
|
|
@@ -1,15 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Applies templates to a value.
|
|
3
|
-
* Replaces {{state.key}} and {{params.key}} patterns with actual values.
|
|
4
|
-
*
|
|
5
|
-
* Note: This function preserves the structure of the input value.
|
|
6
|
-
* Callers passing typed objects (like ScenaristResponse) can safely
|
|
7
|
-
* cast the return value back to the input type.
|
|
8
|
-
*
|
|
9
|
-
* @param value - Value to apply templates to (string, object, array, or primitive)
|
|
10
|
-
* @param templateData - Object containing state and params for template replacement.
|
|
11
|
-
* Can be flat object (backward compatible) or { state: {...}, params: {...} }
|
|
12
|
-
* @returns Value with templates replaced
|
|
13
|
-
*/
|
|
14
1
|
export declare const applyTemplates: (value: unknown, templateData: Record<string, unknown>) => unknown;
|
|
15
2
|
//# sourceMappingURL=template-replacement.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-replacement.d.ts","sourceRoot":"","sources":["../../src/domain/template-replacement.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"template-replacement.d.ts","sourceRoot":"","sources":["../../src/domain/template-replacement.ts"],"names":[],"mappings":"AAqDA,eAAO,MAAM,cAAc,GACzB,OAAO,OAAO,EACd,cAAc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACpC,OAqBF,CAAC"}
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Type guard to check if a value is a plain object (Record).
|
|
3
|
-
* Used to properly narrow types after typeof checks.
|
|
4
|
-
*/
|
|
5
|
-
const isRecord = (value) => {
|
|
6
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Security: Check if a property key could cause prototype pollution.
|
|
10
|
-
* @see https://github.com/citypaul/scenarist/security/code-scanning
|
|
11
|
-
*/
|
|
12
|
-
const isDangerousKey = (key) => {
|
|
13
|
-
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
14
|
-
};
|
|
1
|
+
import { isRecord, isDangerousKey } from "./type-guards.js";
|
|
15
2
|
/**
|
|
16
3
|
* Applies templates to a value.
|
|
17
4
|
* Replaces {{state.key}} and {{params.key}} patterns with actual values.
|
|
@@ -25,45 +12,35 @@ const isDangerousKey = (key) => {
|
|
|
25
12
|
* Can be flat object (backward compatible) or { state: {...}, params: {...} }
|
|
26
13
|
* @returns Value with templates replaced
|
|
27
14
|
*/
|
|
15
|
+
const PURE_TEMPLATE_PATTERN = /^\{\{(state|params)\.([^}]{1,256})\}\}$/;
|
|
16
|
+
const MIXED_TEMPLATE_PATTERN = /\{\{(state|params)\.([^}]{1,256})\}\}/g;
|
|
17
|
+
const applyTemplatesToString = (value, templateData) => {
|
|
18
|
+
const pureTemplateMatch = PURE_TEMPLATE_PATTERN.exec(value);
|
|
19
|
+
if (pureTemplateMatch) {
|
|
20
|
+
const prefix = pureTemplateMatch[1];
|
|
21
|
+
const path = pureTemplateMatch[2];
|
|
22
|
+
const resolvedValue = resolveTemplatePath(templateData, prefix, path);
|
|
23
|
+
return resolvedValue !== undefined ? resolvedValue : null;
|
|
24
|
+
}
|
|
25
|
+
return value.replace(MIXED_TEMPLATE_PATTERN, (match, prefix, path) => {
|
|
26
|
+
const resolvedValue = resolveTemplatePath(templateData, prefix, path);
|
|
27
|
+
if (resolvedValue === undefined) {
|
|
28
|
+
return match;
|
|
29
|
+
}
|
|
30
|
+
return String(resolvedValue);
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const normalizeTemplateData = (templateData) => templateData.state !== undefined || templateData.params !== undefined
|
|
34
|
+
? templateData
|
|
35
|
+
: { state: templateData, params: {} };
|
|
28
36
|
export const applyTemplates = (value, templateData) => {
|
|
29
|
-
|
|
30
|
-
// treat it as a flat state object and wrap it
|
|
31
|
-
const normalizedData = templateData.state !== undefined || templateData.params !== undefined
|
|
32
|
-
? templateData
|
|
33
|
-
: { state: templateData, params: {} };
|
|
34
|
-
// Guard: Handle strings (base case)
|
|
37
|
+
const normalizedData = normalizeTemplateData(templateData);
|
|
35
38
|
if (typeof value === "string") {
|
|
36
|
-
|
|
37
|
-
// Supports both {{state.key}} and {{params.key}}
|
|
38
|
-
// Using {1,256} limit to prevent ReDoS attacks with malicious input
|
|
39
|
-
const pureTemplateMatch = /^\{\{(state|params)\.([^}]{1,256})\}\}$/.exec(value);
|
|
40
|
-
if (pureTemplateMatch) {
|
|
41
|
-
// Pure template: return raw value (preserves type - arrays, numbers, objects)
|
|
42
|
-
const prefix = pureTemplateMatch[1]; // 'state' or 'params'
|
|
43
|
-
const path = pureTemplateMatch[2]; // Guaranteed to exist by regex capture group
|
|
44
|
-
const resolvedValue = resolveTemplatePath(normalizedData, prefix, path);
|
|
45
|
-
// Return raw value if found, otherwise return null (JSON-safe)
|
|
46
|
-
// null is used instead of undefined to ensure JSON serialization preserves the field
|
|
47
|
-
return resolvedValue !== undefined ? resolvedValue : null;
|
|
48
|
-
}
|
|
49
|
-
// Mixed template (has surrounding text): use string replacement
|
|
50
|
-
// Supports both {{state.key}} and {{params.key}}
|
|
51
|
-
// Using {1,256} limit to prevent ReDoS attacks with malicious input
|
|
52
|
-
return value.replace(/\{\{(state|params)\.([^}]{1,256})\}\}/g, (match, prefix, path) => {
|
|
53
|
-
const resolvedValue = resolveTemplatePath(normalizedData, prefix, path);
|
|
54
|
-
// Guard: Missing keys remain as template
|
|
55
|
-
if (resolvedValue === undefined) {
|
|
56
|
-
return match;
|
|
57
|
-
}
|
|
58
|
-
// Convert to string for concatenation with surrounding text
|
|
59
|
-
return String(resolvedValue);
|
|
60
|
-
});
|
|
39
|
+
return applyTemplatesToString(value, normalizedData);
|
|
61
40
|
}
|
|
62
|
-
// Guard: Handle arrays recursively
|
|
63
41
|
if (Array.isArray(value)) {
|
|
64
42
|
return value.map((item) => applyTemplates(item, normalizedData));
|
|
65
43
|
}
|
|
66
|
-
// Guard: Handle objects recursively
|
|
67
44
|
if (typeof value === "object" && value !== null) {
|
|
68
45
|
const result = {};
|
|
69
46
|
for (const [key, val] of Object.entries(value)) {
|
|
@@ -72,7 +49,6 @@ export const applyTemplates = (value, templateData) => {
|
|
|
72
49
|
}
|
|
73
50
|
return result;
|
|
74
51
|
}
|
|
75
|
-
// Primitives (number, boolean, null) returned unchanged
|
|
76
52
|
return value;
|
|
77
53
|
};
|
|
78
54
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-guards.d.ts","sourceRoot":"","sources":["../../src/domain/type-guards.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,OAAkC,CAAC;AAEhF,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenarist/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
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",
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
"LICENSE"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"redos-detector": "^6.1.
|
|
49
|
+
"redos-detector": "^6.1.4",
|
|
50
50
|
"zod": "^4.3.6"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@vitest/coverage-v8": "^4.0.18",
|
|
54
54
|
"@vitest/ui": "^4.0.18",
|
|
55
|
-
"eslint": "^9.39.
|
|
55
|
+
"eslint": "^9.39.3",
|
|
56
56
|
"fast-check": "^4.5.3",
|
|
57
57
|
"typescript": "^5.9.3",
|
|
58
58
|
"vitest": "^4.0.18",
|