@auto-engineer/narrative 1.139.0 → 1.140.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/scripts/convert-flow-exec.js +2 -2
- package/dist/scripts/convert-flow-exec.js.map +1 -1
- package/dist/scripts/print-schema.js +5 -5
- package/dist/scripts/print-schema.js.map +1 -1
- package/dist/src/fluent-builder.d.ts +29 -29
- package/dist/src/fluent-builder.d.ts.map +1 -1
- package/dist/src/fluent-builder.js +81 -81
- package/dist/src/fluent-builder.js.map +1 -1
- package/dist/src/{getNarratives.d.ts → getScenes.d.ts} +6 -6
- package/dist/src/getScenes.d.ts.map +1 -0
- package/dist/src/{getNarratives.js → getScenes.js} +16 -16
- package/dist/src/getScenes.js.map +1 -0
- package/dist/src/id/addAutoIds.d.ts.map +1 -1
- package/dist/src/id/addAutoIds.js +22 -22
- package/dist/src/id/addAutoIds.js.map +1 -1
- package/dist/src/id/hasAllIds.d.ts.map +1 -1
- package/dist/src/id/hasAllIds.js +2 -2
- package/dist/src/id/hasAllIds.js.map +1 -1
- package/dist/src/index.d.ts +8 -8
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/loader/index.js +1 -1
- package/dist/src/loader/index.js.map +1 -1
- package/dist/src/loader/runtime-cjs.js +1 -1
- package/dist/src/loader/runtime-cjs.js.map +1 -1
- package/dist/src/narrative-context.d.ts +9 -9
- package/dist/src/narrative-context.d.ts.map +1 -1
- package/dist/src/narrative-context.js +47 -47
- package/dist/src/narrative-context.js.map +1 -1
- package/dist/src/narrative-registry.d.ts +6 -6
- package/dist/src/narrative-registry.d.ts.map +1 -1
- package/dist/src/narrative-registry.js +26 -26
- package/dist/src/narrative-registry.js.map +1 -1
- package/dist/src/narrative.d.ts +5 -5
- package/dist/src/narrative.d.ts.map +1 -1
- package/dist/src/narrative.js +26 -27
- package/dist/src/narrative.js.map +1 -1
- package/dist/src/parse-graphql-request.d.ts +1 -1
- package/dist/src/parse-graphql-request.d.ts.map +1 -1
- package/dist/src/parse-graphql-request.js +3 -3
- package/dist/src/parse-graphql-request.js.map +1 -1
- package/dist/src/samples/items.narrative.js +2 -2
- package/dist/src/samples/items.narrative.js.map +1 -1
- package/dist/src/samples/mixed-given-types.narrative.js +2 -2
- package/dist/src/samples/mixed-given-types.narrative.js.map +1 -1
- package/dist/src/samples/place-order.narrative.js +2 -2
- package/dist/src/samples/place-order.narrative.js.map +1 -1
- package/dist/src/samples/questionnaires.narrative.js +2 -2
- package/dist/src/samples/questionnaires.narrative.js.map +1 -1
- package/dist/src/samples/seasonal-assistant.schema.json +2 -2
- package/dist/src/samples/test-with-ids.narrative.js +2 -2
- package/dist/src/samples/test-with-ids.narrative.js.map +1 -1
- package/dist/src/schema.d.ts +136 -136
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +76 -76
- package/dist/src/schema.js.map +1 -1
- package/dist/src/slice-builder.d.ts +6 -6
- package/dist/src/slice-builder.d.ts.map +1 -1
- package/dist/src/slice-builder.js +21 -21
- package/dist/src/slice-builder.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/analysis/lint-helpers.js +1 -1
- package/dist/src/transformers/model-to-narrative/analysis/lint-helpers.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/cross-module-imports.js +4 -4
- package/dist/src/transformers/model-to-narrative/cross-module-imports.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/compose.js +3 -3
- package/dist/src/transformers/model-to-narrative/generators/compose.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.d.ts +2 -2
- package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.js +20 -20
- package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.d.ts +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.js +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/module-code.js +14 -14
- package/dist/src/transformers/model-to-narrative/generators/module-code.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/index.d.ts +4 -4
- package/dist/src/transformers/model-to-narrative/index.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/index.js +1 -1
- package/dist/src/transformers/model-to-narrative/spec-traversal.d.ts +2 -2
- package/dist/src/transformers/model-to-narrative/spec-traversal.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/spec-traversal.js +5 -5
- package/dist/src/transformers/model-to-narrative/spec-traversal.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/types.d.ts +1 -1
- package/dist/src/transformers/model-to-narrative/types.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/utils/integration-extractor.d.ts +1 -1
- package/dist/src/transformers/model-to-narrative/utils/integration-extractor.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/utils/integration-extractor.js +4 -4
- package/dist/src/transformers/model-to-narrative/utils/integration-extractor.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/validate-modules.d.ts +1 -1
- package/dist/src/transformers/model-to-narrative/validate-modules.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/validate-modules.js +19 -19
- package/dist/src/transformers/model-to-narrative/validate-modules.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/assemble.d.ts +2 -2
- package/dist/src/transformers/narrative-to-model/assemble.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/assemble.js +10 -3
- package/dist/src/transformers/narrative-to-model/assemble.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/debug.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/debug.js +1 -1
- package/dist/src/transformers/narrative-to-model/debug.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/derive-modules.d.ts +2 -2
- package/dist/src/transformers/narrative-to-model/derive-modules.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/derive-modules.js +9 -9
- package/dist/src/transformers/narrative-to-model/derive-modules.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/index.d.ts +2 -2
- package/dist/src/transformers/narrative-to-model/index.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/index.js +39 -39
- package/dist/src/transformers/narrative-to-model/index.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/spec-processors.js +1 -1
- package/dist/src/transformers/narrative-to-model/spec-processors.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/strings.d.ts +1 -1
- package/dist/src/transformers/narrative-to-model/strings.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/strings.js +6 -6
- package/dist/src/transformers/narrative-to-model/strings.js.map +1 -1
- package/dist/src/validate-slice-requests.d.ts +4 -4
- package/dist/src/validate-slice-requests.d.ts.map +1 -1
- package/dist/src/validate-slice-requests.js +34 -34
- package/dist/src/validate-slice-requests.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/scripts/convert-flow-exec.ts +2 -2
- package/scripts/print-schema.ts +8 -8
- package/src/fluent-builder.specs.ts +3 -3
- package/src/fluent-builder.ts +141 -141
- package/src/{getNarratives.cache.specs.ts → getScenes.cache.specs.ts} +45 -45
- package/src/{getNarratives.specs.ts → getScenes.specs.ts} +302 -300
- package/src/{getNarratives.ts → getScenes.ts} +20 -20
- package/src/id/addAutoIds.specs.ts +105 -105
- package/src/id/addAutoIds.ts +26 -26
- package/src/id/hasAllIds.specs.ts +59 -59
- package/src/id/hasAllIds.ts +6 -6
- package/src/index.ts +12 -13
- package/src/loader/index.ts +1 -1
- package/src/loader/runtime-cjs.ts +1 -1
- package/src/model-to-narrative.specs.ts +133 -135
- package/src/narrative-context.specs.ts +24 -24
- package/src/narrative-context.ts +61 -61
- package/src/narrative-registry.ts +31 -31
- package/src/narrative.ts +31 -33
- package/src/parse-graphql-request.specs.ts +5 -5
- package/src/parse-graphql-request.ts +3 -3
- package/src/samples/items.narrative.ts +2 -2
- package/src/samples/mixed-given-types.narrative.ts +2 -2
- package/src/samples/place-order.narrative.ts +2 -2
- package/src/samples/questionnaires.narrative.ts +2 -2
- package/src/samples/seasonal-assistant.schema.json +2 -2
- package/src/samples/test-with-ids.narrative.ts +2 -2
- package/src/schema.specs.ts +99 -91
- package/src/schema.ts +89 -89
- package/src/slice-builder.ts +30 -30
- package/src/transformers/model-to-narrative/analysis/lint-helpers.ts +1 -1
- package/src/transformers/model-to-narrative/cross-module-imports.specs.ts +43 -43
- package/src/transformers/model-to-narrative/cross-module-imports.ts +4 -4
- package/src/transformers/model-to-narrative/generators/compose.ts +4 -4
- package/src/transformers/model-to-narrative/generators/flow.ts +36 -36
- package/src/transformers/model-to-narrative/generators/imports.ts +1 -1
- package/src/transformers/model-to-narrative/generators/module-code.ts +15 -15
- package/src/transformers/model-to-narrative/index.ts +4 -4
- package/src/transformers/model-to-narrative/modules.specs.ts +58 -58
- package/src/transformers/model-to-narrative/spec-traversal.specs.ts +43 -43
- package/src/transformers/model-to-narrative/spec-traversal.ts +6 -6
- package/src/transformers/model-to-narrative/types.ts +1 -1
- package/src/transformers/model-to-narrative/utils/integration-extractor.ts +5 -5
- package/src/transformers/model-to-narrative/validate-modules.ts +22 -22
- package/src/transformers/narrative-to-model/assemble.ts +12 -4
- package/src/transformers/narrative-to-model/debug.ts +1 -1
- package/src/transformers/narrative-to-model/derive-modules.specs.ts +35 -35
- package/src/transformers/narrative-to-model/derive-modules.ts +11 -11
- package/src/transformers/narrative-to-model/index.ts +47 -47
- package/src/transformers/narrative-to-model/spec-processors.ts +1 -1
- package/src/transformers/narrative-to-model/strings.ts +6 -6
- package/src/transformers/narrative-to-model/type-inference.specs.ts +11 -11
- package/src/validate-slice-requests.specs.ts +113 -113
- package/src/validate-slice-requests.ts +49 -49
- package/dist/src/getNarratives.d.ts.map +0 -1
- package/dist/src/getNarratives.js.map +0 -1
|
@@ -3,15 +3,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
import { InMemoryFileStore } from '@auto-engineer/file-store';
|
|
4
4
|
import { NodeFileStore } from '@auto-engineer/file-store/node';
|
|
5
5
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
import { type Example, type Model, modelToNarrative, type
|
|
6
|
+
import { clearGetScenesCache, getScenes } from './getScenes';
|
|
7
|
+
import { type Example, type Model, modelToNarrative, type QueryMoment, type Scene } from './index';
|
|
8
8
|
import { modelSchema } from './schema';
|
|
9
9
|
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
const pattern = /\.(narrative)\.(ts)$/;
|
|
13
13
|
|
|
14
|
-
describe('
|
|
14
|
+
describe('getScenes', (_mode) => {
|
|
15
15
|
let vfs: NodeFileStore;
|
|
16
16
|
let root: string;
|
|
17
17
|
|
|
@@ -19,8 +19,8 @@ describe('getNarratives', (_mode) => {
|
|
|
19
19
|
vfs = new NodeFileStore();
|
|
20
20
|
root = path.resolve(__dirname);
|
|
21
21
|
});
|
|
22
|
-
it('loads multiple
|
|
23
|
-
const flows = await
|
|
22
|
+
it('loads multiple scenes and generates correct models', async () => {
|
|
23
|
+
const flows = await getScenes({ vfs, root: path.resolve(__dirname), pattern, fastFsScan: true });
|
|
24
24
|
const schemas = flows.toModel();
|
|
25
25
|
|
|
26
26
|
const parseResult = modelSchema.safeParse(schemas);
|
|
@@ -30,11 +30,11 @@ describe('getNarratives', (_mode) => {
|
|
|
30
30
|
expect(parseResult.success).toBe(true);
|
|
31
31
|
|
|
32
32
|
expect(schemas).toHaveProperty('variant', 'specs');
|
|
33
|
-
expect(schemas).toHaveProperty('
|
|
33
|
+
expect(schemas).toHaveProperty('scenes');
|
|
34
34
|
expect(schemas).toHaveProperty('messages');
|
|
35
35
|
expect(schemas).toHaveProperty('integrations');
|
|
36
36
|
|
|
37
|
-
const flowsArray = schemas.
|
|
37
|
+
const flowsArray = schemas.scenes;
|
|
38
38
|
expect(Array.isArray(flowsArray)).toBe(true);
|
|
39
39
|
expect(flowsArray.length).toBeGreaterThanOrEqual(2);
|
|
40
40
|
|
|
@@ -48,24 +48,24 @@ describe('getNarratives', (_mode) => {
|
|
|
48
48
|
expect(placeOrder).toBeDefined();
|
|
49
49
|
|
|
50
50
|
if (items) {
|
|
51
|
-
expect(items.
|
|
52
|
-
const
|
|
53
|
-
expect(
|
|
54
|
-
expect(
|
|
55
|
-
expect(
|
|
56
|
-
if (
|
|
57
|
-
expect(
|
|
58
|
-
expect(Array.isArray(
|
|
59
|
-
expect(
|
|
60
|
-
expect(
|
|
61
|
-
expect(
|
|
62
|
-
if (
|
|
63
|
-
expect(
|
|
51
|
+
expect(items.moments).toHaveLength(2);
|
|
52
|
+
const createItemMoment = items.moments[0];
|
|
53
|
+
expect(createItemMoment.type).toBe('command');
|
|
54
|
+
expect(createItemMoment.name).toBe('Create item');
|
|
55
|
+
expect(createItemMoment.stream).toBe('item-${id}');
|
|
56
|
+
if (createItemMoment.type === 'command') {
|
|
57
|
+
expect(createItemMoment.client.specs).toBeDefined();
|
|
58
|
+
expect(Array.isArray(createItemMoment.client.specs)).toBe(true);
|
|
59
|
+
expect(createItemMoment.client.specs).toHaveLength(1);
|
|
60
|
+
expect(createItemMoment.client.specs[0].type).toBe('describe');
|
|
61
|
+
expect(createItemMoment.client.specs[0].title).toBe('A form that allows users to add items');
|
|
62
|
+
if (createItemMoment.client.specs[0].type === 'describe') {
|
|
63
|
+
expect(createItemMoment.client.specs[0].children).toHaveLength(1);
|
|
64
64
|
}
|
|
65
|
-
expect(
|
|
66
|
-
expect(Array.isArray(
|
|
67
|
-
expect(
|
|
68
|
-
const spec =
|
|
65
|
+
expect(createItemMoment.server.specs).toBeDefined();
|
|
66
|
+
expect(Array.isArray(createItemMoment.server.specs)).toBe(true);
|
|
67
|
+
expect(createItemMoment.server.specs).toHaveLength(1);
|
|
68
|
+
const spec = createItemMoment.server.specs[0];
|
|
69
69
|
expect(spec.feature).toBeDefined();
|
|
70
70
|
expect(spec.rules).toHaveLength(1);
|
|
71
71
|
const rule = spec.rules[0];
|
|
@@ -95,55 +95,55 @@ describe('getNarratives', (_mode) => {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
const
|
|
99
|
-
expect(
|
|
100
|
-
expect(
|
|
101
|
-
expect(
|
|
102
|
-
expect(Array.isArray(
|
|
103
|
-
expect(
|
|
104
|
-
expect(
|
|
105
|
-
expect(
|
|
106
|
-
if (
|
|
107
|
-
expect(
|
|
98
|
+
const viewItemMoment = items.moments[1] as QueryMoment;
|
|
99
|
+
expect(viewItemMoment.type).toBe('query');
|
|
100
|
+
expect(viewItemMoment.name).toBe('view items');
|
|
101
|
+
expect(viewItemMoment.client.specs).toBeDefined();
|
|
102
|
+
expect(Array.isArray(viewItemMoment.client.specs)).toBe(true);
|
|
103
|
+
expect(viewItemMoment.client.specs).toHaveLength(1);
|
|
104
|
+
expect(viewItemMoment.client.specs[0].type).toBe('describe');
|
|
105
|
+
expect(viewItemMoment.client.specs[0].title).toBe('view Items Screen');
|
|
106
|
+
if (viewItemMoment.client.specs[0].type === 'describe') {
|
|
107
|
+
expect(viewItemMoment.client.specs[0].children).toHaveLength(3);
|
|
108
108
|
}
|
|
109
|
-
expect(
|
|
110
|
-
expect(
|
|
109
|
+
expect(viewItemMoment.request).toBeDefined();
|
|
110
|
+
expect(viewItemMoment.request).toMatch(
|
|
111
111
|
/query items\(\$itemId: String!\) {\s+items\(itemId: \$itemId\) {\s+id\s+description\s+}/,
|
|
112
112
|
);
|
|
113
113
|
|
|
114
|
-
const data =
|
|
114
|
+
const data = viewItemMoment?.server?.data;
|
|
115
115
|
if (!data || !Array.isArray(data.items)) throw new Error('No data found in view items slice');
|
|
116
116
|
|
|
117
117
|
expect(data.items).toHaveLength(1);
|
|
118
118
|
expect(data.items[0].target).toMatchObject({ type: 'State', name: 'items' });
|
|
119
119
|
expect(data.items[0].origin).toMatchObject({ name: 'ItemsProjection', type: 'projection' });
|
|
120
120
|
|
|
121
|
-
const specs =
|
|
121
|
+
const specs = viewItemMoment?.server?.specs;
|
|
122
122
|
if (specs == null || specs.length === 0 || specs[0].feature === '')
|
|
123
123
|
throw new Error('No specs found in view items slice');
|
|
124
124
|
expect(specs).toBeDefined();
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
if (placeOrder) {
|
|
128
|
-
expect(placeOrder.
|
|
129
|
-
const
|
|
130
|
-
expect(
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
expect(
|
|
136
|
-
expect(Array.isArray(
|
|
137
|
-
expect(
|
|
138
|
-
expect(
|
|
139
|
-
expect(
|
|
140
|
-
if (
|
|
141
|
-
expect(
|
|
128
|
+
expect(placeOrder.moments).toHaveLength(1);
|
|
129
|
+
const submitOrderMoment = placeOrder.moments[0];
|
|
130
|
+
expect(submitOrderMoment.type).toBe('command');
|
|
131
|
+
expect(submitOrderMoment.name).toBe('Submit order');
|
|
132
|
+
expect(submitOrderMoment.stream).toBe('order-${orderId}');
|
|
133
|
+
|
|
134
|
+
if (submitOrderMoment.type === 'command') {
|
|
135
|
+
expect(submitOrderMoment.client.specs).toBeDefined();
|
|
136
|
+
expect(Array.isArray(submitOrderMoment.client.specs)).toBe(true);
|
|
137
|
+
expect(submitOrderMoment.client.specs).toHaveLength(1);
|
|
138
|
+
expect(submitOrderMoment.client.specs[0].type).toBe('describe');
|
|
139
|
+
expect(submitOrderMoment.client.specs[0].title).toBe('Order submission form');
|
|
140
|
+
if (submitOrderMoment.client.specs[0].type === 'describe') {
|
|
141
|
+
expect(submitOrderMoment.client.specs[0].children).toHaveLength(2);
|
|
142
142
|
}
|
|
143
|
-
expect(
|
|
144
|
-
expect(Array.isArray(
|
|
145
|
-
expect(
|
|
146
|
-
const spec =
|
|
143
|
+
expect(submitOrderMoment.server.specs).toBeDefined();
|
|
144
|
+
expect(Array.isArray(submitOrderMoment.server.specs)).toBe(true);
|
|
145
|
+
expect(submitOrderMoment.server.specs).toHaveLength(1);
|
|
146
|
+
const spec = submitOrderMoment.server.specs[0];
|
|
147
147
|
expect(spec.rules).toHaveLength(1);
|
|
148
148
|
const rule = spec.rules[0];
|
|
149
149
|
expect(rule.examples).toHaveLength(1);
|
|
@@ -206,21 +206,21 @@ describe('getNarratives', (_mode) => {
|
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
it('validates the complete schema with Zod', async () => {
|
|
209
|
-
const flows = await
|
|
209
|
+
const flows = await getScenes({ vfs: vfs, root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
|
|
210
210
|
const schemas = flows.toModel();
|
|
211
211
|
const parsed = modelSchema.parse(schemas);
|
|
212
212
|
expect(parsed.variant).toBe('specs');
|
|
213
|
-
expect(Array.isArray(parsed.
|
|
213
|
+
expect(Array.isArray(parsed.scenes)).toBe(true);
|
|
214
214
|
expect(Array.isArray(parsed.messages)).toBe(true);
|
|
215
215
|
expect(Array.isArray(parsed.integrations)).toBe(true);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it('should handle
|
|
219
|
-
const flows = await
|
|
218
|
+
it('should handle scenes with integrations', async () => {
|
|
219
|
+
const flows = await getScenes({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
|
|
220
220
|
const specsSchema = flows.toModel();
|
|
221
221
|
|
|
222
|
-
const flowsWithIntegrations = specsSchema.
|
|
223
|
-
f.
|
|
222
|
+
const flowsWithIntegrations = specsSchema.scenes.filter((f) =>
|
|
223
|
+
f.moments.some((s) => {
|
|
224
224
|
if (s.type === 'command' || s.type === 'query') {
|
|
225
225
|
return (
|
|
226
226
|
s.server.data?.items?.some(
|
|
@@ -240,11 +240,11 @@ describe('getNarratives', (_mode) => {
|
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
it('should handle react slices correctly', async () => {
|
|
243
|
-
const flows = await
|
|
243
|
+
const flows = await getScenes({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
|
|
244
244
|
const specsSchema = flows.toModel();
|
|
245
245
|
|
|
246
|
-
const
|
|
247
|
-
|
|
246
|
+
const reactMoments = specsSchema.scenes.flatMap((f) => f.moments.filter((s) => s.type === 'react'));
|
|
247
|
+
reactMoments.forEach((slice) => {
|
|
248
248
|
if (slice.type === 'react') {
|
|
249
249
|
expect(slice.server).toBeDefined();
|
|
250
250
|
expect(slice.server.specs).toBeDefined();
|
|
@@ -264,7 +264,7 @@ describe('getNarratives', (_mode) => {
|
|
|
264
264
|
});
|
|
265
265
|
|
|
266
266
|
it('should parse and validate a complete flow with all slice types', async () => {
|
|
267
|
-
const flows = await
|
|
267
|
+
const flows = await getScenes({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
|
|
268
268
|
const schemas = flows.toModel();
|
|
269
269
|
|
|
270
270
|
const validationResult = modelSchema.safeParse(schemas);
|
|
@@ -275,8 +275,8 @@ describe('getNarratives', (_mode) => {
|
|
|
275
275
|
|
|
276
276
|
const validatedData = validationResult.data!;
|
|
277
277
|
expect(
|
|
278
|
-
validatedData.
|
|
279
|
-
flow.
|
|
278
|
+
validatedData.scenes.every((flow) =>
|
|
279
|
+
flow.moments.every((slice) => {
|
|
280
280
|
if (slice.type === 'command' || slice.type === 'query') {
|
|
281
281
|
return slice.client !== undefined && slice.server !== undefined;
|
|
282
282
|
} else if (slice.type === 'react') {
|
|
@@ -290,94 +290,96 @@ describe('getNarratives', (_mode) => {
|
|
|
290
290
|
).toBe(true);
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
it('should have ids for
|
|
294
|
-
const flows = await
|
|
293
|
+
it('should have ids for scenes and slices that have ids', async () => {
|
|
294
|
+
const flows = await getScenes({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
|
|
295
295
|
|
|
296
296
|
const schemas = flows.toModel();
|
|
297
297
|
|
|
298
|
-
const testFlowWithIds = schemas.
|
|
298
|
+
const testFlowWithIds = schemas.scenes.find((f) => f.name === 'Test Flow with IDs');
|
|
299
299
|
if (!testFlowWithIds) return;
|
|
300
|
-
const
|
|
301
|
-
expect(
|
|
302
|
-
expect(
|
|
303
|
-
const
|
|
304
|
-
expect(
|
|
305
|
-
expect(
|
|
306
|
-
const
|
|
307
|
-
expect(
|
|
308
|
-
expect(
|
|
300
|
+
const commandMoment = testFlowWithIds.moments.find((s) => s.name === 'Create test item');
|
|
301
|
+
expect(commandMoment?.id).toBe('SLICE-001');
|
|
302
|
+
expect(commandMoment?.type).toBe('command');
|
|
303
|
+
const queryMoment = testFlowWithIds.moments.find((s) => s.name === 'Get test items');
|
|
304
|
+
expect(queryMoment?.id).toBe('SLICE-002');
|
|
305
|
+
expect(queryMoment?.type).toBe('query');
|
|
306
|
+
const reactMoment = testFlowWithIds.moments.find((s) => s.name === 'React to test event');
|
|
307
|
+
expect(reactMoment?.id).toBe('SLICE-003');
|
|
308
|
+
expect(reactMoment?.type).toBe('react');
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
it('should have ids for command slice rules', async () => {
|
|
312
|
-
const flows = await
|
|
312
|
+
const flows = await getScenes({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
|
|
313
313
|
const schemas = flows.toModel();
|
|
314
314
|
|
|
315
|
-
const testFlowWithIds = schemas.
|
|
315
|
+
const testFlowWithIds = schemas.scenes.find((f) => f.name === 'Test Flow with IDs');
|
|
316
316
|
if (!testFlowWithIds) return;
|
|
317
317
|
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
318
|
+
const commandMoment = testFlowWithIds.moments.find((s) => s.name === 'Create test item');
|
|
319
|
+
if (commandMoment?.type !== 'command') return;
|
|
320
320
|
|
|
321
|
-
expect(
|
|
321
|
+
expect(commandMoment.server.specs[0].rules).toHaveLength(2);
|
|
322
322
|
|
|
323
|
-
const rule1 =
|
|
323
|
+
const rule1 = commandMoment.server.specs[0].rules.find(
|
|
324
324
|
(r) => r.name === 'Valid test items should be created successfully',
|
|
325
325
|
);
|
|
326
326
|
expect(rule1?.id).toBe('RULE-001');
|
|
327
327
|
|
|
328
|
-
const rule2 =
|
|
328
|
+
const rule2 = commandMoment.server.specs[0].rules.find((r) => r.name === 'Invalid test items should be rejected');
|
|
329
329
|
expect(rule2?.id).toBe('RULE-002');
|
|
330
330
|
});
|
|
331
331
|
|
|
332
332
|
it('should have ids for query slice rules', async () => {
|
|
333
|
-
const flows = await
|
|
333
|
+
const flows = await getScenes({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
|
|
334
334
|
const schemas = flows.toModel();
|
|
335
335
|
|
|
336
|
-
const testFlowWithIds = schemas.
|
|
336
|
+
const testFlowWithIds = schemas.scenes.find((f) => f.name === 'Test Flow with IDs');
|
|
337
337
|
if (!testFlowWithIds) return;
|
|
338
338
|
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
339
|
+
const queryMoment = testFlowWithIds.moments.find((s) => s.name === 'Get test items');
|
|
340
|
+
if (queryMoment?.type !== 'query') return;
|
|
341
341
|
|
|
342
|
-
expect(
|
|
342
|
+
expect(queryMoment.server.specs[0].rules).toHaveLength(1);
|
|
343
343
|
|
|
344
|
-
const rule3 =
|
|
344
|
+
const rule3 = queryMoment.server.specs[0].rules.find(
|
|
345
|
+
(r) => r.name === 'Items should be retrievable after creation',
|
|
346
|
+
);
|
|
345
347
|
expect(rule3?.id).toBe('RULE-003');
|
|
346
348
|
});
|
|
347
349
|
|
|
348
350
|
it('should have ids for react slice rules', async () => {
|
|
349
|
-
const flows = await
|
|
351
|
+
const flows = await getScenes({ vfs: vfs, root: root, pattern: /\.(narrative)\.(ts)$/, fastFsScan: true });
|
|
350
352
|
const schemas = flows.toModel();
|
|
351
353
|
|
|
352
|
-
const testFlowWithIds = schemas.
|
|
354
|
+
const testFlowWithIds = schemas.scenes.find((f) => f.name === 'Test Flow with IDs');
|
|
353
355
|
if (!testFlowWithIds) return;
|
|
354
356
|
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
+
const reactMoment = testFlowWithIds.moments.find((s) => s.name === 'React to test event');
|
|
358
|
+
if (reactMoment?.type !== 'react') return;
|
|
357
359
|
|
|
358
|
-
expect(
|
|
360
|
+
expect(reactMoment.server.specs[0].rules).toHaveLength(1);
|
|
359
361
|
|
|
360
|
-
const rule4 =
|
|
362
|
+
const rule4 = reactMoment.server.specs[0].rules.find((r) => r.name === 'System should react to test item creation');
|
|
361
363
|
expect(rule4?.id).toBe('RULE-004');
|
|
362
364
|
});
|
|
363
365
|
|
|
364
366
|
it('should handle when examples correctly', async () => {
|
|
365
|
-
const flows = await
|
|
367
|
+
const flows = await getScenes({
|
|
366
368
|
vfs,
|
|
367
369
|
root,
|
|
368
370
|
pattern: /(?:^|\/)questionnaires\.narrative\.(?:ts|tsx|js|jsx|mjs|cjs)$/,
|
|
369
371
|
});
|
|
370
372
|
const model = flows.toModel();
|
|
371
373
|
|
|
372
|
-
const questionnaireFlow = model.
|
|
374
|
+
const questionnaireFlow = model.scenes.find((f) => f.name === 'Questionnaires');
|
|
373
375
|
expect(questionnaireFlow).toBeDefined();
|
|
374
376
|
|
|
375
377
|
if (questionnaireFlow) {
|
|
376
|
-
const
|
|
377
|
-
expect(
|
|
378
|
+
const submitMoment = questionnaireFlow.moments.find((s) => s.name === 'submits the questionnaire');
|
|
379
|
+
expect(submitMoment?.type).toBe('command');
|
|
378
380
|
|
|
379
|
-
if (
|
|
380
|
-
const example =
|
|
381
|
+
if (submitMoment?.type === 'command') {
|
|
382
|
+
const example = submitMoment.server?.specs?.[0]?.rules[0]?.examples[0];
|
|
381
383
|
const whenStep = example?.steps?.find((s) => s.keyword === 'When');
|
|
382
384
|
if (whenStep && 'text' in whenStep) {
|
|
383
385
|
expect(whenStep.text).toBe('SubmitQuestionnaire');
|
|
@@ -387,7 +389,7 @@ describe('getNarratives', (_mode) => {
|
|
|
387
389
|
});
|
|
388
390
|
|
|
389
391
|
it('should correctly assign commandRef correctly', async () => {
|
|
390
|
-
const flows = await
|
|
392
|
+
const flows = await getScenes({
|
|
391
393
|
vfs,
|
|
392
394
|
root,
|
|
393
395
|
pattern: /(?:^|\/)questionnaires\.narrative\.(?:ts|tsx|js|jsx|mjs|cjs)$/,
|
|
@@ -400,9 +402,9 @@ describe('getNarratives', (_mode) => {
|
|
|
400
402
|
it('should handle experience slice with client specs', async () => {
|
|
401
403
|
const memoryVfs = new InMemoryFileStore();
|
|
402
404
|
const flowWithExperienceContent = `
|
|
403
|
-
import {
|
|
405
|
+
import { scene, experience, it, specs } from '@auto-engineer/narrative';
|
|
404
406
|
|
|
405
|
-
|
|
407
|
+
scene('Test Experience Flow', () => {
|
|
406
408
|
experience('Homepage', 'H1a4Bn6Cy').client(() => {
|
|
407
409
|
specs(() => {
|
|
408
410
|
it('show a hero section with a welcome message');
|
|
@@ -414,26 +416,26 @@ flow('Test Experience Flow', () => {
|
|
|
414
416
|
|
|
415
417
|
await memoryVfs.write('/test/experience.narrative.ts', new TextEncoder().encode(flowWithExperienceContent));
|
|
416
418
|
|
|
417
|
-
const flows = await
|
|
419
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
418
420
|
const model = flows.toModel();
|
|
419
421
|
|
|
420
|
-
const experienceFlow = model.
|
|
422
|
+
const experienceFlow = model.scenes.find((f) => f.name === 'Test Experience Flow');
|
|
421
423
|
expect(experienceFlow).toBeDefined();
|
|
422
424
|
|
|
423
425
|
if (experienceFlow) {
|
|
424
|
-
const
|
|
425
|
-
expect(
|
|
426
|
-
expect(
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
expect(
|
|
430
|
-
expect(
|
|
431
|
-
expect(Array.isArray(
|
|
432
|
-
expect(
|
|
433
|
-
expect(
|
|
434
|
-
expect(
|
|
435
|
-
expect(
|
|
436
|
-
expect(
|
|
426
|
+
const homepageMoment = experienceFlow.moments.find((s) => s.name === 'Homepage');
|
|
427
|
+
expect(homepageMoment).toBeDefined();
|
|
428
|
+
expect(homepageMoment?.type).toBe('experience');
|
|
429
|
+
|
|
430
|
+
if (homepageMoment?.type === 'experience') {
|
|
431
|
+
expect(homepageMoment.client).toBeDefined();
|
|
432
|
+
expect(homepageMoment.client.specs).toBeDefined();
|
|
433
|
+
expect(Array.isArray(homepageMoment.client.specs)).toBe(true);
|
|
434
|
+
expect(homepageMoment.client.specs).toHaveLength(2);
|
|
435
|
+
expect(homepageMoment.client.specs[0].type).toBe('it');
|
|
436
|
+
expect(homepageMoment.client.specs[0].title).toBe('show a hero section with a welcome message');
|
|
437
|
+
expect(homepageMoment.client.specs[1].type).toBe('it');
|
|
438
|
+
expect(homepageMoment.client.specs[1].title).toBe('allow user to start the questionnaire');
|
|
437
439
|
}
|
|
438
440
|
}
|
|
439
441
|
});
|
|
@@ -441,9 +443,9 @@ flow('Test Experience Flow', () => {
|
|
|
441
443
|
it('simulates browser execution with transpiled CommonJS code', async () => {
|
|
442
444
|
const memoryVfs = new InMemoryFileStore();
|
|
443
445
|
const flowContent = `
|
|
444
|
-
import {
|
|
446
|
+
import { scene, experience, it, specs } from '@auto-engineer/narrative';
|
|
445
447
|
|
|
446
|
-
|
|
448
|
+
scene('Browser Test Flow', () => {
|
|
447
449
|
experience('HomePage').client(() => {
|
|
448
450
|
specs(() => {
|
|
449
451
|
it('render correctly');
|
|
@@ -461,12 +463,12 @@ flow('Browser Test Flow', () => {
|
|
|
461
463
|
|
|
462
464
|
await executeAST(['/browser/test.narrative.ts'], memoryVfs, {}, '/browser');
|
|
463
465
|
|
|
464
|
-
const flows = registry.
|
|
466
|
+
const flows = registry.getAllScenes();
|
|
465
467
|
expect(flows).toHaveLength(1);
|
|
466
468
|
expect(flows[0].name).toBe('Browser Test Flow');
|
|
467
|
-
expect(flows[0].
|
|
469
|
+
expect(flows[0].moments).toHaveLength(1);
|
|
468
470
|
|
|
469
|
-
const slice = flows[0].
|
|
471
|
+
const slice = flows[0].moments[0];
|
|
470
472
|
expect(slice.type).toBe('experience');
|
|
471
473
|
expect(slice.name).toBe('HomePage');
|
|
472
474
|
|
|
@@ -487,9 +489,9 @@ flow('Browser Test Flow', () => {
|
|
|
487
489
|
const { registry } = await import('./narrative-registry');
|
|
488
490
|
|
|
489
491
|
const flowContent = `
|
|
490
|
-
import {
|
|
492
|
+
import { scene, experience } from '@auto-engineer/narrative';
|
|
491
493
|
|
|
492
|
-
|
|
494
|
+
scene('Questionnaires', 'Q9m2Kp4Lx', () => {
|
|
493
495
|
experience('Homepage', 'H1a4Bn6Cy').client(() => {});
|
|
494
496
|
});
|
|
495
497
|
`;
|
|
@@ -502,12 +504,12 @@ flow('Questionnaires', 'Q9m2Kp4Lx', () => {
|
|
|
502
504
|
executeAST(['/browser/questionnaires.narrative.ts'], memoryVfs, {}, '/browser'),
|
|
503
505
|
).resolves.toBeDefined();
|
|
504
506
|
|
|
505
|
-
const flows = registry.
|
|
507
|
+
const flows = registry.getAllScenes();
|
|
506
508
|
expect(flows).toHaveLength(1);
|
|
507
509
|
expect(flows[0].name).toBe('Questionnaires');
|
|
508
|
-
expect(flows[0].
|
|
510
|
+
expect(flows[0].moments).toHaveLength(1);
|
|
509
511
|
|
|
510
|
-
const slice = flows[0].
|
|
512
|
+
const slice = flows[0].moments[0];
|
|
511
513
|
expect(slice.type).toBe('experience');
|
|
512
514
|
expect(slice.name).toBe('Homepage');
|
|
513
515
|
});
|
|
@@ -515,7 +517,7 @@ flow('Questionnaires', 'Q9m2Kp4Lx', () => {
|
|
|
515
517
|
it('should handle flow type resolutions correctly', async () => {
|
|
516
518
|
const memoryVfs = new InMemoryFileStore();
|
|
517
519
|
const questionnaireFlowContent = `
|
|
518
|
-
import { data,
|
|
520
|
+
import { data, scene, should, specs, rule, example } from '../narrative';
|
|
519
521
|
import { command, query } from '../fluent-builder';
|
|
520
522
|
import gql from 'graphql-tag';
|
|
521
523
|
import { source } from '../data-narrative-builders';
|
|
@@ -562,7 +564,7 @@ type QuestionnaireProgress = State<
|
|
|
562
564
|
}
|
|
563
565
|
>;
|
|
564
566
|
|
|
565
|
-
|
|
567
|
+
scene('questionnaires-test', () => {
|
|
566
568
|
query('views progress')
|
|
567
569
|
.server(() => {
|
|
568
570
|
specs('Questionnaire progress display', () => {
|
|
@@ -612,9 +614,9 @@ flow('questionnaires-test', () => {
|
|
|
612
614
|
|
|
613
615
|
await memoryVfs.write('/test/questionnaires.narrative.ts', new TextEncoder().encode(questionnaireFlowContent));
|
|
614
616
|
|
|
615
|
-
const flows = await
|
|
617
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
616
618
|
const model = flows.toModel();
|
|
617
|
-
const testFlow = model.
|
|
619
|
+
const testFlow = model.scenes.find((f) => f.name === 'questionnaires-test');
|
|
618
620
|
expect(testFlow).toBeDefined();
|
|
619
621
|
if (testFlow !== null && testFlow !== undefined) {
|
|
620
622
|
validateSubmitQuestionnaireCommand(testFlow);
|
|
@@ -625,21 +627,21 @@ flow('questionnaires-test', () => {
|
|
|
625
627
|
});
|
|
626
628
|
|
|
627
629
|
it('correctly distinguishes between State and Event types in given clauses with empty when', async () => {
|
|
628
|
-
const flows = await
|
|
630
|
+
const flows = await getScenes({ vfs, root, pattern, fastFsScan: true });
|
|
629
631
|
const model = flows.toModel();
|
|
630
632
|
|
|
631
|
-
const mixedGivenFlow = model.
|
|
633
|
+
const mixedGivenFlow = model.scenes.find((f) => f.name === 'Mixed Given Types');
|
|
632
634
|
expect(mixedGivenFlow).toBeDefined();
|
|
633
635
|
|
|
634
636
|
if (!mixedGivenFlow) return;
|
|
635
637
|
|
|
636
|
-
const
|
|
637
|
-
expect(
|
|
638
|
-
expect(
|
|
638
|
+
const queryMoment = mixedGivenFlow.moments.find((s) => s.name === 'system status check');
|
|
639
|
+
expect(queryMoment).toBeDefined();
|
|
640
|
+
expect(queryMoment?.type).toBe('query');
|
|
639
641
|
|
|
640
|
-
if (
|
|
642
|
+
if (queryMoment?.type !== 'query') return;
|
|
641
643
|
|
|
642
|
-
const example =
|
|
644
|
+
const example = queryMoment.server.specs[0].rules[0]?.examples[0];
|
|
643
645
|
expect(example).toBeDefined();
|
|
644
646
|
|
|
645
647
|
if (example !== null && example !== undefined) {
|
|
@@ -651,7 +653,7 @@ flow('questionnaires-test', () => {
|
|
|
651
653
|
});
|
|
652
654
|
|
|
653
655
|
it('does not emit empty generics or empty when clauses', async () => {
|
|
654
|
-
const flows = await
|
|
656
|
+
const flows = await getScenes({
|
|
655
657
|
vfs,
|
|
656
658
|
root,
|
|
657
659
|
pattern: /(?:^|\/)questionnaires\.narrative\.(?:ts|tsx|js|jsx|mjs|cjs)$/,
|
|
@@ -668,7 +670,7 @@ flow('questionnaires-test', () => {
|
|
|
668
670
|
});
|
|
669
671
|
|
|
670
672
|
it('should not generate phantom messages with empty names', async () => {
|
|
671
|
-
const flows = await
|
|
673
|
+
const flows = await getScenes({
|
|
672
674
|
vfs,
|
|
673
675
|
root: root,
|
|
674
676
|
pattern: /(?:^|\/)questionnaires\.narrative\.(?:ts|tsx|js|jsx|mjs|cjs)$/,
|
|
@@ -691,7 +693,7 @@ flow('questionnaires-test', () => {
|
|
|
691
693
|
it('should convert all given events to eventRef', async (): Promise<void> => {
|
|
692
694
|
const memoryVfs = new InMemoryFileStore();
|
|
693
695
|
const todoSummaryFlowContent = `
|
|
694
|
-
import {
|
|
696
|
+
import { scene, query, specs, rule, example, type Event, type State } from '@auto-engineer/narrative';
|
|
695
697
|
|
|
696
698
|
type TodoAdded = Event<
|
|
697
699
|
'TodoAdded',
|
|
@@ -731,7 +733,7 @@ type TodoListSummary = State<
|
|
|
731
733
|
}
|
|
732
734
|
>;
|
|
733
735
|
|
|
734
|
-
|
|
736
|
+
scene('Todo List', () => {
|
|
735
737
|
query('views completion summary')
|
|
736
738
|
.server(() => {
|
|
737
739
|
specs(() => {
|
|
@@ -780,20 +782,20 @@ flow('Todo List', () => {
|
|
|
780
782
|
|
|
781
783
|
await memoryVfs.write('/test/todo-summary.narrative.ts', new TextEncoder().encode(todoSummaryFlowContent));
|
|
782
784
|
|
|
783
|
-
const flows = await
|
|
785
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
784
786
|
const model = flows.toModel();
|
|
785
787
|
|
|
786
|
-
const todoFlow = model.
|
|
788
|
+
const todoFlow = model.scenes.find((f) => f.name === 'Todo List');
|
|
787
789
|
expect(todoFlow).toBeDefined();
|
|
788
790
|
|
|
789
791
|
if (!todoFlow) return;
|
|
790
792
|
|
|
791
|
-
const
|
|
792
|
-
expect(
|
|
793
|
+
const summaryMoment = todoFlow.moments.find((s) => s.name === 'views completion summary');
|
|
794
|
+
expect(summaryMoment?.type).toBe('query');
|
|
793
795
|
|
|
794
|
-
if (
|
|
796
|
+
if (summaryMoment?.type !== 'query') return;
|
|
795
797
|
|
|
796
|
-
const example =
|
|
798
|
+
const example = summaryMoment.server.specs[0].rules[0]?.examples[0];
|
|
797
799
|
expect(example).toBeDefined();
|
|
798
800
|
expect(example.steps).toBeDefined();
|
|
799
801
|
expect(Array.isArray(example.steps)).toBe(true);
|
|
@@ -849,11 +851,11 @@ function validateTodoMessages(model: Model): void {
|
|
|
849
851
|
expect(todoListSummaryState?.type).toBe('state');
|
|
850
852
|
}
|
|
851
853
|
|
|
852
|
-
function validateSubmitQuestionnaireCommand(questionnaireFlow:
|
|
853
|
-
const
|
|
854
|
-
expect(
|
|
855
|
-
if (
|
|
856
|
-
const example =
|
|
854
|
+
function validateSubmitQuestionnaireCommand(questionnaireFlow: Scene): void {
|
|
855
|
+
const submitMoment = questionnaireFlow.moments.find((s) => s.name === 'submits questionnaire');
|
|
856
|
+
expect(submitMoment?.type).toBe('command');
|
|
857
|
+
if (submitMoment?.type === 'command') {
|
|
858
|
+
const example = submitMoment.server?.specs?.[0]?.rules[0]?.examples[0];
|
|
857
859
|
const whenStep = example?.steps?.find((s) => s.keyword === 'When');
|
|
858
860
|
if (whenStep && 'text' in whenStep) {
|
|
859
861
|
expect(whenStep.text).toBe('SubmitQuestionnaire');
|
|
@@ -866,10 +868,10 @@ function validateQuestionAnsweredEvent(model: Model): void {
|
|
|
866
868
|
expect(questionAnsweredMessage?.type).toBe('event');
|
|
867
869
|
}
|
|
868
870
|
|
|
869
|
-
function validateGivenSectionEventRefs(questionnaireFlow:
|
|
870
|
-
const
|
|
871
|
-
if (
|
|
872
|
-
const example =
|
|
871
|
+
function validateGivenSectionEventRefs(questionnaireFlow: Scene): void {
|
|
872
|
+
const viewsMoment = questionnaireFlow.moments.find((s) => s.name === 'views progress');
|
|
873
|
+
if (viewsMoment?.type === 'query') {
|
|
874
|
+
const example = viewsMoment.server?.specs?.[0]?.rules[0]?.examples[0];
|
|
873
875
|
if (example?.steps !== undefined && Array.isArray(example.steps)) {
|
|
874
876
|
const givenStep = example.steps.find((s) => s.keyword === 'Given');
|
|
875
877
|
if (givenStep && 'text' in givenStep) {
|
|
@@ -957,7 +959,7 @@ async function createQuestionnaireBugTestModel(): Promise<Model> {
|
|
|
957
959
|
const memoryVfs = new InMemoryFileStore();
|
|
958
960
|
const questionnaireFlowContent = getQuestionnaireFlowContent();
|
|
959
961
|
await memoryVfs.write('/test/questionnaires-bug.narrative.ts', new TextEncoder().encode(questionnaireFlowContent));
|
|
960
|
-
const flows = await
|
|
962
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
961
963
|
return flows.toModel();
|
|
962
964
|
}
|
|
963
965
|
|
|
@@ -967,7 +969,7 @@ import {
|
|
|
967
969
|
command,
|
|
968
970
|
query,
|
|
969
971
|
experience,
|
|
970
|
-
|
|
972
|
+
scene,
|
|
971
973
|
describe,
|
|
972
974
|
it,
|
|
973
975
|
specs,
|
|
@@ -1017,7 +1019,7 @@ type SubmitQuestionnaire = Command<
|
|
|
1017
1019
|
}
|
|
1018
1020
|
>;
|
|
1019
1021
|
|
|
1020
|
-
|
|
1022
|
+
scene('Questionnaires', 'Q9m2Kp4Lx', () => {
|
|
1021
1023
|
command('sends the questionnaire link', 'S2b5Cp7Dz')
|
|
1022
1024
|
.server(() => {
|
|
1023
1025
|
specs(() => {
|
|
@@ -1086,15 +1088,15 @@ flow('Questionnaires', 'Q9m2Kp4Lx', () => {
|
|
|
1086
1088
|
|
|
1087
1089
|
function validateQuestionnaireBugFix(model: Model): void {
|
|
1088
1090
|
const questionnaireFlow = getQuestionnaireFlowFromModel(model);
|
|
1089
|
-
const
|
|
1090
|
-
const submitExample = getSubmitExample(
|
|
1091
|
+
const submitMoment = getSubmitMoment(questionnaireFlow);
|
|
1092
|
+
const submitExample = getSubmitExample(submitMoment);
|
|
1091
1093
|
|
|
1092
1094
|
validateSubmitCommandRef(submitExample);
|
|
1093
|
-
|
|
1095
|
+
validateLinkMomentCommandRef(questionnaireFlow);
|
|
1094
1096
|
}
|
|
1095
1097
|
|
|
1096
|
-
function getQuestionnaireFlowFromModel(model: Model):
|
|
1097
|
-
const questionnaireFlow = model.
|
|
1098
|
+
function getQuestionnaireFlowFromModel(model: Model): Scene {
|
|
1099
|
+
const questionnaireFlow = model.scenes.find((f) => f.name === 'Questionnaires');
|
|
1098
1100
|
expect(questionnaireFlow).toBeDefined();
|
|
1099
1101
|
if (questionnaireFlow === null || questionnaireFlow === undefined) {
|
|
1100
1102
|
throw new Error('Questionnaire flow not found');
|
|
@@ -1102,26 +1104,26 @@ function getQuestionnaireFlowFromModel(model: Model): Narrative {
|
|
|
1102
1104
|
return questionnaireFlow;
|
|
1103
1105
|
}
|
|
1104
1106
|
|
|
1105
|
-
function
|
|
1107
|
+
function getSubmitMoment(questionnaireFlow: Scene): {
|
|
1106
1108
|
type: 'command';
|
|
1107
1109
|
server: { specs: { type: 'gherkin'; feature: string; rules: { examples: unknown[] }[] }[] };
|
|
1108
1110
|
} {
|
|
1109
|
-
const
|
|
1110
|
-
expect(
|
|
1111
|
-
expect(
|
|
1112
|
-
if (
|
|
1111
|
+
const submitMoment = questionnaireFlow.moments.find((s) => s.name === 'submits the questionnaire');
|
|
1112
|
+
expect(submitMoment).toBeDefined();
|
|
1113
|
+
expect(submitMoment?.type).toBe('command');
|
|
1114
|
+
if (submitMoment?.type !== 'command') {
|
|
1113
1115
|
throw new Error('Submit slice is not a command');
|
|
1114
1116
|
}
|
|
1115
|
-
return
|
|
1117
|
+
return submitMoment as {
|
|
1116
1118
|
type: 'command';
|
|
1117
1119
|
server: { specs: { type: 'gherkin'; feature: string; rules: { examples: unknown[] }[] }[] };
|
|
1118
1120
|
};
|
|
1119
1121
|
}
|
|
1120
1122
|
|
|
1121
|
-
function getSubmitExample(
|
|
1123
|
+
function getSubmitExample(submitMoment: {
|
|
1122
1124
|
server: { specs: { type: 'gherkin'; feature: string; rules: { examples: unknown[] }[] }[] };
|
|
1123
1125
|
}): unknown {
|
|
1124
|
-
const rule =
|
|
1126
|
+
const rule = submitMoment.server?.specs?.[0]?.rules[0];
|
|
1125
1127
|
expect(rule).toBeDefined();
|
|
1126
1128
|
expect(rule?.examples).toHaveLength(1);
|
|
1127
1129
|
const example = rule?.examples[0];
|
|
@@ -1141,11 +1143,11 @@ function validateSubmitCommandRef(example: unknown): void {
|
|
|
1141
1143
|
}
|
|
1142
1144
|
}
|
|
1143
1145
|
|
|
1144
|
-
function
|
|
1145
|
-
const
|
|
1146
|
-
expect(
|
|
1147
|
-
if (
|
|
1148
|
-
const linkExample =
|
|
1146
|
+
function validateLinkMomentCommandRef(questionnaireFlow: Scene): void {
|
|
1147
|
+
const linkMoment = questionnaireFlow.moments.find((s) => s.name === 'sends the questionnaire link');
|
|
1148
|
+
expect(linkMoment?.type).toBe('command');
|
|
1149
|
+
if (linkMoment?.type === 'command') {
|
|
1150
|
+
const linkExample = linkMoment.server?.specs?.[0]?.rules[0]?.examples[0];
|
|
1149
1151
|
const ex = linkExample as { steps?: { keyword: string; text?: string }[] };
|
|
1150
1152
|
const whenStep = ex?.steps?.find((s) => s.keyword === 'When');
|
|
1151
1153
|
if (whenStep && 'text' in whenStep) {
|
|
@@ -1156,8 +1158,8 @@ function validateLinkSliceCommandRef(questionnaireFlow: Narrative): void {
|
|
|
1156
1158
|
|
|
1157
1159
|
function validateCommandRef(model: Model): void {
|
|
1158
1160
|
const questionnaireFlow = getQuestionnaireFlowFromModel(model);
|
|
1159
|
-
const
|
|
1160
|
-
const serverSpecs =
|
|
1161
|
+
const submitMoment = getSubmitMomentFromFlow(questionnaireFlow);
|
|
1162
|
+
const serverSpecs = getServerSpecsFromMoment(submitMoment);
|
|
1161
1163
|
const rule = getFirstRuleFromSpecs(serverSpecs);
|
|
1162
1164
|
const example = getFirstExampleFromRule(rule);
|
|
1163
1165
|
|
|
@@ -1165,18 +1167,18 @@ function validateCommandRef(model: Model): void {
|
|
|
1165
1167
|
validateThenEvents(example);
|
|
1166
1168
|
}
|
|
1167
1169
|
|
|
1168
|
-
function
|
|
1169
|
-
const
|
|
1170
|
-
expect(
|
|
1171
|
-
expect(
|
|
1172
|
-
if (
|
|
1170
|
+
function getSubmitMomentFromFlow(questionnaireFlow: Scene): unknown {
|
|
1171
|
+
const submitMoment = questionnaireFlow.moments.find((s) => s.name === 'submits the questionnaire');
|
|
1172
|
+
expect(submitMoment).toBeDefined();
|
|
1173
|
+
expect(submitMoment?.type).toBe('command');
|
|
1174
|
+
if (submitMoment?.type !== 'command') {
|
|
1173
1175
|
throw new Error('Submit slice is not a command');
|
|
1174
1176
|
}
|
|
1175
|
-
return
|
|
1177
|
+
return submitMoment;
|
|
1176
1178
|
}
|
|
1177
1179
|
|
|
1178
|
-
function
|
|
1179
|
-
const slice =
|
|
1180
|
+
function getServerSpecsFromMoment(submitMoment: unknown): unknown {
|
|
1181
|
+
const slice = submitMoment as { server?: { specs?: unknown[] } };
|
|
1180
1182
|
const serverSpecs = slice.server?.specs;
|
|
1181
1183
|
expect(serverSpecs).toBeDefined();
|
|
1182
1184
|
expect(Array.isArray(serverSpecs)).toBe(true);
|
|
@@ -1242,16 +1244,16 @@ function validateThenEvents(example: unknown): void {
|
|
|
1242
1244
|
}
|
|
1243
1245
|
|
|
1244
1246
|
describe('modules in toModel()', () => {
|
|
1245
|
-
it('should derive modules from
|
|
1247
|
+
it('should derive modules from scenes with different sourceFiles', async () => {
|
|
1246
1248
|
const memoryVfs = new InMemoryFileStore();
|
|
1247
1249
|
|
|
1248
1250
|
const ordersContent = `
|
|
1249
|
-
import {
|
|
1251
|
+
import { scene, command, specs, rule, example, type Command, type Event } from '@auto-engineer/narrative';
|
|
1250
1252
|
|
|
1251
1253
|
type CreateOrder = Command<'CreateOrder', { orderId: string }>;
|
|
1252
1254
|
type OrderCreated = Event<'OrderCreated', { orderId: string; createdAt: Date }>;
|
|
1253
1255
|
|
|
1254
|
-
|
|
1256
|
+
scene('Orders', () => {
|
|
1255
1257
|
command('create order')
|
|
1256
1258
|
.server(() => {
|
|
1257
1259
|
specs(() => {
|
|
@@ -1266,12 +1268,12 @@ flow('Orders', () => {
|
|
|
1266
1268
|
`;
|
|
1267
1269
|
|
|
1268
1270
|
const usersContent = `
|
|
1269
|
-
import {
|
|
1271
|
+
import { scene, command, specs, rule, example, type Command, type Event } from '@auto-engineer/narrative';
|
|
1270
1272
|
|
|
1271
1273
|
type CreateUser = Command<'CreateUser', { userId: string; name: string }>;
|
|
1272
1274
|
type UserCreated = Event<'UserCreated', { userId: string; name: string; createdAt: Date }>;
|
|
1273
1275
|
|
|
1274
|
-
|
|
1276
|
+
scene('Users', () => {
|
|
1275
1277
|
command('create user')
|
|
1276
1278
|
.server(() => {
|
|
1277
1279
|
specs(() => {
|
|
@@ -1288,7 +1290,7 @@ flow('Users', () => {
|
|
|
1288
1290
|
await memoryVfs.write('/test/orders.narrative.ts', new TextEncoder().encode(ordersContent));
|
|
1289
1291
|
await memoryVfs.write('/test/users.narrative.ts', new TextEncoder().encode(usersContent));
|
|
1290
1292
|
|
|
1291
|
-
const flows = await
|
|
1293
|
+
const flows = await getScenes({
|
|
1292
1294
|
vfs: memoryVfs,
|
|
1293
1295
|
root: '/test',
|
|
1294
1296
|
pattern: /\.narrative\.ts$/,
|
|
@@ -1317,12 +1319,12 @@ flow('Users', () => {
|
|
|
1317
1319
|
const memoryVfs = new InMemoryFileStore();
|
|
1318
1320
|
|
|
1319
1321
|
const content = `
|
|
1320
|
-
import {
|
|
1322
|
+
import { scene, command, specs, rule, example, type Command, type Event } from '@auto-engineer/narrative';
|
|
1321
1323
|
|
|
1322
1324
|
type CreateOrder = Command<'CreateOrder', { orderId: string }>;
|
|
1323
1325
|
type OrderCreated = Event<'OrderCreated', { orderId: string }>;
|
|
1324
1326
|
|
|
1325
|
-
|
|
1327
|
+
scene('Orders', () => {
|
|
1326
1328
|
command('create order')
|
|
1327
1329
|
.server(() => {
|
|
1328
1330
|
specs(() => {
|
|
@@ -1338,7 +1340,7 @@ flow('Orders', () => {
|
|
|
1338
1340
|
|
|
1339
1341
|
await memoryVfs.write('/test/orders.narrative.ts', new TextEncoder().encode(content));
|
|
1340
1342
|
|
|
1341
|
-
const flows = await
|
|
1343
|
+
const flows = await getScenes({
|
|
1342
1344
|
vfs: memoryVfs,
|
|
1343
1345
|
root: '/test',
|
|
1344
1346
|
pattern: /\.narrative\.ts$/,
|
|
@@ -1354,17 +1356,17 @@ flow('Orders', () => {
|
|
|
1354
1356
|
expect(declaredNames).toContain('OrderCreated');
|
|
1355
1357
|
});
|
|
1356
1358
|
|
|
1357
|
-
it('should group
|
|
1359
|
+
it('should group scenes from same sourceFile into one module', async () => {
|
|
1358
1360
|
const memoryVfs = new InMemoryFileStore();
|
|
1359
1361
|
|
|
1360
1362
|
const content = `
|
|
1361
|
-
import {
|
|
1363
|
+
import { scene, command, query, specs, rule, example, type Command, type Event, type State } from '@auto-engineer/narrative';
|
|
1362
1364
|
|
|
1363
1365
|
type CreateTodo = Command<'CreateTodo', { todoId: string }>;
|
|
1364
1366
|
type TodoCreated = Event<'TodoCreated', { todoId: string }>;
|
|
1365
1367
|
type TodoList = State<'TodoList', { todos: string[] }>;
|
|
1366
1368
|
|
|
1367
|
-
|
|
1369
|
+
scene('Create Todos', () => {
|
|
1368
1370
|
command('add todo')
|
|
1369
1371
|
.server(() => {
|
|
1370
1372
|
specs(() => {
|
|
@@ -1377,7 +1379,7 @@ flow('Create Todos', () => {
|
|
|
1377
1379
|
});
|
|
1378
1380
|
});
|
|
1379
1381
|
|
|
1380
|
-
|
|
1382
|
+
scene('View Todos', () => {
|
|
1381
1383
|
query('list todos')
|
|
1382
1384
|
.server(() => {
|
|
1383
1385
|
specs(() => {
|
|
@@ -1394,7 +1396,7 @@ flow('View Todos', () => {
|
|
|
1394
1396
|
|
|
1395
1397
|
await memoryVfs.write('/test/todos.narrative.ts', new TextEncoder().encode(content));
|
|
1396
1398
|
|
|
1397
|
-
const flows = await
|
|
1399
|
+
const flows = await getScenes({
|
|
1398
1400
|
vfs: memoryVfs,
|
|
1399
1401
|
root: '/test',
|
|
1400
1402
|
pattern: /\.narrative\.ts$/,
|
|
@@ -1403,23 +1405,23 @@ flow('View Todos', () => {
|
|
|
1403
1405
|
const model = flows.toModel();
|
|
1404
1406
|
|
|
1405
1407
|
expect(model.modules).toHaveLength(1);
|
|
1406
|
-
expect(model.modules[0].contains.
|
|
1408
|
+
expect(model.modules[0].contains.sceneIds).toHaveLength(2);
|
|
1407
1409
|
|
|
1408
|
-
const
|
|
1409
|
-
expect(
|
|
1410
|
-
expect(
|
|
1410
|
+
const sceneNames = model.scenes.map((n) => n.name);
|
|
1411
|
+
expect(sceneNames).toContain('Create Todos');
|
|
1412
|
+
expect(sceneNames).toContain('View Todos');
|
|
1411
1413
|
});
|
|
1412
1414
|
|
|
1413
1415
|
it('should validate model with modules passes schema', async () => {
|
|
1414
1416
|
const memoryVfs = new InMemoryFileStore();
|
|
1415
1417
|
|
|
1416
1418
|
const content = `
|
|
1417
|
-
import {
|
|
1419
|
+
import { scene, command, specs, rule, example, type Command, type Event } from '@auto-engineer/narrative';
|
|
1418
1420
|
|
|
1419
1421
|
type DoSomething = Command<'DoSomething', { id: string }>;
|
|
1420
1422
|
type SomethingDone = Event<'SomethingDone', { id: string }>;
|
|
1421
1423
|
|
|
1422
|
-
|
|
1424
|
+
scene('Test', () => {
|
|
1423
1425
|
command('do something')
|
|
1424
1426
|
.server(() => {
|
|
1425
1427
|
specs(() => {
|
|
@@ -1435,7 +1437,7 @@ flow('Test', () => {
|
|
|
1435
1437
|
|
|
1436
1438
|
await memoryVfs.write('/test/test.narrative.ts', new TextEncoder().encode(content));
|
|
1437
1439
|
|
|
1438
|
-
const flows = await
|
|
1440
|
+
const flows = await getScenes({
|
|
1439
1441
|
vfs: memoryVfs,
|
|
1440
1442
|
root: '/test',
|
|
1441
1443
|
pattern: /\.narrative\.ts$/,
|
|
@@ -1457,12 +1459,12 @@ describe('projection DSL methods', () => {
|
|
|
1457
1459
|
it('should generate correct origin for singleton projection', async () => {
|
|
1458
1460
|
const memoryVfs = new InMemoryFileStore();
|
|
1459
1461
|
const flowContent = `
|
|
1460
|
-
import {
|
|
1462
|
+
import { scene, query, specs, rule, example, data, source, type Event, type State } from '@auto-engineer/narrative';
|
|
1461
1463
|
|
|
1462
1464
|
type TodoAdded = Event<'TodoAdded', { todoId: string; description: string; addedAt: Date }>;
|
|
1463
1465
|
type TodoListSummary = State<'TodoListSummary', { summaryId: string; totalTodos: number }>;
|
|
1464
1466
|
|
|
1465
|
-
|
|
1467
|
+
scene('Projection Test', () => {
|
|
1466
1468
|
query('views summary')
|
|
1467
1469
|
.server(() => {
|
|
1468
1470
|
specs(() => {
|
|
@@ -1480,20 +1482,20 @@ flow('Projection Test', () => {
|
|
|
1480
1482
|
|
|
1481
1483
|
await memoryVfs.write('/test/projection.narrative.ts', new TextEncoder().encode(flowContent));
|
|
1482
1484
|
|
|
1483
|
-
const flows = await
|
|
1485
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
1484
1486
|
const model = flows.toModel();
|
|
1485
1487
|
|
|
1486
|
-
const projectionFlow = model.
|
|
1488
|
+
const projectionFlow = model.scenes.find((f) => f.name === 'Projection Test');
|
|
1487
1489
|
expect(projectionFlow).toBeDefined();
|
|
1488
1490
|
|
|
1489
1491
|
if (!projectionFlow) return;
|
|
1490
1492
|
|
|
1491
|
-
const
|
|
1492
|
-
expect(
|
|
1493
|
+
const summaryMoment = projectionFlow.moments.find((s) => s.name === 'views summary');
|
|
1494
|
+
expect(summaryMoment?.type).toBe('query');
|
|
1493
1495
|
|
|
1494
|
-
if (
|
|
1496
|
+
if (summaryMoment?.type !== 'query') return;
|
|
1495
1497
|
|
|
1496
|
-
const data =
|
|
1498
|
+
const data = summaryMoment.server.data;
|
|
1497
1499
|
expect(data).toBeDefined();
|
|
1498
1500
|
expect(data?.items).toHaveLength(1);
|
|
1499
1501
|
|
|
@@ -1509,12 +1511,12 @@ flow('Projection Test', () => {
|
|
|
1509
1511
|
it('should generate correct origin for regular projection with single idField', async () => {
|
|
1510
1512
|
const memoryVfs = new InMemoryFileStore();
|
|
1511
1513
|
const flowContent = `
|
|
1512
|
-
import {
|
|
1514
|
+
import { scene, query, specs, rule, example, data, source, type Event, type State } from '@auto-engineer/narrative';
|
|
1513
1515
|
|
|
1514
1516
|
type TodoAdded = Event<'TodoAdded', { todoId: string; description: string; addedAt: Date }>;
|
|
1515
1517
|
type TodoState = State<'TodoState', { todoId: string; description: string; status: string }>;
|
|
1516
1518
|
|
|
1517
|
-
|
|
1519
|
+
scene('Projection Test', () => {
|
|
1518
1520
|
query('views todo')
|
|
1519
1521
|
.server(() => {
|
|
1520
1522
|
specs(() => {
|
|
@@ -1532,20 +1534,20 @@ flow('Projection Test', () => {
|
|
|
1532
1534
|
|
|
1533
1535
|
await memoryVfs.write('/test/projection.narrative.ts', new TextEncoder().encode(flowContent));
|
|
1534
1536
|
|
|
1535
|
-
const flows = await
|
|
1537
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
1536
1538
|
const model = flows.toModel();
|
|
1537
1539
|
|
|
1538
|
-
const projectionFlow = model.
|
|
1540
|
+
const projectionFlow = model.scenes.find((f) => f.name === 'Projection Test');
|
|
1539
1541
|
expect(projectionFlow).toBeDefined();
|
|
1540
1542
|
|
|
1541
1543
|
if (!projectionFlow) return;
|
|
1542
1544
|
|
|
1543
|
-
const
|
|
1544
|
-
expect(
|
|
1545
|
+
const todoMoment = projectionFlow.moments.find((s) => s.name === 'views todo');
|
|
1546
|
+
expect(todoMoment?.type).toBe('query');
|
|
1545
1547
|
|
|
1546
|
-
if (
|
|
1548
|
+
if (todoMoment?.type !== 'query') return;
|
|
1547
1549
|
|
|
1548
|
-
const data =
|
|
1550
|
+
const data = todoMoment.server.data;
|
|
1549
1551
|
expect(data).toBeDefined();
|
|
1550
1552
|
expect(data?.items).toHaveLength(1);
|
|
1551
1553
|
|
|
@@ -1561,12 +1563,12 @@ flow('Projection Test', () => {
|
|
|
1561
1563
|
it('should generate correct origin for composite projection with multiple idFields', async () => {
|
|
1562
1564
|
const memoryVfs = new InMemoryFileStore();
|
|
1563
1565
|
const flowContent = `
|
|
1564
|
-
import {
|
|
1566
|
+
import { scene, query, specs, rule, example, data, source, type Event, type State } from '@auto-engineer/narrative';
|
|
1565
1567
|
|
|
1566
1568
|
type UserProjectAssigned = Event<'UserProjectAssigned', { userId: string; projectId: string; assignedAt: Date }>;
|
|
1567
1569
|
type UserProjectState = State<'UserProjectState', { userId: string; projectId: string; role: string }>;
|
|
1568
1570
|
|
|
1569
|
-
|
|
1571
|
+
scene('Projection Test', () => {
|
|
1570
1572
|
query('views user project')
|
|
1571
1573
|
.server(() => {
|
|
1572
1574
|
specs(() => {
|
|
@@ -1584,20 +1586,20 @@ flow('Projection Test', () => {
|
|
|
1584
1586
|
|
|
1585
1587
|
await memoryVfs.write('/test/projection.narrative.ts', new TextEncoder().encode(flowContent));
|
|
1586
1588
|
|
|
1587
|
-
const flows = await
|
|
1589
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
1588
1590
|
const model = flows.toModel();
|
|
1589
1591
|
|
|
1590
|
-
const projectionFlow = model.
|
|
1592
|
+
const projectionFlow = model.scenes.find((f) => f.name === 'Projection Test');
|
|
1591
1593
|
expect(projectionFlow).toBeDefined();
|
|
1592
1594
|
|
|
1593
1595
|
if (!projectionFlow) return;
|
|
1594
1596
|
|
|
1595
|
-
const
|
|
1596
|
-
expect(
|
|
1597
|
+
const userProjectMoment = projectionFlow.moments.find((s) => s.name === 'views user project');
|
|
1598
|
+
expect(userProjectMoment?.type).toBe('query');
|
|
1597
1599
|
|
|
1598
|
-
if (
|
|
1600
|
+
if (userProjectMoment?.type !== 'query') return;
|
|
1599
1601
|
|
|
1600
|
-
const data =
|
|
1602
|
+
const data = userProjectMoment.server.data;
|
|
1601
1603
|
expect(data).toBeDefined();
|
|
1602
1604
|
expect(data?.items).toHaveLength(1);
|
|
1603
1605
|
|
|
@@ -1613,7 +1615,7 @@ flow('Projection Test', () => {
|
|
|
1613
1615
|
it('should validate all three projection patterns together', async () => {
|
|
1614
1616
|
const memoryVfs = new InMemoryFileStore();
|
|
1615
1617
|
const flowContent = `
|
|
1616
|
-
import {
|
|
1618
|
+
import { scene, query, specs, rule, example, data, source, type Event, type State } from '@auto-engineer/narrative';
|
|
1617
1619
|
|
|
1618
1620
|
type TodoAdded = Event<'TodoAdded', { todoId: string; userId: string; projectId: string; description: string; addedAt: Date }>;
|
|
1619
1621
|
|
|
@@ -1621,7 +1623,7 @@ type TodoListSummary = State<'TodoListSummary', { summaryId: string; totalTodos:
|
|
|
1621
1623
|
type TodoState = State<'TodoState', { todoId: string; description: string; status: string }>;
|
|
1622
1624
|
type UserProjectTodos = State<'UserProjectTodos', { userId: string; projectId: string; todos: string[] }>;
|
|
1623
1625
|
|
|
1624
|
-
|
|
1626
|
+
scene('All Projection Patterns', () => {
|
|
1625
1627
|
query('views summary')
|
|
1626
1628
|
.server(() => {
|
|
1627
1629
|
specs(() => {
|
|
@@ -1665,7 +1667,7 @@ flow('All Projection Patterns', () => {
|
|
|
1665
1667
|
|
|
1666
1668
|
await memoryVfs.write('/test/projection.narrative.ts', new TextEncoder().encode(flowContent));
|
|
1667
1669
|
|
|
1668
|
-
const flows = await
|
|
1670
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
1669
1671
|
const model = flows.toModel();
|
|
1670
1672
|
|
|
1671
1673
|
const parseResult = modelSchema.safeParse(model);
|
|
@@ -1674,16 +1676,16 @@ flow('All Projection Patterns', () => {
|
|
|
1674
1676
|
}
|
|
1675
1677
|
expect(parseResult.success).toBe(true);
|
|
1676
1678
|
|
|
1677
|
-
const projectionFlow = model.
|
|
1679
|
+
const projectionFlow = model.scenes.find((f) => f.name === 'All Projection Patterns');
|
|
1678
1680
|
expect(projectionFlow).toBeDefined();
|
|
1679
1681
|
|
|
1680
1682
|
if (!projectionFlow) return;
|
|
1681
1683
|
|
|
1682
|
-
expect(projectionFlow.
|
|
1684
|
+
expect(projectionFlow.moments).toHaveLength(3);
|
|
1683
1685
|
|
|
1684
|
-
const
|
|
1685
|
-
if (
|
|
1686
|
-
const data =
|
|
1686
|
+
const summaryMoment = projectionFlow.moments.find((s) => s.name === 'views summary');
|
|
1687
|
+
if (summaryMoment?.type === 'query') {
|
|
1688
|
+
const data = summaryMoment.server.data;
|
|
1687
1689
|
expect(data?.items?.[0].origin).toMatchObject({
|
|
1688
1690
|
type: 'projection',
|
|
1689
1691
|
name: 'TodoSummary',
|
|
@@ -1691,9 +1693,9 @@ flow('All Projection Patterns', () => {
|
|
|
1691
1693
|
});
|
|
1692
1694
|
}
|
|
1693
1695
|
|
|
1694
|
-
const
|
|
1695
|
-
if (
|
|
1696
|
-
const data =
|
|
1696
|
+
const todoMoment = projectionFlow.moments.find((s) => s.name === 'views todo');
|
|
1697
|
+
if (todoMoment?.type === 'query') {
|
|
1698
|
+
const data = todoMoment.server.data;
|
|
1697
1699
|
expect(data?.items?.[0].origin).toMatchObject({
|
|
1698
1700
|
type: 'projection',
|
|
1699
1701
|
name: 'Todos',
|
|
@@ -1701,9 +1703,9 @@ flow('All Projection Patterns', () => {
|
|
|
1701
1703
|
});
|
|
1702
1704
|
}
|
|
1703
1705
|
|
|
1704
|
-
const
|
|
1705
|
-
if (
|
|
1706
|
-
const data =
|
|
1706
|
+
const userProjectMoment = projectionFlow.moments.find((s) => s.name === 'views user project todos');
|
|
1707
|
+
if (userProjectMoment?.type === 'query') {
|
|
1708
|
+
const data = userProjectMoment.server.data;
|
|
1707
1709
|
expect(data?.items?.[0].origin).toMatchObject({
|
|
1708
1710
|
type: 'projection',
|
|
1709
1711
|
name: 'UserProjectTodos',
|
|
@@ -1716,12 +1718,12 @@ flow('All Projection Patterns', () => {
|
|
|
1716
1718
|
const memoryVfs = new InMemoryFileStore();
|
|
1717
1719
|
|
|
1718
1720
|
const flowContent = `
|
|
1719
|
-
import {
|
|
1721
|
+
import { scene, command, query, specs, rule, example, data, sink, source, type Event, type State } from '@auto-engineer/narrative';
|
|
1720
1722
|
|
|
1721
1723
|
type OrderPlaced = Event<'OrderPlaced', { orderId: string; amount: number }>;
|
|
1722
1724
|
type OrderState = State<'OrderState', { orderId: string; status: string }>;
|
|
1723
1725
|
|
|
1724
|
-
|
|
1726
|
+
scene('Data Item IDs', () => {
|
|
1725
1727
|
command('places order')
|
|
1726
1728
|
.server(() => {
|
|
1727
1729
|
specs(() => {
|
|
@@ -1755,41 +1757,41 @@ flow('Data Item IDs', () => {
|
|
|
1755
1757
|
|
|
1756
1758
|
await memoryVfs.write('/test/data-ids.narrative.ts', new TextEncoder().encode(flowContent));
|
|
1757
1759
|
|
|
1758
|
-
const flows = await
|
|
1760
|
+
const flows = await getScenes({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
1759
1761
|
const model = flows.toModel();
|
|
1760
1762
|
|
|
1761
1763
|
const parseResult = modelSchema.safeParse(model);
|
|
1762
1764
|
expect(parseResult.success).toBe(true);
|
|
1763
1765
|
|
|
1764
|
-
const dataIdsFlow = model.
|
|
1766
|
+
const dataIdsFlow = model.scenes.find((f) => f.name === 'Data Item IDs');
|
|
1765
1767
|
expect(dataIdsFlow).toBeDefined();
|
|
1766
1768
|
|
|
1767
1769
|
if (!dataIdsFlow) return;
|
|
1768
1770
|
|
|
1769
|
-
const
|
|
1770
|
-
if (
|
|
1771
|
-
const sinkData =
|
|
1771
|
+
const commandMoment = dataIdsFlow.moments.find((s) => s.name === 'places order');
|
|
1772
|
+
if (commandMoment?.type === 'command') {
|
|
1773
|
+
const sinkData = commandMoment.server.data;
|
|
1772
1774
|
expect(sinkData?.items).toHaveLength(1);
|
|
1773
1775
|
expect(sinkData?.items?.[0].id).toBe('SINK-001');
|
|
1774
1776
|
}
|
|
1775
1777
|
|
|
1776
|
-
const
|
|
1777
|
-
if (
|
|
1778
|
-
const sourceData =
|
|
1778
|
+
const queryMoment = dataIdsFlow.moments.find((s) => s.name === 'views order status');
|
|
1779
|
+
if (queryMoment?.type === 'query') {
|
|
1780
|
+
const sourceData = queryMoment.server.data;
|
|
1779
1781
|
expect(sourceData?.items).toHaveLength(1);
|
|
1780
1782
|
expect(sourceData?.items?.[0].id).toBe('SOURCE-001');
|
|
1781
1783
|
}
|
|
1782
1784
|
});
|
|
1783
1785
|
});
|
|
1784
1786
|
|
|
1785
|
-
describe('round-trip: model -> narrative code -> VFS ->
|
|
1787
|
+
describe('round-trip: model -> narrative code -> VFS -> getScenes -> toModel', { timeout: 30_000 }, () => {
|
|
1786
1788
|
it('preserves step text values through full round-trip', async () => {
|
|
1787
1789
|
const inputModel: Model = {
|
|
1788
1790
|
variant: 'specs',
|
|
1789
|
-
|
|
1791
|
+
scenes: [
|
|
1790
1792
|
{
|
|
1791
1793
|
name: 'Stay management',
|
|
1792
|
-
|
|
1794
|
+
moments: [
|
|
1793
1795
|
{
|
|
1794
1796
|
type: 'command',
|
|
1795
1797
|
name: 'Publish stay',
|
|
@@ -1865,14 +1867,14 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
1865
1867
|
const generated = await modelToNarrative(inputModel);
|
|
1866
1868
|
|
|
1867
1869
|
const vfs = new InMemoryFileStore();
|
|
1868
|
-
|
|
1870
|
+
clearGetScenesCache();
|
|
1869
1871
|
|
|
1870
1872
|
for (const file of generated.files) {
|
|
1871
1873
|
const filePath = `/project/${file.path}`;
|
|
1872
1874
|
await vfs.write(filePath, new TextEncoder().encode(file.code));
|
|
1873
1875
|
}
|
|
1874
1876
|
|
|
1875
|
-
const result = await
|
|
1877
|
+
const result = await getScenes({
|
|
1876
1878
|
vfs,
|
|
1877
1879
|
root: '/project',
|
|
1878
1880
|
pattern: /\.(narrative)\.(ts)$/,
|
|
@@ -1881,12 +1883,12 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
1881
1883
|
|
|
1882
1884
|
const roundTrippedModel = result.toModel();
|
|
1883
1885
|
|
|
1884
|
-
const stayNarrative = roundTrippedModel.
|
|
1885
|
-
const
|
|
1886
|
-
expect(
|
|
1886
|
+
const stayNarrative = roundTrippedModel.scenes.find((n) => n.name === 'Stay management');
|
|
1887
|
+
const publishMoment = stayNarrative!.moments.find((s) => s.name === 'Publish stay');
|
|
1888
|
+
expect(publishMoment!.type).toBe('command');
|
|
1887
1889
|
|
|
1888
|
-
if (
|
|
1889
|
-
const example =
|
|
1890
|
+
if (publishMoment?.type === 'command') {
|
|
1891
|
+
const example = publishMoment.server.specs[0].rules[0].examples[0];
|
|
1890
1892
|
expect(example.steps).toEqual([
|
|
1891
1893
|
{ keyword: 'Given', text: 'StayCreated', docString: { stayId: 'stay_1', title: 'Beach house' } },
|
|
1892
1894
|
{ keyword: 'When', text: 'PublishStay', docString: { stayId: 'stay_1' } },
|
|
@@ -1902,10 +1904,10 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
1902
1904
|
it('preserves step text with multiple given events and multiple examples', async () => {
|
|
1903
1905
|
const inputModel: Model = {
|
|
1904
1906
|
variant: 'specs',
|
|
1905
|
-
|
|
1907
|
+
scenes: [
|
|
1906
1908
|
{
|
|
1907
1909
|
name: 'Order processing',
|
|
1908
|
-
|
|
1910
|
+
moments: [
|
|
1909
1911
|
{
|
|
1910
1912
|
type: 'command',
|
|
1911
1913
|
name: 'Complete order',
|
|
@@ -2022,14 +2024,14 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2022
2024
|
const generated = await modelToNarrative(inputModel);
|
|
2023
2025
|
|
|
2024
2026
|
const vfs = new InMemoryFileStore();
|
|
2025
|
-
|
|
2027
|
+
clearGetScenesCache();
|
|
2026
2028
|
|
|
2027
2029
|
for (const file of generated.files) {
|
|
2028
2030
|
const filePath = `/project/${file.path}`;
|
|
2029
2031
|
await vfs.write(filePath, new TextEncoder().encode(file.code));
|
|
2030
2032
|
}
|
|
2031
2033
|
|
|
2032
|
-
const result = await
|
|
2034
|
+
const result = await getScenes({
|
|
2033
2035
|
vfs,
|
|
2034
2036
|
root: '/project',
|
|
2035
2037
|
pattern: /\.(narrative)\.(ts)$/,
|
|
@@ -2038,12 +2040,12 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2038
2040
|
|
|
2039
2041
|
const roundTrippedModel = result.toModel();
|
|
2040
2042
|
|
|
2041
|
-
const orderNarrative = roundTrippedModel.
|
|
2042
|
-
const
|
|
2043
|
-
expect(
|
|
2043
|
+
const orderNarrative = roundTrippedModel.scenes.find((n) => n.name === 'Order processing');
|
|
2044
|
+
const completeMoment = orderNarrative!.moments.find((s) => s.name === 'Complete order');
|
|
2045
|
+
expect(completeMoment!.type).toBe('command');
|
|
2044
2046
|
|
|
2045
|
-
if (
|
|
2046
|
-
const examples =
|
|
2047
|
+
if (completeMoment?.type === 'command') {
|
|
2048
|
+
const examples = completeMoment.server.specs[0].rules[0].examples;
|
|
2047
2049
|
|
|
2048
2050
|
expect(examples[0].steps).toEqual([
|
|
2049
2051
|
{ keyword: 'Given', text: 'OrderCreated', docString: { orderId: 'ord_1', customerId: 'cust_1' } },
|
|
@@ -2064,14 +2066,14 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2064
2066
|
}
|
|
2065
2067
|
});
|
|
2066
2068
|
|
|
2067
|
-
it('preserves step text with multiple
|
|
2069
|
+
it('preserves step text with multiple scenes in separate modules', async () => {
|
|
2068
2070
|
const inputModel: Model = {
|
|
2069
2071
|
variant: 'specs',
|
|
2070
|
-
|
|
2072
|
+
scenes: [
|
|
2071
2073
|
{
|
|
2072
2074
|
name: 'Stays',
|
|
2073
2075
|
sourceFile: 'stays.narrative.ts',
|
|
2074
|
-
|
|
2076
|
+
moments: [
|
|
2075
2077
|
{
|
|
2076
2078
|
type: 'command',
|
|
2077
2079
|
name: 'Create stay',
|
|
@@ -2115,7 +2117,7 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2115
2117
|
{
|
|
2116
2118
|
name: 'Bookings',
|
|
2117
2119
|
sourceFile: 'bookings.narrative.ts',
|
|
2118
|
-
|
|
2120
|
+
moments: [
|
|
2119
2121
|
{
|
|
2120
2122
|
type: 'command',
|
|
2121
2123
|
name: 'Book stay',
|
|
@@ -2213,7 +2215,7 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2213
2215
|
{
|
|
2214
2216
|
sourceFile: 'stays.narrative.ts',
|
|
2215
2217
|
isDerived: true,
|
|
2216
|
-
contains: {
|
|
2218
|
+
contains: { sceneIds: [] },
|
|
2217
2219
|
declares: {
|
|
2218
2220
|
messages: [
|
|
2219
2221
|
{ kind: 'command', name: 'CreateStay' },
|
|
@@ -2224,7 +2226,7 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2224
2226
|
{
|
|
2225
2227
|
sourceFile: 'bookings.narrative.ts',
|
|
2226
2228
|
isDerived: true,
|
|
2227
|
-
contains: {
|
|
2229
|
+
contains: { sceneIds: [] },
|
|
2228
2230
|
declares: {
|
|
2229
2231
|
messages: [
|
|
2230
2232
|
{ kind: 'event', name: 'StayPublished' },
|
|
@@ -2239,14 +2241,14 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2239
2241
|
const generated = await modelToNarrative(inputModel);
|
|
2240
2242
|
|
|
2241
2243
|
const vfs = new InMemoryFileStore();
|
|
2242
|
-
|
|
2244
|
+
clearGetScenesCache();
|
|
2243
2245
|
|
|
2244
2246
|
for (const file of generated.files) {
|
|
2245
2247
|
const filePath = `/project/${file.path}`;
|
|
2246
2248
|
await vfs.write(filePath, new TextEncoder().encode(file.code));
|
|
2247
2249
|
}
|
|
2248
2250
|
|
|
2249
|
-
const result = await
|
|
2251
|
+
const result = await getScenes({
|
|
2250
2252
|
vfs,
|
|
2251
2253
|
root: '/project',
|
|
2252
2254
|
pattern: /\.(narrative)\.(ts)$/,
|
|
@@ -2255,12 +2257,12 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2255
2257
|
|
|
2256
2258
|
const roundTrippedModel = result.toModel();
|
|
2257
2259
|
|
|
2258
|
-
const bookingsNarrative = roundTrippedModel.
|
|
2259
|
-
const
|
|
2260
|
-
expect(
|
|
2260
|
+
const bookingsNarrative = roundTrippedModel.scenes.find((n) => n.name === 'Bookings');
|
|
2261
|
+
const bookMoment = bookingsNarrative!.moments.find((s) => s.name === 'Book stay');
|
|
2262
|
+
expect(bookMoment!.type).toBe('command');
|
|
2261
2263
|
|
|
2262
|
-
if (
|
|
2263
|
-
const example =
|
|
2264
|
+
if (bookMoment?.type === 'command') {
|
|
2265
|
+
const example = bookMoment.server.specs[0].rules[0].examples[0];
|
|
2264
2266
|
expect(example.steps).toEqual([
|
|
2265
2267
|
{
|
|
2266
2268
|
keyword: 'Given',
|
|
@@ -2276,12 +2278,12 @@ describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel
|
|
|
2276
2278
|
]);
|
|
2277
2279
|
}
|
|
2278
2280
|
|
|
2279
|
-
const staysNarrative = roundTrippedModel.
|
|
2280
|
-
const
|
|
2281
|
-
expect(
|
|
2281
|
+
const staysNarrative = roundTrippedModel.scenes.find((n) => n.name === 'Stays');
|
|
2282
|
+
const createMoment = staysNarrative!.moments.find((s) => s.name === 'Create stay');
|
|
2283
|
+
expect(createMoment!.type).toBe('command');
|
|
2282
2284
|
|
|
2283
|
-
if (
|
|
2284
|
-
const example =
|
|
2285
|
+
if (createMoment?.type === 'command') {
|
|
2286
|
+
const example = createMoment.server.specs[0].rules[0].examples[0];
|
|
2285
2287
|
expect(example.steps).toEqual([
|
|
2286
2288
|
{ keyword: 'When', text: 'CreateStay', docString: { stayId: 'stay_1', title: 'Cozy cabin' } },
|
|
2287
2289
|
{ keyword: 'Then', text: 'StayCreated', docString: { stayId: 'stay_1', title: 'Cozy cabin' } },
|