@lokascript/domain-flow 2.1.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/README.md +120 -0
- package/dist/index.cjs +2251 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +643 -0
- package/dist/index.d.ts +643 -0
- package/dist/index.js +2169 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
- package/src/__test__/flow-domain.test.ts +696 -0
- package/src/__test__/hateoas-commands.test.ts +520 -0
- package/src/__test__/htmx-generator.test.ts +100 -0
- package/src/__test__/mcp-workflow-server.test.ts +317 -0
- package/src/__test__/pipeline-parser.test.ts +188 -0
- package/src/__test__/route-extractor.test.ts +94 -0
- package/src/generators/flow-generator.ts +338 -0
- package/src/generators/flow-renderer.ts +262 -0
- package/src/generators/htmx-generator.ts +129 -0
- package/src/generators/route-extractor.ts +105 -0
- package/src/generators/workflow-generator.ts +129 -0
- package/src/index.ts +210 -0
- package/src/parser/pipeline-parser.ts +151 -0
- package/src/profiles/index.ts +186 -0
- package/src/runtime/mcp-workflow-server.ts +409 -0
- package/src/runtime/workflow-executor.ts +171 -0
- package/src/schemas/hateoas-schemas.ts +152 -0
- package/src/schemas/index.ts +320 -0
- package/src/siren-agent.d.ts +14 -0
- package/src/tokenizers/index.ts +592 -0
- package/src/types.ts +108 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HATEOAS Command Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates the 4 HATEOAS FlowScript commands (enter, follow, perform, capture)
|
|
5
|
+
* across 8 languages (EN, ES, JA, AR, KO, ZH, TR, FR) covering SVO, SOV, and VSO.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
9
|
+
import { createFlowDSL, toFlowSpec } from '../index.js';
|
|
10
|
+
import { toWorkflowSpec, toSirenGrailSteps } from '../generators/workflow-generator.js';
|
|
11
|
+
import type { MultilingualDSL } from '@lokascript/framework';
|
|
12
|
+
import { extractRoleValue } from '@lokascript/framework';
|
|
13
|
+
|
|
14
|
+
describe('HATEOAS Commands', () => {
|
|
15
|
+
let flow: MultilingualDSL;
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
flow = createFlowDSL();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ===========================================================================
|
|
22
|
+
// English (SVO) — enter
|
|
23
|
+
// ===========================================================================
|
|
24
|
+
|
|
25
|
+
describe('English — enter', () => {
|
|
26
|
+
it('should parse enter with URL', () => {
|
|
27
|
+
const node = flow.parse('enter /api', 'en');
|
|
28
|
+
expect(node.action).toBe('enter');
|
|
29
|
+
expect(extractRoleValue(node, 'source')).toBe('/api');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should compile enter to JS', () => {
|
|
33
|
+
const result = flow.compile('enter /api', 'en');
|
|
34
|
+
expect(result.ok).toBe(true);
|
|
35
|
+
expect(result.code).toContain('SirenAgent');
|
|
36
|
+
expect(result.code).toContain('/api');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should produce correct FlowSpec', () => {
|
|
40
|
+
const node = flow.parse('enter /api', 'en');
|
|
41
|
+
const spec = toFlowSpec(node, 'en');
|
|
42
|
+
expect(spec.action).toBe('enter');
|
|
43
|
+
expect(spec.url).toBe('/api');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ===========================================================================
|
|
48
|
+
// English (SVO) — follow
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
|
|
51
|
+
describe('English — follow', () => {
|
|
52
|
+
it('should parse follow with rel', () => {
|
|
53
|
+
const node = flow.parse('follow orders', 'en');
|
|
54
|
+
expect(node.action).toBe('follow');
|
|
55
|
+
expect(extractRoleValue(node, 'patient')).toBe('orders');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should compile follow to JS', () => {
|
|
59
|
+
const result = flow.compile('follow orders', 'en');
|
|
60
|
+
expect(result.ok).toBe(true);
|
|
61
|
+
expect(result.code).toContain("followLink('orders')");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should produce correct FlowSpec', () => {
|
|
65
|
+
const node = flow.parse('follow orders', 'en');
|
|
66
|
+
const spec = toFlowSpec(node, 'en');
|
|
67
|
+
expect(spec.action).toBe('follow');
|
|
68
|
+
expect(spec.linkRel).toBe('orders');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ===========================================================================
|
|
73
|
+
// English (SVO) — perform
|
|
74
|
+
// ===========================================================================
|
|
75
|
+
|
|
76
|
+
describe('English — perform', () => {
|
|
77
|
+
it('should parse perform with action name', () => {
|
|
78
|
+
const node = flow.parse('perform createOrder', 'en');
|
|
79
|
+
expect(node.action).toBe('perform');
|
|
80
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should parse perform with data source', () => {
|
|
84
|
+
const node = flow.parse('perform createOrder with #checkout', 'en');
|
|
85
|
+
expect(node.action).toBe('perform');
|
|
86
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
87
|
+
expect(extractRoleValue(node, 'source')).toBe('#checkout');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should compile perform to JS', () => {
|
|
91
|
+
const result = flow.compile('perform createOrder', 'en');
|
|
92
|
+
expect(result.ok).toBe(true);
|
|
93
|
+
expect(result.code).toContain("executeAction('createOrder')");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should compile perform with data source', () => {
|
|
97
|
+
const result = flow.compile('perform createOrder with #checkout', 'en');
|
|
98
|
+
expect(result.ok).toBe(true);
|
|
99
|
+
expect(result.code).toContain("executeAction('createOrder', data)");
|
|
100
|
+
expect(result.code).toContain('#checkout');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should produce correct FlowSpec', () => {
|
|
104
|
+
const node = flow.parse('perform createOrder with #checkout', 'en');
|
|
105
|
+
const spec = toFlowSpec(node, 'en');
|
|
106
|
+
expect(spec.action).toBe('perform');
|
|
107
|
+
expect(spec.actionName).toBe('createOrder');
|
|
108
|
+
expect(spec.dataSource).toBe('#checkout');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ===========================================================================
|
|
113
|
+
// English (SVO) — capture
|
|
114
|
+
// ===========================================================================
|
|
115
|
+
|
|
116
|
+
describe('English — capture', () => {
|
|
117
|
+
it('should parse capture with variable name', () => {
|
|
118
|
+
const node = flow.parse('capture as orders', 'en');
|
|
119
|
+
expect(node.action).toBe('capture');
|
|
120
|
+
expect(extractRoleValue(node, 'destination')).toBe('orders');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should parse capture with property path', () => {
|
|
124
|
+
const node = flow.parse('capture message as confirmationText', 'en');
|
|
125
|
+
expect(node.action).toBe('capture');
|
|
126
|
+
expect(extractRoleValue(node, 'patient')).toBe('message');
|
|
127
|
+
expect(extractRoleValue(node, 'destination')).toBe('confirmationText');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should compile capture to JS', () => {
|
|
131
|
+
const result = flow.compile('capture as orders', 'en');
|
|
132
|
+
expect(result.ok).toBe(true);
|
|
133
|
+
expect(result.code).toContain('orders');
|
|
134
|
+
expect(result.code).toContain('properties');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should produce correct FlowSpec', () => {
|
|
138
|
+
const node = flow.parse('capture message as confirmationText', 'en');
|
|
139
|
+
const spec = toFlowSpec(node, 'en');
|
|
140
|
+
expect(spec.action).toBe('capture');
|
|
141
|
+
expect(spec.captureAs).toBe('confirmationText');
|
|
142
|
+
expect(spec.capturePath).toBe('message');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
// Spanish (SVO)
|
|
148
|
+
// ===========================================================================
|
|
149
|
+
|
|
150
|
+
describe('Spanish (SVO)', () => {
|
|
151
|
+
it('should parse enter in Spanish', () => {
|
|
152
|
+
const node = flow.parse('entrar /api', 'es');
|
|
153
|
+
expect(node.action).toBe('enter');
|
|
154
|
+
expect(extractRoleValue(node, 'source')).toBe('/api');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should parse follow in Spanish', () => {
|
|
158
|
+
const node = flow.parse('seguir orders', 'es');
|
|
159
|
+
expect(node.action).toBe('follow');
|
|
160
|
+
expect(extractRoleValue(node, 'patient')).toBe('orders');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should parse perform in Spanish', () => {
|
|
164
|
+
const node = flow.parse('ejecutar createOrder con #checkout', 'es');
|
|
165
|
+
expect(node.action).toBe('perform');
|
|
166
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
167
|
+
expect(extractRoleValue(node, 'source')).toBe('#checkout');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should parse capture in Spanish', () => {
|
|
171
|
+
const node = flow.parse('capturar como orders', 'es');
|
|
172
|
+
expect(node.action).toBe('capture');
|
|
173
|
+
expect(extractRoleValue(node, 'destination')).toBe('orders');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ===========================================================================
|
|
178
|
+
// Japanese (SOV)
|
|
179
|
+
// ===========================================================================
|
|
180
|
+
|
|
181
|
+
describe('Japanese (SOV)', () => {
|
|
182
|
+
it('should parse enter in Japanese', () => {
|
|
183
|
+
const node = flow.parse('/api 入る', 'ja');
|
|
184
|
+
expect(node.action).toBe('enter');
|
|
185
|
+
expect(extractRoleValue(node, 'source')).toBe('/api');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should parse follow in Japanese', () => {
|
|
189
|
+
const node = flow.parse('orders 辿る', 'ja');
|
|
190
|
+
expect(node.action).toBe('follow');
|
|
191
|
+
expect(extractRoleValue(node, 'patient')).toBe('orders');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should parse perform in Japanese', () => {
|
|
195
|
+
const node = flow.parse('#checkout で createOrder 実行', 'ja');
|
|
196
|
+
expect(node.action).toBe('perform');
|
|
197
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
198
|
+
expect(extractRoleValue(node, 'source')).toBe('#checkout');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should parse capture in Japanese', () => {
|
|
202
|
+
const node = flow.parse('orders として 取得変数', 'ja');
|
|
203
|
+
expect(node.action).toBe('capture');
|
|
204
|
+
expect(extractRoleValue(node, 'destination')).toBe('orders');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ===========================================================================
|
|
209
|
+
// Arabic (VSO)
|
|
210
|
+
// ===========================================================================
|
|
211
|
+
|
|
212
|
+
describe('Arabic (VSO)', () => {
|
|
213
|
+
it('should parse enter in Arabic', () => {
|
|
214
|
+
const node = flow.parse('ادخل /api', 'ar');
|
|
215
|
+
expect(node.action).toBe('enter');
|
|
216
|
+
expect(extractRoleValue(node, 'source')).toBe('/api');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should parse follow in Arabic', () => {
|
|
220
|
+
const node = flow.parse('اتبع orders', 'ar');
|
|
221
|
+
expect(node.action).toBe('follow');
|
|
222
|
+
expect(extractRoleValue(node, 'patient')).toBe('orders');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should parse perform in Arabic', () => {
|
|
226
|
+
const node = flow.parse('نفّذ createOrder ب #checkout', 'ar');
|
|
227
|
+
expect(node.action).toBe('perform');
|
|
228
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
229
|
+
expect(extractRoleValue(node, 'source')).toBe('#checkout');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should parse capture in Arabic', () => {
|
|
233
|
+
const node = flow.parse('التقط ك orders', 'ar');
|
|
234
|
+
expect(node.action).toBe('capture');
|
|
235
|
+
expect(extractRoleValue(node, 'destination')).toBe('orders');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ===========================================================================
|
|
240
|
+
// Korean (SOV)
|
|
241
|
+
// ===========================================================================
|
|
242
|
+
|
|
243
|
+
describe('Korean (SOV)', () => {
|
|
244
|
+
it('should parse enter in Korean', () => {
|
|
245
|
+
const node = flow.parse('/api 진입', 'ko');
|
|
246
|
+
expect(node.action).toBe('enter');
|
|
247
|
+
expect(extractRoleValue(node, 'source')).toBe('/api');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should parse follow in Korean', () => {
|
|
251
|
+
const node = flow.parse('orders 따라가기', 'ko');
|
|
252
|
+
expect(node.action).toBe('follow');
|
|
253
|
+
expect(extractRoleValue(node, 'patient')).toBe('orders');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should parse perform in Korean', () => {
|
|
257
|
+
const node = flow.parse('#checkout 로 createOrder 실행', 'ko');
|
|
258
|
+
expect(node.action).toBe('perform');
|
|
259
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
260
|
+
expect(extractRoleValue(node, 'source')).toBe('#checkout');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should parse capture in Korean', () => {
|
|
264
|
+
const node = flow.parse('orders 로 캡처', 'ko');
|
|
265
|
+
expect(node.action).toBe('capture');
|
|
266
|
+
expect(extractRoleValue(node, 'destination')).toBe('orders');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ===========================================================================
|
|
271
|
+
// Chinese (SVO)
|
|
272
|
+
// ===========================================================================
|
|
273
|
+
|
|
274
|
+
describe('Chinese (SVO)', () => {
|
|
275
|
+
it('should parse enter in Chinese', () => {
|
|
276
|
+
const node = flow.parse('进入 /api', 'zh');
|
|
277
|
+
expect(node.action).toBe('enter');
|
|
278
|
+
expect(extractRoleValue(node, 'source')).toBe('/api');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should parse follow in Chinese', () => {
|
|
282
|
+
const node = flow.parse('跟随 orders', 'zh');
|
|
283
|
+
expect(node.action).toBe('follow');
|
|
284
|
+
expect(extractRoleValue(node, 'patient')).toBe('orders');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should parse perform in Chinese', () => {
|
|
288
|
+
const node = flow.parse('执行 createOrder 用 #checkout', 'zh');
|
|
289
|
+
expect(node.action).toBe('perform');
|
|
290
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
291
|
+
expect(extractRoleValue(node, 'source')).toBe('#checkout');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should parse capture in Chinese', () => {
|
|
295
|
+
const node = flow.parse('捕获 为 orders', 'zh');
|
|
296
|
+
expect(node.action).toBe('capture');
|
|
297
|
+
expect(extractRoleValue(node, 'destination')).toBe('orders');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ===========================================================================
|
|
302
|
+
// Turkish (SOV)
|
|
303
|
+
// ===========================================================================
|
|
304
|
+
|
|
305
|
+
describe('Turkish (SOV)', () => {
|
|
306
|
+
it('should parse enter in Turkish', () => {
|
|
307
|
+
const node = flow.parse('/api gir', 'tr');
|
|
308
|
+
expect(node.action).toBe('enter');
|
|
309
|
+
expect(extractRoleValue(node, 'source')).toBe('/api');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should parse follow in Turkish', () => {
|
|
313
|
+
const node = flow.parse('orders izle', 'tr');
|
|
314
|
+
expect(node.action).toBe('follow');
|
|
315
|
+
expect(extractRoleValue(node, 'patient')).toBe('orders');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should parse perform in Turkish', () => {
|
|
319
|
+
const node = flow.parse('#checkout ile createOrder yürüt', 'tr');
|
|
320
|
+
expect(node.action).toBe('perform');
|
|
321
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
322
|
+
expect(extractRoleValue(node, 'source')).toBe('#checkout');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should parse capture in Turkish', () => {
|
|
326
|
+
const node = flow.parse('orders olarak yakala', 'tr');
|
|
327
|
+
expect(node.action).toBe('capture');
|
|
328
|
+
expect(extractRoleValue(node, 'destination')).toBe('orders');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ===========================================================================
|
|
333
|
+
// French (SVO)
|
|
334
|
+
// ===========================================================================
|
|
335
|
+
|
|
336
|
+
describe('French (SVO)', () => {
|
|
337
|
+
it('should parse enter in French', () => {
|
|
338
|
+
const node = flow.parse('entrer /api', 'fr');
|
|
339
|
+
expect(node.action).toBe('enter');
|
|
340
|
+
expect(extractRoleValue(node, 'source')).toBe('/api');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should parse follow in French', () => {
|
|
344
|
+
const node = flow.parse('suivre orders', 'fr');
|
|
345
|
+
expect(node.action).toBe('follow');
|
|
346
|
+
expect(extractRoleValue(node, 'patient')).toBe('orders');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should parse perform in French', () => {
|
|
350
|
+
const node = flow.parse('exécuter createOrder avec #checkout', 'fr');
|
|
351
|
+
expect(node.action).toBe('perform');
|
|
352
|
+
expect(extractRoleValue(node, 'patient')).toBe('createOrder');
|
|
353
|
+
expect(extractRoleValue(node, 'source')).toBe('#checkout');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should parse capture in French', () => {
|
|
357
|
+
const node = flow.parse('capturer comme orders', 'fr');
|
|
358
|
+
expect(node.action).toBe('capture');
|
|
359
|
+
expect(extractRoleValue(node, 'destination')).toBe('orders');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ===========================================================================
|
|
364
|
+
// Semantic Equivalence (all 8 languages)
|
|
365
|
+
// ===========================================================================
|
|
366
|
+
|
|
367
|
+
describe('Semantic Equivalence', () => {
|
|
368
|
+
it('should produce identical FlowSpec for enter across SVO languages', () => {
|
|
369
|
+
const inputs: [string, string][] = [
|
|
370
|
+
['enter /api', 'en'],
|
|
371
|
+
['entrar /api', 'es'],
|
|
372
|
+
['进入 /api', 'zh'],
|
|
373
|
+
['entrer /api', 'fr'],
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
for (const [input, lang] of inputs) {
|
|
377
|
+
const node = flow.parse(input, lang);
|
|
378
|
+
const spec = toFlowSpec(node, lang);
|
|
379
|
+
expect(spec.action).toBe('enter');
|
|
380
|
+
expect(spec.url).toBe('/api');
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should produce identical FlowSpec for follow across SVO languages', () => {
|
|
385
|
+
const inputs: [string, string][] = [
|
|
386
|
+
['follow orders', 'en'],
|
|
387
|
+
['seguir orders', 'es'],
|
|
388
|
+
['跟随 orders', 'zh'],
|
|
389
|
+
['suivre orders', 'fr'],
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
for (const [input, lang] of inputs) {
|
|
393
|
+
const node = flow.parse(input, lang);
|
|
394
|
+
const spec = toFlowSpec(node, lang);
|
|
395
|
+
expect(spec.action).toBe('follow');
|
|
396
|
+
expect(spec.linkRel).toBe('orders');
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ===========================================================================
|
|
402
|
+
// Validation
|
|
403
|
+
// ===========================================================================
|
|
404
|
+
|
|
405
|
+
describe('Validation', () => {
|
|
406
|
+
it('should validate correct HATEOAS command syntax', () => {
|
|
407
|
+
expect(flow.validate('enter /api', 'en').valid).toBe(true);
|
|
408
|
+
expect(flow.validate('follow orders', 'en').valid).toBe(true);
|
|
409
|
+
expect(flow.validate('perform createOrder', 'en').valid).toBe(true);
|
|
410
|
+
expect(flow.validate('capture as orders', 'en').valid).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should validate HATEOAS commands in all 8 languages', () => {
|
|
414
|
+
const enterInputs: [string, string][] = [
|
|
415
|
+
['enter /api', 'en'],
|
|
416
|
+
['entrar /api', 'es'],
|
|
417
|
+
['/api 入る', 'ja'],
|
|
418
|
+
['ادخل /api', 'ar'],
|
|
419
|
+
['/api 진입', 'ko'],
|
|
420
|
+
['进入 /api', 'zh'],
|
|
421
|
+
['/api gir', 'tr'],
|
|
422
|
+
['entrer /api', 'fr'],
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
for (const [input, lang] of enterInputs) {
|
|
426
|
+
const result = flow.validate(input, lang);
|
|
427
|
+
expect(result.valid, `Failed for ${lang}: ${input}`).toBe(true);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ===========================================================================
|
|
433
|
+
// WorkflowSpec Generation
|
|
434
|
+
// ===========================================================================
|
|
435
|
+
|
|
436
|
+
describe('WorkflowSpec', () => {
|
|
437
|
+
it('should build a workflow from a sequence of HATEOAS commands', () => {
|
|
438
|
+
const specs = [
|
|
439
|
+
toFlowSpec(flow.parse('enter /api', 'en'), 'en'),
|
|
440
|
+
toFlowSpec(flow.parse('follow orders', 'en'), 'en'),
|
|
441
|
+
toFlowSpec(flow.parse('perform createOrder with #checkout', 'en'), 'en'),
|
|
442
|
+
toFlowSpec(flow.parse('capture as orderId', 'en'), 'en'),
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
const workflow = toWorkflowSpec(specs);
|
|
446
|
+
expect(workflow.entryPoint).toBe('/api');
|
|
447
|
+
expect(workflow.steps).toHaveLength(2); // follow + perform (capture attaches to perform)
|
|
448
|
+
|
|
449
|
+
expect(workflow.steps[0]).toEqual({ type: 'navigate', rel: 'orders' });
|
|
450
|
+
expect(workflow.steps[1]).toEqual({
|
|
451
|
+
type: 'action',
|
|
452
|
+
action: 'createOrder',
|
|
453
|
+
dataSource: '#checkout',
|
|
454
|
+
capture: { orderId: 'properties' },
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should throw if no enter command is found', () => {
|
|
459
|
+
const specs = [toFlowSpec(flow.parse('follow orders', 'en'), 'en')];
|
|
460
|
+
expect(() => toWorkflowSpec(specs)).toThrow('enter');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should convert to siren-grail step format', () => {
|
|
464
|
+
const specs = [
|
|
465
|
+
toFlowSpec(flow.parse('enter /api', 'en'), 'en'),
|
|
466
|
+
toFlowSpec(flow.parse('follow orders', 'en'), 'en'),
|
|
467
|
+
toFlowSpec(flow.parse('perform createOrder', 'en'), 'en'),
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
const workflow = toWorkflowSpec(specs);
|
|
471
|
+
const steps = toSirenGrailSteps(workflow);
|
|
472
|
+
|
|
473
|
+
expect(steps).toEqual([
|
|
474
|
+
{ type: 'navigate', rel: 'orders' },
|
|
475
|
+
{ type: 'action', action: 'createOrder' },
|
|
476
|
+
]);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should build workflow from multilingual commands', () => {
|
|
480
|
+
// Same workflow in Japanese SOV
|
|
481
|
+
const specs = [
|
|
482
|
+
toFlowSpec(flow.parse('/api 入る', 'ja'), 'ja'),
|
|
483
|
+
toFlowSpec(flow.parse('orders 辿る', 'ja'), 'ja'),
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
const workflow = toWorkflowSpec(specs);
|
|
487
|
+
expect(workflow.entryPoint).toBe('/api');
|
|
488
|
+
expect(workflow.steps[0]).toEqual({ type: 'navigate', rel: 'orders' });
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should handle standalone capture as navigate self', () => {
|
|
492
|
+
const specs = [
|
|
493
|
+
toFlowSpec(flow.parse('enter /api', 'en'), 'en'),
|
|
494
|
+
toFlowSpec(flow.parse('capture as data', 'en'), 'en'),
|
|
495
|
+
];
|
|
496
|
+
|
|
497
|
+
const workflow = toWorkflowSpec(specs);
|
|
498
|
+
expect(workflow.steps[0]).toEqual({
|
|
499
|
+
type: 'navigate',
|
|
500
|
+
rel: 'self',
|
|
501
|
+
capture: { data: 'properties' },
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should handle capture with property path', () => {
|
|
506
|
+
const specs = [
|
|
507
|
+
toFlowSpec(flow.parse('enter /api', 'en'), 'en'),
|
|
508
|
+
toFlowSpec(flow.parse('follow orders', 'en'), 'en'),
|
|
509
|
+
toFlowSpec(flow.parse('capture message as confirmationText', 'en'), 'en'),
|
|
510
|
+
];
|
|
511
|
+
|
|
512
|
+
const workflow = toWorkflowSpec(specs);
|
|
513
|
+
expect(workflow.steps[0]).toEqual({
|
|
514
|
+
type: 'navigate',
|
|
515
|
+
rel: 'orders',
|
|
516
|
+
capture: { confirmationText: 'message' },
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTMX Generator Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests HTMX attribute generation from FlowSpec objects.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
8
|
+
import { createFlowDSL, toFlowSpec, generateHTMX } from '../index.js';
|
|
9
|
+
import type { MultilingualDSL } from '@lokascript/framework';
|
|
10
|
+
|
|
11
|
+
describe('HTMX Generator', () => {
|
|
12
|
+
let flow: MultilingualDSL;
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
flow = createFlowDSL();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('fetch → hx-get', () => {
|
|
19
|
+
it('should generate hx-get for fetch', () => {
|
|
20
|
+
const node = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
21
|
+
const spec = toFlowSpec(node, 'en');
|
|
22
|
+
const result = generateHTMX(spec);
|
|
23
|
+
expect(result).not.toBeNull();
|
|
24
|
+
expect(result!.attrs['hx-get']).toBe('/api/users');
|
|
25
|
+
expect(result!.attrs['hx-target']).toBe('#user-list');
|
|
26
|
+
expect(result!.attrs['hx-swap']).toBe('innerHTML');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should add note about JSON responses', () => {
|
|
30
|
+
const node = flow.parse('fetch /api/users as json', 'en');
|
|
31
|
+
const spec = toFlowSpec(node, 'en');
|
|
32
|
+
const result = generateHTMX(spec);
|
|
33
|
+
expect(result!.notes.length).toBeGreaterThan(0);
|
|
34
|
+
expect(result!.notes[0]).toContain('JSON');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('poll → hx-get + hx-trigger', () => {
|
|
39
|
+
it('should generate hx-trigger every Ns for poll', () => {
|
|
40
|
+
const node = flow.parse('poll /api/status every 5s into #dashboard', 'en');
|
|
41
|
+
const spec = toFlowSpec(node, 'en');
|
|
42
|
+
const result = generateHTMX(spec);
|
|
43
|
+
expect(result).not.toBeNull();
|
|
44
|
+
expect(result!.attrs['hx-get']).toBe('/api/status');
|
|
45
|
+
expect(result!.attrs['hx-trigger']).toBe('every 5s');
|
|
46
|
+
expect(result!.attrs['hx-target']).toBe('#dashboard');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should convert ms interval to seconds', () => {
|
|
50
|
+
const node = flow.parse('poll /api/status every 30s', 'en');
|
|
51
|
+
const spec = toFlowSpec(node, 'en');
|
|
52
|
+
const result = generateHTMX(spec);
|
|
53
|
+
expect(result!.attrs['hx-trigger']).toBe('every 30s');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('stream → sse extension', () => {
|
|
58
|
+
it('should generate SSE extension attributes', () => {
|
|
59
|
+
const node = flow.parse('stream /api/events as sse into #event-log', 'en');
|
|
60
|
+
const spec = toFlowSpec(node, 'en');
|
|
61
|
+
const result = generateHTMX(spec);
|
|
62
|
+
expect(result).not.toBeNull();
|
|
63
|
+
expect(result!.attrs['hx-ext']).toBe('sse');
|
|
64
|
+
expect(result!.attrs['sse-connect']).toBe('/api/events');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should note SSE extension requirement', () => {
|
|
68
|
+
const node = flow.parse('stream /api/events as sse', 'en');
|
|
69
|
+
const spec = toFlowSpec(node, 'en');
|
|
70
|
+
const result = generateHTMX(spec);
|
|
71
|
+
expect(result!.notes.some(n => n.includes('sse extension'))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('submit → hx-post', () => {
|
|
76
|
+
it('should generate hx-post for submit', () => {
|
|
77
|
+
const node = flow.parse('submit #checkout to /api/order as json', 'en');
|
|
78
|
+
const spec = toFlowSpec(node, 'en');
|
|
79
|
+
const result = generateHTMX(spec);
|
|
80
|
+
expect(result).not.toBeNull();
|
|
81
|
+
expect(result!.attrs['hx-post']).toBe('/api/order');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should include json encoding for json submit', () => {
|
|
85
|
+
const node = flow.parse('submit #checkout to /api/order as json', 'en');
|
|
86
|
+
const spec = toFlowSpec(node, 'en');
|
|
87
|
+
const result = generateHTMX(spec);
|
|
88
|
+
expect(result!.attrs['hx-encoding']).toBe('application/json');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('transform → null', () => {
|
|
93
|
+
it('should return null for transform (no HTMX equivalent)', () => {
|
|
94
|
+
const node = flow.parse('transform data with uppercase', 'en');
|
|
95
|
+
const spec = toFlowSpec(node, 'en');
|
|
96
|
+
const result = generateHTMX(spec);
|
|
97
|
+
expect(result).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|