@auto-engineer/server-generator-apollo-emmett 0.10.3 → 0.10.5

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/src/codegen/extract/events.d.ts +2 -2
  3. package/dist/src/codegen/extract/events.d.ts.map +1 -1
  4. package/dist/src/codegen/extract/events.js +16 -6
  5. package/dist/src/codegen/extract/events.js.map +1 -1
  6. package/dist/src/codegen/extract/gwt.js +7 -22
  7. package/dist/src/codegen/extract/gwt.js.map +1 -1
  8. package/dist/src/codegen/extract/imports.d.ts +29 -0
  9. package/dist/src/codegen/extract/imports.d.ts.map +1 -0
  10. package/dist/src/codegen/extract/imports.js +55 -0
  11. package/dist/src/codegen/extract/imports.js.map +1 -0
  12. package/dist/src/codegen/extract/index.d.ts +1 -0
  13. package/dist/src/codegen/extract/index.d.ts.map +1 -1
  14. package/dist/src/codegen/extract/index.js +1 -0
  15. package/dist/src/codegen/extract/index.js.map +1 -1
  16. package/dist/src/codegen/extract/messages.d.ts.map +1 -1
  17. package/dist/src/codegen/extract/messages.js +33 -7
  18. package/dist/src/codegen/extract/messages.js.map +1 -1
  19. package/dist/src/codegen/extract/query.d.ts +3 -1
  20. package/dist/src/codegen/extract/query.d.ts.map +1 -1
  21. package/dist/src/codegen/extract/query.js +12 -12
  22. package/dist/src/codegen/extract/query.js.map +1 -1
  23. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  24. package/dist/src/codegen/scaffoldFromSchema.js +9 -1
  25. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  26. package/dist/src/codegen/templates/command/decide.specs.specs.ts +235 -8
  27. package/dist/src/codegen/templates/command/decide.specs.ts +8 -8
  28. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
  29. package/dist/src/codegen/templates/command/decide.ts.ejs +2 -2
  30. package/dist/src/codegen/templates/command/events.ts.ejs +2 -2
  31. package/dist/src/codegen/templates/command/evolve.ts.ejs +3 -3
  32. package/dist/src/codegen/templates/command/handle.specs.ts +6 -6
  33. package/dist/src/codegen/templates/command/handle.ts.ejs +3 -3
  34. package/dist/src/codegen/templates/query/projection.specs.specs.ts +623 -0
  35. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +174 -52
  36. package/dist/src/codegen/templates/query/projection.ts.ejs +30 -29
  37. package/dist/src/codegen/templates/react/react.specs.specs.ts +7 -4
  38. package/dist/src/codegen/templates/react/react.specs.ts.ejs +118 -67
  39. package/dist/src/codegen/types.d.ts +2 -0
  40. package/dist/src/codegen/types.d.ts.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +4 -4
  43. package/src/codegen/extract/events.ts +20 -3
  44. package/src/codegen/extract/gwt.ts +10 -26
  45. package/src/codegen/extract/imports.ts +71 -0
  46. package/src/codegen/extract/index.ts +1 -0
  47. package/src/codegen/extract/messages.ts +34 -7
  48. package/src/codegen/extract/query.ts +17 -19
  49. package/src/codegen/scaffoldFromSchema.ts +13 -0
  50. package/src/codegen/templates/command/decide.specs.specs.ts +235 -8
  51. package/src/codegen/templates/command/decide.specs.ts +8 -8
  52. package/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
  53. package/src/codegen/templates/command/decide.ts.ejs +2 -2
  54. package/src/codegen/templates/command/events.ts.ejs +2 -2
  55. package/src/codegen/templates/command/evolve.ts.ejs +3 -3
  56. package/src/codegen/templates/command/handle.specs.ts +6 -6
  57. package/src/codegen/templates/command/handle.ts.ejs +3 -3
  58. package/src/codegen/templates/query/projection.specs.specs.ts +623 -0
  59. package/src/codegen/templates/query/projection.specs.ts.ejs +174 -52
  60. package/src/codegen/templates/query/projection.ts.ejs +30 -29
  61. package/src/codegen/templates/react/react.specs.specs.ts +7 -4
  62. package/src/codegen/templates/react/react.specs.ts.ejs +118 -67
  63. package/src/codegen/types.ts +2 -0
  64. package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.d.ts +0 -2
  65. package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.d.ts.map +0 -1
  66. package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.js +0 -168
  67. package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.js.map +0 -1
  68. package/dist/src/codegen/templates/query/projection.specs.specs..ts +0 -296
  69. package/src/codegen/scaffoldFromSchema.query-slice-register.specs.ts +0 -179
  70. package/src/codegen/templates/query/projection.specs.specs..ts +0 -296
