@auto-engineer/narrative 1.128.2 → 1.130.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/ketchup-plan.md CHANGED
@@ -1,4 +1,4 @@
1
- # Ketchup Plan: DataTarget Schema Target-Only Event Items
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 DataTargetSchema to schema.ts, DataTarget type, DataTargetItem, exports (81188d6b)
10
- - [x] Burst 2: Add target() builder factory in data-narrative-builders.ts (757b8475)
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.128.2",
30
- "@auto-engineer/id": "1.128.2",
31
- "@auto-engineer/message-bus": "1.128.2"
29
+ "@auto-engineer/file-store": "1.130.0",
30
+ "@auto-engineer/id": "1.130.0",
31
+ "@auto-engineer/message-bus": "1.130.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.128.2",
41
+ "version": "1.130.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 { parseTypeDefinitions } from './ts-utils';
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
+ });
@@ -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