@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.
package/package.json CHANGED
@@ -23,9 +23,9 @@
23
23
  "typescript": "^5.9.2",
24
24
  "zod": "^3.22.4",
25
25
  "zod-to-json-schema": "^3.22.3",
26
- "@auto-engineer/file-store": "0.13.1",
27
- "@auto-engineer/message-bus": "0.13.1",
28
- "@auto-engineer/id": "0.13.1"
26
+ "@auto-engineer/file-store": "0.13.2",
27
+ "@auto-engineer/id": "0.13.2",
28
+ "@auto-engineer/message-bus": "0.13.2"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.0.0",
@@ -33,12 +33,12 @@
33
33
  "eslint-plugin-sort-keys-fix": "^1.1.2",
34
34
  "fake-indexeddb": "^6.0.0",
35
35
  "tsx": "^4.20.3",
36
- "@auto-engineer/cli": "0.13.1"
36
+ "@auto-engineer/cli": "0.13.2"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "version": "0.13.1",
41
+ "version": "0.13.2",
42
42
  "scripts": {
43
43
  "build": "tsx scripts/build.ts",
44
44
  "test": "vitest run --reporter=dot",
@@ -286,4 +286,374 @@ describe('addAutoIds', () => {
286
286
  expect(result.narratives[1].sourceFile).toBe('/path/to/homepage.narrative.ts');
287
287
  expect(result.narratives[2].sourceFile).toBe('/path/to/homepage.narrative.ts');
288
288
  });
289
+
290
+ it('should assign IDs to specs', () => {
291
+ const modelWithSpecs: Model = {
292
+ variant: 'specs',
293
+ narratives: [
294
+ {
295
+ name: 'Test Flow',
296
+ slices: [
297
+ {
298
+ type: 'command',
299
+ name: 'Test Command',
300
+ client: { specs: [] },
301
+ server: {
302
+ description: 'Test server',
303
+ specs: [
304
+ {
305
+ type: 'gherkin',
306
+ feature: 'Test Feature',
307
+ rules: [],
308
+ },
309
+ {
310
+ id: 'EXISTING-SPEC-001',
311
+ type: 'gherkin',
312
+ feature: 'Existing Feature',
313
+ rules: [],
314
+ },
315
+ ],
316
+ },
317
+ },
318
+ ],
319
+ },
320
+ ],
321
+ messages: [],
322
+ integrations: [],
323
+ };
324
+
325
+ const result = addAutoIds(modelWithSpecs);
326
+ const slice = result.narratives[0].slices[0];
327
+
328
+ if ('server' in slice && slice.server?.specs != null && Array.isArray(slice.server.specs)) {
329
+ expect(slice.server.specs[0].id).toMatch(AUTO_ID_REGEX);
330
+ expect(slice.server.specs[1].id).toBe('EXISTING-SPEC-001');
331
+ }
332
+ });
333
+
334
+ it('should assign IDs to steps', () => {
335
+ const modelWithSteps: Model = {
336
+ variant: 'specs',
337
+ narratives: [
338
+ {
339
+ name: 'Test Flow',
340
+ slices: [
341
+ {
342
+ type: 'command',
343
+ name: 'Test Command',
344
+ client: { specs: [] },
345
+ server: {
346
+ description: 'Test server',
347
+ specs: [
348
+ {
349
+ type: 'gherkin',
350
+ feature: 'Test Feature',
351
+ rules: [
352
+ {
353
+ name: 'Test Rule',
354
+ examples: [
355
+ {
356
+ name: 'Test Example',
357
+ steps: [
358
+ { keyword: 'Given', text: 'TestState', docString: { value: 'test' } },
359
+ { keyword: 'When', text: 'TestCommand' },
360
+ { id: 'EXISTING-STEP-001', keyword: 'Then', text: 'TestEvent' },
361
+ ],
362
+ },
363
+ ],
364
+ },
365
+ ],
366
+ },
367
+ ],
368
+ },
369
+ },
370
+ ],
371
+ },
372
+ ],
373
+ messages: [],
374
+ integrations: [],
375
+ };
376
+
377
+ const result = addAutoIds(modelWithSteps);
378
+ const slice = result.narratives[0].slices[0];
379
+
380
+ if ('server' in slice && slice.server?.specs != null && Array.isArray(slice.server.specs)) {
381
+ const steps = slice.server.specs[0].rules[0].examples[0].steps;
382
+ expect(steps[0].id).toMatch(AUTO_ID_REGEX);
383
+ expect(steps[1].id).toMatch(AUTO_ID_REGEX);
384
+ expect(steps[2].id).toBe('EXISTING-STEP-001');
385
+ }
386
+ });
387
+
388
+ it('should preserve existing example IDs', () => {
389
+ const modelWithExistingExampleId: Model = {
390
+ variant: 'specs',
391
+ narratives: [
392
+ {
393
+ name: 'Test Flow',
394
+ slices: [
395
+ {
396
+ type: 'command',
397
+ name: 'Test Command',
398
+ client: { specs: [] },
399
+ server: {
400
+ description: 'Test server',
401
+ specs: [
402
+ {
403
+ type: 'gherkin',
404
+ feature: 'Test Feature',
405
+ rules: [
406
+ {
407
+ name: 'Test Rule',
408
+ examples: [
409
+ {
410
+ name: 'Example without id',
411
+ steps: [{ keyword: 'Given', text: 'TestState' }],
412
+ },
413
+ {
414
+ id: 'EXISTING-EXAMPLE-001',
415
+ name: 'Example with existing id',
416
+ steps: [{ keyword: 'Given', text: 'TestState' }],
417
+ },
418
+ ],
419
+ },
420
+ ],
421
+ },
422
+ ],
423
+ },
424
+ },
425
+ ],
426
+ },
427
+ ],
428
+ messages: [],
429
+ integrations: [],
430
+ };
431
+
432
+ const result = addAutoIds(modelWithExistingExampleId);
433
+ const slice = result.narratives[0].slices[0];
434
+
435
+ if ('server' in slice && slice.server?.specs != null && Array.isArray(slice.server.specs)) {
436
+ const examples = slice.server.specs[0].rules[0].examples;
437
+ expect(examples[0].id).toMatch(AUTO_ID_REGEX);
438
+ expect(examples[1].id).toBe('EXISTING-EXAMPLE-001');
439
+ }
440
+ });
441
+
442
+ it('should assign IDs to steps with errors', () => {
443
+ const modelWithErrorSteps: Model = {
444
+ variant: 'specs',
445
+ narratives: [
446
+ {
447
+ name: 'Test Flow',
448
+ slices: [
449
+ {
450
+ type: 'command',
451
+ name: 'Test Command',
452
+ client: { specs: [] },
453
+ server: {
454
+ description: 'Test server',
455
+ specs: [
456
+ {
457
+ type: 'gherkin',
458
+ feature: 'Test Feature',
459
+ rules: [
460
+ {
461
+ name: 'Error Rule',
462
+ examples: [
463
+ {
464
+ name: 'Error Example',
465
+ steps: [
466
+ { keyword: 'Given', text: 'TestState' },
467
+ { keyword: 'When', text: 'InvalidCommand' },
468
+ { keyword: 'Then', error: { type: 'ValidationError', message: 'Invalid input' } },
469
+ ],
470
+ },
471
+ ],
472
+ },
473
+ ],
474
+ },
475
+ ],
476
+ },
477
+ },
478
+ ],
479
+ },
480
+ ],
481
+ messages: [],
482
+ integrations: [],
483
+ };
484
+
485
+ const result = addAutoIds(modelWithErrorSteps);
486
+ const slice = result.narratives[0].slices[0];
487
+
488
+ if ('server' in slice && slice.server?.specs != null && Array.isArray(slice.server.specs)) {
489
+ const steps = slice.server.specs[0].rules[0].examples[0].steps;
490
+ expect(steps[0].id).toMatch(AUTO_ID_REGEX);
491
+ expect(steps[1].id).toMatch(AUTO_ID_REGEX);
492
+ expect(steps[2].id).toMatch(AUTO_ID_REGEX);
493
+ }
494
+ });
495
+
496
+ it('should assign IDs to client it specs', () => {
497
+ const modelWithClientSpecs: Model = {
498
+ variant: 'specs',
499
+ narratives: [
500
+ {
501
+ name: 'Test Flow',
502
+ slices: [
503
+ {
504
+ type: 'experience',
505
+ name: 'Test Experience',
506
+ client: {
507
+ specs: [
508
+ { type: 'it', title: 'first test' },
509
+ { type: 'it', id: 'EXISTING-IT-001', title: 'second test with id' },
510
+ ],
511
+ },
512
+ },
513
+ ],
514
+ },
515
+ ],
516
+ messages: [],
517
+ integrations: [],
518
+ };
519
+
520
+ const result = addAutoIds(modelWithClientSpecs);
521
+ const slice = result.narratives[0].slices[0];
522
+
523
+ if ('client' in slice && slice.client?.specs != null) {
524
+ expect(slice.client.specs[0].id).toMatch(AUTO_ID_REGEX);
525
+ expect(slice.client.specs[1].id).toBe('EXISTING-IT-001');
526
+ }
527
+ });
528
+
529
+ it('should assign IDs to client describe specs', () => {
530
+ const modelWithDescribe: Model = {
531
+ variant: 'specs',
532
+ narratives: [
533
+ {
534
+ name: 'Test Flow',
535
+ slices: [
536
+ {
537
+ type: 'experience',
538
+ name: 'Test Experience',
539
+ client: {
540
+ specs: [
541
+ {
542
+ type: 'describe',
543
+ title: 'describe without id',
544
+ children: [{ type: 'it', title: 'nested it' }],
545
+ },
546
+ {
547
+ type: 'describe',
548
+ id: 'EXISTING-DESC-001',
549
+ title: 'describe with id',
550
+ children: [],
551
+ },
552
+ ],
553
+ },
554
+ },
555
+ ],
556
+ },
557
+ ],
558
+ messages: [],
559
+ integrations: [],
560
+ };
561
+
562
+ const result = addAutoIds(modelWithDescribe);
563
+ const slice = result.narratives[0].slices[0];
564
+
565
+ if ('client' in slice && slice.client?.specs != null) {
566
+ expect(slice.client.specs[0].id).toMatch(AUTO_ID_REGEX);
567
+ expect(slice.client.specs[1].id).toBe('EXISTING-DESC-001');
568
+ }
569
+ });
570
+
571
+ it('should assign IDs to nested client specs', () => {
572
+ const modelWithNestedSpecs: Model = {
573
+ variant: 'specs',
574
+ narratives: [
575
+ {
576
+ name: 'Test Flow',
577
+ slices: [
578
+ {
579
+ type: 'experience',
580
+ name: 'Test Experience',
581
+ client: {
582
+ specs: [
583
+ {
584
+ type: 'describe',
585
+ title: 'outer describe',
586
+ children: [
587
+ { type: 'it', title: 'outer it' },
588
+ {
589
+ type: 'describe',
590
+ title: 'inner describe',
591
+ children: [
592
+ { type: 'it', title: 'inner it 1' },
593
+ { type: 'it', title: 'inner it 2' },
594
+ ],
595
+ },
596
+ ],
597
+ },
598
+ ],
599
+ },
600
+ },
601
+ ],
602
+ },
603
+ ],
604
+ messages: [],
605
+ integrations: [],
606
+ };
607
+
608
+ const result = addAutoIds(modelWithNestedSpecs);
609
+ const slice = result.narratives[0].slices[0];
610
+
611
+ if ('client' in slice && slice.client?.specs != null) {
612
+ const outerDescribe = slice.client.specs[0];
613
+ expect(outerDescribe.id).toMatch(AUTO_ID_REGEX);
614
+
615
+ if (outerDescribe.type === 'describe' && outerDescribe.children) {
616
+ expect(outerDescribe.children[0].id).toMatch(AUTO_ID_REGEX);
617
+
618
+ const innerDescribe = outerDescribe.children[1];
619
+ expect(innerDescribe.id).toMatch(AUTO_ID_REGEX);
620
+
621
+ if (innerDescribe.type === 'describe' && innerDescribe.children) {
622
+ expect(innerDescribe.children[0].id).toMatch(AUTO_ID_REGEX);
623
+ expect(innerDescribe.children[1].id).toMatch(AUTO_ID_REGEX);
624
+
625
+ expect(innerDescribe.children[0].id).not.toBe(innerDescribe.children[1].id);
626
+ }
627
+ }
628
+ }
629
+ });
630
+
631
+ it('should not mutate original client specs', () => {
632
+ const modelWithClientSpecs: Model = {
633
+ variant: 'specs',
634
+ narratives: [
635
+ {
636
+ name: 'Test Flow',
637
+ slices: [
638
+ {
639
+ type: 'experience',
640
+ name: 'Test Experience',
641
+ client: {
642
+ specs: [{ type: 'it', title: 'test' }],
643
+ },
644
+ },
645
+ ],
646
+ },
647
+ ],
648
+ messages: [],
649
+ integrations: [],
650
+ };
651
+
652
+ const originalSpec = modelWithClientSpecs.narratives[0].slices[0];
653
+ addAutoIds(modelWithClientSpecs);
654
+
655
+ if ('client' in originalSpec && originalSpec.client?.specs != null) {
656
+ expect(originalSpec.client.specs[0].id).toBeUndefined();
657
+ }
658
+ });
289
659
  });
