@auto-engineer/server-generator-apollo-emmett 1.149.0 → 1.152.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +96 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
- package/dist/src/codegen/extract/type-helpers.js +3 -2
- package/dist/src/codegen/extract/type-helpers.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +44 -6
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templateHelpers.d.ts +1 -0
- package/dist/src/codegen/templateHelpers.d.ts.map +1 -1
- package/dist/src/codegen/templateHelpers.js +12 -0
- package/dist/src/codegen/templateHelpers.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +688 -11
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +90 -3
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +6 -3
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +3 -8
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +63 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +8 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +19 -5
- package/dist/src/codegen/templates/react/react.ts.ejs +71 -19
- package/dist/src/codegen/templates/react/react.ts.specs.ts +139 -0
- package/dist/src/codegen/templates/react/register.ts.ejs +34 -9
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +5 -5
- package/package.json +4 -4
- package/src/codegen/extract/type-helpers.specs.ts +21 -0
- package/src/codegen/extract/type-helpers.ts +3 -2
- package/src/codegen/scaffoldFromSchema.filter.specs.ts +110 -1
- package/src/codegen/scaffoldFromSchema.ts +47 -8
- package/src/codegen/templateHelpers.specs.ts +21 -1
- package/src/codegen/templateHelpers.ts +9 -0
- package/src/codegen/templates/command/decide.specs.specs.ts +688 -11
- package/src/codegen/templates/command/decide.specs.ts.ejs +90 -3
- package/src/codegen/templates/query/projection.specs.specs.ts +6 -3
- package/src/codegen/templates/query/projection.specs.ts.ejs +3 -8
- package/src/codegen/templates/query/query.resolver.specs.ts +63 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +8 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +19 -5
- package/src/codegen/templates/react/react.ts.ejs +71 -19
- package/src/codegen/templates/react/react.ts.specs.ts +139 -0
- package/src/codegen/templates/react/register.ts.ejs +34 -9
|
@@ -248,7 +248,7 @@ describe('spec.ts.ejs', () => {
|
|
|
248
248
|
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
249
249
|
|
|
250
250
|
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
251
|
-
"import { describe, it } from 'vitest';
|
|
251
|
+
"import { describe, expect, it } from 'vitest';
|
|
252
252
|
import { DeciderSpecification } from '@event-driven-io/emmett';
|
|
253
253
|
import { decide } from './decide';
|
|
254
254
|
import { evolve } from './evolve';
|
|
@@ -297,6 +297,41 @@ describe('spec.ts.ejs', () => {
|
|
|
297
297
|
);
|
|
298
298
|
});
|
|
299
299
|
});
|
|
300
|
+
|
|
301
|
+
describe('field completeness', () => {
|
|
302
|
+
it('should include all required fields in ListingRemoved', () => {
|
|
303
|
+
const state = [
|
|
304
|
+
{
|
|
305
|
+
type: 'ListingCreated' as const,
|
|
306
|
+
data: {
|
|
307
|
+
propertyId: 'listing_123',
|
|
308
|
+
listedAt: new Date('2024-01-15T10:00:00Z'),
|
|
309
|
+
rating: 4.8,
|
|
310
|
+
metadata: { foo: 'bar' },
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
].reduce((s, e) => evolve(s, e), initialState());
|
|
314
|
+
const result = decide(
|
|
315
|
+
{
|
|
316
|
+
type: 'RemoveListing',
|
|
317
|
+
data: {
|
|
318
|
+
propertyId: 'listing_123',
|
|
319
|
+
},
|
|
320
|
+
metadata: { now: new Date() },
|
|
321
|
+
},
|
|
322
|
+
state,
|
|
323
|
+
);
|
|
324
|
+
const evtArray = Array.isArray(result) ? result : [result];
|
|
325
|
+
|
|
326
|
+
expect(evtArray[0]).toMatchObject({
|
|
327
|
+
type: 'ListingRemoved',
|
|
328
|
+
data: expect.objectContaining({
|
|
329
|
+
propertyId: expect.any(String),
|
|
330
|
+
removedAt: expect.any(Date),
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
300
335
|
"
|
|
301
336
|
`);
|
|
302
337
|
});
|
|
@@ -443,7 +478,7 @@ describe('spec.ts.ejs', () => {
|
|
|
443
478
|
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
444
479
|
|
|
445
480
|
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
446
|
-
"import { describe, it } from 'vitest';
|
|
481
|
+
"import { describe, expect, it } from 'vitest';
|
|
447
482
|
import { DeciderSpecification } from '@event-driven-io/emmett';
|
|
448
483
|
import { decide } from './decide';
|
|
449
484
|
import { evolve } from './evolve';
|
|
@@ -521,6 +556,47 @@ describe('spec.ts.ejs', () => {
|
|
|
521
556
|
);
|
|
522
557
|
});
|
|
523
558
|
});
|
|
559
|
+
|
|
560
|
+
describe('field completeness', () => {
|
|
561
|
+
it('should include all required fields in QuestionAnswered, QuestionnaireEditRejected', () => {
|
|
562
|
+
const state = initialState();
|
|
563
|
+
const result = decide(
|
|
564
|
+
{
|
|
565
|
+
type: 'AnswerQuestion',
|
|
566
|
+
data: {
|
|
567
|
+
questionnaireId: 'q-001',
|
|
568
|
+
participantId: 'participant-abc',
|
|
569
|
+
questionId: 'q1',
|
|
570
|
+
answer: 'Yes',
|
|
571
|
+
},
|
|
572
|
+
metadata: { now: new Date() },
|
|
573
|
+
},
|
|
574
|
+
state,
|
|
575
|
+
);
|
|
576
|
+
const evtArray = Array.isArray(result) ? result : [result];
|
|
577
|
+
|
|
578
|
+
expect(evtArray[0]).toMatchObject({
|
|
579
|
+
type: 'QuestionAnswered',
|
|
580
|
+
data: expect.objectContaining({
|
|
581
|
+
questionnaireId: expect.any(String),
|
|
582
|
+
participantId: expect.any(String),
|
|
583
|
+
questionId: expect.any(String),
|
|
584
|
+
answer: expect.any(Object),
|
|
585
|
+
savedAt: expect.any(Date),
|
|
586
|
+
}),
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
expect(evtArray[1]).toMatchObject({
|
|
590
|
+
type: 'QuestionnaireEditRejected',
|
|
591
|
+
data: expect.objectContaining({
|
|
592
|
+
questionnaireId: expect.any(String),
|
|
593
|
+
participantId: expect.any(String),
|
|
594
|
+
reason: expect.any(String),
|
|
595
|
+
attemptedAt: expect.any(Date),
|
|
596
|
+
}),
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
});
|
|
524
600
|
"
|
|
525
601
|
`);
|
|
526
602
|
});
|
|
@@ -706,7 +782,7 @@ describe('spec.ts.ejs', () => {
|
|
|
706
782
|
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
707
783
|
|
|
708
784
|
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
709
|
-
"import { describe, it } from 'vitest';
|
|
785
|
+
"import { describe, expect, it } from 'vitest';
|
|
710
786
|
import { DeciderSpecification } from '@event-driven-io/emmett';
|
|
711
787
|
import { decide } from './decide';
|
|
712
788
|
import { evolve } from './evolve';
|
|
@@ -754,6 +830,40 @@ describe('spec.ts.ejs', () => {
|
|
|
754
830
|
);
|
|
755
831
|
});
|
|
756
832
|
});
|
|
833
|
+
|
|
834
|
+
describe('field completeness', () => {
|
|
835
|
+
it('should include all required fields in StatsUpdated, WorkoutLogged', () => {
|
|
836
|
+
const state = initialState();
|
|
837
|
+
const result = decide(
|
|
838
|
+
{
|
|
839
|
+
type: 'SubmitWorkout',
|
|
840
|
+
data: {
|
|
841
|
+
workoutId: 'w1',
|
|
842
|
+
exercise: 'squat',
|
|
843
|
+
},
|
|
844
|
+
metadata: { now: new Date() },
|
|
845
|
+
},
|
|
846
|
+
state,
|
|
847
|
+
);
|
|
848
|
+
const evtArray = Array.isArray(result) ? result : [result];
|
|
849
|
+
|
|
850
|
+
expect(evtArray[0]).toMatchObject({
|
|
851
|
+
type: 'StatsUpdated',
|
|
852
|
+
data: expect.objectContaining({
|
|
853
|
+
workoutId: expect.any(String),
|
|
854
|
+
count: expect.any(Number),
|
|
855
|
+
}),
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
expect(evtArray[1]).toMatchObject({
|
|
859
|
+
type: 'WorkoutLogged',
|
|
860
|
+
data: expect.objectContaining({
|
|
861
|
+
workoutId: expect.any(String),
|
|
862
|
+
loggedAt: expect.any(Date),
|
|
863
|
+
}),
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
});
|
|
757
867
|
"
|
|
758
868
|
`);
|
|
759
869
|
});
|
|
@@ -1215,14 +1325,19 @@ describe('spec.ts.ejs', () => {
|
|
|
1215
1325
|
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
1216
1326
|
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
1217
1327
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
expect(
|
|
1222
|
-
expect(
|
|
1223
|
-
expect(
|
|
1224
|
-
expect(
|
|
1225
|
-
expect(
|
|
1328
|
+
const contents = specFile?.contents ?? '';
|
|
1329
|
+
// workoutId should NOT appear in the DeciderSpecification expectEvents block
|
|
1330
|
+
const deciderSection = contents.split("describe('field completeness'")[0];
|
|
1331
|
+
expect(deciderSection).not.toContain('workoutId:');
|
|
1332
|
+
expect(contents).toContain("memberId: 'mem_001'");
|
|
1333
|
+
expect(contents).toContain("date: '2024-01-15'");
|
|
1334
|
+
expect(contents).toContain("exercises: ['bench press']");
|
|
1335
|
+
expect(contents).toContain('const expectEvents');
|
|
1336
|
+
expect(contents).toContain('events as Events[]');
|
|
1337
|
+
expect(contents).toContain('.then(\n');
|
|
1338
|
+
expect(contents).toContain('expectEvents(');
|
|
1339
|
+
// workoutId is not traceable to any Given event, so uses expect.any()
|
|
1340
|
+
expect(contents).toContain('workoutId: expect.any(String)');
|
|
1226
1341
|
});
|
|
1227
1342
|
|
|
1228
1343
|
it('should keep non-command fields whose key+value match a Given event', async () => {
|
|
@@ -1388,6 +1503,166 @@ describe('spec.ts.ejs', () => {
|
|
|
1388
1503
|
expect(decideFile?.contents).not.toContain('`NotFoundError`');
|
|
1389
1504
|
});
|
|
1390
1505
|
|
|
1506
|
+
it('should exclude state-only-traceable fields from main test then block', async () => {
|
|
1507
|
+
const spec: SpecsSchema = {
|
|
1508
|
+
variant: 'specs',
|
|
1509
|
+
scenes: [
|
|
1510
|
+
{
|
|
1511
|
+
name: 'Exercise scene',
|
|
1512
|
+
moments: [
|
|
1513
|
+
{
|
|
1514
|
+
type: 'command',
|
|
1515
|
+
name: 'Log exercise',
|
|
1516
|
+
stream: 'exercise',
|
|
1517
|
+
client: { specs: [] },
|
|
1518
|
+
server: {
|
|
1519
|
+
description: '',
|
|
1520
|
+
specs: [
|
|
1521
|
+
{
|
|
1522
|
+
type: 'gherkin',
|
|
1523
|
+
feature: 'Log exercise',
|
|
1524
|
+
rules: [
|
|
1525
|
+
{
|
|
1526
|
+
name: 'Should log exercise',
|
|
1527
|
+
examples: [
|
|
1528
|
+
{
|
|
1529
|
+
name: 'Exercise logged from draft',
|
|
1530
|
+
steps: [
|
|
1531
|
+
{
|
|
1532
|
+
keyword: 'Given',
|
|
1533
|
+
text: 'ExerciseDraft',
|
|
1534
|
+
docString: { id: 'e1', userId: 'u1' },
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
keyword: 'When',
|
|
1538
|
+
text: 'LogExercise',
|
|
1539
|
+
docString: { sets: 3, reps: 10 },
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
keyword: 'Then',
|
|
1543
|
+
text: 'ExerciseLogged',
|
|
1544
|
+
docString: { id: 'e1', userId: 'u1', sets: 3, reps: 10 },
|
|
1545
|
+
},
|
|
1546
|
+
],
|
|
1547
|
+
},
|
|
1548
|
+
],
|
|
1549
|
+
},
|
|
1550
|
+
],
|
|
1551
|
+
},
|
|
1552
|
+
],
|
|
1553
|
+
},
|
|
1554
|
+
},
|
|
1555
|
+
],
|
|
1556
|
+
},
|
|
1557
|
+
],
|
|
1558
|
+
messages: [
|
|
1559
|
+
{
|
|
1560
|
+
type: 'state',
|
|
1561
|
+
name: 'ExerciseDraft',
|
|
1562
|
+
fields: [
|
|
1563
|
+
{ name: 'id', type: 'string', required: true },
|
|
1564
|
+
{ name: 'userId', type: 'string', required: true },
|
|
1565
|
+
],
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
type: 'command',
|
|
1569
|
+
name: 'LogExercise',
|
|
1570
|
+
fields: [
|
|
1571
|
+
{ name: 'sets', type: 'number', required: true },
|
|
1572
|
+
{ name: 'reps', type: 'number', required: true },
|
|
1573
|
+
],
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
type: 'event',
|
|
1577
|
+
name: 'ExerciseLogged',
|
|
1578
|
+
source: 'internal',
|
|
1579
|
+
fields: [
|
|
1580
|
+
{ name: 'id', type: 'string', required: true },
|
|
1581
|
+
{ name: 'userId', type: 'string', required: true },
|
|
1582
|
+
{ name: 'sets', type: 'number', required: true },
|
|
1583
|
+
{ name: 'reps', type: 'number', required: true },
|
|
1584
|
+
],
|
|
1585
|
+
},
|
|
1586
|
+
],
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
1590
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
1591
|
+
|
|
1592
|
+
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
1593
|
+
"import { describe, expect, it } from 'vitest';
|
|
1594
|
+
import { DeciderSpecification } from '@event-driven-io/emmett';
|
|
1595
|
+
import { decide } from './decide';
|
|
1596
|
+
import { evolve } from './evolve';
|
|
1597
|
+
import { initialState, State } from './state';
|
|
1598
|
+
import type { ExerciseLogged } from './events';
|
|
1599
|
+
import type { LogExercise } from './commands';
|
|
1600
|
+
|
|
1601
|
+
describe('Should log exercise', () => {
|
|
1602
|
+
type Events = ExerciseLogged;
|
|
1603
|
+
|
|
1604
|
+
const given = DeciderSpecification.for<LogExercise, Events, State>({
|
|
1605
|
+
decide,
|
|
1606
|
+
evolve,
|
|
1607
|
+
initialState,
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
|
|
1611
|
+
|
|
1612
|
+
it('Exercise logged from draft', () => {
|
|
1613
|
+
given([])
|
|
1614
|
+
.when({
|
|
1615
|
+
type: 'LogExercise',
|
|
1616
|
+
data: {
|
|
1617
|
+
sets: 3,
|
|
1618
|
+
reps: 10,
|
|
1619
|
+
},
|
|
1620
|
+
metadata: { now: new Date() },
|
|
1621
|
+
})
|
|
1622
|
+
|
|
1623
|
+
.then(
|
|
1624
|
+
expectEvents({
|
|
1625
|
+
type: 'ExerciseLogged',
|
|
1626
|
+
data: {
|
|
1627
|
+
sets: 3,
|
|
1628
|
+
reps: 10,
|
|
1629
|
+
},
|
|
1630
|
+
}),
|
|
1631
|
+
);
|
|
1632
|
+
});
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
describe('field completeness', () => {
|
|
1636
|
+
it('should include all required fields in ExerciseLogged', () => {
|
|
1637
|
+
const state = initialState();
|
|
1638
|
+
const result = decide(
|
|
1639
|
+
{
|
|
1640
|
+
type: 'LogExercise',
|
|
1641
|
+
data: {
|
|
1642
|
+
sets: 3,
|
|
1643
|
+
reps: 10,
|
|
1644
|
+
},
|
|
1645
|
+
metadata: { now: new Date() },
|
|
1646
|
+
},
|
|
1647
|
+
state,
|
|
1648
|
+
);
|
|
1649
|
+
const evtArray = Array.isArray(result) ? result : [result];
|
|
1650
|
+
|
|
1651
|
+
expect(evtArray[0]).toMatchObject({
|
|
1652
|
+
type: 'ExerciseLogged',
|
|
1653
|
+
data: expect.objectContaining({
|
|
1654
|
+
id: expect.any(String),
|
|
1655
|
+
userId: expect.any(String),
|
|
1656
|
+
sets: expect.any(Number),
|
|
1657
|
+
reps: expect.any(Number),
|
|
1658
|
+
}),
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
"
|
|
1663
|
+
`);
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1391
1666
|
it('should omit state-based Given refs from given() array', async () => {
|
|
1392
1667
|
const spec: SpecsSchema = {
|
|
1393
1668
|
variant: 'specs',
|
|
@@ -2116,4 +2391,406 @@ describe('spec.ts.ejs', () => {
|
|
|
2116
2391
|
expect(specFile?.contents).toContain('should emit OrderPlaced for PlaceOrder');
|
|
2117
2392
|
expect(specFile?.contents).not.toContain('for valid');
|
|
2118
2393
|
});
|
|
2394
|
+
|
|
2395
|
+
it('should generate field completeness assertion for nonCommandFields', async () => {
|
|
2396
|
+
const spec: SpecsSchema = {
|
|
2397
|
+
variant: 'specs',
|
|
2398
|
+
scenes: [
|
|
2399
|
+
{
|
|
2400
|
+
name: 'Booking scene',
|
|
2401
|
+
moments: [
|
|
2402
|
+
{
|
|
2403
|
+
type: 'command',
|
|
2404
|
+
name: 'Request booking',
|
|
2405
|
+
client: { specs: [] },
|
|
2406
|
+
server: {
|
|
2407
|
+
description: '',
|
|
2408
|
+
specs: [
|
|
2409
|
+
{
|
|
2410
|
+
type: 'gherkin',
|
|
2411
|
+
feature: 'Request booking spec',
|
|
2412
|
+
rules: [
|
|
2413
|
+
{
|
|
2414
|
+
name: 'Booking can be requested',
|
|
2415
|
+
examples: [
|
|
2416
|
+
{
|
|
2417
|
+
name: 'Guest requests a booking',
|
|
2418
|
+
steps: [
|
|
2419
|
+
{
|
|
2420
|
+
keyword: 'When',
|
|
2421
|
+
text: 'RequestBooking',
|
|
2422
|
+
docString: { listingId: 'l1', guestId: 'g1', nights: 3 },
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
keyword: 'Then',
|
|
2426
|
+
text: 'BookingRequested',
|
|
2427
|
+
docString: {
|
|
2428
|
+
listingId: 'l1',
|
|
2429
|
+
guestId: 'g1',
|
|
2430
|
+
nights: 3,
|
|
2431
|
+
bookingId: 'b1',
|
|
2432
|
+
totalPrice: 750,
|
|
2433
|
+
status: 'pending',
|
|
2434
|
+
},
|
|
2435
|
+
},
|
|
2436
|
+
],
|
|
2437
|
+
},
|
|
2438
|
+
],
|
|
2439
|
+
},
|
|
2440
|
+
],
|
|
2441
|
+
},
|
|
2442
|
+
],
|
|
2443
|
+
},
|
|
2444
|
+
},
|
|
2445
|
+
],
|
|
2446
|
+
},
|
|
2447
|
+
],
|
|
2448
|
+
messages: [
|
|
2449
|
+
{
|
|
2450
|
+
type: 'command',
|
|
2451
|
+
name: 'RequestBooking',
|
|
2452
|
+
fields: [
|
|
2453
|
+
{ name: 'listingId', type: 'string', required: true },
|
|
2454
|
+
{ name: 'guestId', type: 'string', required: true },
|
|
2455
|
+
{ name: 'nights', type: 'number', required: true },
|
|
2456
|
+
],
|
|
2457
|
+
},
|
|
2458
|
+
{
|
|
2459
|
+
type: 'event',
|
|
2460
|
+
name: 'BookingRequested',
|
|
2461
|
+
source: 'internal',
|
|
2462
|
+
fields: [
|
|
2463
|
+
{ name: 'listingId', type: 'string', required: true },
|
|
2464
|
+
{ name: 'guestId', type: 'string', required: true },
|
|
2465
|
+
{ name: 'nights', type: 'number', required: true },
|
|
2466
|
+
{ name: 'bookingId', type: 'string', required: true },
|
|
2467
|
+
{ name: 'totalPrice', type: 'number', required: true },
|
|
2468
|
+
{ name: 'status', type: 'string', required: true },
|
|
2469
|
+
],
|
|
2470
|
+
},
|
|
2471
|
+
],
|
|
2472
|
+
};
|
|
2473
|
+
|
|
2474
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
2475
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
2476
|
+
const contents = specFile?.contents ?? '';
|
|
2477
|
+
|
|
2478
|
+
// Should include expect in vitest import
|
|
2479
|
+
expect(contents).toContain("import { describe, expect, it } from 'vitest'");
|
|
2480
|
+
|
|
2481
|
+
// Should generate field completeness block
|
|
2482
|
+
expect(contents).toContain('expect.objectContaining');
|
|
2483
|
+
|
|
2484
|
+
// Command fields use expect.any(Constructor)
|
|
2485
|
+
expect(contents).toContain('listingId: expect.any(String)');
|
|
2486
|
+
expect(contents).toContain('guestId: expect.any(String)');
|
|
2487
|
+
expect(contents).toContain('nights: expect.any(Number)');
|
|
2488
|
+
|
|
2489
|
+
// NonCommandFields without Given-event traceability use expect.any()
|
|
2490
|
+
expect(contents).toContain('bookingId: expect.any(String)');
|
|
2491
|
+
expect(contents).toContain('totalPrice: expect.any(Number)');
|
|
2492
|
+
expect(contents).toContain('status: expect.any(String)');
|
|
2493
|
+
});
|
|
2494
|
+
|
|
2495
|
+
it('should not generate field completeness block when all event fields are command fields', async () => {
|
|
2496
|
+
const spec: SpecsSchema = {
|
|
2497
|
+
variant: 'specs',
|
|
2498
|
+
scenes: [
|
|
2499
|
+
{
|
|
2500
|
+
name: 'Simple scene',
|
|
2501
|
+
moments: [
|
|
2502
|
+
{
|
|
2503
|
+
type: 'command',
|
|
2504
|
+
name: 'Rename item',
|
|
2505
|
+
client: { specs: [] },
|
|
2506
|
+
server: {
|
|
2507
|
+
description: '',
|
|
2508
|
+
specs: [
|
|
2509
|
+
{
|
|
2510
|
+
type: 'gherkin',
|
|
2511
|
+
feature: 'Rename item spec',
|
|
2512
|
+
rules: [
|
|
2513
|
+
{
|
|
2514
|
+
name: 'Item can be renamed',
|
|
2515
|
+
examples: [
|
|
2516
|
+
{
|
|
2517
|
+
name: 'Rename succeeds',
|
|
2518
|
+
steps: [
|
|
2519
|
+
{
|
|
2520
|
+
keyword: 'When',
|
|
2521
|
+
text: 'RenameItem',
|
|
2522
|
+
docString: { itemId: 'i1', newName: 'Widget' },
|
|
2523
|
+
},
|
|
2524
|
+
{
|
|
2525
|
+
keyword: 'Then',
|
|
2526
|
+
text: 'ItemRenamed',
|
|
2527
|
+
docString: { itemId: 'i1', newName: 'Widget' },
|
|
2528
|
+
},
|
|
2529
|
+
],
|
|
2530
|
+
},
|
|
2531
|
+
],
|
|
2532
|
+
},
|
|
2533
|
+
],
|
|
2534
|
+
},
|
|
2535
|
+
],
|
|
2536
|
+
},
|
|
2537
|
+
},
|
|
2538
|
+
],
|
|
2539
|
+
},
|
|
2540
|
+
],
|
|
2541
|
+
messages: [
|
|
2542
|
+
{
|
|
2543
|
+
type: 'command',
|
|
2544
|
+
name: 'RenameItem',
|
|
2545
|
+
fields: [
|
|
2546
|
+
{ name: 'itemId', type: 'string', required: true },
|
|
2547
|
+
{ name: 'newName', type: 'string', required: true },
|
|
2548
|
+
],
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
type: 'event',
|
|
2552
|
+
name: 'ItemRenamed',
|
|
2553
|
+
source: 'internal',
|
|
2554
|
+
fields: [
|
|
2555
|
+
{ name: 'itemId', type: 'string', required: true },
|
|
2556
|
+
{ name: 'newName', type: 'string', required: true },
|
|
2557
|
+
],
|
|
2558
|
+
},
|
|
2559
|
+
],
|
|
2560
|
+
};
|
|
2561
|
+
|
|
2562
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
2563
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
2564
|
+
const contents = specFile?.contents ?? '';
|
|
2565
|
+
|
|
2566
|
+
// Should NOT generate field completeness block
|
|
2567
|
+
expect(contents).not.toContain('expect.objectContaining');
|
|
2568
|
+
expect(contents).not.toContain('expect.any');
|
|
2569
|
+
// Should still use plain import without expect
|
|
2570
|
+
expect(contents).toContain("import { describe, it } from 'vitest'");
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2573
|
+
it('should not duplicate field completeness assertions when multiple examples produce the same event type', async () => {
|
|
2574
|
+
const spec: SpecsSchema = {
|
|
2575
|
+
variant: 'specs',
|
|
2576
|
+
scenes: [
|
|
2577
|
+
{
|
|
2578
|
+
name: 'Booking scene',
|
|
2579
|
+
moments: [
|
|
2580
|
+
{
|
|
2581
|
+
type: 'command',
|
|
2582
|
+
name: 'Book appointment',
|
|
2583
|
+
client: { specs: [] },
|
|
2584
|
+
server: {
|
|
2585
|
+
description: '',
|
|
2586
|
+
specs: [
|
|
2587
|
+
{
|
|
2588
|
+
type: 'gherkin',
|
|
2589
|
+
feature: 'Book appointment spec',
|
|
2590
|
+
rules: [
|
|
2591
|
+
{
|
|
2592
|
+
name: 'Appointment can be booked',
|
|
2593
|
+
examples: [
|
|
2594
|
+
{
|
|
2595
|
+
name: 'Successful booking',
|
|
2596
|
+
steps: [
|
|
2597
|
+
{
|
|
2598
|
+
keyword: 'Given',
|
|
2599
|
+
text: 'AppointmentDrafted',
|
|
2600
|
+
docString: { appointmentId: 'apt1', status: 'draft' },
|
|
2601
|
+
},
|
|
2602
|
+
{
|
|
2603
|
+
keyword: 'When',
|
|
2604
|
+
text: 'BookAppointment',
|
|
2605
|
+
docString: { appointmentId: 'apt1', barberId: 'barb1' },
|
|
2606
|
+
},
|
|
2607
|
+
{
|
|
2608
|
+
keyword: 'Then',
|
|
2609
|
+
text: 'AppointmentBooked',
|
|
2610
|
+
docString: { appointmentId: 'apt1', barberId: 'barb1', status: 'booked' },
|
|
2611
|
+
},
|
|
2612
|
+
],
|
|
2613
|
+
},
|
|
2614
|
+
{
|
|
2615
|
+
name: 'Draft already submitted',
|
|
2616
|
+
steps: [
|
|
2617
|
+
{
|
|
2618
|
+
keyword: 'Given',
|
|
2619
|
+
text: 'AppointmentDrafted',
|
|
2620
|
+
docString: { appointmentId: 'apt1', status: 'submitted' },
|
|
2621
|
+
},
|
|
2622
|
+
{
|
|
2623
|
+
keyword: 'When',
|
|
2624
|
+
text: 'BookAppointment',
|
|
2625
|
+
docString: { appointmentId: 'apt1', barberId: 'barb1' },
|
|
2626
|
+
},
|
|
2627
|
+
{
|
|
2628
|
+
keyword: 'Then',
|
|
2629
|
+
text: 'AppointmentBooked',
|
|
2630
|
+
docString: { appointmentId: 'apt1', barberId: 'barb1', status: 'booked' },
|
|
2631
|
+
},
|
|
2632
|
+
],
|
|
2633
|
+
},
|
|
2634
|
+
],
|
|
2635
|
+
},
|
|
2636
|
+
],
|
|
2637
|
+
},
|
|
2638
|
+
],
|
|
2639
|
+
},
|
|
2640
|
+
},
|
|
2641
|
+
],
|
|
2642
|
+
},
|
|
2643
|
+
],
|
|
2644
|
+
messages: [
|
|
2645
|
+
{
|
|
2646
|
+
type: 'command',
|
|
2647
|
+
name: 'BookAppointment',
|
|
2648
|
+
fields: [
|
|
2649
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
2650
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
2651
|
+
],
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
type: 'event',
|
|
2655
|
+
name: 'AppointmentBooked',
|
|
2656
|
+
source: 'internal',
|
|
2657
|
+
fields: [
|
|
2658
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
2659
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
2660
|
+
{ name: 'status', type: 'string', required: true },
|
|
2661
|
+
],
|
|
2662
|
+
},
|
|
2663
|
+
{
|
|
2664
|
+
type: 'event',
|
|
2665
|
+
name: 'AppointmentDrafted',
|
|
2666
|
+
source: 'internal',
|
|
2667
|
+
fields: [
|
|
2668
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
2669
|
+
{ name: 'status', type: 'string', required: true },
|
|
2670
|
+
],
|
|
2671
|
+
},
|
|
2672
|
+
],
|
|
2673
|
+
};
|
|
2674
|
+
|
|
2675
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
2676
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
2677
|
+
|
|
2678
|
+
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
2679
|
+
"import { describe, expect, it } from 'vitest';
|
|
2680
|
+
import { DeciderSpecification } from '@event-driven-io/emmett';
|
|
2681
|
+
import { decide } from './decide';
|
|
2682
|
+
import { evolve } from './evolve';
|
|
2683
|
+
import { initialState, State } from './state';
|
|
2684
|
+
import type { AppointmentBooked, AppointmentDrafted } from './events';
|
|
2685
|
+
import type { BookAppointment } from './commands';
|
|
2686
|
+
|
|
2687
|
+
describe('Appointment can be booked', () => {
|
|
2688
|
+
type Events = AppointmentBooked | AppointmentDrafted;
|
|
2689
|
+
|
|
2690
|
+
const given = DeciderSpecification.for<BookAppointment, Events, State>({
|
|
2691
|
+
decide,
|
|
2692
|
+
evolve,
|
|
2693
|
+
initialState,
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
|
|
2697
|
+
|
|
2698
|
+
it('Successful booking', () => {
|
|
2699
|
+
given([
|
|
2700
|
+
{
|
|
2701
|
+
type: 'AppointmentDrafted',
|
|
2702
|
+
data: {
|
|
2703
|
+
appointmentId: 'apt1',
|
|
2704
|
+
status: 'draft',
|
|
2705
|
+
},
|
|
2706
|
+
},
|
|
2707
|
+
])
|
|
2708
|
+
.when({
|
|
2709
|
+
type: 'BookAppointment',
|
|
2710
|
+
data: {
|
|
2711
|
+
appointmentId: 'apt1',
|
|
2712
|
+
barberId: 'barb1',
|
|
2713
|
+
},
|
|
2714
|
+
metadata: { now: new Date() },
|
|
2715
|
+
})
|
|
2716
|
+
|
|
2717
|
+
.then(
|
|
2718
|
+
expectEvents({
|
|
2719
|
+
type: 'AppointmentBooked',
|
|
2720
|
+
data: {
|
|
2721
|
+
appointmentId: 'apt1',
|
|
2722
|
+
barberId: 'barb1',
|
|
2723
|
+
},
|
|
2724
|
+
}),
|
|
2725
|
+
);
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2728
|
+
it('Draft already submitted', () => {
|
|
2729
|
+
given([
|
|
2730
|
+
{
|
|
2731
|
+
type: 'AppointmentDrafted',
|
|
2732
|
+
data: {
|
|
2733
|
+
appointmentId: 'apt1',
|
|
2734
|
+
status: 'submitted',
|
|
2735
|
+
},
|
|
2736
|
+
},
|
|
2737
|
+
])
|
|
2738
|
+
.when({
|
|
2739
|
+
type: 'BookAppointment',
|
|
2740
|
+
data: {
|
|
2741
|
+
appointmentId: 'apt1',
|
|
2742
|
+
barberId: 'barb1',
|
|
2743
|
+
},
|
|
2744
|
+
metadata: { now: new Date() },
|
|
2745
|
+
})
|
|
2746
|
+
|
|
2747
|
+
.then(
|
|
2748
|
+
expectEvents({
|
|
2749
|
+
type: 'AppointmentBooked',
|
|
2750
|
+
data: {
|
|
2751
|
+
appointmentId: 'apt1',
|
|
2752
|
+
barberId: 'barb1',
|
|
2753
|
+
},
|
|
2754
|
+
}),
|
|
2755
|
+
);
|
|
2756
|
+
});
|
|
2757
|
+
});
|
|
2758
|
+
|
|
2759
|
+
describe('field completeness', () => {
|
|
2760
|
+
it('should include all required fields in AppointmentBooked', () => {
|
|
2761
|
+
const state = [
|
|
2762
|
+
{
|
|
2763
|
+
type: 'AppointmentDrafted' as const,
|
|
2764
|
+
data: {
|
|
2765
|
+
appointmentId: 'apt1',
|
|
2766
|
+
status: 'draft',
|
|
2767
|
+
},
|
|
2768
|
+
},
|
|
2769
|
+
].reduce((s, e) => evolve(s, e), initialState());
|
|
2770
|
+
const result = decide(
|
|
2771
|
+
{
|
|
2772
|
+
type: 'BookAppointment',
|
|
2773
|
+
data: {
|
|
2774
|
+
appointmentId: 'apt1',
|
|
2775
|
+
barberId: 'barb1',
|
|
2776
|
+
},
|
|
2777
|
+
metadata: { now: new Date() },
|
|
2778
|
+
},
|
|
2779
|
+
state,
|
|
2780
|
+
);
|
|
2781
|
+
const evtArray = Array.isArray(result) ? result : [result];
|
|
2782
|
+
|
|
2783
|
+
expect(evtArray[0]).toMatchObject({
|
|
2784
|
+
type: 'AppointmentBooked',
|
|
2785
|
+
data: expect.objectContaining({
|
|
2786
|
+
appointmentId: expect.any(String),
|
|
2787
|
+
barberId: expect.any(String),
|
|
2788
|
+
status: expect.any(String),
|
|
2789
|
+
}),
|
|
2790
|
+
});
|
|
2791
|
+
});
|
|
2792
|
+
});
|
|
2793
|
+
"
|
|
2794
|
+
`);
|
|
2795
|
+
});
|
|
2119
2796
|
});
|