@auto-engineer/server-generator-apollo-emmett 1.88.0 → 1.90.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.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +5 -5
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +90 -0
  5. package/dist/src/codegen/extract/slice-normalizer.d.ts.map +1 -1
  6. package/dist/src/codegen/extract/slice-normalizer.js +14 -0
  7. package/dist/src/codegen/extract/slice-normalizer.js.map +1 -1
  8. package/dist/src/codegen/extract/type-helpers.d.ts +10 -0
  9. package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
  10. package/dist/src/codegen/extract/type-helpers.js +17 -0
  11. package/dist/src/codegen/extract/type-helpers.js.map +1 -1
  12. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  13. package/dist/src/codegen/scaffoldFromSchema.js +6 -4
  14. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  15. package/dist/src/codegen/templates/command/decide.specs.specs.ts +293 -34
  16. package/dist/src/codegen/templates/command/decide.specs.ts +34 -14
  17. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
  18. package/dist/src/codegen/templates/command/decide.ts.ejs +32 -4
  19. package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
  20. package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
  21. package/dist/src/codegen/templates/query/projection.specs.specs.ts +124 -0
  22. package/dist/src/codegen/templates/query/projection.specs.ts +20 -0
  23. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
  24. package/dist/src/codegen/templates/query/projection.ts.ejs +5 -0
  25. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
  26. package/dist/src/codegen/templates/react/react.specs.specs.ts +115 -0
  27. package/dist/src/codegen/templates/react/react.specs.ts +9 -2
  28. package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -3
  29. package/dist/src/codegen/templates/react/react.ts.ejs +22 -9
  30. package/dist/src/codegen/templates/react/react.ts.specs.ts +253 -0
  31. package/dist/src/codegen/templates/react/register.specs.ts +27 -23
  32. package/dist/src/codegen/templates/react/register.ts.ejs +5 -1
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/ketchup-plan.md +12 -3
  35. package/package.json +4 -4
  36. package/src/codegen/extract/slice-normalizer.specs.ts +83 -0
  37. package/src/codegen/extract/slice-normalizer.ts +15 -0
  38. package/src/codegen/extract/type-helpers.specs.ts +77 -1
  39. package/src/codegen/extract/type-helpers.ts +23 -0
  40. package/src/codegen/formatTsValueSimple.specs.ts +8 -0
  41. package/src/codegen/scaffoldFromSchema.ts +7 -3
  42. package/src/codegen/templates/command/decide.specs.specs.ts +293 -34
  43. package/src/codegen/templates/command/decide.specs.ts +34 -14
  44. package/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
  45. package/src/codegen/templates/command/decide.ts.ejs +32 -4
  46. package/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
  47. package/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
  48. package/src/codegen/templates/query/projection.specs.specs.ts +124 -0
  49. package/src/codegen/templates/query/projection.specs.ts +20 -0
  50. package/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
  51. package/src/codegen/templates/query/projection.ts.ejs +5 -0
  52. package/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
  53. package/src/codegen/templates/react/react.specs.specs.ts +115 -0
  54. package/src/codegen/templates/react/react.specs.ts +9 -2
  55. package/src/codegen/templates/react/react.specs.ts.ejs +1 -3
  56. package/src/codegen/templates/react/react.ts.ejs +22 -9
  57. package/src/codegen/templates/react/react.ts.specs.ts +253 -0
  58. package/src/codegen/templates/react/register.specs.ts +27 -23
  59. package/src/codegen/templates/react/register.ts.ejs +5 -1
  60. package/dist/src/codegen/extract/graphql.d.ts +0 -14
  61. package/dist/src/codegen/extract/graphql.d.ts.map +0 -1
  62. package/dist/src/codegen/extract/graphql.js +0 -81
  63. package/dist/src/codegen/extract/graphql.js.map +0 -1
  64. package/src/codegen/extract/graphql.ts +0 -103
@@ -117,6 +117,8 @@ describe('spec.ts.ejs', () => {
117
117
  initialState,
118
118
  });
119
119
 