@@ -1,5 +1,5 @@
1
1
  import { generateAutoId } from './generators';
2
- import { Model, Slice, Spec, Rule, Example } from '../index';
2
+ import { Model, Slice, Spec, Rule, Example, Step, ClientSpecNode } from '../index';
3
3
 
4
4
  function ensureId(item: { id?: string }): void {
5
5
  if (item.id === undefined || item.id === '') {
@@ -7,10 +7,19 @@ function ensureId(item: { id?: string }): void {
7
7
  }
8
8
  }
9
9
 
10
+ function processSteps(steps: Step[]): Step[] {
11
+ return steps.map((step) => {
12
+ const stepCopy = { ...step };
13
+ ensureId(stepCopy);
14
+ return stepCopy;
15
+ });
16
+ }
17
+
10
18
  function processExamples(examples: Example[]): Example[] {
11
19
  return examples.map((example) => {
12
20
  const exampleCopy = { ...example };
13
21
  ensureId(exampleCopy);
22
+ exampleCopy.steps = processSteps(example.steps);
14
23
  return exampleCopy;
15
24
  });
16
25
  }
@@ -25,10 +34,12 @@ function processRules(rules: Rule[]): Rule[] {
25
34
  }
26
35
 
27
36
  function processSpecs(specs: Spec[]): Spec[] {
28
- return specs.map((spec) => ({
29
- ...spec,
30
- rules: processRules(spec.rules),
31
- }));
37
+ return specs.map((spec) => {
38
+ const specCopy = { ...spec };
39
+ ensureId(specCopy);
40
+ specCopy.rules = processRules(spec.rules);
41
+ return specCopy;
42
+ });
32
43
  }
33
44
 
34
45
  function processServerSpecs(slice: Slice): Slice {
@@ -45,9 +56,25 @@ function processServerSpecs(slice: Slice): Slice {
45
56
  return modifiedSlice;
46
57
  }
47
58
 
59
+ function processClientSpecNodes(nodes: ClientSpecNode[]): ClientSpecNode[] {
60
+ return nodes.map((node) => {
61
+ const nodeCopy = { ...node };
62
+ ensureId(nodeCopy);
63
+ if (nodeCopy.type === 'describe' && nodeCopy.children) {
64
+ nodeCopy.children = processClientSpecNodes(nodeCopy.children);
65
+ }
66
+ return nodeCopy;
67
+ });
68
+ }
69
+
48
70
  function processClientSpecs(slice: Slice): Slice {
49
- // Client specs use string rules (no IDs needed), so nothing to process
50
- return slice;
71
+ if (!('client' in slice) || slice.client?.specs === undefined || !Array.isArray(slice.client.specs)) return slice;
72
+
73
+ const modifiedSlice = structuredClone(slice);
74
+ if ('client' in modifiedSlice && modifiedSlice.client?.specs !== undefined) {
75
+ modifiedSlice.client.specs = processClientSpecNodes(modifiedSlice.client.specs);
76
+ }
77
+ return modifiedSlice;
51
78
  }
52
79
 
53
80
  function processSlice(slice: Slice): Slice {