@auto-engineer/narrative 0.21.0 → 0.21.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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +9 -0
  3. package/dist/tsconfig.tsbuildinfo +1 -1
  4. package/package.json +4 -4
  5. package/.turbo/turbo-format.log +0 -4
  6. package/.turbo/turbo-lint.log +0 -4
  7. package/.turbo/turbo-test.log +0 -14
  8. package/.turbo/turbo-type-check.log +0 -5
  9. package/dist/src/commands/export-schema-runner.d.ts +0 -3
  10. package/dist/src/commands/export-schema-runner.d.ts.map +0 -1
  11. package/dist/src/commands/export-schema-runner.js +0 -53
  12. package/dist/src/commands/export-schema-runner.js.map +0 -1
  13. package/dist/src/fluent-builder.specs.d.ts +0 -2
  14. package/dist/src/fluent-builder.specs.d.ts.map +0 -1
  15. package/dist/src/fluent-builder.specs.js +0 -28
  16. package/dist/src/fluent-builder.specs.js.map +0 -1
  17. package/dist/src/getNarratives.cache.specs.d.ts +0 -2
  18. package/dist/src/getNarratives.cache.specs.d.ts.map +0 -1
  19. package/dist/src/getNarratives.cache.specs.js +0 -234
  20. package/dist/src/getNarratives.cache.specs.js.map +0 -1
  21. package/dist/src/getNarratives.specs.d.ts +0 -2
  22. package/dist/src/getNarratives.specs.d.ts.map +0 -1
  23. package/dist/src/getNarratives.specs.js +0 -1294
  24. package/dist/src/getNarratives.specs.js.map +0 -1
  25. package/dist/src/id/addAutoIds.specs.d.ts +0 -2
  26. package/dist/src/id/addAutoIds.specs.d.ts.map +0 -1
  27. package/dist/src/id/addAutoIds.specs.js +0 -265
  28. package/dist/src/id/addAutoIds.specs.js.map +0 -1
  29. package/dist/src/id/hasAllIds.specs.d.ts +0 -2
  30. package/dist/src/id/hasAllIds.specs.d.ts.map +0 -1
  31. package/dist/src/id/hasAllIds.specs.js +0 -231
  32. package/dist/src/id/hasAllIds.specs.js.map +0 -1
  33. package/dist/src/model-to-narrative.specs.d.ts +0 -2
  34. package/dist/src/model-to-narrative.specs.d.ts.map +0 -1
  35. package/dist/src/model-to-narrative.specs.js +0 -2378
  36. package/dist/src/model-to-narrative.specs.js.map +0 -1
  37. package/dist/src/narrative-context.specs.d.ts +0 -2
  38. package/dist/src/narrative-context.specs.d.ts.map +0 -1
  39. package/dist/src/narrative-context.specs.js +0 -185
  40. package/dist/src/narrative-context.specs.js.map +0 -1
  41. package/dist/src/transformers/narrative-to-model/type-inference.specs.d.ts +0 -2
  42. package/dist/src/transformers/narrative-to-model/type-inference.specs.d.ts.map +0 -1
  43. package/dist/src/transformers/narrative-to-model/type-inference.specs.js +0 -167
  44. package/dist/src/transformers/narrative-to-model/type-inference.specs.js.map +0 -1