120
+ const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
121
+
120
122
  it('User creates listing with valid data', () => {
121
123
  given([])
122
124
  .when({
@@ -136,8 +138,8 @@ describe('spec.ts.ejs', () => {
136
138
  metadata: { now: new Date() },
137
139
  })
138
140
 
139
- .then([
140
- {
141
+ .then(
142
+ expectEvents({
141
143
  type: 'ListingCreated',
142
144
  data: {
143
145
  propertyId: 'listing_123',
@@ -145,8 +147,8 @@ describe('spec.ts.ejs', () => {
145
147
  rating: 4.8,
146
148
  metadata: { foo: 'bar' },
147
149
  },
148
- },
149
- ]);
150
+ }),
151
+ );
150
152
  });
151
153
  });
152
154
  "
@@ -263,6 +265,8 @@ describe('spec.ts.ejs', () => {
263
265
  initialState,
264
266
  });
265
267
 
268
+ const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
269
+
266
270
  it('Existing listing can be removed', () => {
267
271
  given([
268
272
  {
@@ -283,15 +287,14 @@ describe('spec.ts.ejs', () => {
283
287
  metadata: { now: new Date() },
284
288
  })
285
289
 
286
- .then([
287
- {
290
+ .then(
291
+ expectEvents({
288
292
  type: 'ListingRemoved',
289
293
  data: {
290
294
  propertyId: 'listing_123',
291
- removedAt: new Date('2024-01-16T10:00:00Z'),
292
295
  },
293
- },
294
- ]);
296
+ }),
297
+ );
295
298
  });
296
299
  });
297
300
  "
@@ -457,6 +460,8 @@ describe('spec.ts.ejs', () => {
457
460
  initialState,
458
461
  });
459
462
 
463
+ const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
464
+
460
465
  it('no questions have been answered yet', () => {
461
466
  given([])
462
467
  .when({
@@ -470,18 +475,17 @@ describe('spec.ts.ejs', () => {
470
475
  metadata: { now: new Date() },
471
476
  })
472
477
 
473
- .then([
474
- {
478
+ .then(
479
+ expectEvents({
475
480
  type: 'QuestionAnswered',
476
481
  data: {
477
482
  questionnaireId: 'q-001',
478
483
  participantId: 'participant-abc',
479
484
  questionId: 'q1',
480
485
  answer: 'Yes',
481
- savedAt: new Date('2030-01-01T09:05:00.000Z'),
482
486
  },
483
- },
484
- ]);
487
+ }),
488
+ );
485
489
  });
486
490
 
487
491
  it('all questions have already been answered and submitted', () => {
@@ -506,17 +510,15 @@ describe('spec.ts.ejs', () => {
506
510
  metadata: { now: new Date() },
507
511
  })
508
512
 
509
- .then([
510
- {
513
+ .then(
514
+ expectEvents({
511
515
  type: 'QuestionnaireEditRejected',
512
516
  data: {
513
517
  questionnaireId: 'q-001',
514
518
  participantId: 'participant-abc',
515
- reason: 'Questionnaire already submitted',
516
- attemptedAt: new Date('2030-01-01T09:05:00.000Z'),
517
519
  },
518
- },
519
- ]);
520
+ }),
521
+ );
520
522
  });
521
523
  });
522
524
  "
@@ -721,6 +723,8 @@ describe('spec.ts.ejs', () => {
721
723
  initialState,
722
724
  });
723
725
 
726
+ const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
727
+
724
728
  it('Workout submitted updates stats', () => {
725
729
  given([])
726
730
  .when({
@@ -732,22 +736,22 @@ describe('spec.ts.ejs', () => {
732
736
  metadata: { now: new Date() },
733
737
  })
734
738
 
735
- .then([
736
- {
737
- type: 'StatsUpdated',
738
- data: {
739
- workoutId: 'w1',
740
- count: 1,
739
+ .then(
740
+ expectEvents(
741
+ {
742
+ type: 'StatsUpdated',
743
+ data: {
744
+ workoutId: 'w1',
745
+ },
741
746
  },
742
- },
743
- {
744
- type: 'WorkoutLogged',
745
- data: {
746
- workoutId: 'w1',
747
- loggedAt: new Date('2024-01-01T00:00:00Z'),
747
+ {
748
+ type: 'WorkoutLogged',
749
+ data: {
750
+ workoutId: 'w1',
751
+ },
748
752
  },
749
- },
750
- ]);
753
+ ),
754
+ );
751
755
  });
752
756
  });
753
757
  "
@@ -1128,4 +1132,259 @@ describe('spec.ts.ejs', () => {
1128
1132
  expect(specFile?.contents).not.toContain("new Date('2024-01-20')");
1129
1133
  expect(specFile?.contents).not.toContain("new Date('2024-06-30')");
1130
1134
  });
1135
+
1136
+ it('should omit non-command fields from Then assertions (Emmett subset matching)', async () => {
1137
+ const spec: SpecsSchema = {
1138
+ variant: 'specs',
1139
+ narratives: [
1140
+ {
1141
+ name: 'Fitness flow',
1142
+ slices: [
1143
+ {
1144
+ type: 'command',
1145
+ name: 'Log workout',
1146
+ client: { specs: [] },
1147
+ server: {
1148
+ description: '',
1149
+ specs: [
1150
+ {
1151
+ type: 'gherkin',
1152
+ feature: 'Log workout',
1153
+ rules: [
1154
+ {
1155
+ name: 'Should log workout',
1156
+ examples: [
1157
+ {
1158
+ name: 'Workout logged successfully',
1159
+ steps: [
1160
+ {
1161
+ keyword: 'When',
1162
+ text: 'LogWorkout',
1163
+ docString: {
1164
+ memberId: 'mem_001',
1165
+ date: '2024-01-15',
1166
+ exercises: ['bench press'],
1167
+ },
1168
+ },
1169
+ {
1170
+ keyword: 'Then',
1171
+ text: 'WorkoutLogged',
1172
+ docString: {
1173
+ workoutId: 'wkt_456',
1174
+ memberId: 'mem_001',
1175
+ date: '2024-01-15',
1176
+ exercises: ['bench press'],
1177
+ },
1178
+ },
1179
+ ],
1180
+ },
1181
+ ],
1182
+ },
1183
+ ],
1184
+ },
1185
+ ],
1186
+ },
1187
+ },
1188
+ ],
1189
+ },
1190
+ ],
1191
+ messages: [
1192
+ {
1193
+ type: 'command',
1194
+ name: 'LogWorkout',
1195
+ fields: [
1196
+ { name: 'memberId', type: 'string', required: true },
1197
+ { name: 'date', type: 'string', required: true },
1198
+ { name: 'exercises', type: 'string[]', required: true },
1199
+ ],
1200
+ },
1201
+ {
1202
+ type: 'event',
1203
+ name: 'WorkoutLogged',
1204
+ source: 'internal',
1205
+ fields: [
1206
+ { name: 'workoutId', type: 'string', required: true },
1207
+ { name: 'memberId', type: 'string', required: true },
1208
+ { name: 'date', type: 'string', required: true },
1209
+ { name: 'exercises', type: 'string[]', required: true },
1210
+ ],
1211
+ },
1212
+ ],
1213
+ };
1214
+
1215
+ const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
1216
+ const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
1217
+
1218
+ expect(specFile?.contents).not.toContain('workoutId:');
1219
+ expect(specFile?.contents).toContain("memberId: 'mem_001'");
1220
+ expect(specFile?.contents).toContain("date: '2024-01-15'");
1221
+ expect(specFile?.contents).toContain("exercises: ['bench press']");
1222
+ expect(specFile?.contents).toContain('const expectEvents');
1223
+ expect(specFile?.contents).toContain('events as Events[]');
1224
+ expect(specFile?.contents).toContain('.then(\n');
1225
+ expect(specFile?.contents).toContain('expectEvents(');
1226
+ });
1227
+
1228
+ it('should keep non-command fields whose key+value match a Given event', async () => {
1229
+ const spec: SpecsSchema = {
1230
+ variant: 'specs',
1231
+ narratives: [
1232
+ {
1233
+ name: 'Subscription flow',
1234
+ slices: [
1235
+ {
1236
+ type: 'command',
1237
+ name: 'Renew subscription',
1238
+ client: { specs: [] },
1239
+ server: {
1240
+ description: '',
1241
+ specs: [
1242
+ {
1243
+ type: 'gherkin',
1244
+ feature: 'Renew subscription',
1245
+ rules: [
1246
+ {
1247
+ name: 'Should renew',
1248
+ examples: [
1249
+ {
1250
+ name: 'Renewal preserves total points from state',
1251
+ steps: [
1252
+ {
1253
+ keyword: 'Given',
1254
+ text: 'SubscriptionCreated',
1255
+ docString: { subId: 's1', totalPoints: 100 },
1256
+ },
1257
+ {
1258
+ keyword: 'When',
1259
+ text: 'RenewSubscription',
1260
+ docString: { subId: 's1' },
1261
+ },
1262
+ {
1263
+ keyword: 'Then',
1264
+ text: 'SubscriptionRenewed',
1265
+ docString: { subId: 's1', totalPoints: 100 },
1266
+ },
1267
+ ],
1268
+ },
1269
+ ],
1270
+ },
1271
+ ],
1272
+ },
1273
+ ],
1274
+ },
1275
+ },
1276
+ ],
1277
+ },
1278
+ ],
1279
+ messages: [
1280
+ {
1281
+ type: 'command',
1282
+ name: 'RenewSubscription',
1283
+ fields: [{ name: 'subId', type: 'string', required: true }],
1284
+ },
1285
+ {
1286
+ type: 'event',
1287
+ name: 'SubscriptionCreated',
1288
+ source: 'internal',
1289
+ fields: [
1290
+ { name: 'subId', type: 'string', required: true },
1291
+ { name: 'totalPoints', type: 'number', required: true },
1292
+ ],
1293
+ },
1294
+ {
1295
+ type: 'event',
1296
+ name: 'SubscriptionRenewed',
1297
+ source: 'internal',
1298
+ fields: [
1299
+ { name: 'subId', type: 'string', required: true },
1300
+ { name: 'totalPoints', type: 'number', required: true },
1301
+ ],
1302
+ },
1303
+ ],
1304
+ };
1305
+
1306
+ const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
1307
+ const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
1308
+
1309
+ expect(specFile?.contents).toContain('totalPoints: 100');
1310
+ });
1311
+
1312
+ it('should list non-command fields in decide scaffold comments', async () => {
1313
+ const spec: SpecsSchema = {
1314
+ variant: 'specs',
1315
+ narratives: [
1316
+ {
1317
+ name: 'Fitness flow',
1318
+ slices: [
1319
+ {
1320
+ type: 'command',
1321
+ name: 'Log workout',
1322
+ client: { specs: [] },
1323
+ server: {
1324
+ description: '',
1325
+ specs: [
1326
+ {
1327
+ type: 'gherkin',
1328
+ feature: 'Log workout',
1329
+ rules: [
1330
+ {
1331
+ name: 'Should log workout',
1332
+ examples: [
1333
+ {
1334
+ name: 'Workout logged successfully',
1335
+ steps: [
1336
+ {
1337
+ keyword: 'When',
1338
+ text: 'LogWorkout',
1339
+ docString: { memberId: 'mem_001', date: '2024-01-15' },
1340
+ },
1341
+ {
1342
+ keyword: 'Then',
1343
+ text: 'WorkoutLogged',
1344
+ docString: { workoutId: 'wkt_456', memberId: 'mem_001', date: '2024-01-15' },
1345
+ },
1346
+ ],
1347
+ },
1348
+ ],
1349
+ },
1350
+ ],
1351
+ },
1352
+ ],
1353
+ },
1354
+ },
1355
+ ],
1356
+ },
1357
+ ],
1358
+ messages: [
1359
+ {
1360
+ type: 'command',
1361
+ name: 'LogWorkout',
1362
+ fields: [
1363
+ { name: 'memberId', type: 'string', required: true },
1364
+ { name: 'date', type: 'string', required: true },
1365
+ ],
1366
+ },
1367
+ {
1368
+ type: 'event',
1369
+ name: 'WorkoutLogged',
1370
+ source: 'internal',
1371
+ fields: [
1372
+ { name: 'workoutId', type: 'string', required: true },
1373
+ { name: 'memberId', type: 'string', required: true },
1374
+ { name: 'date', type: 'string', required: true },
1375
+ ],
1376
+ },
1377
+ ],
1378
+ };
1379
+
1380
+ const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
1381
+ const decideFile = plans.find((p) => p.outputPath.endsWith('decide.ts'));
1382
+
1383
+ expect(decideFile?.contents).toContain('REQUIRED: Your return value MUST include ALL fields');
1384
+ expect(decideFile?.contents).toContain('Do NOT use');
1385
+ expect(decideFile?.contents).toContain('Fields NOT in command input');
1386
+ expect(decideFile?.contents).toContain('workoutId: string');
1387
+ expect(decideFile?.contents).toContain('...command.data');
1388
+ expect(decideFile?.contents).not.toContain('`NotFoundError`');
1389
+ });
1131
1390
  });
@@ -97,8 +97,8 @@ describe('decide.ts.ejs', () => {
97
97
  * You should:
98
98
  * - Validate the command input fields
99
99
  * - Inspect the current domain \`_state\` to determine if the command is allowed
100
- * - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
101
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
100
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`
101
+ * ⚠️ Error constructors: IllegalStateError takes a string message
102
102
  * - If valid, return one or more events with the correct structure
103
103
  *
104
104
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
@@ -107,10 +107,12 @@ describe('decide.ts.ejs', () => {
107
107
  * - Should create listing with valid data
108
108
  */
109
109
 
110
+ // All event fields come from command input — use ...command.data to pass them through.
111
+
110
112
  // return {
111
113
  // type: 'ListingCreated',
112
114
  // data: { ...command.data },
113
- // } as ListingCreated;
115
+ // };
114
116
 
115
117
  throw new IllegalStateError('Not yet implemented: ' + command.type);
116
118
  }
@@ -230,8 +232,8 @@ describe('decide.ts.ejs', () => {
230
232
  * You should:
231
233
  * - Validate the command input fields
232
234
  * - Inspect the current domain \`_state\` to determine if the command is allowed
233
- * - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
234
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
235
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`
236
+ * ⚠️ Error constructors: IllegalStateError takes a string message
235
237
  * - If valid, return one or more events with the correct structure
236
238
  *
237
239
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
@@ -240,10 +242,18 @@ describe('decide.ts.ejs', () => {
240
242
  * - Should remove existing listing
241
243
  */
242
244
 
245
+ // ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
246
+ // Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
247
+ // Do NOT use 'as ListingRemoved' to silence missing fields.
248
+ //
249
+ // Fields from command input → use ...command.data or command.data.<fieldName>
250
+ // Fields NOT in command input → produce dynamically (never hardcode):
251
+ // removedAt: Date — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
252
+
243
253
  // return {
244
254
  // type: 'ListingRemoved',
245
- // data: { ...command.data },
246
- // } as ListingRemoved;
255
+ // data: { ...command.data, /* + dynamically produce: removedAt */ },
256
+ // };
247
257
 
248
258
  throw new IllegalStateError('Not yet implemented: ' + command.type);
249
259
  }
@@ -383,8 +393,8 @@ describe('decide.ts.ejs', () => {
383
393
  * You should:
384
394
  * - Validate the command input fields
385
395
  * - Inspect the current domain \`_state\` to determine if the command is allowed
386
- * - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
387
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
396
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`, \`ValidationError\`
397
+ * ⚠️ Error constructors: IllegalStateError takes a string message, ValidationError takes a string message
388
398
  * - If valid, return one or more events with the correct structure
389
399
  *
390
400
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
@@ -397,10 +407,12 @@ describe('decide.ts.ejs', () => {
397
407
  throw new ValidationError('Title must not be empty');
398
408
  }
399
409
 
410
+ // All event fields come from command input — use ...command.data to pass them through.
411
+
400
412
  // return {
401
413
  // type: 'ListingCreated',
402
414
  // data: { ...command.data },
403
- // } as ListingCreated;
415
+ // };
404
416
 
405
417
  throw new IllegalStateError('Not yet implemented: ' + command.type);
406
418
  }
@@ -581,8 +593,8 @@ describe('decide.ts.ejs', () => {
581
593
  * - Validate the command input fields
582
594
  * - Inspect the current domain \`_state\` to determine if the command is allowed
583
595
  * - Use \`products\` (integration result) to enrich or filter the output
584
- * - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
585
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
596
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`
597
+ * ⚠️ Error constructors: IllegalStateError takes a string message
586
598
  * - If valid, return one or more events with the correct structure
587
599
  *
588
600
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
@@ -599,10 +611,18 @@ describe('decide.ts.ejs', () => {
599
611
  * - Should suggest items successfully
600
612
  */
601
613
 
614
+ // ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
615
+ // Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
616
+ // Do NOT use 'as ItemsSuggested' to silence missing fields.
617
+ //
618
+ // Fields from command input → use ...command.data or command.data.<fieldName>
619
+ // Fields NOT in command input → produce dynamically (never hardcode):
620
+ // items: Array<object> — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
621
+
602
622
  // return {
603
623
  // type: 'ItemsSuggested',
604
- // data: { ...command.data },
605
- // } as ItemsSuggested;
624
+ // data: { ...command.data, /* + dynamically produce: items */ },
625
+ // };
606
626
 
607
627
  throw new IllegalStateError('Not yet implemented: ' + command.type);
608
628
  }
@@ -54,29 +54,50 @@ for (const [importPath, eventTypes] of testEventsByPath.entries()) {
54
54
 
55
55
  const uniqueEventTypes = Array.from(new Set(allEvents.map(e => e?.type).filter(Boolean))).sort();
56
56
 
57
- function findDerivedDateValue(eventResults, commandSchema, givenEvents) {
58
- const commandFields = new Set(commandSchema?.fields?.map(f => f.name) || []);
57
+ function findDerivedDateInfo(eventResults, commandFieldNames, givenEvents) {
59
58
  const givenValues = new Set();
60
59
  for (const g of givenEvents || []) {
61
60
  for (const val of Object.values(g.exampleData || {})) {
62
61
  if (typeof val === 'string') givenValues.add(val);
63
62
  }
64
63
  }
65
- const candidates = new Set();
64
+ const fieldsByDate = new Map();
66
65
  for (const e of eventResults) {
67
- const data = e.exampleData || {};
68
- for (const [key, val] of Object.entries(data)) {
66
+ for (const [key, val] of Object.entries(e.exampleData || {})) {
69
67
  if (
70
- !commandFields.has(key) &&
68
+ !commandFieldNames.has(key) &&
71
69
  typeof val === 'string' &&
72
70
  /^\d{4}-\d{2}-\d{2}$/.test(val) &&
73
71
  !givenValues.has(val)
74
72
  ) {
75
- candidates.add(val);
73
+ if (!fieldsByDate.has(val)) fieldsByDate.set(val, []);
74
+ fieldsByDate.get(val).push(key);
76
75
  }
77
76
  }
78
77
  }
79
- return candidates.size === 1 ? [...candidates][0] : null;
78
+ if (fieldsByDate.size !== 1) return { date: null, fields: [] };
79
+ const [date, fields] = [...fieldsByDate.entries()][0];
80
+ return { date, fields };
81
+ }
82
+
83
+ function isKeyTraceable(key, value, givenEvents) {
84
+ if (value === null || value === undefined || typeof value === 'object') return false;
85
+ for (const g of givenEvents || []) {
86
+ if ((g.exampleData || {})[key] === value) return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ function buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, givenEvents) {
92
+ const keep = new Set([...commandFieldNames, ...derivedDateFieldNames]);
93
+ for (const e of eventResults) {
94
+ for (const [key, value] of Object.entries(e.exampleData || {})) {
95
+ if (!keep.has(key) && isKeyTraceable(key, value, givenEvents)) {
96
+ keep.add(key);
97
+ }
98
+ }
99
+ }
100
+ return keep;
80
101
  }
81
102
  _%>
82
103
  import { describe, it } from 'vitest';
@@ -99,6 +120,9 @@ describe('<%= ruleDescription %>', () => {
99
120
  initialState,
100
121
  });
101
122
 
123
+ const expectEvents = (...events: Array<{ type: string; data: unknown }>) =>
124
+ events as Events[];
125
+
102
126
  <% for (const { commandName, gwt } of ruleGwts) {
103
127
  const schema = commandSchemasByName[commandName];
104
128
  const example = gwt.when;
@@ -122,19 +146,28 @@ describe('<%= ruleDescription %>', () => {
122
146
  .when({
123
147
  type: '<%= example.commandRef %>',
124
148
  data: <%- formatDataObject(example.exampleData, schema) %>,
125
- <% const derivedDate = findDerivedDateValue(eventResults, schema, gwt.given); -%>
149
+ <% const commandFieldNames = new Set(schema?.fields?.map(f => f.name) || []);
150
+ const { date: derivedDate, fields: derivedDateFieldNames } = findDerivedDateInfo(eventResults, commandFieldNames, gwt.given);
151
+ const keepFieldNames = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, gwt.given);
152
+ -%>
126
153
  metadata: { now: <%= derivedDate ? `new Date('${derivedDate}')` : 'new Date()' %> },
127
154
  })
128
155
  <% if (errorResult) { %>
129
156
  .thenThrows((err) => err instanceof <%= errorResult.errorType %> && err.message === '<%= errorResult.message || '' %>');
130
157
  <% } else { %>
131
158
 
132
- .then([
133
- <%- eventResults.map(e => `{
159
+ .then(expectEvents(
160
+ <%- eventResults.map(e => {
161
+ const evtSchema = events.find(evt => evt.type === e.eventRef);
162
+ const filteredData = Object.fromEntries(
163
+ Object.entries(e.exampleData || {}).filter(([key]) => keepFieldNames.has(key))
164
+ );
165
+ return `{
134
166
  type: '${e.eventRef}',
135
- data: ${formatDataObject(e.exampleData, events.find(evt => evt.type === e.eventRef))}
136
- }`).join(',\n ') %>
137
- ]);
167
+ data: ${formatDataObject(filteredData, evtSchema)}
168
+ }`;
169
+ }).join(',\n ') %>
170
+ ));
138
171
  <% } %>
139
172
  });
140
173
  <% } %>