@auto-engineer/narrative 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +11 -0
- package/dist/src/commands/export-schema-runner.js +1 -1
- package/dist/src/commands/export-schema-runner.js.map +1 -1
- package/dist/src/fluent-builder.js +3 -3
- package/dist/src/fluent-builder.js.map +1 -1
- package/dist/src/getNarratives.specs.js +149 -153
- package/dist/src/getNarratives.specs.js.map +1 -1
- package/dist/src/id/addAutoIds.d.ts.map +1 -1
- package/dist/src/id/addAutoIds.js +23 -10
- package/dist/src/id/addAutoIds.js.map +1 -1
- package/dist/src/id/addAutoIds.specs.js +54 -45
- package/dist/src/id/addAutoIds.specs.js.map +1 -1
- package/dist/src/id/hasAllIds.d.ts.map +1 -1
- package/dist/src/id/hasAllIds.js +8 -3
- package/dist/src/id/hasAllIds.js.map +1 -1
- package/dist/src/id/hasAllIds.specs.js +142 -215
- package/dist/src/id/hasAllIds.specs.js.map +1 -1
- package/dist/src/index.d.ts +6 -8
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/loader/graph.d.ts.map +1 -1
- package/dist/src/loader/graph.js +13 -6
- package/dist/src/loader/graph.js.map +1 -1
- package/dist/src/loader/ts-utils.d.ts +1 -0
- package/dist/src/loader/ts-utils.d.ts.map +1 -1
- package/dist/src/loader/ts-utils.js +95 -16
- package/dist/src/loader/ts-utils.js.map +1 -1
- package/dist/src/model-to-narrative.specs.js +531 -449
- package/dist/src/model-to-narrative.specs.js.map +1 -1
- package/dist/src/narrative-context.d.ts +8 -8
- package/dist/src/narrative-context.d.ts.map +1 -1
- package/dist/src/narrative-context.js +111 -301
- package/dist/src/narrative-context.js.map +1 -1
- package/dist/src/narrative-context.specs.js +15 -55
- package/dist/src/narrative-context.specs.js.map +1 -1
- package/dist/src/narrative.d.ts +19 -22
- package/dist/src/narrative.d.ts.map +1 -1
- package/dist/src/narrative.js +42 -71
- package/dist/src/narrative.js.map +1 -1
- package/dist/src/samples/test-with-ids.narrative.js +13 -29
- package/dist/src/samples/test-with-ids.narrative.js.map +1 -1
- package/dist/src/schema.d.ts +2704 -8293
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +26 -47
- package/dist/src/schema.js.map +1 -1
- package/dist/src/slice-builder.js +3 -3
- package/dist/src/slice-builder.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.js +118 -74
- package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/gwt.d.ts +9 -1
- package/dist/src/transformers/model-to-narrative/generators/gwt.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/gwt.js +112 -112
- package/dist/src/transformers/model-to-narrative/generators/gwt.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.d.ts +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.js +13 -9
- package/dist/src/transformers/model-to-narrative/generators/imports.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/index.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/index.js +50 -23
- package/dist/src/transformers/narrative-to-model/index.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/type-inference.specs.js +100 -90
- package/dist/src/transformers/narrative-to-model/type-inference.specs.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/commands/export-schema-runner.ts +3 -1
- package/src/fluent-builder.ts +3 -3
- package/src/getNarratives.specs.ts +168 -176
- package/src/id/addAutoIds.specs.ts +54 -47
- package/src/id/addAutoIds.ts +28 -11
- package/src/id/hasAllIds.specs.ts +147 -245
- package/src/id/hasAllIds.ts +11 -4
- package/src/index.ts +9 -12
- package/src/loader/graph.ts +23 -6
- package/src/loader/ts-utils.ts +169 -26
- package/src/model-to-narrative.specs.ts +531 -449
- package/src/narrative-context.specs.ts +73 -116
- package/src/narrative-context.ts +127 -374
- package/src/narrative.ts +70 -120
- package/src/samples/test-with-ids.narrative.ts +23 -31
- package/src/schema.ts +33 -52
- package/src/slice-builder.ts +3 -3
- package/src/transformers/model-to-narrative/generators/flow.ts +191 -85
- package/src/transformers/model-to-narrative/generators/gwt.ts +195 -178
- package/src/transformers/model-to-narrative/generators/imports.ts +13 -9
- package/src/transformers/narrative-to-model/index.ts +87 -26
- package/src/transformers/narrative-to-model/type-inference.specs.ts +100 -90
package/src/narrative-context.ts
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
import createDebug from 'debug';
|
|
2
2
|
import type { DataSinkItem, DataSourceItem, DataItem, DataSink, DataSource } from './types';
|
|
3
3
|
import type { GivenTypeInfo } from './loader/ts-utils';
|
|
4
|
-
import { Narrative, Slice,
|
|
4
|
+
import { Narrative, Slice, CommandSlice, QuerySlice, ExperienceSlice } from './index';
|
|
5
5
|
import type { ClientSpecNode } from './schema';
|
|
6
|
+
import type { z } from 'zod';
|
|
7
|
+
import type { StepSchema, ExampleSchema, SpecSchema, RuleSchema } from './schema';
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
for (const [key, value] of Object.entries(context)) {
|
|
12
|
-
if (value !== undefined) {
|
|
13
|
-
filtered[key] = value;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return Object.keys(filtered).length > 0 ? filtered : undefined;
|
|
18
|
-
}
|
|
9
|
+
type Step = z.infer<typeof StepSchema>;
|
|
10
|
+
type Example = z.infer<typeof ExampleSchema>;
|
|
11
|
+
type Spec = z.infer<typeof SpecSchema>;
|
|
12
|
+
type Rule = z.infer<typeof RuleSchema>;
|
|
19
13
|
|
|
20
14
|
const debug = createDebug('auto:narrative:context:given-types');
|
|
21
15
|
|
|
16
|
+
type MajorKeyword = 'Given' | 'When' | 'Then';
|
|
17
|
+
|
|
22
18
|
interface NarrativeContext {
|
|
23
19
|
narrative: Narrative;
|
|
24
20
|
currentSliceIndex: number | null;
|
|
@@ -27,13 +23,16 @@ interface NarrativeContext {
|
|
|
27
23
|
currentRuleIndex: number | null;
|
|
28
24
|
currentExampleIndex: number | null;
|
|
29
25
|
clientSpecStack: ClientSpecNode[];
|
|
26
|
+
lastMajorKeyword: MajorKeyword | null;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
let context: NarrativeContext | null = null;
|
|
33
30
|
let givenTypesByFile: Map<string, GivenTypeInfo[]> = new Map();
|
|
34
31
|
let whenTypesByFile: Map<string, GivenTypeInfo[]> = new Map();
|
|
32
|
+
let thenTypesByFile: Map<string, GivenTypeInfo[]> = new Map();
|
|
35
33
|
const givenCallCounters: Map<string, number> = new Map();
|
|
36
34
|
const whenCallCounters: Map<string, number> = new Map();
|
|
35
|
+
const thenCallCounters: Map<string, number> = new Map();
|
|
37
36
|
|
|
38
37
|
export function setGivenTypesByFile(types: Map<string, GivenTypeInfo[]>): void {
|
|
39
38
|
const whenTypes = types.get('__whenTypes') as Map<string, GivenTypeInfo[]> | undefined;
|
|
@@ -42,9 +41,16 @@ export function setGivenTypesByFile(types: Map<string, GivenTypeInfo[]>): void {
|
|
|
42
41
|
types.delete('__whenTypes');
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
const thenTypes = types.get('__thenTypes') as Map<string, GivenTypeInfo[]> | undefined;
|
|
45
|
+
if (thenTypes) {
|
|
46
|
+
thenTypesByFile = thenTypes;
|
|
47
|
+
types.delete('__thenTypes');
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
givenTypesByFile = types;
|
|
46
51
|
givenCallCounters.clear();
|
|
47
52
|
whenCallCounters.clear();
|
|
53
|
+
thenCallCounters.clear();
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
export function startNarrative(name: string, id?: string): Narrative {
|
|
@@ -52,6 +58,7 @@ export function startNarrative(name: string, id?: string): Narrative {
|
|
|
52
58
|
if (sourceFile !== null && sourceFile !== undefined && sourceFile !== '') {
|
|
53
59
|
givenCallCounters.set(sourceFile, 0);
|
|
54
60
|
whenCallCounters.set(sourceFile, 0);
|
|
61
|
+
thenCallCounters.set(sourceFile, 0);
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
const narrative: Narrative = {
|
|
@@ -68,6 +75,7 @@ export function startNarrative(name: string, id?: string): Narrative {
|
|
|
68
75
|
currentRuleIndex: null,
|
|
69
76
|
currentExampleIndex: null,
|
|
70
77
|
clientSpecStack: [],
|
|
78
|
+
lastMajorKeyword: null,
|
|
71
79
|
};
|
|
72
80
|
return narrative;
|
|
73
81
|
}
|
|
@@ -91,15 +99,20 @@ export function addSlice(slice: Slice): void {
|
|
|
91
99
|
context.currentSliceIndex = context.narrative.slices.length - 1;
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
function getServerSpecs(
|
|
95
|
-
slice: Slice,
|
|
96
|
-
): { name: string; rules: { id?: string; description: string; examples: Example[] }[] } | undefined {
|
|
102
|
+
function getServerSpecs(slice: Slice): Spec[] | undefined {
|
|
97
103
|
if ('server' in slice) {
|
|
98
104
|
return slice.server?.specs;
|
|
99
105
|
}
|
|
100
106
|
return undefined;
|
|
101
107
|
}
|
|
102
108
|
|
|
109
|
+
function getCurrentSpec(slice: Slice): Spec | undefined {
|
|
110
|
+
if (!context || context.currentSpecIndex === null) return undefined;
|
|
111
|
+
const specs = getServerSpecs(slice);
|
|
112
|
+
if (!specs) return undefined;
|
|
113
|
+
return specs[context.currentSpecIndex];
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
function getCurrentExample(slice: Slice): Example | undefined {
|
|
104
117
|
if (
|
|
105
118
|
!context ||
|
|
@@ -110,11 +123,10 @@ function getCurrentExample(slice: Slice): Example | undefined {
|
|
|
110
123
|
return undefined;
|
|
111
124
|
}
|
|
112
125
|
|
|
113
|
-
const spec =
|
|
126
|
+
const spec = getCurrentSpec(slice);
|
|
114
127
|
if (!spec) return undefined;
|
|
115
128
|
|
|
116
|
-
|
|
117
|
-
return objectRules[context.currentRuleIndex]?.examples[context.currentExampleIndex];
|
|
129
|
+
return spec.rules[context.currentRuleIndex]?.examples[context.currentExampleIndex];
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
export function startClientBlock(slice: Slice): void {
|
|
@@ -153,19 +165,19 @@ export function startServerBlock(slice: Slice, description: string = ''): void {
|
|
|
153
165
|
if (slice.type === 'command') {
|
|
154
166
|
slice.server = {
|
|
155
167
|
description,
|
|
156
|
-
specs:
|
|
168
|
+
specs: [],
|
|
157
169
|
data: undefined,
|
|
158
170
|
};
|
|
159
171
|
} else if (slice.type === 'query') {
|
|
160
172
|
slice.server = {
|
|
161
173
|
description,
|
|
162
|
-
specs:
|
|
174
|
+
specs: [],
|
|
163
175
|
data: undefined,
|
|
164
176
|
};
|
|
165
177
|
} else if (slice.type === 'react') {
|
|
166
178
|
slice.server = {
|
|
167
179
|
description: description || undefined,
|
|
168
|
-
specs:
|
|
180
|
+
specs: [],
|
|
169
181
|
data: undefined,
|
|
170
182
|
};
|
|
171
183
|
}
|
|
@@ -179,23 +191,25 @@ export function endServerBlock(): void {
|
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
|
|
182
|
-
function
|
|
194
|
+
function addServerSpec(slice: Slice, feature: string): void {
|
|
183
195
|
if ('server' in slice && slice.server != null) {
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
const newSpec: Spec = {
|
|
197
|
+
type: 'gherkin',
|
|
198
|
+
feature,
|
|
186
199
|
rules: [],
|
|
187
200
|
};
|
|
188
|
-
|
|
201
|
+
slice.server.specs.push(newSpec);
|
|
202
|
+
if (context) context.currentSpecIndex = slice.server.specs.length - 1;
|
|
189
203
|
}
|
|
190
204
|
}
|
|
191
205
|
|
|
192
|
-
export function pushSpec(
|
|
206
|
+
export function pushSpec(feature: string): void {
|
|
193
207
|
if (!context || !context.currentSpecTarget) throw new Error('No active spec target');
|
|
194
208
|
const slice = getCurrentSlice();
|
|
195
209
|
if (!slice) throw new Error('No active slice');
|
|
196
210
|
|
|
197
211
|
if (context.currentSpecTarget === 'server') {
|
|
198
|
-
|
|
212
|
+
addServerSpec(slice, feature);
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
|
|
@@ -293,193 +307,107 @@ function stripTypeDiscriminator(items: DataItem[]): (DataSink | DataSource)[] {
|
|
|
293
307
|
});
|
|
294
308
|
}
|
|
295
309
|
|
|
296
|
-
export function recordRule(
|
|
310
|
+
export function recordRule(name: string, id?: string): void {
|
|
297
311
|
if (!context || context.currentSpecIndex === null) throw new Error('No active spec context');
|
|
298
312
|
const slice = getCurrentSlice();
|
|
299
313
|
if (!slice) throw new Error('No active slice');
|
|
300
314
|
|
|
301
|
-
const spec =
|
|
315
|
+
const spec = getCurrentSpec(slice);
|
|
302
316
|
if (!spec) throw new Error('No active specs for current slice');
|
|
303
317
|
|
|
304
|
-
const
|
|
305
|
-
objectRules.push({
|
|
318
|
+
const newRule: Rule = {
|
|
306
319
|
id,
|
|
307
|
-
|
|
320
|
+
name,
|
|
308
321
|
examples: [],
|
|
309
|
-
}
|
|
310
|
-
|
|
322
|
+
};
|
|
323
|
+
spec.rules.push(newRule);
|
|
324
|
+
context.currentRuleIndex = spec.rules.length - 1;
|
|
311
325
|
}
|
|
312
326
|
|
|
313
|
-
export function recordExample(
|
|
327
|
+
export function recordExample(name: string, id?: string): void {
|
|
314
328
|
if (!context || context.currentSpecIndex === null || context.currentRuleIndex === null) {
|
|
315
329
|
throw new Error('No active rule context');
|
|
316
330
|
}
|
|
317
331
|
const slice = getCurrentSlice();
|
|
318
332
|
if (!slice) throw new Error('No active slice');
|
|
319
333
|
|
|
320
|
-
const spec =
|
|
334
|
+
const spec = getCurrentSpec(slice);
|
|
321
335
|
if (!spec) throw new Error('No active specs for current slice');
|
|
322
336
|
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
});
|
|
329
|
-
context.currentExampleIndex = rule.examples.length - 1;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function processItemWithASTMatch(
|
|
333
|
-
item: unknown,
|
|
334
|
-
matchingType: import('./loader/ts-utils').GivenTypeInfo,
|
|
335
|
-
contextParam?: Record<string, string>,
|
|
336
|
-
): { [key: string]: unknown; exampleData: unknown; context?: Record<string, string> } {
|
|
337
|
-
const refType = getRefTypeFromClassification(matchingType.classification);
|
|
338
|
-
return {
|
|
339
|
-
[refType]: matchingType.typeName,
|
|
340
|
-
exampleData: ensureMessageFormat(item).data,
|
|
341
|
-
...(contextParam && { context: contextParam }),
|
|
337
|
+
const rule = spec.rules[context.currentRuleIndex];
|
|
338
|
+
const newExample: Example = {
|
|
339
|
+
id,
|
|
340
|
+
name,
|
|
341
|
+
steps: [],
|
|
342
342
|
};
|
|
343
|
+
rule.examples.push(newExample);
|
|
344
|
+
context.currentExampleIndex = rule.examples.length - 1;
|
|
345
|
+
context.lastMajorKeyword = null;
|
|
343
346
|
}
|
|
344
347
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
contextParam?: Record<string, string>,
|
|
348
|
-
): Array<{ [key: string]: unknown; exampleData: unknown; context?: Record<string, string> }> {
|
|
349
|
-
const sourceFile = context?.narrative.sourceFile;
|
|
348
|
+
type StepKeyword = 'Given' | 'When' | 'Then' | 'And';
|
|
349
|
+
type ErrorType = 'IllegalStateError' | 'ValidationError' | 'NotFoundError';
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const currentCount = givenCallCounters.get(sourceFile) ?? 0;
|
|
354
|
-
givenCallCounters.set(sourceFile, currentCount + 1);
|
|
355
|
-
|
|
356
|
-
// Look up AST-extracted type info by ordinal position
|
|
357
|
-
const givenTypes = givenTypesByFile.get(sourceFile) || [];
|
|
358
|
-
const matchingType = givenTypes[currentCount];
|
|
359
|
-
|
|
360
|
-
if (matchingType !== null && matchingType !== undefined) {
|
|
361
|
-
debug('AST match for %s at ordinal %d: %s', sourceFile, currentCount, matchingType.typeName);
|
|
362
|
-
return processItemWithASTMatch(item, matchingType, contextParam);
|
|
363
|
-
} else {
|
|
364
|
-
debug('No AST match for %s at ordinal %d, item: %o', sourceFile, currentCount, item);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// Fallback: emit explicit InferredType for downstream processing
|
|
368
|
-
return {
|
|
369
|
-
eventRef: 'InferredType',
|
|
370
|
-
exampleData: ensureMessageFormat(item).data,
|
|
371
|
-
...(contextParam && { context: contextParam }),
|
|
372
|
-
};
|
|
373
|
-
});
|
|
351
|
+
function isValidSourceFile(sourceFile: string | null | undefined): sourceFile is string {
|
|
352
|
+
return sourceFile !== null && sourceFile !== undefined && sourceFile !== '';
|
|
374
353
|
}
|
|
375
354
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
context.currentRuleIndex === null ||
|
|
381
|
-
context.currentExampleIndex === null
|
|
382
|
-
) {
|
|
383
|
-
throw new Error('No active example context');
|
|
384
|
-
}
|
|
385
|
-
const slice = getCurrentSlice();
|
|
386
|
-
if (!slice) throw new Error('No active slice');
|
|
387
|
-
|
|
388
|
-
const example = getCurrentExample(slice);
|
|
389
|
-
if (!example) throw new Error('No active example for current slice');
|
|
390
|
-
|
|
391
|
-
const items = processGivenItems(data, normalizeContext(contextParam));
|
|
392
|
-
example.given = items as typeof example.given;
|
|
355
|
+
function isValidMatchingType(
|
|
356
|
+
matchingType: import('./loader/ts-utils').GivenTypeInfo | undefined,
|
|
357
|
+
): matchingType is import('./loader/ts-utils').GivenTypeInfo {
|
|
358
|
+
return matchingType !== null && matchingType !== undefined && matchingType.typeName !== '';
|
|
393
359
|
}
|
|
394
360
|
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
context.currentSpecIndex === null ||
|
|
399
|
-
context.currentRuleIndex === null ||
|
|
400
|
-
context.currentExampleIndex === null
|
|
401
|
-
) {
|
|
402
|
-
throw new Error('No active example context');
|
|
403
|
-
}
|
|
404
|
-
const slice = getCurrentSlice();
|
|
405
|
-
if (!slice) throw new Error('No active slice');
|
|
406
|
-
|
|
407
|
-
const example = getCurrentExample(slice);
|
|
408
|
-
if (!example) throw new Error('No active example for current slice');
|
|
409
|
-
|
|
410
|
-
const items = processGivenItems(data, normalizeContext(contextParam));
|
|
411
|
-
|
|
412
|
-
if (example.given && Array.isArray(example.given)) {
|
|
413
|
-
example.given.push(...(items as NonNullable<typeof example.given>));
|
|
414
|
-
} else {
|
|
415
|
-
example.given = items as NonNullable<typeof example.given>;
|
|
361
|
+
function resolveEffectiveKeyword(keyword: StepKeyword): MajorKeyword {
|
|
362
|
+
if (keyword !== 'And') {
|
|
363
|
+
return keyword;
|
|
416
364
|
}
|
|
365
|
+
return context?.lastMajorKeyword ?? 'Given';
|
|
417
366
|
}
|
|
418
367
|
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
): void {
|
|
425
|
-
const ordinal = incrementWhenCounter();
|
|
426
|
-
|
|
427
|
-
if (typeof data === 'object' && data !== null && Object.keys(data).length === 0) {
|
|
428
|
-
if (sliceType === 'query') {
|
|
429
|
-
example.when = { eventRef: '', exampleData: {} };
|
|
430
|
-
} else {
|
|
431
|
-
example.when = { commandRef: '', exampleData: {} };
|
|
432
|
-
}
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
368
|
+
function getTypesByFileForKeyword(keyword: MajorKeyword): Map<string, GivenTypeInfo[]> {
|
|
369
|
+
if (keyword === 'Given') return givenTypesByFile;
|
|
370
|
+
if (keyword === 'When') return whenTypesByFile;
|
|
371
|
+
return thenTypesByFile;
|
|
372
|
+
}
|
|
435
373
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
} else {
|
|
441
|
-
example.when = convertToCommandOrEventExample(data, contextParam, ordinal);
|
|
442
|
-
}
|
|
374
|
+
function getCountersForKeyword(keyword: MajorKeyword): Map<string, number> {
|
|
375
|
+
if (keyword === 'Given') return givenCallCounters;
|
|
376
|
+
if (keyword === 'When') return whenCallCounters;
|
|
377
|
+
return thenCallCounters;
|
|
443
378
|
}
|
|
444
379
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
context.currentRuleIndex === null ||
|
|
450
|
-
context.currentExampleIndex === null
|
|
451
|
-
) {
|
|
452
|
-
throw new Error('No active example context');
|
|
380
|
+
function getTypeNameFromAST(effectiveKeyword: MajorKeyword): string | null {
|
|
381
|
+
const sourceFile = context?.narrative.sourceFile;
|
|
382
|
+
if (!isValidSourceFile(sourceFile)) {
|
|
383
|
+
return null;
|
|
453
384
|
}
|
|
454
|
-
const slice = getCurrentSlice();
|
|
455
|
-
if (!slice) throw new Error('No active slice');
|
|
456
385
|
|
|
457
|
-
const
|
|
458
|
-
|
|
386
|
+
const typesByFile = getTypesByFileForKeyword(effectiveKeyword);
|
|
387
|
+
const counters = getCountersForKeyword(effectiveKeyword);
|
|
459
388
|
|
|
460
|
-
|
|
461
|
-
|
|
389
|
+
const currentCount = counters.get(sourceFile) ?? 0;
|
|
390
|
+
counters.set(sourceFile, currentCount + 1);
|
|
462
391
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
!context ||
|
|
466
|
-
context.currentSpecIndex === null ||
|
|
467
|
-
context.currentRuleIndex === null ||
|
|
468
|
-
context.currentExampleIndex === null
|
|
469
|
-
) {
|
|
470
|
-
throw new Error('No active example context');
|
|
471
|
-
}
|
|
472
|
-
const slice = getCurrentSlice();
|
|
473
|
-
if (!slice) throw new Error('No active slice');
|
|
392
|
+
const types = typesByFile.get(sourceFile) || [];
|
|
393
|
+
const matchingType = types[currentCount];
|
|
474
394
|
|
|
475
|
-
|
|
476
|
-
|
|
395
|
+
if (isValidMatchingType(matchingType)) {
|
|
396
|
+
debug(
|
|
397
|
+
'AST match for %s keyword %s at ordinal %d: %s',
|
|
398
|
+
sourceFile,
|
|
399
|
+
effectiveKeyword,
|
|
400
|
+
currentCount,
|
|
401
|
+
matchingType.typeName,
|
|
402
|
+
);
|
|
403
|
+
return matchingType.typeName;
|
|
404
|
+
}
|
|
477
405
|
|
|
478
|
-
|
|
479
|
-
|
|
406
|
+
debug('No AST match for %s keyword %s at ordinal %d', sourceFile, effectiveKeyword, currentCount);
|
|
407
|
+
return null;
|
|
480
408
|
}
|
|
481
409
|
|
|
482
|
-
|
|
410
|
+
function getActiveExampleContext(): { example: Example } {
|
|
483
411
|
if (
|
|
484
412
|
!context ||
|
|
485
413
|
context.currentSpecIndex === null ||
|
|
@@ -490,220 +418,45 @@ export function recordAndThenData(data: unknown[], contextParam?: Partial<Record
|
|
|
490
418
|
}
|
|
491
419
|
const slice = getCurrentSlice();
|
|
492
420
|
if (!slice) throw new Error('No active slice');
|
|
493
|
-
|
|
494
421
|
const example = getCurrentExample(slice);
|
|
495
422
|
if (!example) throw new Error('No active example for current slice');
|
|
496
|
-
|
|
497
|
-
const outcomes = data.map((item) => convertToOutcomeExample(item, slice.type, normalizeContext(contextParam)));
|
|
498
|
-
example.then.push(...(outcomes as typeof example.then));
|
|
423
|
+
return { example };
|
|
499
424
|
}
|
|
500
425
|
|
|
501
|
-
function
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
): { eventRef: string; exampleData: Record<string, unknown>; context?: Record<string, string> } {
|
|
505
|
-
const message = ensureMessageFormat(item);
|
|
506
|
-
return {
|
|
507
|
-
eventRef: message.type,
|
|
508
|
-
exampleData: message.data,
|
|
509
|
-
...(contextParam && { context: contextParam }),
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function getRefTypeFromClassification(classification: string): string {
|
|
514
|
-
if (classification === 'event') return 'eventRef';
|
|
515
|
-
if (classification === 'command') return 'commandRef';
|
|
516
|
-
return 'stateRef';
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function isValidSourceFile(sourceFile: string | null | undefined): sourceFile is string {
|
|
520
|
-
return sourceFile !== null && sourceFile !== undefined && sourceFile !== '';
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function isValidMatchingType(
|
|
524
|
-
matchingType: import('./loader/ts-utils').GivenTypeInfo | undefined,
|
|
525
|
-
): matchingType is import('./loader/ts-utils').GivenTypeInfo {
|
|
526
|
-
return matchingType !== null && matchingType !== undefined && matchingType.typeName !== '';
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function incrementWhenCounter(): number {
|
|
530
|
-
const sourceFile = context?.narrative.sourceFile;
|
|
426
|
+
export function recordStep(keyword: StepKeyword, text: string, data: unknown): void {
|
|
427
|
+
const { example } = getActiveExampleContext();
|
|
428
|
+
const effectiveKeyword = resolveEffectiveKeyword(keyword);
|
|
531
429
|
|
|
532
|
-
if (
|
|
533
|
-
|
|
430
|
+
if (keyword !== 'And') {
|
|
431
|
+
context!.lastMajorKeyword = keyword as MajorKeyword;
|
|
534
432
|
}
|
|
535
433
|
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
debug('[when-counter] incremented counter for %s to %d', sourceFile, currentCount + 1);
|
|
434
|
+
const shouldInferType = text === 'InferredType';
|
|
435
|
+
const typeName = shouldInferType ? (getTypeNameFromAST(effectiveKeyword) ?? 'InferredType') : text;
|
|
540
436
|
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function tryGetWhenTypeFromAST(
|
|
545
|
-
item: unknown,
|
|
546
|
-
ordinal: number,
|
|
547
|
-
): { commandRef: string; exampleData: Record<string, unknown> } | null {
|
|
548
|
-
const sourceFile = context?.narrative.sourceFile;
|
|
437
|
+
const docString = typeof data === 'object' && data !== null ? (data as Record<string, unknown>) : undefined;
|
|
549
438
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (!isValidSourceFile(sourceFile)) {
|
|
555
|
-
debug('[when-ast] sourceFile is null/undefined/empty, returning null');
|
|
556
|
-
return null;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const whenTypes = whenTypesByFile.get(sourceFile) || [];
|
|
560
|
-
debug('[when-ast] sourceFile=%s, ordinal=%d, whenTypes.length=%d', sourceFile, ordinal, whenTypes.length);
|
|
561
|
-
if (whenTypes.length > 0) {
|
|
562
|
-
debug(
|
|
563
|
-
'[when-ast] available types: %o',
|
|
564
|
-
whenTypes.map((t) => ({ typeName: t.typeName, classification: t.classification })),
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const matchingType = whenTypes[ordinal];
|
|
569
|
-
|
|
570
|
-
if (!isValidMatchingType(matchingType)) {
|
|
571
|
-
debug('[when-ast] No valid AST match for when at %s ordinal %d, falling back', sourceFile, ordinal);
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const refType = getRefTypeFromClassification(matchingType.classification);
|
|
576
|
-
debug(
|
|
577
|
-
'[when-ast] ✅ AST match for when at %s ordinal %d: %s -> %s',
|
|
578
|
-
sourceFile,
|
|
579
|
-
ordinal,
|
|
580
|
-
matchingType.typeName,
|
|
581
|
-
refType,
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
return {
|
|
585
|
-
[refType]: matchingType.typeName,
|
|
586
|
-
exampleData: ensureMessageFormat(item).data,
|
|
587
|
-
} as { commandRef: string; exampleData: Record<string, unknown> };
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function convertToCommandOrEventExample(
|
|
591
|
-
item: unknown,
|
|
592
|
-
contextParam?: Record<string, string>,
|
|
593
|
-
ordinal?: number,
|
|
594
|
-
):
|
|
595
|
-
| { commandRef: string; exampleData: Record<string, unknown> }
|
|
596
|
-
| { eventRef: string; exampleData: Record<string, unknown> }[] {
|
|
597
|
-
if (ordinal !== undefined && ordinal >= 0) {
|
|
598
|
-
const astResult = tryGetWhenTypeFromAST(item, ordinal);
|
|
599
|
-
if (astResult) return astResult;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Fallback to the original logic
|
|
603
|
-
const message = ensureMessageFormat(item);
|
|
604
|
-
return {
|
|
605
|
-
commandRef: message.type,
|
|
606
|
-
exampleData: message.data,
|
|
607
|
-
...(contextParam && { context: contextParam }),
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function convertToOutcomeExample(
|
|
612
|
-
item: unknown,
|
|
613
|
-
sliceType: string,
|
|
614
|
-
contextParam?: Record<string, string>,
|
|
615
|
-
):
|
|
616
|
-
| { eventRef: string; exampleData: Record<string, unknown>; context?: Record<string, string> }
|
|
617
|
-
| { stateRef: string; exampleData: Record<string, unknown>; context?: Record<string, string> }
|
|
618
|
-
| { commandRef: string; exampleData: Record<string, unknown>; context?: Record<string, string> }
|
|
619
|
-
| { errorType: 'IllegalStateError' | 'ValidationError' | 'NotFoundError'; message?: string } {
|
|
620
|
-
const message = ensureMessageFormat(item);
|
|
621
|
-
|
|
622
|
-
// Check if it's an error
|
|
623
|
-
if (message.type === 'Error' || 'errorType' in message.data) {
|
|
624
|
-
return {
|
|
625
|
-
errorType:
|
|
626
|
-
(message.data.errorType as 'IllegalStateError' | 'ValidationError' | 'NotFoundError') || 'IllegalStateError',
|
|
627
|
-
message: message.data.message as string | undefined,
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (sliceType === 'command') {
|
|
632
|
-
return {
|
|
633
|
-
eventRef: message.type,
|
|
634
|
-
exampleData: message.data,
|
|
635
|
-
};
|
|
636
|
-
} else if (sliceType === 'query') {
|
|
637
|
-
return {
|
|
638
|
-
stateRef: message.type,
|
|
639
|
-
exampleData: message.data,
|
|
640
|
-
};
|
|
641
|
-
} else if (sliceType === 'react') {
|
|
642
|
-
return {
|
|
643
|
-
commandRef: message.type,
|
|
644
|
-
exampleData: message.data,
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return {
|
|
649
|
-
eventRef: message.type,
|
|
650
|
-
exampleData: message.data,
|
|
651
|
-
...(contextParam && { context: contextParam }),
|
|
439
|
+
const step: Step = {
|
|
440
|
+
keyword,
|
|
441
|
+
text: typeName,
|
|
442
|
+
...(docString !== undefined && Object.keys(docString).length > 0 ? { docString } : {}),
|
|
652
443
|
};
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function ensureMessageFormat(item: unknown): { type: string; data: Record<string, unknown> } {
|
|
656
|
-
if (typeof item !== 'object' || item === null) {
|
|
657
|
-
throw new Error('Invalid message format');
|
|
658
|
-
}
|
|
659
|
-
const obj = item as Record<string, unknown>;
|
|
660
|
-
|
|
661
|
-
if ('type' in obj && typeof obj.type === 'string') {
|
|
662
|
-
return parseMessageObject(obj);
|
|
663
|
-
}
|
|
664
444
|
|
|
665
|
-
|
|
666
|
-
// The type will be inferred during schema processing from the TypeScript types
|
|
667
|
-
return {
|
|
668
|
-
type: 'InferredType', // Placeholder - will be resolved during schema generation
|
|
669
|
-
data: obj,
|
|
670
|
-
};
|
|
445
|
+
example.steps.push(step);
|
|
671
446
|
}
|
|
672
447
|
|
|
673
|
-
function
|
|
674
|
-
|
|
675
|
-
if (hasValidDataProperty(obj)) {
|
|
676
|
-
return { type: obj.type as string, data: obj.data as Record<string, unknown> };
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Handle builder format with __messageCategory
|
|
680
|
-
if ('__messageCategory' in obj) {
|
|
681
|
-
return parseBuilderFormat(obj);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Handle legacy format where properties are at top level
|
|
685
|
-
return parseLegacyFormat(obj);
|
|
686
|
-
}
|
|
448
|
+
export function recordErrorStep(errorType: ErrorType, message?: string): void {
|
|
449
|
+
const { example } = getActiveExampleContext();
|
|
687
450
|
|
|
688
|
-
|
|
689
|
-
return 'data' in obj && typeof obj.data === 'object' && obj.data !== null;
|
|
690
|
-
}
|
|
451
|
+
context!.lastMajorKeyword = 'Then';
|
|
691
452
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
type: obj.type as string,
|
|
699
|
-
data,
|
|
453
|
+
const step: Step = {
|
|
454
|
+
keyword: 'Then',
|
|
455
|
+
error: {
|
|
456
|
+
type: errorType,
|
|
457
|
+
...(message !== undefined ? { message } : {}),
|
|
458
|
+
},
|
|
700
459
|
};
|
|
701
|
-
}
|
|
702
460
|
|
|
703
|
-
|
|
704
|
-
const data = Object.fromEntries(Object.entries(obj).filter(([key]) => key !== 'type'));
|
|
705
|
-
return {
|
|
706
|
-
type: obj.type as string,
|
|
707
|
-
data,
|
|
708
|
-
};
|
|
461
|
+
example.steps.push(step);
|
|
709
462
|
}
|