@auto-engineer/narrative 0.13.1 → 0.13.2

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.
@@ -52,6 +52,7 @@ describe('hasAllIds', () => {
52
52
  description: 'Test server',
53
53
  specs: [
54
54
  {
55
+ id: 'SPEC-001',
55
56
  type: 'gherkin',
56
57
  feature: 'Test specs',
57
58
  rules: [
@@ -72,6 +73,66 @@ describe('hasAllIds', () => {
72
73
  integrations: [],
73
74
  });
74
75
 
76
+ const createModelWithFullIds = (): Model => ({
77
+ variant: 'specs',
78
+ narratives: [
79
+ {
80
+ name: 'Test Flow with Full IDs',
81
+ id: 'FLOW-001',
82
+ slices: [
83
+ {
84
+ type: 'command',
85
+ name: 'Test slice with ID',
86
+ id: 'SLICE-001',
87
+ client: { specs: [] },
88
+ server: {
89
+ description: 'Test server',
90
+ specs: [
91
+ {
92
+ id: 'SPEC-001',
93
+ type: 'gherkin',
94
+ feature: 'Test specs',
95
+ rules: [
96
+ {
97
+ id: 'RULE-001',
98
+ name: 'Test rule with ID',
99
+ examples: [
100
+ {
101
+ id: 'EXAMPLE-001',
102
+ name: 'Test example',
103
+ steps: [
104
+ {
105
+ id: 'STEP-001',
106
+ keyword: 'Given',
107
+ text: 'TestState',
108
+ docString: { value: 'test' },
109
+ },
110
+ {
111
+ id: 'STEP-002',
112
+ keyword: 'When',
113
+ text: 'TestCommand',
114
+ },
115
+ {
116
+ id: 'STEP-003',
117
+ keyword: 'Then',
118
+ text: 'TestEvent',
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ },
128
+ },
129
+ ],
130
+ },
131
+ ],
132
+ messages: [],
133
+ integrations: [],
134
+ });
135
+
75
136
  const createMultipleFlowsModel = (includeAllIds: boolean, includeAllSliceIds: boolean): Model => ({
76
137
  variant: 'specs',
77
138
  narratives: [
@@ -84,7 +145,7 @@ describe('hasAllIds', () => {
84
145
  name: 'Active Surveys Summary',
85
146
  id: 'slice1',
86
147
  type: 'experience',
87
- client: { specs: [{ type: 'it', title: 'show active surveys summary' }] },
148
+ client: { specs: [{ type: 'it', id: 'it1', title: 'show active surveys summary' }] },
88
149
  },
89
150
  ],
90
151
  },
@@ -97,7 +158,7 @@ describe('hasAllIds', () => {
97
158
  name: 'Create Survey Form',
98
159
  id: includeAllSliceIds ? 'slice2' : undefined,
99
160
  type: 'experience',
100
- client: { specs: [{ type: 'it', title: 'allow entering survey title' }] },
161
+ client: { specs: [{ type: 'it', id: 'it2', title: 'allow entering survey title' }] },
101
162
  },
102
163
  ],
103
164
  },
@@ -110,7 +171,7 @@ describe('hasAllIds', () => {
110
171
  name: 'Response Rate Charts',
111
172
  id: 'slice3',
112
173
  type: 'experience',
113
- client: { specs: [{ type: 'it', title: 'show daily response rate charts' }] },
174
+ client: { specs: [{ type: 'it', id: 'it3', title: 'show daily response rate charts' }] },
114
175
  },
115
176
  ],
116
177
  },
@@ -166,4 +227,218 @@ describe('hasAllIds', () => {
166
227
  const model = createMultipleFlowsModel(true, false);
167
228
  expect(hasAllIds(model)).toBe(false);
168
229
  });
230
+
231
+ it('should return false if any spec is missing an ID', () => {
232
+ const model = createModelWithFullIds();
233
+ const modifiedModel = structuredClone(model);
234
+ const slice = modifiedModel.narratives[0].slices[0];
235
+ if ('server' in slice && slice.server?.specs !== undefined && Array.isArray(slice.server.specs)) {
236
+ slice.server.specs[0].id = '';
237
+ }
238
+ expect(hasAllIds(modifiedModel)).toBe(false);
239
+ });
240
+
241
+ it('should return false if any example is missing an ID', () => {
242
+ const model = createModelWithFullIds();
243
+ const modifiedModel = structuredClone(model);
244
+ const slice = modifiedModel.narratives[0].slices[0];
245
+ if ('server' in slice && slice.server?.specs !== undefined && Array.isArray(slice.server.specs)) {
246
+ slice.server.specs[0].rules[0].examples[0].id = '';
247
+ }
248
+ expect(hasAllIds(modifiedModel)).toBe(false);
249
+ });
250
+
251
+ it('should return false if any step is missing an ID', () => {
252
+ const model = createModelWithFullIds();
253
+ const modifiedModel = structuredClone(model);
254
+ const slice = modifiedModel.narratives[0].slices[0];
255
+ if ('server' in slice && slice.server?.specs !== undefined && Array.isArray(slice.server.specs)) {
256
+ slice.server.specs[0].rules[0].examples[0].steps[0].id = '';
257
+ }
258
+ expect(hasAllIds(modifiedModel)).toBe(false);
259
+ });
260
+
261
+ it('should return false if step with error is missing an ID', () => {
262
+ const model: Model = {
263
+ variant: 'specs',
264
+ narratives: [
265
+ {
266
+ name: 'Test Flow',
267
+ id: 'FLOW-001',
268
+ slices: [
269
+ {
270
+ type: 'command',
271
+ name: 'Test slice',
272
+ id: 'SLICE-001',
273
+ client: { specs: [] },
274
+ server: {
275
+ description: 'Test server',
276
+ specs: [
277
+ {
278
+ id: 'SPEC-001',
279
+ type: 'gherkin',
280
+ feature: 'Test specs',
281
+ rules: [
282
+ {
283
+ id: 'RULE-001',
284
+ name: 'Test rule',
285
+ examples: [
286
+ {
287
+ id: 'EXAMPLE-001',
288
+ name: 'Error example',
289
+ steps: [
290
+ {
291
+ id: 'STEP-001',
292
+ keyword: 'Given',
293
+ text: 'TestState',
294
+ },
295
+ {
296
+ keyword: 'Then',
297
+ error: { type: 'ValidationError', message: 'Invalid input' },
298
+ },
299
+ ],
300
+ },
301
+ ],
302
+ },
303
+ ],
304
+ },
305
+ ],
306
+ },
307
+ },
308
+ ],
309
+ },
310
+ ],
311
+ messages: [],
312
+ integrations: [],
313
+ };
314
+ expect(hasAllIds(model)).toBe(false);
315
+ });
316
+
317
+ it('should return false if client it spec is missing an ID', () => {
318
+ const model: Model = {
319
+ variant: 'specs',
320
+ narratives: [
321
+ {
322
+ name: 'Test Flow',
323
+ id: 'FLOW-001',
324
+ slices: [
325
+ {
326
+ name: 'Test slice',
327
+ id: 'SLICE-001',
328
+ type: 'experience',
329
+ client: {
330
+ specs: [{ type: 'it', title: 'test without id' }],
331
+ },
332
+ },
333
+ ],
334
+ },
335
+ ],
336
+ messages: [],
337
+ integrations: [],
338
+ };
339
+ expect(hasAllIds(model)).toBe(false);
340
+ });
341
+
342
+ it('should return false if client describe spec is missing an ID', () => {
343
+ const model: Model = {
344
+ variant: 'specs',
345
+ narratives: [
346
+ {
347
+ name: 'Test Flow',
348
+ id: 'FLOW-001',
349
+ slices: [
350
+ {
351
+ name: 'Test slice',
352
+ id: 'SLICE-001',
353
+ type: 'experience',
354
+ client: {
355
+ specs: [
356
+ {
357
+ type: 'describe',
358
+ title: 'test describe without id',
359
+ children: [{ type: 'it', id: 'IT-001', title: 'nested it with id' }],
360
+ },
361
+ ],
362
+ },
363
+ },
364
+ ],
365
+ },
366
+ ],
367
+ messages: [],
368
+ integrations: [],
369
+ };
370
+ expect(hasAllIds(model)).toBe(false);
371
+ });
372
+
373
+ it('should return false if nested client it spec is missing an ID', () => {
374
+ const model: Model = {
375
+ variant: 'specs',
376
+ narratives: [
377
+ {
378
+ name: 'Test Flow',
379
+ id: 'FLOW-001',
380
+ slices: [
381
+ {
382
+ name: 'Test slice',
383
+ id: 'SLICE-001',
384
+ type: 'experience',
385
+ client: {
386
+ specs: [
387
+ {
388
+ type: 'describe',
389
+ id: 'DESC-001',
390
+ title: 'test describe with id',
391
+ children: [{ type: 'it', title: 'nested it without id' }],
392
+ },
393
+ ],
394
+ },
395
+ },
396
+ ],
397
+ },
398
+ ],
399
+ messages: [],
400
+ integrations: [],
401
+ };
402
+ expect(hasAllIds(model)).toBe(false);
403
+ });
404
+
405
+ it('should return true for client specs with all IDs', () => {
406
+ const model: Model = {
407
+ variant: 'specs',
408
+ narratives: [
409
+ {
410
+ name: 'Test Flow',
411
+ id: 'FLOW-001',
412
+ slices: [
413
+ {
414
+ name: 'Test slice',
415
+ id: 'SLICE-001',
416
+ type: 'experience',
417
+ client: {
418
+ specs: [
419
+ {
420
+ type: 'describe',
421
+ id: 'DESC-001',
422
+ title: 'test describe',
423
+ children: [
424
+ { type: 'it', id: 'IT-001', title: 'first it' },
425
+ {
426
+ type: 'describe',
427
+ id: 'DESC-002',
428
+ title: 'nested describe',
429
+ children: [{ type: 'it', id: 'IT-002', title: 'nested it' }],
430
+ },
431
+ ],
432
+ },
433
+ ],
434
+ },
435
+ },
436
+ ],
437
+ },
438
+ ],
439
+ messages: [],
440
+ integrations: [],
441
+ };
442
+ expect(hasAllIds(model)).toBe(true);
443
+ });
169
444
  });
@@ -1,15 +1,23 @@
1
- import { Model, Slice, Spec, Rule } from '../index';
1
+ import { Model, Slice, Spec, Rule, Example, Step, ClientSpecNode } from '../index';
2
2
 
3
3
  function hasValidId(item: { id?: string }): boolean {
4
4
  return item.id !== undefined && item.id !== '';
5
5
  }
6
6
 
7
+ function hasStepIds(steps: Step[]): boolean {
8
+ return steps.every((step) => hasValidId(step));
9
+ }
10
+
11
+ function hasExampleIds(examples: Example[]): boolean {
12
+ return examples.every((example) => hasValidId(example) && hasStepIds(example.steps));
13
+ }
14
+
7
15
  function hasRuleIds(rules: Rule[]): boolean {
8
- return rules.every((rule) => hasValidId(rule));
16
+ return rules.every((rule) => hasValidId(rule) && hasExampleIds(rule.examples));
9
17
  }
10
18
 
11
19
  function hasSpecIds(specs: Spec[]): boolean {
12
- return specs.every((spec) => hasRuleIds(spec.rules));
20
+ return specs.every((spec) => hasValidId(spec) && hasRuleIds(spec.rules));
13
21
  }
14
22
 
15
23
  function hasServerSpecIds(slice: Slice): boolean {
@@ -17,8 +25,19 @@ function hasServerSpecIds(slice: Slice): boolean {
17
25
  return hasSpecIds(slice.server.specs);
18
26
  }
19
27
 
20
- function hasClientSpecIds(_slice: Slice): boolean {
21
- return true;
28
+ function hasClientSpecNodeIds(nodes: ClientSpecNode[]): boolean {
29
+ return nodes.every((node) => {
30
+ if (!hasValidId(node)) return false;
31
+ if (node.type === 'describe' && node.children) {
32
+ return hasClientSpecNodeIds(node.children);
33
+ }
34
+ return true;
35
+ });
36
+ }
37
+
38
+ function hasClientSpecIds(slice: Slice): boolean {
39
+ if (!('client' in slice) || slice.client?.specs === undefined || !Array.isArray(slice.client.specs)) return true;
40
+ return hasClientSpecNodeIds(slice.client.specs);
22
41
  }
23
42
 
24
43
  function hasSliceIds(slice: Slice): boolean {
package/src/schema.ts CHANGED
@@ -158,12 +158,14 @@ const StepErrorSchema = z.object({
158
158
  });
159
159
 
160
160
  const StepWithDocStringSchema = z.object({
161
+ id: z.string().optional().describe('Optional unique identifier for the step'),
161
162
  keyword: z.enum(['Given', 'When', 'Then', 'And']).describe('Gherkin keyword'),
162
163
  text: z.string().describe('The type name (e.g., AddTodo, TodoAdded)'),
163
164
  docString: z.record(z.unknown()).optional().describe('The example data'),
164
165
  });
165
166
 
166
167
  const StepWithErrorSchema = z.object({
168
+ id: z.string().optional().describe('Optional unique identifier for the step'),
167
169
  keyword: z.literal('Then').describe('Error steps use Then keyword'),
168
170
  error: StepErrorSchema.describe('Error details'),
169
171
  });
@@ -188,6 +190,7 @@ const RuleSchema = z
188
190
 
189
191
  const SpecSchema = z
190
192
  .object({
193
+ id: z.string().optional().describe('Optional unique identifier for the spec'),
191
194
  type: z.literal('gherkin').describe('Specification type'),
192
195
  feature: z.string().describe('Feature name'),
193
196
  rules: z.array(RuleSchema).describe('Business rules for this spec'),