@@ -0,0 +1,623 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
3
+ import { Model as SpecsSchema } from '@auto-engineer/flow';
4
+
5
+ describe('projection.specs.ts.ejs', () => {
6
+ it('should generate a valid test spec for a query slice projection', async () => {
7
+ const spec: SpecsSchema = {
8
+ variant: 'specs',
9
+ flows: [
10
+ {
11
+ name: 'listing-flow',
12
+ slices: [
13
+ {
14
+ type: 'command',
15
+ name: 'CreateListing',
16
+ stream: 'listing-${propertyId}',
17
+ client: { description: '' },
18
+ server: {
19
+ description: '',
20
+ specs: {
21
+ name: 'CreateListing command',
22
+ rules: [
23
+ {
24
+ description: 'Should handle listing operations',
25
+ examples: [
26
+ {
27
+ description: 'User creates listing successfully',
28
+ when: {
29
+ commandRef: 'CreateListing',
30
+ exampleData: {
31
+ propertyId: 'listing_123',
32
+ title: 'Sea View Flat',
33
+ pricePerNight: 120,
34
+ location: 'Brighton',
35
+ maxGuests: 4,
36
+ },
37
+ },
38
+ then: [
39
+ {
40
+ eventRef: 'ListingCreated',
41
+ exampleData: {
42
+ propertyId: 'listing_123',
43
+ title: 'Sea View Flat',
44
+ pricePerNight: 120,
45
+ location: 'Brighton',
46
+ maxGuests: 4,
47
+ },
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ description: 'User removes listing successfully',
53
+ when: {
54
+ commandRef: 'RemoveListing',
55
+ exampleData: {
56
+ propertyId: 'listing_123',
57
+ },
58
+ },
59
+ then: [
60
+ {
61
+ eventRef: 'ListingRemoved',
62
+ exampleData: {},
63
+ },
64
+ ],
65
+ },
66
+ ],
67
+ },
68
+ ],
69
+ },
70
+ },
71
+ },
72
+ {
73
+ type: 'query',
74
+ name: 'search-listings',
75
+ stream: 'listings',
76
+ client: { description: '' },
77
+ server: {
78
+ description: '',
79
+ data: [
80
+ {
81
+ origin: {
82
+ type: 'projection',
83
+ idField: 'propertyId',
84
+ name: 'AvailablePropertiesProjection',
85
+ },
86
+ target: {
87
+ type: 'State',
88
+ name: 'AvailableListings',
89
+ },
90
+ },
91
+ ],
92
+ specs: {
93
+ name: 'Search listings query',
94
+ rules: [
95
+ {
96
+ description: 'Should project listings correctly',
97
+ examples: [
98
+ {
99
+ description: 'Listing created shows in search results',
100
+ when: [
101
+ {
102
+ eventRef: 'ListingCreated',
103
+ exampleData: {
104
+ propertyId: 'listing_123',
105
+ title: 'Sea View Flat',
106
+ pricePerNight: 120,
107
+ location: 'Brighton',
108
+ maxGuests: 4,
109
+ },
110
+ },
111
+ ],
112
+ then: [
113
+ {
114
+ stateRef: 'AvailableListings',
115
+ exampleData: {
116
+ propertyId: 'listing_123',
117
+ title: 'Sea View Flat',
118
+ pricePerNight: 120,
119
+ location: 'Brighton',
120
+ maxGuests: 4,
121
+ },
122
+ },
123
+ ],
124
+ },
125
+ ],
126
+ },
127
+ ],
128
+ },
129
+ },
130
+ },
131
+ ],
132
+ },
133
+ ],
134
+ messages: [
135
+ {
136
+ type: 'command',
137
+ name: 'CreateListing',
138
+ fields: [
139
+ { name: 'propertyId', type: 'string', required: true },
140
+ { name: 'title', type: 'string', required: true },
141
+ { name: 'pricePerNight', type: 'number', required: true },
142
+ { name: 'location', type: 'string', required: true },
143
+ { name: 'maxGuests', type: 'number', required: true },
144
+ ],
145
+ },
146
+ {
147
+ type: 'command',
148
+ name: 'RemoveListing',
149
+ fields: [{ name: 'propertyId', type: 'string', required: true }],
150
+ },
151
+ {
152
+ type: 'event',
153
+ name: 'ListingCreated',
154
+ source: 'internal',
155
+ fields: [
156
+ { name: 'propertyId', type: 'string', required: true },
157
+ { name: 'title', type: 'string', required: true },
158
+ { name: 'pricePerNight', type: 'number', required: true },
159
+ { name: 'location', type: 'string', required: true },
160
+ { name: 'maxGuests', type: 'number', required: true },
161
+ ],
162
+ },
163
+ {
164
+ type: 'event',
165
+ name: 'ListingRemoved',
166
+ source: 'internal',
167
+ fields: [{ name: 'propertyId', type: 'string', required: true }],
168
+ },
169
+ {
170
+ type: 'state',
171
+ name: 'AvailableListings',
172
+ fields: [
173
+ { name: 'propertyId', type: 'string', required: true },
174
+ { name: 'title', type: 'string', required: true },
175
+ { name: 'pricePerNight', type: 'number', required: true },
176
+ { name: 'location', type: 'string', required: true },
177
+ { name: 'maxGuests', type: 'number', required: true },
178
+ ],
179
+ },
180
+ ],
181
+ } as SpecsSchema;
182
+
183
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
184
+ const specFile = plans.find((p) => p.outputPath.endsWith('projection.specs.ts'));
185
+
186
+ expect(specFile?.contents).toMatchInlineSnapshot(`
187
+ "import { describe, it, beforeEach, expect } from 'vitest';
188
+ import { InMemoryProjectionSpec } from '@event-driven-io/emmett';
189
+ import { projection } from './projection';
190
+ import type { ListingCreated } from '../create-listing/events';
191
+ import { AvailableListings } from './state';
192
+
193
+ type ProjectionEvent = ListingCreated;
194
+
195
+ describe('Should project listings correctly', () => {
196
+ let given: InMemoryProjectionSpec<ProjectionEvent>;
197
+
198
+ beforeEach(() => {
199
+ given = InMemoryProjectionSpec.for({ projection });
200
+ });
201
+
202
+ it('Listing created shows in search results', () =>
203
+ given([])
204
+ .when([
205
+ {
206
+ type: 'ListingCreated',
207
+ data: {
208
+ propertyId: 'listing_123',
209
+ title: 'Sea View Flat',
210
+ pricePerNight: 120,
211
+ location: 'Brighton',
212
+ maxGuests: 4,
213
+ },
214
+ metadata: {
215
+ streamName: 'listings',
216
+ streamPosition: 1n,
217
+ globalPosition: 1n,
218
+ },
219
+ },
220
+ ])
221
+ .then(async (state) => {
222
+ const document = await state.database
223
+ .collection<AvailableListings>('AvailablePropertiesProjection')
224
+ .findOne((doc) => doc.propertyId === 'listing_123');
225
+
226
+ const expected: AvailableListings = {
227
+ propertyId: 'listing_123',
228
+ title: 'Sea View Flat',
229
+ pricePerNight: 120,
230
+ location: 'Brighton',
231
+ maxGuests: 4,
232
+ };
233
+
234
+ expect(document).toMatchObject(expected);
235
+ }));
236
+ });
237
+ "
238
+ `);
239
+ });
240
+
241
+ it('should generate a valid test spec for a model with given/when/then pattern', async () => {
242
+ const questionnaireSpec: SpecsSchema = {
243
+ variant: 'specs',
244
+ flows: [
245
+ {
246
+ name: 'Questionnaires',
247
+ slices: [
248
+ {
249
+ name: 'views the questionnaire',
250
+ type: 'query',
251
+ client: { description: '' },
252
+ server: {
253
+ description: '',
254
+ data: [
255
+ {
256
+ target: {
257
+ type: 'State',
258
+ name: 'QuestionnaireProgress',
259
+ },
260
+ origin: {
261
+ type: 'projection',
262
+ name: 'Questionnaires',
263
+ idField: 'questionnaire-participantId',
264
+ },
265
+ },
266
+ ],
267
+ specs: {
268
+ name: '',
269
+ rules: [
270
+ {
271
+ description: 'questionnaires show current progress',
272
+ examples: [
273
+ {
274
+ description: 'a question has already been answered',
275
+ given: [
276
+ {
277
+ eventRef: 'QuestionnaireLinkSent',
278
+ exampleData: {
279
+ questionnaireId: 'q-001',
280
+ participantId: 'participant-abc',
281
+ link: 'https://app.example.com/q/q-001?participant=participant-abc',
282
+ sentAt: '2030-01-01T09:00:00.000Z',
283
+ },
284
+ },
285
+ ],
286
+ when: [
287
+ {
288
+ eventRef: 'QuestionAnswered',
289
+ exampleData: {
290
+ questionnaireId: 'q-001',
291
+ participantId: 'participant-abc',
292
+ questionId: 'q1',
293
+ answer: 'Yes',
294
+ savedAt: '2030-01-01T09:05:00.000Z',
295
+ },
296
+ },
297
+ ],
298
+ then: [
299
+ {
300
+ stateRef: 'QuestionnaireProgress',
301
+ exampleData: {
302
+ questionnaireId: 'q-001',
303
+ participantId: 'participant-abc',
304
+ status: 'in_progress',
305
+ currentQuestionId: 'q2',
306
+ remainingQuestions: ['q2', 'q3'],
307
+ answers: [
308
+ {
309
+ questionId: 'q1',
310
+ value: 'Yes',
311
+ },
312
+ ],
313
+ },
314
+ },
315
+ ],
316
+ },
317
+ ],
318
+ },
319
+ ],
320
+ },
321
+ },
322
+ },
323
+ ],
324
+ },
325
+ ],
326
+ messages: [
327
+ {
328
+ type: 'event',
329
+ name: 'QuestionnaireLinkSent',
330
+ fields: [
331
+ { name: 'questionnaireId', type: 'string', required: true },
332
+ { name: 'participantId', type: 'string', required: true },
333
+ { name: 'link', type: 'string', required: true },
334
+ { name: 'sentAt', type: 'Date', required: true },
335
+ ],
336
+ source: 'internal',
337
+ },
338
+ {
339
+ type: 'event',
340
+ name: 'QuestionAnswered',
341
+ fields: [
342
+ { name: 'questionnaireId', type: 'string', required: true },
343
+ { name: 'participantId', type: 'string', required: true },
344
+ { name: 'questionId', type: 'string', required: true },
345
+ { name: 'answer', type: 'unknown', required: true },
346
+ { name: 'savedAt', type: 'Date', required: true },
347
+ ],
348
+ source: 'internal',
349
+ },
350
+ {
351
+ type: 'state',
352
+ name: 'QuestionnaireProgress',
353
+ fields: [
354
+ { name: 'questionnaireId', type: 'string', required: true },
355
+ { name: 'participantId', type: 'string', required: true },
356
+ { name: 'status', type: '"in_progress" | "ready_to_submit" | "submitted"', required: true },
357
+ { name: 'currentQuestionId', type: 'string | null', required: true },
358
+ { name: 'remainingQuestions', type: 'Array<string>', required: true },
359
+ { name: 'answers', type: 'Array<{ questionId: string; value: unknown }>', required: true },
360
+ ],
361
+ },
362
+ ],
363
+ } as SpecsSchema;
364
+
365
+ const plans = await generateScaffoldFilePlans(
366
+ questionnaireSpec.flows,
367
+ questionnaireSpec.messages,
368
+ undefined,
369
+ 'src/domain/flows',
370
+ );
371
+ const specFile = plans.find((p) => p.outputPath.endsWith('projection.specs.ts'));
372
+
373
+ expect(specFile?.contents).toContain('a question has already been answered');
374
+ expect(specFile?.contents).toContain('QuestionnaireLinkSent');
375
+ expect(specFile?.contents).toContain('QuestionAnswered');
376
+ expect(specFile?.contents).toContain('given([');
377
+ expect(specFile?.contents).toContain('.when([');
378
+ });
379
+
380
+ it('should include all events from both given and when clauses in projection imports and types', async () => {
381
+ const spec: SpecsSchema = {
382
+ variant: 'specs',
383
+ flows: [
384
+ {
385
+ name: 'questionnaires',
386
+ slices: [
387
+ {
388
+ type: 'command',
389
+ name: 'sends-the-questionnaire-link',
390
+ client: { description: '' },
391
+ server: {
392
+ description: '',
393
+ specs: {
394
+ name: 'Sends questionnaire link',
395
+ rules: [
396
+ {
397
+ description: 'sends questionnaire link to participant',
398
+ examples: [
399
+ {
400
+ description: 'sends link successfully',
401
+ when: {
402
+ commandRef: 'SendQuestionnaireLink',
403
+ exampleData: {
404
+ questionnaireId: 'q-001',
405
+ participantId: 'participant-abc',
406
+ },
407
+ },
408
+ then: [
409
+ {
410
+ eventRef: 'QuestionnaireLinkSent', // This event is produced here
411
+ exampleData: {
412
+ questionnaireId: 'q-001',
413
+ participantId: 'participant-abc',
414
+ link: 'https://app.example.com/q/q-001?participant=participant-abc',
415
+ sentAt: new Date('2030-01-01T09:00:00Z'),
416
+ },
417
+ },
418
+ ],
419
+ },
420
+ ],
421
+ },
422
+ ],
423
+ },
424
+ },
425
+ },
426
+ {
427
+ type: 'command',
428
+ name: 'submits-a-questionnaire-answer',
429
+ client: { description: '' },
430
+ server: {
431
+ description: '',
432
+ specs: {
433
+ name: 'Submits questionnaire answer',
434
+ rules: [
435
+ {
436
+ description: 'submits answer successfully',
437
+ examples: [
438
+ {
439
+ description: 'answers question',
440
+ when: {
441
+ commandRef: 'AnswerQuestion',
442
+ exampleData: {
443
+ questionnaireId: 'q-001',
444
+ participantId: 'participant-abc',
445
+ questionId: 'q1',
446
+ answer: 'Yes',
447
+ },
448
+ },
449
+ then: [
450
+ {
451
+ eventRef: 'QuestionAnswered', // This event is produced here
452
+ exampleData: {
453
+ questionnaireId: 'q-001',
454
+ participantId: 'participant-abc',
455
+ questionId: 'q1',
456
+ answer: 'Yes',
457
+ savedAt: new Date('2030-01-01T09:05:00Z'),
458
+ },
459
+ },
460
+ ],
461
+ },
462
+ ],
463
+ },
464
+ ],
465
+ },
466
+ },
467
+ },
468
+ {
469
+ type: 'query',
470
+ name: 'views-the-questionnaire',
471
+ client: { description: '' },
472
+ server: {
473
+ description: '',
474
+ specs: {
475
+ name: 'Views the questionnaire',
476
+ rules: [
477
+ {
478
+ description: 'questionnaires show current progress',
479
+ examples: [
480
+ {
481
+ description: 'a question has already been answered',
482
+ given: [
483
+ {
484
+ eventRef: 'QuestionnaireLinkSent',
485
+ exampleData: {
486
+ questionnaireId: 'q-001',
487
+ participantId: 'participant-abc',
488
+ link: 'https://app.example.com/q/q-001?participant=participant-abc',
489
+ sentAt: new Date('2030-01-01T09:00:00Z'),
490
+ },
491
+ },
492
+ ],
493
+ when: [
494
+ {
495
+ eventRef: 'QuestionAnswered', // This should be included in imports!
496
+ exampleData: {
497
+ questionnaireId: 'q-001',
498
+ participantId: 'participant-abc',
499
+ questionId: 'q1',
500
+ answer: 'Yes',
501
+ savedAt: new Date('2030-01-01T09:05:00Z'),
502
+ },
503
+ },
504
+ ],
505
+ then: [
506
+ {
507
+ stateRef: 'QuestionnaireProgress',
508
+ exampleData: {
509
+ questionnaireId: 'q-001',
510
+ participantId: 'participant-abc',
511
+ status: 'in_progress',
512
+ currentQuestionId: 'q2',
513
+ remainingQuestions: ['q2'],
514
+ answers: [{ questionId: 'q1', value: 'Yes' }],
515
+ },
516
+ },
517
+ ],
518
+ },
519
+ ],
520
+ },
521
+ ],
522
+ },
523
+ data: [
524
+ {
525
+ origin: { name: 'Questionnaires', idField: 'questionnaireId-participantId' },
526
+ target: { name: 'QuestionnaireProgress' },
527
+ },
528
+ ],
529
+ },
530
+ },
531
+ ],
532
+ },
533
+ ],
534
+ messages: [
535
+ {
536
+ type: 'event',
537
+ name: 'QuestionnaireLinkSent',
538
+ fields: [
539
+ { name: 'questionnaireId', type: 'string', required: true },
540
+ { name: 'participantId', type: 'string', required: true },
541
+ { name: 'link', type: 'string', required: true },
542
+ { name: 'sentAt', type: 'Date', required: true },
543
+ ],
544
+ },
545
+ {
546
+ type: 'event',
547
+ name: 'QuestionAnswered',
548
+ fields: [
549
+ { name: 'questionnaireId', type: 'string', required: true },
550
+ { name: 'participantId', type: 'string', required: true },
551
+ { name: 'questionId', type: 'string', required: true },
552
+ { name: 'answer', type: 'unknown', required: true },
553
+ { name: 'savedAt', type: 'Date', required: true },
554
+ ],
555
+ },
556
+ {
557
+ type: 'state',
558
+ name: 'QuestionnaireProgress',
559
+ fields: [
560
+ { name: 'questionnaireId', type: 'string', required: true },
561
+ { name: 'participantId', type: 'string', required: true },
562
+ { name: 'status', type: '"in_progress" | "ready_to_submit" | "submitted"', required: true },
563
+ { name: 'currentQuestionId', type: 'string | null', required: true },
564
+ { name: 'remainingQuestions', type: 'Array<string>', required: true },
565
+ { name: 'answers', type: 'Array<{ questionId: string; value: unknown }>', required: true },
566
+ ],
567
+ },
568
+ ],
569
+ } as SpecsSchema;
570
+
571
+ const plans = await generateScaffoldFilePlans(
572
+ spec.flows,
573
+ [
574
+ {
575
+ type: 'command',
576
+ name: 'SendQuestionnaireLink',
577
+ fields: [
578
+ { name: 'questionnaireId', type: 'string', required: true },
579
+ { name: 'participantId', type: 'string', required: true },
580
+ ],
581
+ },
582
+ {
583
+ type: 'command',
584
+ name: 'AnswerQuestion',
585
+ fields: [
586
+ { name: 'questionnaireId', type: 'string', required: true },
587
+ { name: 'participantId', type: 'string', required: true },
588
+ { name: 'questionId', type: 'string', required: true },
589
+ { name: 'answer', type: 'unknown', required: true },
590
+ ],
591
+ },
592
+ ...spec.messages,
593
+ ],
594
+ undefined,
595
+ 'src/domain/flows',
596
+ );
597
+
598
+ // Check projection.specs.ts file
599
+ const specsFile = plans.find((p) => p.outputPath.endsWith('projection.specs.ts'));
600
+ expect(specsFile?.contents).toBeDefined();
601
+
602
+ // Must import BOTH event types
603
+ expect(specsFile?.contents).toContain('import type { QuestionnaireLinkSent }');
604
+ expect(specsFile?.contents).toContain('import type { QuestionAnswered }');
605
+
606
+ // Union type must include BOTH events (order may vary due to sorting)
607
+ expect(specsFile?.contents).toContain('type ProjectionEvent = QuestionAnswered | QuestionnaireLinkSent');
608
+
609
+ // Check projection.ts file
610
+ const projectionFile = plans.find((p) => p.outputPath.endsWith('projection.ts'));
611
+ expect(projectionFile?.contents).toBeDefined();
612
+
613
+ // Must import BOTH event types
614
+ expect(projectionFile?.contents).toContain('import type { QuestionnaireLinkSent }');
615
+ expect(projectionFile?.contents).toContain('import type { QuestionAnswered }');
616
+
617
+ // AllEvents type must include BOTH events (order may vary due to sorting)
618
+ expect(projectionFile?.contents).toContain('type AllEvents = QuestionAnswered | QuestionnaireLinkSent');
619
+
620
+ // canHandle must include BOTH events
621
+ expect(projectionFile?.contents).toContain("canHandle: ['QuestionnaireLinkSent', 'QuestionAnswered']");
622
+ });
623
+ });