@auto-engineer/narrative 1.128.1 → 1.129.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 +60 -0
- package/dist/src/loader/ts-utils.d.ts.map +1 -1
- package/dist/src/loader/ts-utils.js +11 -0
- package/dist/src/loader/ts-utils.js.map +1 -1
- package/dist/src/parse-graphql-request.d.ts.map +1 -1
- package/dist/src/parse-graphql-request.js +5 -4
- package/dist/src/parse-graphql-request.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +3 -5
- package/package.json +4 -4
- package/src/getNarratives.specs.ts +509 -1
- package/src/loader/ts-utils.specs.ts +93 -1
- package/src/loader/ts-utils.ts +8 -0
- package/src/parse-graphql-request.specs.ts +33 -0
- package/src/parse-graphql-request.ts +6 -5
package/ketchup-plan.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Ketchup Plan:
|
|
1
|
+
# Ketchup Plan: Fix Scaffolder ListType Handling + node:crypto Protocol
|
|
2
2
|
|
|
3
3
|
## TODO
|
|
4
4
|
|
|
@@ -6,7 +6,5 @@
|
|
|
6
6
|
|
|
7
7
|
## DONE
|
|
8
8
|
|
|
9
|
-
- [x] Burst 1: Add
|
|
10
|
-
- [x] Burst 2:
|
|
11
|
-
- [x] Burst 3: Update stripTypeDiscriminator — completed in burst 1 (type change propagated)
|
|
12
|
-
- [x] Burst 4: Update flow code generator to handle target-only items (057c6e56)
|
|
9
|
+
- [x] Burst 1: Add isArray to getTypeName, handle ListType kind, update parseGraphQlRequest tsType wrapping — with test cases for [String!], [String!]!, [CustomType] (db71f007)
|
|
10
|
+
- [x] Burst 2: Change 'crypto' to 'node:crypto' in handle.ts.ejs template
|
package/package.json
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"typescript": "^5.9.2",
|
|
27
27
|
"zod": "^3.22.4",
|
|
28
28
|
"zod-to-json-schema": "^3.22.3",
|
|
29
|
-
"@auto-engineer/file-store": "1.
|
|
30
|
-
"@auto-engineer/id": "1.
|
|
31
|
-
"@auto-engineer/message-bus": "1.
|
|
29
|
+
"@auto-engineer/file-store": "1.129.0",
|
|
30
|
+
"@auto-engineer/id": "1.129.0",
|
|
31
|
+
"@auto-engineer/message-bus": "1.129.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^20.0.0",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"version": "1.
|
|
41
|
+
"version": "1.129.0",
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsx scripts/build.ts",
|
|
44
44
|
"test": "vitest run --reporter=dot",
|
|
@@ -3,7 +3,7 @@ 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 { getNarratives } from './getNarratives';
|
|
6
|
+
import { clearGetNarrativesCache, getNarratives } from './getNarratives';
|
|
7
7
|
import { type Example, type Model, modelToNarrative, type Narrative, type QuerySlice } from './index';
|
|
8
8
|
import { modelSchema } from './schema';
|
|
9
9
|
|
|
@@ -1781,3 +1781,511 @@ flow('Data Item IDs', () => {
|
|
|
1781
1781
|
}
|
|
1782
1782
|
});
|
|
1783
1783
|
});
|
|
1784
|
+
|
|
1785
|
+
describe('round-trip: model -> narrative code -> VFS -> getNarratives -> toModel', { timeout: 30_000 }, () => {
|
|
1786
|
+
it('preserves step text values through full round-trip', async () => {
|
|
1787
|
+
const inputModel: Model = {
|
|
1788
|
+
variant: 'specs',
|
|
1789
|
+
narratives: [
|
|
1790
|
+
{
|
|
1791
|
+
name: 'Stay management',
|
|
1792
|
+
slices: [
|
|
1793
|
+
{
|
|
1794
|
+
type: 'command',
|
|
1795
|
+
name: 'Publish stay',
|
|
1796
|
+
stream: 'stay-${stayId}',
|
|
1797
|
+
client: { specs: [] },
|
|
1798
|
+
server: {
|
|
1799
|
+
description: '',
|
|
1800
|
+
specs: [
|
|
1801
|
+
{
|
|
1802
|
+
type: 'gherkin',
|
|
1803
|
+
feature: 'Publishing stays',
|
|
1804
|
+
rules: [
|
|
1805
|
+
{
|
|
1806
|
+
name: 'A valid stay can be published',
|
|
1807
|
+
examples: [
|
|
1808
|
+
{
|
|
1809
|
+
name: 'publish a draft stay',
|
|
1810
|
+
steps: [
|
|
1811
|
+
{
|
|
1812
|
+
keyword: 'Given',
|
|
1813
|
+
text: 'StayCreated',
|
|
1814
|
+
docString: { stayId: 'stay_1', title: 'Beach house' },
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
keyword: 'When',
|
|
1818
|
+
text: 'PublishStay',
|
|
1819
|
+
docString: { stayId: 'stay_1' },
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
keyword: 'Then',
|
|
1823
|
+
text: 'StayPublished',
|
|
1824
|
+
docString: { stayId: 'stay_1', publishedAt: '2024-06-01T00:00:00.000Z' },
|
|
1825
|
+
},
|
|
1826
|
+
],
|
|
1827
|
+
},
|
|
1828
|
+
],
|
|
1829
|
+
},
|
|
1830
|
+
],
|
|
1831
|
+
},
|
|
1832
|
+
],
|
|
1833
|
+
data: undefined,
|
|
1834
|
+
},
|
|
1835
|
+
},
|
|
1836
|
+
],
|
|
1837
|
+
},
|
|
1838
|
+
],
|
|
1839
|
+
messages: [
|
|
1840
|
+
{
|
|
1841
|
+
type: 'event',
|
|
1842
|
+
name: 'StayCreated',
|
|
1843
|
+
fields: [
|
|
1844
|
+
{ name: 'stayId', type: 'string', required: true },
|
|
1845
|
+
{ name: 'title', type: 'string', required: true },
|
|
1846
|
+
],
|
|
1847
|
+
},
|
|
1848
|
+
{
|
|
1849
|
+
type: 'command',
|
|
1850
|
+
name: 'PublishStay',
|
|
1851
|
+
fields: [{ name: 'stayId', type: 'string', required: true }],
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
type: 'event',
|
|
1855
|
+
name: 'StayPublished',
|
|
1856
|
+
fields: [
|
|
1857
|
+
{ name: 'stayId', type: 'string', required: true },
|
|
1858
|
+
{ name: 'publishedAt', type: 'string', required: true },
|
|
1859
|
+
],
|
|
1860
|
+
},
|
|
1861
|
+
],
|
|
1862
|
+
modules: [],
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
const generated = await modelToNarrative(inputModel);
|
|
1866
|
+
|
|
1867
|
+
const vfs = new InMemoryFileStore();
|
|
1868
|
+
clearGetNarrativesCache();
|
|
1869
|
+
|
|
1870
|
+
for (const file of generated.files) {
|
|
1871
|
+
const filePath = `/project/${file.path}`;
|
|
1872
|
+
await vfs.write(filePath, new TextEncoder().encode(file.code));
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const result = await getNarratives({
|
|
1876
|
+
vfs,
|
|
1877
|
+
root: '/project',
|
|
1878
|
+
pattern: /\.(narrative)\.(ts)$/,
|
|
1879
|
+
fastFsScan: true,
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
const roundTrippedModel = result.toModel();
|
|
1883
|
+
|
|
1884
|
+
const stayNarrative = roundTrippedModel.narratives.find((n) => n.name === 'Stay management');
|
|
1885
|
+
const publishSlice = stayNarrative!.slices.find((s) => s.name === 'Publish stay');
|
|
1886
|
+
expect(publishSlice!.type).toBe('command');
|
|
1887
|
+
|
|
1888
|
+
if (publishSlice?.type === 'command') {
|
|
1889
|
+
const example = publishSlice.server.specs[0].rules[0].examples[0];
|
|
1890
|
+
expect(example.steps).toEqual([
|
|
1891
|
+
{ keyword: 'Given', text: 'StayCreated', docString: { stayId: 'stay_1', title: 'Beach house' } },
|
|
1892
|
+
{ keyword: 'When', text: 'PublishStay', docString: { stayId: 'stay_1' } },
|
|
1893
|
+
{
|
|
1894
|
+
keyword: 'Then',
|
|
1895
|
+
text: 'StayPublished',
|
|
1896
|
+
docString: { stayId: 'stay_1', publishedAt: '2024-06-01T00:00:00.000Z' },
|
|
1897
|
+
},
|
|
1898
|
+
]);
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
it('preserves step text with multiple given events and multiple examples', async () => {
|
|
1903
|
+
const inputModel: Model = {
|
|
1904
|
+
variant: 'specs',
|
|
1905
|
+
narratives: [
|
|
1906
|
+
{
|
|
1907
|
+
name: 'Order processing',
|
|
1908
|
+
slices: [
|
|
1909
|
+
{
|
|
1910
|
+
type: 'command',
|
|
1911
|
+
name: 'Complete order',
|
|
1912
|
+
stream: 'order-${orderId}',
|
|
1913
|
+
client: { specs: [] },
|
|
1914
|
+
server: {
|
|
1915
|
+
description: '',
|
|
1916
|
+
specs: [
|
|
1917
|
+
{
|
|
1918
|
+
type: 'gherkin',
|
|
1919
|
+
feature: 'Completing orders',
|
|
1920
|
+
rules: [
|
|
1921
|
+
{
|
|
1922
|
+
name: 'An order with items can be completed',
|
|
1923
|
+
examples: [
|
|
1924
|
+
{
|
|
1925
|
+
name: 'complete an order with payment',
|
|
1926
|
+
steps: [
|
|
1927
|
+
{
|
|
1928
|
+
keyword: 'Given',
|
|
1929
|
+
text: 'OrderCreated',
|
|
1930
|
+
docString: { orderId: 'ord_1', customerId: 'cust_1' },
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
keyword: 'And',
|
|
1934
|
+
text: 'PaymentReceived',
|
|
1935
|
+
docString: { orderId: 'ord_1', amount: 100 },
|
|
1936
|
+
},
|
|
1937
|
+
{
|
|
1938
|
+
keyword: 'When',
|
|
1939
|
+
text: 'CompleteOrder',
|
|
1940
|
+
docString: { orderId: 'ord_1' },
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
keyword: 'Then',
|
|
1944
|
+
text: 'OrderCompleted',
|
|
1945
|
+
docString: { orderId: 'ord_1', completedAt: '2024-06-01T00:00:00.000Z' },
|
|
1946
|
+
},
|
|
1947
|
+
],
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
name: 'reject order without payment',
|
|
1951
|
+
steps: [
|
|
1952
|
+
{
|
|
1953
|
+
keyword: 'Given',
|
|
1954
|
+
text: 'OrderCreated',
|
|
1955
|
+
docString: { orderId: 'ord_2', customerId: 'cust_2' },
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
keyword: 'When',
|
|
1959
|
+
text: 'CompleteOrder',
|
|
1960
|
+
docString: { orderId: 'ord_2' },
|
|
1961
|
+
},
|
|
1962
|
+
{
|
|
1963
|
+
keyword: 'Then',
|
|
1964
|
+
text: 'OrderRejected',
|
|
1965
|
+
docString: { orderId: 'ord_2', reason: 'no payment' },
|
|
1966
|
+
},
|
|
1967
|
+
],
|
|
1968
|
+
},
|
|
1969
|
+
],
|
|
1970
|
+
},
|
|
1971
|
+
],
|
|
1972
|
+
},
|
|
1973
|
+
],
|
|
1974
|
+
data: undefined,
|
|
1975
|
+
},
|
|
1976
|
+
},
|
|
1977
|
+
],
|
|
1978
|
+
},
|
|
1979
|
+
],
|
|
1980
|
+
messages: [
|
|
1981
|
+
{
|
|
1982
|
+
type: 'event',
|
|
1983
|
+
name: 'OrderCreated',
|
|
1984
|
+
fields: [
|
|
1985
|
+
{ name: 'orderId', type: 'string', required: true },
|
|
1986
|
+
{ name: 'customerId', type: 'string', required: true },
|
|
1987
|
+
],
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
type: 'event',
|
|
1991
|
+
name: 'PaymentReceived',
|
|
1992
|
+
fields: [
|
|
1993
|
+
{ name: 'orderId', type: 'string', required: true },
|
|
1994
|
+
{ name: 'amount', type: 'number', required: true },
|
|
1995
|
+
],
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
type: 'command',
|
|
1999
|
+
name: 'CompleteOrder',
|
|
2000
|
+
fields: [{ name: 'orderId', type: 'string', required: true }],
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
type: 'event',
|
|
2004
|
+
name: 'OrderCompleted',
|
|
2005
|
+
fields: [
|
|
2006
|
+
{ name: 'orderId', type: 'string', required: true },
|
|
2007
|
+
{ name: 'completedAt', type: 'string', required: true },
|
|
2008
|
+
],
|
|
2009
|
+
},
|
|
2010
|
+
{
|
|
2011
|
+
type: 'event',
|
|
2012
|
+
name: 'OrderRejected',
|
|
2013
|
+
fields: [
|
|
2014
|
+
{ name: 'orderId', type: 'string', required: true },
|
|
2015
|
+
{ name: 'reason', type: 'string', required: true },
|
|
2016
|
+
],
|
|
2017
|
+
},
|
|
2018
|
+
],
|
|
2019
|
+
modules: [],
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
const generated = await modelToNarrative(inputModel);
|
|
2023
|
+
|
|
2024
|
+
const vfs = new InMemoryFileStore();
|
|
2025
|
+
clearGetNarrativesCache();
|
|
2026
|
+
|
|
2027
|
+
for (const file of generated.files) {
|
|
2028
|
+
const filePath = `/project/${file.path}`;
|
|
2029
|
+
await vfs.write(filePath, new TextEncoder().encode(file.code));
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
const result = await getNarratives({
|
|
2033
|
+
vfs,
|
|
2034
|
+
root: '/project',
|
|
2035
|
+
pattern: /\.(narrative)\.(ts)$/,
|
|
2036
|
+
fastFsScan: true,
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
const roundTrippedModel = result.toModel();
|
|
2040
|
+
|
|
2041
|
+
const orderNarrative = roundTrippedModel.narratives.find((n) => n.name === 'Order processing');
|
|
2042
|
+
const completeSlice = orderNarrative!.slices.find((s) => s.name === 'Complete order');
|
|
2043
|
+
expect(completeSlice!.type).toBe('command');
|
|
2044
|
+
|
|
2045
|
+
if (completeSlice?.type === 'command') {
|
|
2046
|
+
const examples = completeSlice.server.specs[0].rules[0].examples;
|
|
2047
|
+
|
|
2048
|
+
expect(examples[0].steps).toEqual([
|
|
2049
|
+
{ keyword: 'Given', text: 'OrderCreated', docString: { orderId: 'ord_1', customerId: 'cust_1' } },
|
|
2050
|
+
{ keyword: 'And', text: 'PaymentReceived', docString: { orderId: 'ord_1', amount: 100 } },
|
|
2051
|
+
{ keyword: 'When', text: 'CompleteOrder', docString: { orderId: 'ord_1' } },
|
|
2052
|
+
{
|
|
2053
|
+
keyword: 'Then',
|
|
2054
|
+
text: 'OrderCompleted',
|
|
2055
|
+
docString: { orderId: 'ord_1', completedAt: '2024-06-01T00:00:00.000Z' },
|
|
2056
|
+
},
|
|
2057
|
+
]);
|
|
2058
|
+
|
|
2059
|
+
expect(examples[1].steps).toEqual([
|
|
2060
|
+
{ keyword: 'Given', text: 'OrderCreated', docString: { orderId: 'ord_2', customerId: 'cust_2' } },
|
|
2061
|
+
{ keyword: 'When', text: 'CompleteOrder', docString: { orderId: 'ord_2' } },
|
|
2062
|
+
{ keyword: 'Then', text: 'OrderRejected', docString: { orderId: 'ord_2', reason: 'no payment' } },
|
|
2063
|
+
]);
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
it('preserves step text with multiple narratives in separate modules', async () => {
|
|
2068
|
+
const inputModel: Model = {
|
|
2069
|
+
variant: 'specs',
|
|
2070
|
+
narratives: [
|
|
2071
|
+
{
|
|
2072
|
+
name: 'Stays',
|
|
2073
|
+
sourceFile: 'stays.narrative.ts',
|
|
2074
|
+
slices: [
|
|
2075
|
+
{
|
|
2076
|
+
type: 'command',
|
|
2077
|
+
name: 'Create stay',
|
|
2078
|
+
stream: 'stay-${stayId}',
|
|
2079
|
+
client: { specs: [] },
|
|
2080
|
+
server: {
|
|
2081
|
+
description: '',
|
|
2082
|
+
specs: [
|
|
2083
|
+
{
|
|
2084
|
+
type: 'gherkin',
|
|
2085
|
+
feature: 'Creating stays',
|
|
2086
|
+
rules: [
|
|
2087
|
+
{
|
|
2088
|
+
name: 'A valid stay can be created',
|
|
2089
|
+
examples: [
|
|
2090
|
+
{
|
|
2091
|
+
name: 'create a new stay',
|
|
2092
|
+
steps: [
|
|
2093
|
+
{
|
|
2094
|
+
keyword: 'When',
|
|
2095
|
+
text: 'CreateStay',
|
|
2096
|
+
docString: { stayId: 'stay_1', title: 'Cozy cabin' },
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
keyword: 'Then',
|
|
2100
|
+
text: 'StayCreated',
|
|
2101
|
+
docString: { stayId: 'stay_1', title: 'Cozy cabin' },
|
|
2102
|
+
},
|
|
2103
|
+
],
|
|
2104
|
+
},
|
|
2105
|
+
],
|
|
2106
|
+
},
|
|
2107
|
+
],
|
|
2108
|
+
},
|
|
2109
|
+
],
|
|
2110
|
+
data: undefined,
|
|
2111
|
+
},
|
|
2112
|
+
},
|
|
2113
|
+
],
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
name: 'Bookings',
|
|
2117
|
+
sourceFile: 'bookings.narrative.ts',
|
|
2118
|
+
slices: [
|
|
2119
|
+
{
|
|
2120
|
+
type: 'command',
|
|
2121
|
+
name: 'Book stay',
|
|
2122
|
+
stream: 'booking-${bookingId}',
|
|
2123
|
+
client: { specs: [] },
|
|
2124
|
+
server: {
|
|
2125
|
+
description: '',
|
|
2126
|
+
specs: [
|
|
2127
|
+
{
|
|
2128
|
+
type: 'gherkin',
|
|
2129
|
+
feature: 'Booking stays',
|
|
2130
|
+
rules: [
|
|
2131
|
+
{
|
|
2132
|
+
name: 'A published stay can be booked',
|
|
2133
|
+
examples: [
|
|
2134
|
+
{
|
|
2135
|
+
name: 'book a published stay',
|
|
2136
|
+
steps: [
|
|
2137
|
+
{
|
|
2138
|
+
keyword: 'Given',
|
|
2139
|
+
text: 'StayPublished',
|
|
2140
|
+
docString: { stayId: 'stay_1', publishedAt: '2024-01-01T00:00:00.000Z' },
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
keyword: 'When',
|
|
2144
|
+
text: 'BookStay',
|
|
2145
|
+
docString: { stayId: 'stay_1', guestId: 'guest_1' },
|
|
2146
|
+
},
|
|
2147
|
+
{
|
|
2148
|
+
keyword: 'Then',
|
|
2149
|
+
text: 'StayBooked',
|
|
2150
|
+
docString: {
|
|
2151
|
+
stayId: 'stay_1',
|
|
2152
|
+
guestId: 'guest_1',
|
|
2153
|
+
bookedAt: '2024-02-01T00:00:00.000Z',
|
|
2154
|
+
},
|
|
2155
|
+
},
|
|
2156
|
+
],
|
|
2157
|
+
},
|
|
2158
|
+
],
|
|
2159
|
+
},
|
|
2160
|
+
],
|
|
2161
|
+
},
|
|
2162
|
+
],
|
|
2163
|
+
data: undefined,
|
|
2164
|
+
},
|
|
2165
|
+
},
|
|
2166
|
+
],
|
|
2167
|
+
},
|
|
2168
|
+
],
|
|
2169
|
+
messages: [
|
|
2170
|
+
{
|
|
2171
|
+
type: 'command',
|
|
2172
|
+
name: 'CreateStay',
|
|
2173
|
+
fields: [
|
|
2174
|
+
{ name: 'stayId', type: 'string', required: true },
|
|
2175
|
+
{ name: 'title', type: 'string', required: true },
|
|
2176
|
+
],
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
type: 'event',
|
|
2180
|
+
name: 'StayCreated',
|
|
2181
|
+
fields: [
|
|
2182
|
+
{ name: 'stayId', type: 'string', required: true },
|
|
2183
|
+
{ name: 'title', type: 'string', required: true },
|
|
2184
|
+
],
|
|
2185
|
+
},
|
|
2186
|
+
{
|
|
2187
|
+
type: 'event',
|
|
2188
|
+
name: 'StayPublished',
|
|
2189
|
+
fields: [
|
|
2190
|
+
{ name: 'stayId', type: 'string', required: true },
|
|
2191
|
+
{ name: 'publishedAt', type: 'string', required: true },
|
|
2192
|
+
],
|
|
2193
|
+
},
|
|
2194
|
+
{
|
|
2195
|
+
type: 'command',
|
|
2196
|
+
name: 'BookStay',
|
|
2197
|
+
fields: [
|
|
2198
|
+
{ name: 'stayId', type: 'string', required: true },
|
|
2199
|
+
{ name: 'guestId', type: 'string', required: true },
|
|
2200
|
+
],
|
|
2201
|
+
},
|
|
2202
|
+
{
|
|
2203
|
+
type: 'event',
|
|
2204
|
+
name: 'StayBooked',
|
|
2205
|
+
fields: [
|
|
2206
|
+
{ name: 'stayId', type: 'string', required: true },
|
|
2207
|
+
{ name: 'guestId', type: 'string', required: true },
|
|
2208
|
+
{ name: 'bookedAt', type: 'string', required: true },
|
|
2209
|
+
],
|
|
2210
|
+
},
|
|
2211
|
+
],
|
|
2212
|
+
modules: [
|
|
2213
|
+
{
|
|
2214
|
+
sourceFile: 'stays.narrative.ts',
|
|
2215
|
+
isDerived: true,
|
|
2216
|
+
contains: { narrativeIds: [] },
|
|
2217
|
+
declares: {
|
|
2218
|
+
messages: [
|
|
2219
|
+
{ kind: 'command', name: 'CreateStay' },
|
|
2220
|
+
{ kind: 'event', name: 'StayCreated' },
|
|
2221
|
+
],
|
|
2222
|
+
},
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
sourceFile: 'bookings.narrative.ts',
|
|
2226
|
+
isDerived: true,
|
|
2227
|
+
contains: { narrativeIds: [] },
|
|
2228
|
+
declares: {
|
|
2229
|
+
messages: [
|
|
2230
|
+
{ kind: 'event', name: 'StayPublished' },
|
|
2231
|
+
{ kind: 'command', name: 'BookStay' },
|
|
2232
|
+
{ kind: 'event', name: 'StayBooked' },
|
|
2233
|
+
],
|
|
2234
|
+
},
|
|
2235
|
+
},
|
|
2236
|
+
],
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
const generated = await modelToNarrative(inputModel);
|
|
2240
|
+
|
|
2241
|
+
const vfs = new InMemoryFileStore();
|
|
2242
|
+
clearGetNarrativesCache();
|
|
2243
|
+
|
|
2244
|
+
for (const file of generated.files) {
|
|
2245
|
+
const filePath = `/project/${file.path}`;
|
|
2246
|
+
await vfs.write(filePath, new TextEncoder().encode(file.code));
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
const result = await getNarratives({
|
|
2250
|
+
vfs,
|
|
2251
|
+
root: '/project',
|
|
2252
|
+
pattern: /\.(narrative)\.(ts)$/,
|
|
2253
|
+
fastFsScan: true,
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
const roundTrippedModel = result.toModel();
|
|
2257
|
+
|
|
2258
|
+
const bookingsNarrative = roundTrippedModel.narratives.find((n) => n.name === 'Bookings');
|
|
2259
|
+
const bookSlice = bookingsNarrative!.slices.find((s) => s.name === 'Book stay');
|
|
2260
|
+
expect(bookSlice!.type).toBe('command');
|
|
2261
|
+
|
|
2262
|
+
if (bookSlice?.type === 'command') {
|
|
2263
|
+
const example = bookSlice.server.specs[0].rules[0].examples[0];
|
|
2264
|
+
expect(example.steps).toEqual([
|
|
2265
|
+
{
|
|
2266
|
+
keyword: 'Given',
|
|
2267
|
+
text: 'StayPublished',
|
|
2268
|
+
docString: { stayId: 'stay_1', publishedAt: '2024-01-01T00:00:00.000Z' },
|
|
2269
|
+
},
|
|
2270
|
+
{ keyword: 'When', text: 'BookStay', docString: { stayId: 'stay_1', guestId: 'guest_1' } },
|
|
2271
|
+
{
|
|
2272
|
+
keyword: 'Then',
|
|
2273
|
+
text: 'StayBooked',
|
|
2274
|
+
docString: { stayId: 'stay_1', guestId: 'guest_1', bookedAt: '2024-02-01T00:00:00.000Z' },
|
|
2275
|
+
},
|
|
2276
|
+
]);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
const staysNarrative = roundTrippedModel.narratives.find((n) => n.name === 'Stays');
|
|
2280
|
+
const createSlice = staysNarrative!.slices.find((s) => s.name === 'Create stay');
|
|
2281
|
+
expect(createSlice!.type).toBe('command');
|
|
2282
|
+
|
|
2283
|
+
if (createSlice?.type === 'command') {
|
|
2284
|
+
const example = createSlice.server.specs[0].rules[0].examples[0];
|
|
2285
|
+
expect(example.steps).toEqual([
|
|
2286
|
+
{ keyword: 'When', text: 'CreateStay', docString: { stayId: 'stay_1', title: 'Cozy cabin' } },
|
|
2287
|
+
{ keyword: 'Then', text: 'StayCreated', docString: { stayId: 'stay_1', title: 'Cozy cabin' } },
|
|
2288
|
+
]);
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
});
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseGivenTypeArguments,
|
|
5
|
+
parseThenTypeArguments,
|
|
6
|
+
parseTypeDefinitions,
|
|
7
|
+
parseWhenTypeArguments,
|
|
8
|
+
} from './ts-utils';
|
|
9
|
+
import { createVfsCompilerHost } from './vfs-compiler-host';
|
|
10
|
+
|
|
11
|
+
function createProgramFromSource(source: string, fileName: string = '/test.ts') {
|
|
12
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.ES2020, true, ts.ScriptKind.TS);
|
|
13
|
+
const sourceFiles = new Map<string, ts.SourceFile>([[fileName, sourceFile]]);
|
|
14
|
+
const host = createVfsCompilerHost(ts, sourceFiles);
|
|
15
|
+
const program = ts.createProgram(
|
|
16
|
+
[fileName],
|
|
17
|
+
{
|
|
18
|
+
target: ts.ScriptTarget.ES2020,
|
|
19
|
+
module: ts.ModuleKind.ESNext,
|
|
20
|
+
strict: true,
|
|
21
|
+
skipLibCheck: true,
|
|
22
|
+
skipDefaultLibCheck: true,
|
|
23
|
+
},
|
|
24
|
+
host,
|
|
25
|
+
);
|
|
26
|
+
const checker = program.getTypeChecker();
|
|
27
|
+
const programSourceFile = program.getSourceFile(fileName)!;
|
|
28
|
+
return { checker, sourceFile: programSourceFile };
|
|
29
|
+
}
|
|
4
30
|
|
|
5
31
|
describe('parseTypeDefinitions', () => {
|
|
6
32
|
it('extracts data fields with Array<T> generic syntax', () => {
|
|
@@ -23,3 +49,69 @@ describe('parseTypeDefinitions', () => {
|
|
|
23
49
|
});
|
|
24
50
|
});
|
|
25
51
|
});
|
|
52
|
+
|
|
53
|
+
describe('parseGivenTypeArguments fallback', () => {
|
|
54
|
+
it('uses raw type text when tryUnwrapGeneric fails to resolve', () => {
|
|
55
|
+
const source = `
|
|
56
|
+
example('test')
|
|
57
|
+
.given<UnresolvableType>({ id: '1' })
|
|
58
|
+
.when<AnotherUnresolvable>({ name: 'foo' });
|
|
59
|
+
`;
|
|
60
|
+
const { checker, sourceFile } = createProgramFromSource(source);
|
|
61
|
+
const typeMap = new Map();
|
|
62
|
+
const typesByFile = new Map();
|
|
63
|
+
|
|
64
|
+
const result = parseGivenTypeArguments(ts, checker, sourceFile, typeMap, typesByFile);
|
|
65
|
+
|
|
66
|
+
expect(result).toEqual([expect.objectContaining({ ordinal: 0, typeName: 'UnresolvableType' })]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('preserves ordinal alignment when some types resolve and others do not', () => {
|
|
70
|
+
const source = `
|
|
71
|
+
type KnownEvent = Event<'KnownEvent', { id: string }>;
|
|
72
|
+
example('test')
|
|
73
|
+
.given<KnownEvent>({ id: '1' })
|
|
74
|
+
.and<UnknownType>({ name: 'foo' });
|
|
75
|
+
`;
|
|
76
|
+
const typeMap = parseTypeDefinitions(ts, '/test.ts', source);
|
|
77
|
+
const typesByFile = new Map([['/test.ts', typeMap]]);
|
|
78
|
+
const { checker, sourceFile } = createProgramFromSource(source);
|
|
79
|
+
|
|
80
|
+
const result = parseGivenTypeArguments(ts, checker, sourceFile, typeMap, typesByFile);
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual([
|
|
83
|
+
expect.objectContaining({ ordinal: 0, typeName: 'KnownEvent', classification: 'event' }),
|
|
84
|
+
expect.objectContaining({ ordinal: 1, typeName: 'UnknownType' }),
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('parseWhenTypeArguments fallback', () => {
|
|
90
|
+
it('uses raw type text when tryUnwrapGeneric fails to resolve', () => {
|
|
91
|
+
const source = `
|
|
92
|
+
example('test')
|
|
93
|
+
.when<UnresolvableCommand>({ id: '1' });
|
|
94
|
+
`;
|
|
95
|
+
const { checker, sourceFile } = createProgramFromSource(source);
|
|
96
|
+
|
|
97
|
+
const result = parseWhenTypeArguments(ts, checker, sourceFile, new Map(), new Map());
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual([expect.objectContaining({ ordinal: 0, typeName: 'UnresolvableCommand' })]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('parseThenTypeArguments fallback', () => {
|
|
104
|
+
it('uses raw type text when tryUnwrapGeneric fails to resolve', () => {
|
|
105
|
+
const source = `
|
|
106
|
+
example('test')
|
|
107
|
+
.given<SomeType>({ id: '1' })
|
|
108
|
+
.when<SomeCommand>({ name: 'foo' })
|
|
109
|
+
.then<UnresolvableResult>({ status: 'ok' });
|
|
110
|
+
`;
|
|
111
|
+
const { checker, sourceFile } = createProgramFromSource(source);
|
|
112
|
+
|
|
113
|
+
const result = parseThenTypeArguments(ts, checker, sourceFile, new Map(), new Map());
|
|
114
|
+
|
|
115
|
+
expect(result).toEqual([expect.objectContaining({ ordinal: 0, typeName: 'UnresolvableResult' })]);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/loader/ts-utils.ts
CHANGED
|
@@ -594,6 +594,9 @@ function processGivenOrAndCallExpression(
|
|
|
594
594
|
givenTypes.push(
|
|
595
595
|
createGivenTypeInfo(sourceFile, node, ordinal, genericResult.typeName, genericResult.classification),
|
|
596
596
|
);
|
|
597
|
+
} else {
|
|
598
|
+
const rawText = typeArg.getText(sourceFile);
|
|
599
|
+
givenTypes.push(createGivenTypeInfo(sourceFile, node, ordinal, rawText, 'event'));
|
|
597
600
|
}
|
|
598
601
|
}
|
|
599
602
|
|
|
@@ -656,6 +659,9 @@ function processWhenCallExpression(
|
|
|
656
659
|
whenTypes.push(
|
|
657
660
|
createGivenTypeInfo(sourceFile, node, ordinal, genericResult.typeName, genericResult.classification),
|
|
658
661
|
);
|
|
662
|
+
} else {
|
|
663
|
+
const rawText = typeArg.getText(sourceFile);
|
|
664
|
+
whenTypes.push(createGivenTypeInfo(sourceFile, node, ordinal, rawText, 'command'));
|
|
659
665
|
}
|
|
660
666
|
}
|
|
661
667
|
|
|
@@ -828,6 +834,8 @@ function processThenCallWithTypeArg(
|
|
|
828
834
|
thenTypes.push(
|
|
829
835
|
createGivenTypeInfo(sourceFile, node, thenTypes.length, genericResult.typeName, genericResult.classification),
|
|
830
836
|
);
|
|
837
|
+
} else {
|
|
838
|
+
thenTypes.push(createGivenTypeInfo(sourceFile, node, thenTypes.length, typeArgText, 'event'));
|
|
831
839
|
}
|
|
832
840
|
}
|
|
833
841
|
|