@@ -1,1294 +0,0 @@
1
- import { beforeEach, describe, expect, it } from 'vitest';
2
- import { modelSchema } from './schema.js';
3
- import { modelToNarrative } from './index.js';
4
- import { fileURLToPath } from 'url';
5
- import path from 'path';
6
- import { InMemoryFileStore, NodeFileStore } from '@auto-engineer/file-store';
7
- import { getNarratives } from './getNarratives.js';
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
- const pattern = /\.(narrative)\.(ts)$/;
11
- describe('getNarratives', (_mode) => {
12
- let vfs;
13
- let root;
14
- beforeEach(() => {
15
- vfs = new NodeFileStore();
16
- root = path.resolve(__dirname);
17
- });
18
- // eslint-disable-next-line complexity
19
- it('loads multiple narratives and generates correct models', async () => {
20
- const flows = await getNarratives({ vfs, root: path.resolve(__dirname), pattern, fastFsScan: true });
21
- const schemas = flows.toModel();
22
- const parseResult = modelSchema.safeParse(schemas);
23
- if (!parseResult.success) {
24
- console.error(`Schema validation errors:`, parseResult.error.format());
25
- }
26
- expect(parseResult.success).toBe(true);
27
- expect(schemas).toHaveProperty('variant', 'specs');
28
- expect(schemas).toHaveProperty('narratives');
29
- expect(schemas).toHaveProperty('messages');
30
- expect(schemas).toHaveProperty('integrations');
31
- const flowsArray = schemas.narratives;
32
- expect(Array.isArray(flowsArray)).toBe(true);
33
- expect(flowsArray.length).toBeGreaterThanOrEqual(2);
34
- const names = flowsArray.map((f) => f.name);
35
- expect(names).toContain('items');
36
- expect(names).toContain('Place order');
37
- const items = flowsArray.find((f) => f.name === 'items');
38
- const placeOrder = flowsArray.find((f) => f.name === 'Place order');
39
- expect(items).toBeDefined();
40
- expect(placeOrder).toBeDefined();
41
- if (items) {
42
- expect(items.slices).toHaveLength(2);
43
- const createItemSlice = items.slices[0];
44
- expect(createItemSlice.type).toBe('command');
45
- expect(createItemSlice.name).toBe('Create item');
46
- expect(createItemSlice.stream).toBe('item-${id}');
47
- if (createItemSlice.type === 'command') {
48
- expect(createItemSlice.client.specs).toBeDefined();
49
- expect(createItemSlice.client.specs?.name).toBe('A form that allows users to add items');
50
- expect(createItemSlice.client.specs?.rules).toHaveLength(1);
51
- expect(createItemSlice.server.specs).toBeDefined();
52
- const spec = createItemSlice.server.specs;
53
- expect(spec.name).toBeDefined();
54
- expect(spec.rules).toHaveLength(1);
55
- const rule = spec.rules[0];
56
- expect(rule.description).toBeDefined();
57
- expect(rule.examples).toHaveLength(1);
58
- const example = rule.examples[0];
59
- expect(typeof example.when === 'object' && !Array.isArray(example.when)).toBe(true);
60
- if (typeof example.when === 'object' && !Array.isArray(example.when)) {
61
- if ('commandRef' in example.when) {
62
- expect(example.when.commandRef).toBe('CreateItem');
63
- }
64
- expect(example.when.exampleData).toMatchObject({
65
- itemId: 'item_123',
66
- description: 'A new item',
67
- });
68
- }
69
- expect(example.then).toHaveLength(1);
70
- expect(example.then[0]).toMatchObject({
71
- eventRef: 'ItemCreated',
72
- exampleData: {
73
- id: 'item_123',
74
- description: 'A new item',
75
- addedAt: new Date('2024-01-15T10:00:00.000Z'),
76
- },
77
- });
78
- }
79
- const viewItemSlice = items.slices[1];
80
- expect(viewItemSlice.type).toBe('query');
81
- expect(viewItemSlice.name).toBe('view items');
82
- expect(viewItemSlice.client.specs).toBeDefined();
83
- expect(viewItemSlice.client.specs?.name).toBe('view Items Screen');
84
- expect(viewItemSlice.client.specs?.rules).toHaveLength(3);
85
- expect(viewItemSlice.request).toBeDefined();
86
- expect(viewItemSlice.request).toMatch(/query items\(\$itemId: String!\) {\s+items\(itemId: \$itemId\) {\s+id\s+description\s+}/);
87
- const data = viewItemSlice?.server?.data;
88
- if (!data || !Array.isArray(data))
89
- throw new Error('No data found in view items slice');
90
- expect(data).toHaveLength(1);
91
- expect(data[0].target).toMatchObject({ type: 'State', name: 'items' });
92
- expect(data[0].origin).toMatchObject({ name: 'ItemsProjection', type: 'projection' });
93
- const specs = viewItemSlice?.server?.specs;
94
- if (specs == null || specs.name === '')
95
- throw new Error('No specs found in view items slice');
96
- expect(specs).toBeDefined();
97
- }
98
- if (placeOrder) {
99
- expect(placeOrder.slices).toHaveLength(1);
100
- const submitOrderSlice = placeOrder.slices[0];
101
- expect(submitOrderSlice.type).toBe('command');
102
- expect(submitOrderSlice.name).toBe('Submit order');
103
- expect(submitOrderSlice.stream).toBe('order-${orderId}');
104
- if (submitOrderSlice.type === 'command') {
105
- expect(submitOrderSlice.client.specs).toBeDefined();
106
- expect(submitOrderSlice.client.specs?.name).toBe('Order submission form');
107
- expect(submitOrderSlice.client.specs?.rules).toHaveLength(2);
108
- expect(submitOrderSlice.server.specs).toBeDefined();
109
- const spec = submitOrderSlice.server.specs;
110
- expect(spec.rules).toHaveLength(1);
111
- const rule = spec.rules[0];
112
- expect(rule.examples).toHaveLength(1);
113
- const example = rule.examples[0];
114
- expect(typeof example.when === 'object' && !Array.isArray(example.when)).toBe(true);
115
- if (typeof example.when === 'object' && !Array.isArray(example.when)) {
116
- if ('commandRef' in example.when) {
117
- expect(example.when.commandRef).toBe('PlaceOrder');
118
- }
119
- expect(example.when.exampleData).toMatchObject({ productId: 'product_789', quantity: 3 });
120
- }
121
- expect(example.then).toHaveLength(1);
122
- expect(example.then[0]).toMatchObject({
123
- eventRef: 'OrderPlaced',
124
- exampleData: {
125
- orderId: 'order_001',
126
- productId: 'product_789',
127
- quantity: 3,
128
- placedAt: new Date('2024-01-20T10:00:00.000Z'),
129
- },
130
- });
131
- }
132
- }
133
- const messages = schemas.messages;
134
- expect(messages.length).toBeGreaterThan(0);
135
- const commandMessages = messages.filter((m) => m.type === 'command');
136
- const eventMessages = messages.filter((m) => m.type === 'event');
137
- expect(commandMessages.some((m) => m.name === 'CreateItem')).toBe(true);
138
- expect(commandMessages.some((m) => m.name === 'PlaceOrder')).toBe(true);
139
- expect(eventMessages.some((m) => m.name === 'ItemCreated')).toBe(true);
140
- expect(eventMessages.some((m) => m.name === 'OrderPlaced')).toBe(true);
141
- const createItemCommand = commandMessages.find((m) => m.name === 'CreateItem');
142
- if (createItemCommand) {
143
- expect(createItemCommand.fields).toContainEqual(expect.objectContaining({ name: 'itemId', type: 'string', required: true }));
144
- expect(createItemCommand.fields).toContainEqual(expect.objectContaining({ name: 'description', type: 'string', required: true }));
145
- }
146
- const itemCreatedEvent = eventMessages.find((m) => m.name === 'ItemCreated');
147
- if (itemCreatedEvent) {
148
- expect(itemCreatedEvent.fields).toContainEqual(expect.objectContaining({ name: 'id', type: 'string', required: true }));
149
- expect(itemCreatedEvent.fields).toContainEqual(expect.objectContaining({ name: 'description', type: 'string', required: true }));
150
- expect(itemCreatedEvent.fields).toContainEqual(expect.objectContaining({ name: 'addedAt', type: 'Date', required: true }));
151
- }
152
- });
153
- it('validates the complete schema with Zod', async () => {
154
- const flows = await getNarratives({ vfs: vfs, root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
155
- const schemas = flows.toModel();
156
- const parsed = modelSchema.parse(schemas);
157
- expect(parsed.variant).toBe('specs');
158
- expect(Array.isArray(parsed.narratives)).toBe(true);
159
- expect(Array.isArray(parsed.messages)).toBe(true);
160
- expect(Array.isArray(parsed.integrations)).toBe(true);
161
- });
162
- it('should handle narratives with integrations', async () => {
163
- const flows = await getNarratives({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
164
- const specsSchema = flows.toModel();
165
- const flowsWithIntegrations = specsSchema.narratives.filter((f) => f.slices.some((s) => {
166
- if (s.type === 'command' || s.type === 'query') {
167
- return (s.server.data?.some((d) => ('destination' in d && d.destination?.type === 'integration') ||
168
- ('origin' in d && d.origin?.type === 'integration')) ?? false);
169
- }
170
- return false;
171
- }));
172
- if (flowsWithIntegrations.length > 0) {
173
- expect(specsSchema?.integrations?.length ?? 0).toBeGreaterThan(0);
174
- }
175
- });
176
- it('should handle react slices correctly', async () => {
177
- const flows = await getNarratives({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
178
- const specsSchema = flows.toModel();
179
- const reactSlices = specsSchema.narratives.flatMap((f) => f.slices.filter((s) => s.type === 'react'));
180
- reactSlices.forEach((slice) => {
181
- if (slice.type === 'react') {
182
- expect(slice.server).toBeDefined();
183
- expect(slice.server.specs).toBeDefined();
184
- expect(typeof slice.server.specs === 'object' && !Array.isArray(slice.server.specs)).toBe(true);
185
- const spec = slice.server.specs;
186
- expect(spec.rules).toBeDefined();
187
- expect(Array.isArray(spec.rules)).toBe(true);
188
- spec.rules.forEach((rule) => {
189
- rule.examples.forEach((example) => {
190
- expect(example.when).toBeDefined();
191
- expect(Array.isArray(example.when)).toBe(true);
192
- expect(example.then).toBeDefined();
193
- expect(Array.isArray(example.then)).toBe(true);
194
- });
195
- });
196
- }
197
- });
198
- });
199
- it('should parse and validate a complete flow with all slice types', async () => {
200
- const flows = await getNarratives({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
201
- const schemas = flows.toModel();
202
- const validationResult = modelSchema.safeParse(schemas);
203
- if (!validationResult.success) {
204
- console.error(`Validation errors:`, JSON.stringify(validationResult.error.format(), null, 2));
205
- }
206
- expect(validationResult.success).toBe(true);
207
- const validatedData = validationResult.data;
208
- expect(validatedData.narratives.every((flow) => flow.slices.every((slice) => {
209
- if (slice.type === 'command' || slice.type === 'query') {
210
- return slice.client !== undefined && slice.server !== undefined;
211
- }
212
- else if (slice.type === 'react') {
213
- return slice.server !== undefined;
214
- }
215
- else if (slice.type === 'experience') {
216
- return slice.client !== undefined;
217
- }
218
- return false;
219
- }))).toBe(true);
220
- });
221
- it('should have ids for narratives and slices that have ids', async () => {
222
- const flows = await getNarratives({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
223
- const schemas = flows.toModel();
224
- const testFlowWithIds = schemas.narratives.find((f) => f.name === 'Test Flow with IDs');
225
- if (!testFlowWithIds)
226
- return;
227
- const commandSlice = testFlowWithIds.slices.find((s) => s.name === 'Create test item');
228
- expect(commandSlice?.id).toBe('SLICE-001');
229
- expect(commandSlice?.type).toBe('command');
230
- const querySlice = testFlowWithIds.slices.find((s) => s.name === 'Get test items');
231
- expect(querySlice?.id).toBe('SLICE-002');
232
- expect(querySlice?.type).toBe('query');
233
- const reactSlice = testFlowWithIds.slices.find((s) => s.name === 'React to test event');
234
- expect(reactSlice?.id).toBe('SLICE-003');
235
- expect(reactSlice?.type).toBe('react');
236
- });
237
- it('should have ids for command slice rules', async () => {
238
- const flows = await getNarratives({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
239
- const schemas = flows.toModel();
240
- const testFlowWithIds = schemas.narratives.find((f) => f.name === 'Test Flow with IDs');
241
- if (!testFlowWithIds)
242
- return;
243
- const commandSlice = testFlowWithIds.slices.find((s) => s.name === 'Create test item');
244
- if (commandSlice?.type !== 'command')
245
- return;
246
- expect(commandSlice.server.specs.rules).toHaveLength(2);
247
- const rule1 = commandSlice.server.specs.rules.find((r) => r.description === 'Valid test items should be created successfully');
248
- expect(rule1?.id).toBe('RULE-001');
249
- const rule2 = commandSlice.server.specs.rules.find((r) => r.description === 'Invalid test items should be rejected');
250
- expect(rule2?.id).toBe('RULE-002');
251
- });
252
- it('should have ids for query slice rules', async () => {
253
- const flows = await getNarratives({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
254
- const schemas = flows.toModel();
255
- const testFlowWithIds = schemas.narratives.find((f) => f.name === 'Test Flow with IDs');
256
- if (!testFlowWithIds)
257
- return;
258
- const querySlice = testFlowWithIds.slices.find((s) => s.name === 'Get test items');
259
- if (querySlice?.type !== 'query')
260
- return;
261
- expect(querySlice.server.specs.rules).toHaveLength(1);
262
- const rule3 = querySlice.server.specs.rules.find((r) => r.description === 'Items should be retrievable after creation');
263
- expect(rule3?.id).toBe('RULE-003');
264
- });
265
- it('should have ids for react slice rules', async () => {
266
- const flows = await getNarratives({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
267
- const schemas = flows.toModel();
268
- const testFlowWithIds = schemas.narratives.find((f) => f.name === 'Test Flow with IDs');
269
- if (!testFlowWithIds)
270
- return;
271
- const reactSlice = testFlowWithIds.slices.find((s) => s.name === 'React to test event');
272
- if (reactSlice?.type !== 'react')
273
- return;
274
- expect(reactSlice.server.specs.rules).toHaveLength(1);
275
- const rule4 = reactSlice.server.specs.rules.find((r) => r.description === 'System should react to test item creation');
276
- expect(rule4?.id).toBe('RULE-004');
277
- });
278
- it('should handle when examples correctly', async () => {
279
- const flows = await getNarratives({
280
- vfs,
281
- root,
282
- pattern: /(?:^|\/)questionnaires\.narrative\.(?:ts|tsx|js|jsx|mjs|cjs)$/,
283
- });
284
- const model = flows.toModel();
285
- const questionnaireFlow = model.narratives.find((f) => f.name === 'Questionnaires');
286
- expect(questionnaireFlow).toBeDefined();
287
- if (questionnaireFlow) {
288
- const submitSlice = questionnaireFlow.slices.find((s) => s.name === 'submits the questionnaire');
289
- expect(submitSlice?.type).toBe('command');
290
- if (submitSlice?.type === 'command') {
291
- const example = submitSlice.server?.specs?.rules[0]?.examples[0];
292
- if (example !== null &&
293
- example !== undefined &&
294
- typeof example.when === 'object' &&
295
- example.when !== null &&
296
- !Array.isArray(example.when) &&
297
- 'commandRef' in example.when) {
298
- expect(example.when.commandRef).toBe('SubmitQuestionnaire');
299
- }
300
- }
301
- }
302
- });
303
- it('should correctly assign commandRef correctly', async () => {
304
- const flows = await getNarratives({
305
- vfs,
306
- root,
307
- pattern: /(?:^|\/)questionnaires\.narrative\.(?:ts|tsx|js|jsx|mjs|cjs)$/,
308
- fastFsScan: true,
309
- });
310
- const model = flows.toModel();
311
- validateCommandRef(model);
312
- });
313
- it('should handle experience slice with client specs', async () => {
314
- const memoryVfs = new InMemoryFileStore();
315
- const flowWithExperienceContent = `
316
- import { flow, experience, should, specs } from '@auto-engineer/narrative';
317
-
318
- flow('Test Experience Flow', () => {
319
- experience('Homepage', 'AUTO-H1a4Bn6Cy').client(() => {
320
- specs(() => {
321
- should('show a hero section with a welcome message');
322
- should('allow user to start the questionnaire');
323
- });
324
- });
325
- });
326
- `;
327
- await memoryVfs.write('/test/experience.narrative.ts', new TextEncoder().encode(flowWithExperienceContent));
328
- const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
329
- const model = flows.toModel();
330
- const experienceFlow = model.narratives.find((f) => f.name === 'Test Experience Flow');
331
- expect(experienceFlow).toBeDefined();
332
- if (experienceFlow) {
333
- const homepageSlice = experienceFlow.slices.find((s) => s.name === 'Homepage');
334
- expect(homepageSlice).toBeDefined();
335
- expect(homepageSlice?.type).toBe('experience');
336
- if (homepageSlice?.type === 'experience') {
337
- expect(homepageSlice.client).toBeDefined();
338
- expect(homepageSlice.client.specs).toBeDefined();
339
- expect(homepageSlice.client.specs?.rules).toBeDefined();
340
- expect(homepageSlice.client.specs?.rules).toHaveLength(2);
341
- const rules = homepageSlice.client.specs?.rules;
342
- if (rules && Array.isArray(rules)) {
343
- expect(rules).toHaveLength(2);
344
- expect(rules[0]).toBe('show a hero section with a welcome message');
345
- expect(rules[1]).toBe('allow user to start the questionnaire');
346
- }
347
- }
348
- }
349
- });
350
- it('simulates browser execution with transpiled CommonJS code', async () => {
351
- const memoryVfs = new InMemoryFileStore();
352
- const flowContent = `
353
- import { flow, experience, should, specs } from '@auto-engineer/narrative';
354
-
355
- flow('Browser Test Flow', () => {
356
- experience('HomePage').client(() => {
357
- specs(() => {
358
- should('render correctly');
359
- });
360
- });
361
- });
362
- `;
363
- await memoryVfs.write('/browser/test.narrative.ts', new TextEncoder().encode(flowContent));
364
- const { executeAST } = await import('./loader/index.js');
365
- const { registry } = await import('./narrative-registry.js');
366
- registry.clearAll();
367
- await executeAST(['/browser/test.narrative.ts'], memoryVfs, {}, '/browser');
368
- const flows = registry.getAllNarratives();
369
- expect(flows).toHaveLength(1);
370
- expect(flows[0].name).toBe('Browser Test Flow');
371
- expect(flows[0].slices).toHaveLength(1);
372
- const slice = flows[0].slices[0];
373
- expect(slice.type).toBe('experience');
374
- expect(slice.name).toBe('HomePage');
375
- if (slice.type === 'experience') {
376
- expect(slice.client).toBeDefined();
377
- expect(slice.client.specs).toBeDefined();
378
- expect(slice.client.specs?.rules).toHaveLength(1);
379
- expect(slice.client.specs?.rules?.[0]).toBe('render correctly');
380
- }
381
- });
382
- it('handles experience slice with ES module interop correctly', async () => {
383
- const memoryVfs = new InMemoryFileStore();
384
- const { executeAST } = await import('./loader/index.js');
385
- const { registry } = await import('./narrative-registry.js');
386
- const flowContent = `
387
- import { flow, experience } from '@auto-engineer/narrative';
388
-
389
- flow('Questionnaires', 'AUTO-Q9m2Kp4Lx', () => {
390
- experience('Homepage', 'AUTO-H1a4Bn6Cy').client(() => {});
391
- });
392
- `;
393
- await memoryVfs.write('/browser/questionnaires.narrative.ts', new TextEncoder().encode(flowContent));
394
- registry.clearAll();
395
- await expect(executeAST(['/browser/questionnaires.narrative.ts'], memoryVfs, {}, '/browser')).resolves.toBeDefined();
396
- const flows = registry.getAllNarratives();
397
- expect(flows).toHaveLength(1);
398
- expect(flows[0].name).toBe('Questionnaires');
399
- expect(flows[0].slices).toHaveLength(1);
400
- const slice = flows[0].slices[0];
401
- expect(slice.type).toBe('experience');
402
- expect(slice.name).toBe('Homepage');
403
- });
404
- it('should handle flow type resolutions correctly', async () => {
405
- const memoryVfs = new InMemoryFileStore();
406
- const questionnaireFlowContent = `
407
- import { data, flow, should, specs, rule, example } from '../narrative.js';
408
- import { command, query } from '../fluent-builder.js';
409
- import gql from 'graphql-tag';
410
- import { source } from '../data-narrative-builders.js';
411
- import { type Event, type Command, type State } from '../types.js';
412
-
413
- type QuestionAnswered = Event<
414
- 'QuestionAnswered',
415
- {
416
- questionnaireId: string;
417
- participantId: string;
418
- questionId: string;
419
- answer: unknown;
420
- savedAt: Date;
421
- }
422
- >;
423
-
424
- type SubmitQuestionnaire = Command<
425
- 'SubmitQuestionnaire',
426
- {
427
- questionnaireId: string;
428
- participantId: string;
429
- }
430
- >;
431
-
432
- type AnswerQuestion = Command<
433
- 'AnswerQuestion',
434
- {
435
- questionnaireId: string;
436
- participantId: string;
437
- questionId: string;
438
- answer: unknown;
439
- }
440
- >;
441
-
442
- type QuestionnaireProgress = State<
443
- 'QuestionnaireProgress',
444
- {
445
- questionnaireId: string;
446
- participantId: string;
447
- status: 'in_progress' | 'ready_to_submit' | 'submitted';
448
- currentQuestionId: string | null;
449
- remainingQuestions: string[];
450
- answers: { questionId: string; value: unknown }[];
451
- }
452
- >;
453
-
454
- flow('questionnaires-test', () => {
455
- query('views progress')
456
- .server(() => {
457
- specs('Questionnaire progress display', () => {
458
- rule('shows answered questions', () => {
459
- example('question already answered')
460
- .given<QuestionAnswered>({
461
- questionnaireId: 'q-001',
462
- participantId: 'participant-abc',
463
- questionId: 'q1',
464
- answer: 'Yes',
465
- savedAt: new Date('2030-01-01T09:05:00Z'),
466
- })
467
- .when({})
468
- .then<QuestionnaireProgress>({
469
- questionnaireId: 'q-001',
470
- participantId: 'participant-abc',
471
- status: 'in_progress',
472
- currentQuestionId: 'q2',
473
- remainingQuestions: ['q2', 'q3'],
474
- answers: [{ questionId: 'q1', value: 'Yes' }],
475
- });
476
- });
477
- });
478
- });
479
-
480
- command('submits questionnaire')
481
- .server(() => {
482
- specs('Questionnaire submission', () => {
483
- rule('allows submission when ready', () => {
484
- example('submit completed questionnaire')
485
- .when<SubmitQuestionnaire>({
486
- questionnaireId: 'q-001',
487
- participantId: 'participant-abc',
488
- })
489
- .then<QuestionAnswered>({
490
- questionnaireId: 'q-001',
491
- participantId: 'participant-abc',
492
- questionId: 'final',
493
- answer: 'submitted',
494
- savedAt: new Date('2030-01-01T09:10:00Z'),
495
- });
496
- });
497
- });
498
- });
499
- });
500
- `;
501
- await memoryVfs.write('/test/questionnaires.narrative.ts', new TextEncoder().encode(questionnaireFlowContent));
502
- const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
503
- const model = flows.toModel();
504
- const testFlow = model.narratives.find((f) => f.name === 'questionnaires-test');
505
- expect(testFlow).toBeDefined();
506
- if (testFlow !== null && testFlow !== undefined) {
507
- validateSubmitQuestionnaireCommand(testFlow);
508
- validateQuestionAnsweredEvent(model);
509
- validateGivenSectionEventRefs(testFlow);
510
- validateCurrentQuestionIdType(model);
511
- }
512
- });
513
- it('correctly distinguishes between State and Event types in given clauses with empty when', async () => {
514
- const flows = await getNarratives({ vfs, root, pattern, fastFsScan: true });
515
- const model = flows.toModel();
516
- const mixedGivenFlow = model.narratives.find((f) => f.name === 'Mixed Given Types');
517
- expect(mixedGivenFlow).toBeDefined();
518
- if (!mixedGivenFlow)
519
- return;
520
- const querySlice = mixedGivenFlow.slices.find((s) => s.name === 'system status check');
521
- expect(querySlice).toBeDefined();
522
- expect(querySlice?.type).toBe('query');
523
- if (querySlice?.type !== 'query')
524
- return;
525
- const example = querySlice.server.specs.rules[0]?.examples[0];
526
- expect(example).toBeDefined();
527
- if (example !== null && example !== undefined) {
528
- validateMixedGivenTypes(example);
529
- validateEmptyWhenClause(example);
530
- validateThenClause(example);
531
- validateMixedGivenTypeMessages(model);
532
- }
533
- });
534
- it('does not emit empty generics or empty when clauses', async () => {
535
- const flows = await getNarratives({
536
- vfs,
537
- root,
538
- pattern: /(?:^|\/)questionnaires\.narrative\.(?:ts|tsx|js|jsx|mjs|cjs)$/,
539
- fastFsScan: true,
540
- });
541
- const model = flows.toModel();
542
- const code = await modelToNarrative(model);
543
- expect(code).not.toMatch(/\.when<>\(\{\}\)/);
544
- expect(code).not.toMatch(/\.when<\s*\{\s*}\s*>\(\{}\)/);
545
- expect(code).not.toMatch(/\.when\(\{}\)/);
546
- });
547
- it('should not generate phantom messages with empty names', async () => {
548
- const flows = await getNarratives({
549
- vfs,
550
- root: root,
551
- pattern: /(?:^|\/)questionnaires\.narrative\.(?:ts|tsx|js|jsx|mjs|cjs)$/,
552
- fastFsScan: true,
553
- });
554
- const model = flows.toModel();
555
- const phantomMessages = model.messages.filter((message) => message.name === '');
556
- expect(phantomMessages).toHaveLength(0);
557
- const allMessages = model.messages;
558
- expect(allMessages.every((message) => message.name.length > 0)).toBe(true);
559
- });
560
- it('reproduces the questionnaires bug: submits the questionnaire should use SubmitQuestionnaire, not SendQuestionnaireLink', async () => {
561
- const model = await createQuestionnaireBugTestModel();
562
- validateQuestionnaireBugFix(model);
563
- });
564
- it('should convert all given events to eventRef', async function () {
565
- const memoryVfs = new InMemoryFileStore();
566
- const todoSummaryFlowContent = `
567
- import { flow, query, specs, rule, example, type Event, type State } from '@auto-engineer/narrative';
568
-
569
- type TodoAdded = Event<
570
- 'TodoAdded',
571
- {
572
- todoId: string;
573
- description: string;
574
- status: 'pending';
575
- addedAt: Date;
576
- }
577
- >;
578
-
579
- type TodoMarkedInProgress = Event<
580
- 'TodoMarkedInProgress',
581
- {
582
- todoId: string;
583
- markedAt: Date;
584
- }
585
- >;
586
-
587
- type TodoMarkedComplete = Event<
588
- 'TodoMarkedComplete',
589
- {
590
- todoId: string;
591
- completedAt: Date;
592
- }
593
- >;
594
-
595
- type TodoListSummary = State<
596
- 'TodoListSummary',
597
- {
598
- summaryId: string;
599
- totalTodos: number;
600
- pendingCount: number;
601
- inProgressCount: number;
602
- completedCount: number;
603
- completionPercentage: number;
604
- }
605
- >;
606
-
607
- flow('Todo List', () => {
608
- query('views completion summary')
609
- .server(() => {
610
- specs(() => {
611
- rule('summary shows overall todo list statistics', () => {
612
- example('calculates summary from multiple todos')
613
- .given<TodoAdded>({
614
- todoId: 'todo-001',
615
- description: 'Buy groceries',
616
- status: 'pending',
617
- addedAt: new Date('2030-01-01T09:00:00Z'),
618
- })
619
- .and<TodoAdded>({
620
- todoId: 'todo-002',
621
- description: 'Write report',
622
- status: 'pending',
623
- addedAt: new Date('2030-01-01T09:10:00Z'),
624
- })
625
- .and<TodoAdded>({
626
- todoId: 'todo-003',
627
- description: 'Call client',
628
- status: 'pending',
629
- addedAt: new Date('2030-01-01T09:20:00Z'),
630
- })
631
- .and<TodoMarkedInProgress>({
632
- todoId: 'todo-001',
633
- markedAt: new Date('2030-01-01T10:00:00Z'),
634
- })
635
- .and<TodoMarkedComplete>({
636
- todoId: 'todo-002',
637
- completedAt: new Date('2030-01-01T11:00:00Z'),
638
- })
639
- .when({})
640
- .then<TodoListSummary>({
641
- summaryId: 'main-summary',
642
- totalTodos: 3,
643
- pendingCount: 1,
644
- inProgressCount: 1,
645
- completedCount: 1,
646
- completionPercentage: 33,
647
- });
648
- });
649
- });
650
- });
651
- });
652
- `;
653
- await memoryVfs.write('/test/todo-summary.narrative.ts', new TextEncoder().encode(todoSummaryFlowContent));
654
- const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
655
- const model = flows.toModel();
656
- const todoFlow = model.narratives.find((f) => f.name === 'Todo List');
657
- expect(todoFlow).toBeDefined();
658
- if (!todoFlow)
659
- return;
660
- const summarySlice = todoFlow.slices.find((s) => s.name === 'views completion summary');
661
- expect(summarySlice?.type).toBe('query');
662
- if (summarySlice?.type !== 'query')
663
- return;
664
- const example = summarySlice.server.specs.rules[0]?.examples[0];
665
- expect(example).toBeDefined();
666
- expect(example.given).toBeDefined();
667
- expect(Array.isArray(example.given)).toBe(true);
668
- expect(example.given).toHaveLength(5);
669
- if (!example.given) {
670
- throw new Error('expected example.given to be defined');
671
- }
672
- validateGivenItemsHaveEventRef(example.given);
673
- validateTodoEventRefs(example.given);
674
- validateTodoMessages(model);
675
- });
676
- });
677
- function validateGivenItemsHaveEventRef(given) {
678
- for (let i = 0; i < given.length; i++) {
679
- const givenItem = given[i];
680
- if (typeof givenItem === 'object' && givenItem !== null) {
681
- expect('eventRef' in givenItem).toBe(true);
682
- expect('stateRef' in givenItem).toBe(false);
683
- }
684
- }
685
- }
686
- function expectEventRef(item, expectedType) {
687
- if (item !== null && item !== undefined && typeof item === 'object' && 'eventRef' in item) {
688
- expect(item.eventRef).toBe(expectedType);
689
- }
690
- }
691
- function validateTodoEventRefs(given) {
692
- expectEventRef(given[0], 'TodoAdded');
693
- expectEventRef(given[1], 'TodoAdded');
694
- expectEventRef(given[2], 'TodoAdded');
695
- expectEventRef(given[3], 'TodoMarkedInProgress');
696
- expectEventRef(given[4], 'TodoMarkedComplete');
697
- }
698
- function validateTodoMessages(model) {
699
- const todoAddedEvent = model.messages.find((m) => m.name === 'TodoAdded');
700
- expect(todoAddedEvent).toBeDefined();
701
- expect(todoAddedEvent?.type).toBe('event');
702
- const todoMarkedInProgressEvent = model.messages.find((m) => m.name === 'TodoMarkedInProgress');
703
- expect(todoMarkedInProgressEvent).toBeDefined();
704
- expect(todoMarkedInProgressEvent?.type).toBe('event');
705
- const todoMarkedCompleteEvent = model.messages.find((m) => m.name === 'TodoMarkedComplete');
706
- expect(todoMarkedCompleteEvent).toBeDefined();
707
- expect(todoMarkedCompleteEvent?.type).toBe('event');
708
- const todoListSummaryState = model.messages.find((m) => m.name === 'TodoListSummary');
709
- expect(todoListSummaryState).toBeDefined();
710
- expect(todoListSummaryState?.type).toBe('state');
711
- }
712
- function validateSubmitQuestionnaireCommand(questionnaireFlow) {
713
- const submitSlice = questionnaireFlow.slices.find((s) => s.name === 'submits questionnaire');
714
- expect(submitSlice?.type).toBe('command');
715
- if (submitSlice?.type === 'command') {
716
- const example = submitSlice.server?.specs?.rules[0]?.examples[0];
717
- if (example !== null &&
718
- example !== undefined &&
719
- typeof example.when === 'object' &&
720
- example.when !== null &&
721
- !Array.isArray(example.when) &&
722
- 'commandRef' in example.when) {
723
- expect(example.when.commandRef).toBe('SubmitQuestionnaire');
724
- }
725
- }
726
- }
727
- function validateQuestionAnsweredEvent(model) {
728
- const questionAnsweredMessage = model.messages.find((m) => m.name === 'QuestionAnswered');
729
- expect(questionAnsweredMessage?.type).toBe('event');
730
- }
731
- function validateGivenSectionEventRefs(questionnaireFlow) {
732
- const viewsSlice = questionnaireFlow.slices.find((s) => s.name === 'views progress');
733
- if (viewsSlice?.type === 'query') {
734
- const example = viewsSlice.server?.specs?.rules[0]?.examples[0];
735
- if (example?.given && Array.isArray(example.given) && example.given.length > 0) {
736
- const givenItem = example.given[0];
737
- if (typeof givenItem === 'object' && givenItem !== null) {
738
- expect('eventRef' in givenItem).toBe(true);
739
- expect('stateRef' in givenItem).toBe(false);
740
- if ('eventRef' in givenItem) {
741
- expect(givenItem.eventRef).toBe('QuestionAnswered');
742
- }
743
- }
744
- }
745
- }
746
- }
747
- function validateCurrentQuestionIdType(model) {
748
- const progressMessage = model.messages.find((m) => m.name === 'QuestionnaireProgress');
749
- expect(progressMessage?.type).toBe('state');
750
- const currentQuestionIdField = progressMessage?.fields.find((f) => f.name === 'currentQuestionId');
751
- expect(currentQuestionIdField?.type).toBe('string | null');
752
- }
753
- function validateMixedGivenTypes(example) {
754
- expect(example.description).toBe('system with 2 items reaches max of 2');
755
- expect(example.given).toBeDefined();
756
- expect(Array.isArray(example.given)).toBe(true);
757
- if (!example.given)
758
- return;
759
- expect(example.given).toHaveLength(4);
760
- const firstGiven = example.given[0];
761
- expect('stateRef' in firstGiven).toBe(true);
762
- expect('eventRef' in firstGiven).toBe(false);
763
- if ('stateRef' in firstGiven) {
764
- expect(firstGiven.stateRef).toBe('ConfigState');
765
- }
766
- const secondGiven = example.given[1];
767
- expect('eventRef' in secondGiven).toBe(true);
768
- if ('eventRef' in secondGiven) {
769
- expect(secondGiven.eventRef).toBe('SystemInitialized');
770
- }
771
- const thirdGiven = example.given[2];
772
- expect('eventRef' in thirdGiven).toBe(true);
773
- if ('eventRef' in thirdGiven) {
774
- expect(thirdGiven.eventRef).toBe('ItemAdded');
775
- }
776
- const fourthGiven = example.given[3];
777
- expect('eventRef' in fourthGiven).toBe(true);
778
- if ('eventRef' in fourthGiven) {
779
- expect(fourthGiven.eventRef).toBe('ItemAdded');
780
- }
781
- }
782
- function validateEmptyWhenClause(example) {
783
- expect(example.when).toBeUndefined();
784
- }
785
- function validateThenClause(example) {
786
- expect(example.then).toBeDefined();
787
- expect(Array.isArray(example.then)).toBe(true);
788
- expect(example.then).toHaveLength(1);
789
- const thenOutcome = example.then[0];
790
- expect('stateRef' in thenOutcome).toBe(true);
791
- if ('stateRef' in thenOutcome) {
792
- expect(thenOutcome.stateRef).toBe('SystemStatus');
793
- }
794
- }
795
- function validateMixedGivenTypeMessages(model) {
796
- const configStateMessage = model.messages.find((m) => m.name === 'ConfigState');
797
- expect(configStateMessage).toBeDefined();
798
- expect(configStateMessage?.type).toBe('state');
799
- const systemInitializedMessage = model.messages.find((m) => m.name === 'SystemInitialized');
800
- expect(systemInitializedMessage).toBeDefined();
801
- expect(systemInitializedMessage?.type).toBe('event');
802
- const itemAddedMessage = model.messages.find((m) => m.name === 'ItemAdded');
803
- expect(itemAddedMessage).toBeDefined();
804
- expect(itemAddedMessage?.type).toBe('event');
805
- const systemStatusMessage = model.messages.find((m) => m.name === 'SystemStatus');
806
- expect(systemStatusMessage).toBeDefined();
807
- expect(systemStatusMessage?.type).toBe('state');
808
- }
809
- async function createQuestionnaireBugTestModel() {
810
- const memoryVfs = new InMemoryFileStore();
811
- const questionnaireFlowContent = getQuestionnaireFlowContent();
812
- await memoryVfs.write('/test/questionnaires-bug.narrative.ts', new TextEncoder().encode(questionnaireFlowContent));
813
- const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
814
- return flows.toModel();
815
- }
816
- function getQuestionnaireFlowContent() {
817
- return `
818
- import {
819
- command,
820
- query,
821
- experience,
822
- flow,
823
- should,
824
- specs,
825
- rule,
826
- example,
827
- gql,
828
- source,
829
- data,
830
- sink,
831
- type Command,
832
- type Event,
833
- type State,
834
- } from '@auto-engineer/narrative';
835
-
836
- type SendQuestionnaireLink = Command<
837
- 'SendQuestionnaireLink',
838
- {
839
- questionnaireId: string;
840
- participantId: string;
841
- }
842
- >;
843
-
844
- type QuestionnaireLinkSent = Event<
845
- 'QuestionnaireLinkSent',
846
- {
847
- questionnaireId: string;
848
- participantId: string;
849
- link: string;
850
- sentAt: Date;
851
- }
852
- >;
853
-
854
- type QuestionnaireSubmitted = Event<
855
- 'QuestionnaireSubmitted',
856
- {
857
- questionnaireId: string;
858
- participantId: string;
859
- submittedAt: Date;
860
- }
861
- >;
862
-
863
- type SubmitQuestionnaire = Command<
864
- 'SubmitQuestionnaire',
865
- {
866
- questionnaireId: string;
867
- participantId: string;
868
- }
869
- >;
870
-
871
- flow('Questionnaires', 'AUTO-Q9m2Kp4Lx', () => {
872
- command('sends the questionnaire link', 'AUTO-S2b5Cp7Dz')
873
- .server(() => {
874
- specs(() => {
875
- rule('questionnaire link is sent to participant', 'AUTO-r0A1Bo8X', () => {
876
- example('sends the questionnaire link successfully')
877
- .when<SendQuestionnaireLink>({
878
- questionnaireId: 'q-001',
879
- participantId: 'participant-abc',
880
- })
881
- .then<QuestionnaireLinkSent>({
882
- questionnaireId: 'q-001',
883
- participantId: 'participant-abc',
884
- link: 'https://app.example.com/q/q-001?participant=participant-abc',
885
- sentAt: new Date('2030-01-01T09:00:00Z'),
886
- });
887
- });
888
- });
889
- data([sink().event('QuestionnaireLinkSent').toStream('questionnaire-participantId')]);
890
- })
891
- .request(gql\`
892
- mutation SendQuestionnaireLink($input: SendQuestionnaireLinkInput!) {
893
- sendQuestionnaireLink(input: $input) {
894
- success
895
- }
896
- }
897
- \`)
898
- .client(() => {
899
- specs('Questionnaire Link', () => {
900
- should('display a confirmation message when the link is sent');
901
- should('handle errors when the link cannot be sent');
902
- });
903
- });
904
-
905
- command('submits the questionnaire', 'AUTO-T5k9Jw3V')
906
- .server(() => {
907
- specs(() => {
908
- rule('questionnaire allowed to be submitted when all questions are answered', 'AUTO-r4H0Lx4U', () => {
909
- example('submits the questionnaire successfully')
910
- .when<SubmitQuestionnaire>({
911
- questionnaireId: 'q-001',
912
- participantId: 'participant-abc',
913
- })
914
- .then<QuestionnaireSubmitted>({
915
- questionnaireId: 'q-001',
916
- participantId: 'participant-abc',
917
- submittedAt: new Date('2030-01-01T09:00:00Z'),
918
- });
919
- });
920
- });
921
- data([sink().event('QuestionnaireSubmitted').toStream('questionnaire-participantId')]);
922
- })
923
- .request(gql\`
924
- mutation SubmitQuestionnaire($input: SubmitQuestionnaireInput!) {
925
- submitQuestionnaire(input: $input) {
926
- success
927
- }
928
- }
929
- \`)
930
- .client(() => {
931
- specs('Submission Confirmation', () => {
932
- should('display a confirmation message upon successful submission');
933
- });
934
- });
935
- });`;
936
- }
937
- function validateQuestionnaireBugFix(model) {
938
- const questionnaireFlow = getQuestionnaireFlowFromModel(model);
939
- const submitSlice = getSubmitSlice(questionnaireFlow);
940
- const submitExample = getSubmitExample(submitSlice);
941
- validateSubmitCommandRef(submitExample);
942
- validateLinkSliceCommandRef(questionnaireFlow);
943
- }
944
- function getQuestionnaireFlowFromModel(model) {
945
- const questionnaireFlow = model.narratives.find((f) => f.name === 'Questionnaires');
946
- expect(questionnaireFlow).toBeDefined();
947
- if (questionnaireFlow === null || questionnaireFlow === undefined) {
948
- throw new Error('Questionnaire flow not found');
949
- }
950
- return questionnaireFlow;
951
- }
952
- function getSubmitSlice(questionnaireFlow) {
953
- const submitSlice = questionnaireFlow.slices.find((s) => s.name === 'submits the questionnaire');
954
- expect(submitSlice).toBeDefined();
955
- expect(submitSlice?.type).toBe('command');
956
- if (submitSlice?.type !== 'command') {
957
- throw new Error('Submit slice is not a command');
958
- }
959
- return submitSlice;
960
- }
961
- function getSubmitExample(submitSlice) {
962
- const rule = submitSlice.server?.specs?.rules[0];
963
- expect(rule).toBeDefined();
964
- expect(rule?.examples).toHaveLength(1);
965
- const example = rule?.examples[0];
966
- expect(example?.description).toBe('submits the questionnaire successfully');
967
- return example;
968
- }
969
- function validateSubmitCommandRef(example) {
970
- const ex = example;
971
- expect(ex?.when).toBeDefined();
972
- if (typeof ex?.when === 'object' && ex.when !== null && !Array.isArray(ex.when) && 'commandRef' in ex.when) {
973
- expect(ex.when.commandRef).toBe('SubmitQuestionnaire');
974
- expect(ex.when.commandRef).not.toBe('SendQuestionnaireLink');
975
- }
976
- else {
977
- throw new Error('Expected when to have commandRef property');
978
- }
979
- }
980
- function validateLinkSliceCommandRef(questionnaireFlow) {
981
- const linkSlice = questionnaireFlow.slices.find((s) => s.name === 'sends the questionnaire link');
982
- expect(linkSlice?.type).toBe('command');
983
- if (linkSlice?.type === 'command') {
984
- const linkExample = linkSlice.server?.specs?.rules[0]?.examples[0];
985
- const ex = linkExample;
986
- if (typeof ex?.when === 'object' && ex.when !== null && !Array.isArray(ex.when) && 'commandRef' in ex.when) {
987
- expect(ex.when.commandRef).toBe('SendQuestionnaireLink');
988
- }
989
- }
990
- }
991
- function validateCommandRef(model) {
992
- const questionnaireFlow = getQuestionnaireFlowFromModel(model);
993
- const submitSlice = getSubmitSliceFromFlow(questionnaireFlow);
994
- const serverSpecs = getServerSpecsFromSlice(submitSlice);
995
- const rule = getFirstRuleFromSpecs(serverSpecs);
996
- const example = getFirstExampleFromRule(rule);
997
- validateExampleCommandRef(example);
998
- validateThenEvents(example);
999
- }
1000
- function getSubmitSliceFromFlow(questionnaireFlow) {
1001
- const submitSlice = questionnaireFlow.slices.find((s) => s.name === 'submits the questionnaire');
1002
- expect(submitSlice).toBeDefined();
1003
- expect(submitSlice?.type).toBe('command');
1004
- if (submitSlice?.type !== 'command') {
1005
- throw new Error('Submit slice is not a command');
1006
- }
1007
- return submitSlice;
1008
- }
1009
- function getServerSpecsFromSlice(submitSlice) {
1010
- const slice = submitSlice;
1011
- const serverSpecs = slice.server?.specs;
1012
- expect(serverSpecs).toBeDefined();
1013
- const specs = serverSpecs;
1014
- expect(specs?.rules).toBeDefined();
1015
- expect(specs?.rules).toHaveLength(1);
1016
- return serverSpecs;
1017
- }
1018
- function getFirstRuleFromSpecs(serverSpecs) {
1019
- const specs = serverSpecs;
1020
- const rule = specs?.rules?.[0];
1021
- expect(rule).toBeDefined();
1022
- const r = rule;
1023
- expect(r?.description).toBe('questionnaire allowed to be submitted when all questions are answered');
1024
- expect(r?.examples).toBeDefined();
1025
- expect(r?.examples).toHaveLength(1);
1026
- return rule;
1027
- }
1028
- function getFirstExampleFromRule(rule) {
1029
- const r = rule;
1030
- const example = r?.examples?.[0];
1031
- expect(example).toBeDefined();
1032
- const ex = example;
1033
- expect(ex?.description).toBe('submits the questionnaire successfully');
1034
- return example;
1035
- }
1036
- function validateExampleCommandRef(example) {
1037
- const ex = example;
1038
- expect(ex?.when).toBeDefined();
1039
- if (typeof ex?.when === 'object' && ex.when !== null && !Array.isArray(ex.when) && 'commandRef' in ex.when) {
1040
- expect(ex.when.commandRef).toBe('SubmitQuestionnaire');
1041
- expect(ex.when.commandRef).not.toBe('SendQuestionnaireLink');
1042
- expect(ex.when.exampleData).toEqual({
1043
- questionnaireId: 'q-001',
1044
- participantId: 'participant-abc',
1045
- });
1046
- }
1047
- else {
1048
- throw new Error('Expected when to have commandRef property');
1049
- }
1050
- }
1051
- function validateThenEvents(example) {
1052
- const ex = example;
1053
- expect(ex?.then).toBeDefined();
1054
- expect(Array.isArray(ex?.then)).toBe(true);
1055
- expect(ex?.then).toHaveLength(1);
1056
- const thenEvent = ex?.then?.[0];
1057
- if (thenEvent !== null && thenEvent !== undefined && 'eventRef' in thenEvent) {
1058
- const event = thenEvent;
1059
- expect(event.eventRef).toBe('QuestionnaireSubmitted');
1060
- expect(event.exampleData).toEqual({
1061
- questionnaireId: 'q-001',
1062
- participantId: 'participant-abc',
1063
- submittedAt: new Date('2030-01-01T09:00:00.000Z'),
1064
- });
1065
- }
1066
- }
1067
- describe('projection DSL methods', () => {
1068
- it('should generate correct origin for singleton projection', async () => {
1069
- const memoryVfs = new InMemoryFileStore();
1070
- const flowContent = `
1071
- import { flow, query, specs, rule, example, data, source, type Event, type State } from '@auto-engineer/narrative';
1072
-
1073
- type TodoAdded = Event<'TodoAdded', { todoId: string; description: string; addedAt: Date }>;
1074
- type TodoListSummary = State<'TodoListSummary', { summaryId: string; totalTodos: number }>;
1075
-
1076
- flow('Projection Test', () => {
1077
- query('views summary')
1078
- .server(() => {
1079
- specs(() => {
1080
- rule('shows summary', () => {
1081
- example('summary')
1082
- .given<TodoAdded>({ todoId: 'todo-001', description: 'Test', addedAt: new Date('2030-01-01T09:00:00Z') })
1083
- .when({})
1084
- .then<TodoListSummary>({ summaryId: 'main', totalTodos: 1 });
1085
- });
1086
- });
1087
- data([source().state<TodoListSummary>('TodoListSummary').fromSingletonProjection('TodoSummary')]);
1088
- });
1089
- });
1090
- `;
1091
- await memoryVfs.write('/test/projection.narrative.ts', new TextEncoder().encode(flowContent));
1092
- const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
1093
- const model = flows.toModel();
1094
- const projectionFlow = model.narratives.find((f) => f.name === 'Projection Test');
1095
- expect(projectionFlow).toBeDefined();
1096
- if (!projectionFlow)
1097
- return;
1098
- const summarySlice = projectionFlow.slices.find((s) => s.name === 'views summary');
1099
- expect(summarySlice?.type).toBe('query');
1100
- if (summarySlice?.type !== 'query')
1101
- return;
1102
- const data = summarySlice.server.data;
1103
- expect(data).toBeDefined();
1104
- expect(data).toHaveLength(1);
1105
- expect(data?.[0].origin).toMatchObject({
1106
- type: 'projection',
1107
- name: 'TodoSummary',
1108
- singleton: true,
1109
- });
1110
- expect(data?.[0].origin).not.toHaveProperty('idField');
1111
- });
1112
- it('should generate correct origin for regular projection with single idField', async () => {
1113
- const memoryVfs = new InMemoryFileStore();
1114
- const flowContent = `
1115
- import { flow, query, specs, rule, example, data, source, type Event, type State } from '@auto-engineer/narrative';
1116
-
1117
- type TodoAdded = Event<'TodoAdded', { todoId: string; description: string; addedAt: Date }>;
1118
- type TodoState = State<'TodoState', { todoId: string; description: string; status: string }>;
1119
-
1120
- flow('Projection Test', () => {
1121
- query('views todo')
1122
- .server(() => {
1123
- specs(() => {
1124
- rule('shows todo', () => {
1125
- example('todo')
1126
- .given<TodoAdded>({ todoId: 'todo-001', description: 'Test', addedAt: new Date('2030-01-01T09:00:00Z') })
1127
- .when({})
1128
- .then<TodoState>({ todoId: 'todo-001', description: 'Test', status: 'pending' });
1129
- });
1130
- });
1131
- data([source().state<TodoState>('TodoState').fromProjection('Todos', 'todoId')]);
1132
- });
1133
- });
1134
- `;
1135
- await memoryVfs.write('/test/projection.narrative.ts', new TextEncoder().encode(flowContent));
1136
- const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
1137
- const model = flows.toModel();
1138
- const projectionFlow = model.narratives.find((f) => f.name === 'Projection Test');
1139
- expect(projectionFlow).toBeDefined();
1140
- if (!projectionFlow)
1141
- return;
1142
- const todoSlice = projectionFlow.slices.find((s) => s.name === 'views todo');
1143
- expect(todoSlice?.type).toBe('query');
1144
- if (todoSlice?.type !== 'query')
1145
- return;
1146
- const data = todoSlice.server.data;
1147
- expect(data).toBeDefined();
1148
- expect(data).toHaveLength(1);
1149
- expect(data?.[0].origin).toMatchObject({
1150
- type: 'projection',
1151
- name: 'Todos',
1152
- idField: 'todoId',
1153
- });
1154
- expect(data?.[0].origin).not.toHaveProperty('singleton');
1155
- });
1156
- it('should generate correct origin for composite projection with multiple idFields', async () => {
1157
- const memoryVfs = new InMemoryFileStore();
1158
- const flowContent = `
1159
- import { flow, query, specs, rule, example, data, source, type Event, type State } from '@auto-engineer/narrative';
1160
-
1161
- type UserProjectAssigned = Event<'UserProjectAssigned', { userId: string; projectId: string; assignedAt: Date }>;
1162
- type UserProjectState = State<'UserProjectState', { userId: string; projectId: string; role: string }>;
1163
-
1164
- flow('Projection Test', () => {
1165
- query('views user project')
1166
- .server(() => {
1167
- specs(() => {
1168
- rule('shows user project', () => {
1169
- example('user project')
1170
- .given<UserProjectAssigned>({ userId: 'user-001', projectId: 'proj-001', assignedAt: new Date('2030-01-01T09:00:00Z') })
1171
- .when({})
1172
- .then<UserProjectState>({ userId: 'user-001', projectId: 'proj-001', role: 'admin' });
1173
- });
1174
- });
1175
- data([source().state<UserProjectState>('UserProjectState').fromCompositeProjection('UserProjects', ['userId', 'projectId'])]);
1176
- });
1177
- });
1178
- `;
1179
- await memoryVfs.write('/test/projection.narrative.ts', new TextEncoder().encode(flowContent));
1180
- const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
1181
- const model = flows.toModel();
1182
- const projectionFlow = model.narratives.find((f) => f.name === 'Projection Test');
1183
- expect(projectionFlow).toBeDefined();
1184
- if (!projectionFlow)
1185
- return;
1186
- const userProjectSlice = projectionFlow.slices.find((s) => s.name === 'views user project');
1187
- expect(userProjectSlice?.type).toBe('query');
1188
- if (userProjectSlice?.type !== 'query')
1189
- return;
1190
- const data = userProjectSlice.server.data;
1191
- expect(data).toBeDefined();
1192
- expect(data).toHaveLength(1);
1193
- expect(data?.[0].origin).toMatchObject({
1194
- type: 'projection',
1195
- name: 'UserProjects',
1196
- idField: ['userId', 'projectId'],
1197
- });
1198
- expect(data?.[0].origin).not.toHaveProperty('singleton');
1199
- });
1200
- it('should validate all three projection patterns together', async () => {
1201
- const memoryVfs = new InMemoryFileStore();
1202
- const flowContent = `
1203
- import { flow, query, specs, rule, example, data, source, type Event, type State } from '@auto-engineer/narrative';
1204
-
1205
- type TodoAdded = Event<'TodoAdded', { todoId: string; userId: string; projectId: string; description: string; addedAt: Date }>;
1206
-
1207
- type TodoListSummary = State<'TodoListSummary', { summaryId: string; totalTodos: number }>;
1208
- type TodoState = State<'TodoState', { todoId: string; description: string; status: string }>;
1209
- type UserProjectTodos = State<'UserProjectTodos', { userId: string; projectId: string; todos: string[] }>;
1210
-
1211
- flow('All Projection Patterns', () => {
1212
- query('views summary')
1213
- .server(() => {
1214
- specs(() => {
1215
- rule('shows summary', () => {
1216
- example('summary')
1217
- .given<TodoAdded>({ todoId: 'todo-001', userId: 'u1', projectId: 'p1', description: 'Test', addedAt: new Date('2030-01-01T09:00:00Z') })
1218
- .when({})
1219
- .then<TodoListSummary>({ summaryId: 'main', totalTodos: 1 });
1220
- });
1221
- });
1222
- data([source().state<TodoListSummary>('TodoListSummary').fromSingletonProjection('TodoSummary')]);
1223
- });
1224
-
1225
- query('views todo')
1226
- .server(() => {
1227
- specs(() => {
1228
- rule('shows todo', () => {
1229
- example('todo')
1230
- .given<TodoAdded>({ todoId: 'todo-001', userId: 'u1', projectId: 'p1', description: 'Test', addedAt: new Date('2030-01-01T09:00:00Z') })
1231
- .when({})
1232
- .then<TodoState>({ todoId: 'todo-001', description: 'Test', status: 'pending' });
1233
- });
1234
- });
1235
- data([source().state<TodoState>('TodoState').fromProjection('Todos', 'todoId')]);
1236
- });
1237
-
1238
- query('views user project todos')
1239
- .server(() => {
1240
- specs(() => {
1241
- rule('shows user project todos', () => {
1242
- example('user project todos')
1243
- .given<TodoAdded>({ todoId: 'todo-001', userId: 'u1', projectId: 'p1', description: 'Test', addedAt: new Date('2030-01-01T09:00:00Z') })
1244
- .when({})
1245
- .then<UserProjectTodos>({ userId: 'u1', projectId: 'p1', todos: ['todo-001'] });
1246
- });
1247
- });
1248
- data([source().state<UserProjectTodos>('UserProjectTodos').fromCompositeProjection('UserProjectTodos', ['userId', 'projectId'])]);
1249
- });
1250
- });
1251
- `;
1252
- await memoryVfs.write('/test/projection.narrative.ts', new TextEncoder().encode(flowContent));
1253
- const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
1254
- const model = flows.toModel();
1255
- const parseResult = modelSchema.safeParse(model);
1256
- if (!parseResult.success) {
1257
- console.error('Schema validation errors:', parseResult.error.format());
1258
- }
1259
- expect(parseResult.success).toBe(true);
1260
- const projectionFlow = model.narratives.find((f) => f.name === 'All Projection Patterns');
1261
- expect(projectionFlow).toBeDefined();
1262
- if (!projectionFlow)
1263
- return;
1264
- expect(projectionFlow.slices).toHaveLength(3);
1265
- const summarySlice = projectionFlow.slices.find((s) => s.name === 'views summary');
1266
- if (summarySlice?.type === 'query') {
1267
- const data = summarySlice.server.data;
1268
- expect(data?.[0].origin).toMatchObject({
1269
- type: 'projection',
1270
- name: 'TodoSummary',
1271
- singleton: true,
1272
- });
1273
- }
1274
- const todoSlice = projectionFlow.slices.find((s) => s.name === 'views todo');
1275
- if (todoSlice?.type === 'query') {
1276
- const data = todoSlice.server.data;
1277
- expect(data?.[0].origin).toMatchObject({
1278
- type: 'projection',
1279
- name: 'Todos',
1280
- idField: 'todoId',
1281
- });
1282
- }
1283
- const userProjectSlice = projectionFlow.slices.find((s) => s.name === 'views user project todos');
1284
- if (userProjectSlice?.type === 'query') {
1285
- const data = userProjectSlice.server.data;
1286
- expect(data?.[0].origin).toMatchObject({
1287
- type: 'projection',
1288
- name: 'UserProjectTodos',
1289
- idField: ['userId', 'projectId'],
1290
- });
1291
- }
1292
- });
1293
- });
1294
- //# sourceMappingURL=getNarratives.specs.js.map