@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,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowScript Domain Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates the multilingual FlowScript DSL across 8 languages (EN, ES, JA, AR, KO, ZH, TR, FR)
|
|
5
|
+
* covering SVO, SOV, and VSO word orders.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
9
|
+
import { createFlowDSL, renderFlow, toFlowSpec } from '../index.js';
|
|
10
|
+
import type { MultilingualDSL } from '@lokascript/framework';
|
|
11
|
+
import { extractRoleValue } from '@lokascript/framework';
|
|
12
|
+
import type { FlowSpec } from '../types.js';
|
|
13
|
+
import { parseDuration } from '../generators/flow-generator.js';
|
|
14
|
+
|
|
15
|
+
describe('FlowScript Domain', () => {
|
|
16
|
+
let flow: MultilingualDSL;
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
flow = createFlowDSL();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ===========================================================================
|
|
23
|
+
// Language Support
|
|
24
|
+
// ===========================================================================
|
|
25
|
+
|
|
26
|
+
describe('Language Support', () => {
|
|
27
|
+
it('should support 8 languages', () => {
|
|
28
|
+
const languages = flow.getSupportedLanguages();
|
|
29
|
+
expect(languages).toContain('en');
|
|
30
|
+
expect(languages).toContain('es');
|
|
31
|
+
expect(languages).toContain('ja');
|
|
32
|
+
expect(languages).toContain('ar');
|
|
33
|
+
expect(languages).toContain('ko');
|
|
34
|
+
expect(languages).toContain('zh');
|
|
35
|
+
expect(languages).toContain('tr');
|
|
36
|
+
expect(languages).toContain('fr');
|
|
37
|
+
expect(languages).toHaveLength(8);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should reject unsupported language', () => {
|
|
41
|
+
expect(() => flow.parse('fetch /api/users', 'de')).toThrow();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ===========================================================================
|
|
46
|
+
// Duration Parsing
|
|
47
|
+
// ===========================================================================
|
|
48
|
+
|
|
49
|
+
describe('Duration Parsing', () => {
|
|
50
|
+
it('should parse milliseconds', () => {
|
|
51
|
+
expect(parseDuration('500ms')).toBe(500);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should parse seconds', () => {
|
|
55
|
+
expect(parseDuration('5s')).toBe(5000);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should parse minutes', () => {
|
|
59
|
+
expect(parseDuration('1m')).toBe(60000);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should parse hours', () => {
|
|
63
|
+
expect(parseDuration('1h')).toBe(3600000);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should parse plain numbers as ms', () => {
|
|
67
|
+
expect(parseDuration('1000')).toBe(1000);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ===========================================================================
|
|
72
|
+
// English (SVO) — fetch
|
|
73
|
+
// ===========================================================================
|
|
74
|
+
|
|
75
|
+
describe('English — fetch', () => {
|
|
76
|
+
it('should parse simple fetch', () => {
|
|
77
|
+
const node = flow.parse('fetch /api/users', 'en');
|
|
78
|
+
expect(node.action).toBe('fetch');
|
|
79
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should parse fetch with format', () => {
|
|
83
|
+
const node = flow.parse('fetch /api/users as json', 'en');
|
|
84
|
+
expect(node.action).toBe('fetch');
|
|
85
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
86
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should parse fetch with target', () => {
|
|
90
|
+
const node = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
91
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
92
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
93
|
+
expect(extractRoleValue(node, 'destination')).toBe('#user-list');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should compile fetch to JS', () => {
|
|
97
|
+
const result = flow.compile('fetch /api/users as json into #user-list', 'en');
|
|
98
|
+
expect(result.ok).toBe(true);
|
|
99
|
+
expect(result.code).toContain("fetch('/api/users')");
|
|
100
|
+
expect(result.code).toContain('.json()');
|
|
101
|
+
expect(result.code).toContain('#user-list');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should produce correct FlowSpec', () => {
|
|
105
|
+
const node = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
106
|
+
const spec = toFlowSpec(node, 'en');
|
|
107
|
+
expect(spec.action).toBe('fetch');
|
|
108
|
+
expect(spec.url).toBe('/api/users');
|
|
109
|
+
expect(spec.responseFormat).toBe('json');
|
|
110
|
+
expect(spec.target).toBe('#user-list');
|
|
111
|
+
expect(spec.method).toBe('GET');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ===========================================================================
|
|
116
|
+
// English (SVO) — poll
|
|
117
|
+
// ===========================================================================
|
|
118
|
+
|
|
119
|
+
describe('English — poll', () => {
|
|
120
|
+
it('should parse poll with interval', () => {
|
|
121
|
+
const node = flow.parse('poll /api/status every 5s', 'en');
|
|
122
|
+
expect(node.action).toBe('poll');
|
|
123
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/status');
|
|
124
|
+
expect(extractRoleValue(node, 'duration')).toBe('5s');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should parse poll with target', () => {
|
|
128
|
+
const node = flow.parse('poll /api/status every 5s into #dashboard', 'en');
|
|
129
|
+
expect(extractRoleValue(node, 'destination')).toBe('#dashboard');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should compile poll to JS with setInterval', () => {
|
|
133
|
+
const result = flow.compile('poll /api/status every 5s into #dashboard', 'en');
|
|
134
|
+
expect(result.ok).toBe(true);
|
|
135
|
+
expect(result.code).toContain('setInterval');
|
|
136
|
+
expect(result.code).toContain("fetch('/api/status')");
|
|
137
|
+
expect(result.code).toContain('5000');
|
|
138
|
+
expect(result.code).toContain('#dashboard');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should produce correct FlowSpec for poll', () => {
|
|
142
|
+
const node = flow.parse('poll /api/status every 5s into #dashboard', 'en');
|
|
143
|
+
const spec = toFlowSpec(node, 'en');
|
|
144
|
+
expect(spec.action).toBe('poll');
|
|
145
|
+
expect(spec.url).toBe('/api/status');
|
|
146
|
+
expect(spec.intervalMs).toBe(5000);
|
|
147
|
+
expect(spec.target).toBe('#dashboard');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ===========================================================================
|
|
152
|
+
// English (SVO) — stream
|
|
153
|
+
// ===========================================================================
|
|
154
|
+
|
|
155
|
+
describe('English — stream', () => {
|
|
156
|
+
it('should parse stream', () => {
|
|
157
|
+
const node = flow.parse('stream /api/events as sse into #event-log', 'en');
|
|
158
|
+
expect(node.action).toBe('stream');
|
|
159
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/events');
|
|
160
|
+
expect(extractRoleValue(node, 'style')).toBe('sse');
|
|
161
|
+
expect(extractRoleValue(node, 'destination')).toBe('#event-log');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should compile stream to EventSource', () => {
|
|
165
|
+
const result = flow.compile('stream /api/events as sse into #event-log', 'en');
|
|
166
|
+
expect(result.ok).toBe(true);
|
|
167
|
+
expect(result.code).toContain('EventSource');
|
|
168
|
+
expect(result.code).toContain('/api/events');
|
|
169
|
+
expect(result.code).toContain('#event-log');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should produce sse responseFormat in FlowSpec', () => {
|
|
173
|
+
const node = flow.parse('stream /api/events as sse', 'en');
|
|
174
|
+
const spec = toFlowSpec(node, 'en');
|
|
175
|
+
expect(spec.responseFormat).toBe('sse');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ===========================================================================
|
|
180
|
+
// English (SVO) — submit
|
|
181
|
+
// ===========================================================================
|
|
182
|
+
|
|
183
|
+
describe('English — submit', () => {
|
|
184
|
+
it('should parse submit', () => {
|
|
185
|
+
const node = flow.parse('submit #checkout to /api/order as json', 'en');
|
|
186
|
+
expect(node.action).toBe('submit');
|
|
187
|
+
expect(extractRoleValue(node, 'patient')).toBe('#checkout');
|
|
188
|
+
expect(extractRoleValue(node, 'destination')).toBe('/api/order');
|
|
189
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should compile submit to POST fetch', () => {
|
|
193
|
+
const result = flow.compile('submit #checkout to /api/order as json', 'en');
|
|
194
|
+
expect(result.ok).toBe(true);
|
|
195
|
+
expect(result.code).toContain("method: 'POST'");
|
|
196
|
+
expect(result.code).toContain('/api/order');
|
|
197
|
+
expect(result.code).toContain('application/json');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should produce POST method in FlowSpec', () => {
|
|
201
|
+
const node = flow.parse('submit #checkout to /api/order', 'en');
|
|
202
|
+
const spec = toFlowSpec(node, 'en');
|
|
203
|
+
expect(spec.method).toBe('POST');
|
|
204
|
+
expect(spec.formSelector).toBe('#checkout');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ===========================================================================
|
|
209
|
+
// English (SVO) — transform
|
|
210
|
+
// ===========================================================================
|
|
211
|
+
|
|
212
|
+
describe('English — transform', () => {
|
|
213
|
+
it('should parse transform', () => {
|
|
214
|
+
const node = flow.parse('transform data with uppercase', 'en');
|
|
215
|
+
expect(node.action).toBe('transform');
|
|
216
|
+
expect(extractRoleValue(node, 'patient')).toBe('data');
|
|
217
|
+
expect(extractRoleValue(node, 'instrument')).toBe('uppercase');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should compile transform to function call', () => {
|
|
221
|
+
const result = flow.compile('transform data with uppercase', 'en');
|
|
222
|
+
expect(result.ok).toBe(true);
|
|
223
|
+
expect(result.code).toContain('uppercase');
|
|
224
|
+
expect(result.code).toContain('data');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ===========================================================================
|
|
229
|
+
// Spanish (SVO)
|
|
230
|
+
// ===========================================================================
|
|
231
|
+
|
|
232
|
+
describe('Spanish (SVO)', () => {
|
|
233
|
+
it('should parse fetch in Spanish', () => {
|
|
234
|
+
const node = flow.parse('obtener /api/users como json en #user-list', 'es');
|
|
235
|
+
expect(node.action).toBe('fetch');
|
|
236
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
237
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
238
|
+
expect(extractRoleValue(node, 'destination')).toBe('#user-list');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should parse poll in Spanish', () => {
|
|
242
|
+
const node = flow.parse('sondear /api/status cada 5s', 'es');
|
|
243
|
+
expect(node.action).toBe('poll');
|
|
244
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/status');
|
|
245
|
+
expect(extractRoleValue(node, 'duration')).toBe('5s');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should parse submit in Spanish', () => {
|
|
249
|
+
const node = flow.parse('enviar #checkout a /api/order como json', 'es');
|
|
250
|
+
expect(node.action).toBe('submit');
|
|
251
|
+
expect(extractRoleValue(node, 'patient')).toBe('#checkout');
|
|
252
|
+
expect(extractRoleValue(node, 'destination')).toBe('/api/order');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should compile Spanish fetch to same JS as English', () => {
|
|
256
|
+
const enResult = flow.compile('fetch /api/users as json into #user-list', 'en');
|
|
257
|
+
const esResult = flow.compile('obtener /api/users como json en #user-list', 'es');
|
|
258
|
+
expect(enResult.ok).toBe(true);
|
|
259
|
+
expect(esResult.ok).toBe(true);
|
|
260
|
+
// Same JS output regardless of source language
|
|
261
|
+
expect(enResult.code).toBe(esResult.code);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ===========================================================================
|
|
266
|
+
// Japanese (SOV)
|
|
267
|
+
// ===========================================================================
|
|
268
|
+
|
|
269
|
+
describe('Japanese (SOV)', () => {
|
|
270
|
+
it('should parse fetch in Japanese SOV order', () => {
|
|
271
|
+
const node = flow.parse('/api/users json で 取得', 'ja');
|
|
272
|
+
expect(node.action).toBe('fetch');
|
|
273
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
274
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should parse poll in Japanese', () => {
|
|
278
|
+
const node = flow.parse('/api/status 5s ごとに ポーリング', 'ja');
|
|
279
|
+
expect(node.action).toBe('poll');
|
|
280
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/status');
|
|
281
|
+
expect(extractRoleValue(node, 'duration')).toBe('5s');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should produce same FlowSpec as English', () => {
|
|
285
|
+
const enNode = flow.parse('fetch /api/users as json', 'en');
|
|
286
|
+
const jaNode = flow.parse('/api/users json で 取得', 'ja');
|
|
287
|
+
const enSpec = toFlowSpec(enNode, 'en');
|
|
288
|
+
const jaSpec = toFlowSpec(jaNode, 'ja');
|
|
289
|
+
expect(enSpec.action).toBe(jaSpec.action);
|
|
290
|
+
expect(enSpec.url).toBe(jaSpec.url);
|
|
291
|
+
expect(enSpec.responseFormat).toBe(jaSpec.responseFormat);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ===========================================================================
|
|
296
|
+
// Arabic (VSO)
|
|
297
|
+
// ===========================================================================
|
|
298
|
+
|
|
299
|
+
describe('Arabic (VSO)', () => {
|
|
300
|
+
it('should parse fetch in Arabic VSO order', () => {
|
|
301
|
+
const node = flow.parse('جلب /api/users ك json في #user-list', 'ar');
|
|
302
|
+
expect(node.action).toBe('fetch');
|
|
303
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
304
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
305
|
+
expect(extractRoleValue(node, 'destination')).toBe('#user-list');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should parse stream in Arabic', () => {
|
|
309
|
+
const node = flow.parse('بث /api/events ك sse في #event-log', 'ar');
|
|
310
|
+
expect(node.action).toBe('stream');
|
|
311
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/events');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should produce same FlowSpec as English', () => {
|
|
315
|
+
const enNode = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
316
|
+
const arNode = flow.parse('جلب /api/users ك json في #user-list', 'ar');
|
|
317
|
+
const enSpec = toFlowSpec(enNode, 'en');
|
|
318
|
+
const arSpec = toFlowSpec(arNode, 'ar');
|
|
319
|
+
expect(enSpec.action).toBe(arSpec.action);
|
|
320
|
+
expect(enSpec.url).toBe(arSpec.url);
|
|
321
|
+
expect(enSpec.responseFormat).toBe(arSpec.responseFormat);
|
|
322
|
+
expect(enSpec.target).toBe(arSpec.target);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ===========================================================================
|
|
327
|
+
// Korean (SOV)
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
|
|
330
|
+
describe('Korean (SOV)', () => {
|
|
331
|
+
it('should parse fetch in Korean SOV order', () => {
|
|
332
|
+
const node = flow.parse('/api/users json 로 가져오기', 'ko');
|
|
333
|
+
expect(node.action).toBe('fetch');
|
|
334
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
335
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should parse poll in Korean', () => {
|
|
339
|
+
const node = flow.parse('/api/status 5s 마다 폴링', 'ko');
|
|
340
|
+
expect(node.action).toBe('poll');
|
|
341
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/status');
|
|
342
|
+
expect(extractRoleValue(node, 'duration')).toBe('5s');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should parse stream in Korean', () => {
|
|
346
|
+
const node = flow.parse('/api/events sse 로 스트리밍', 'ko');
|
|
347
|
+
expect(node.action).toBe('stream');
|
|
348
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/events');
|
|
349
|
+
expect(extractRoleValue(node, 'style')).toBe('sse');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should parse transform in Korean', () => {
|
|
353
|
+
// SOV: instrument(sovPos:2) before patient(sovPos:1), marker after value
|
|
354
|
+
const node = flow.parse('uppercase 로 data 변환', 'ko');
|
|
355
|
+
expect(node.action).toBe('transform');
|
|
356
|
+
expect(extractRoleValue(node, 'patient')).toBe('data');
|
|
357
|
+
expect(extractRoleValue(node, 'instrument')).toBe('uppercase');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should produce same FlowSpec as English', () => {
|
|
361
|
+
const enNode = flow.parse('fetch /api/users as json', 'en');
|
|
362
|
+
const koNode = flow.parse('/api/users json 로 가져오기', 'ko');
|
|
363
|
+
const enSpec = toFlowSpec(enNode, 'en');
|
|
364
|
+
const koSpec = toFlowSpec(koNode, 'ko');
|
|
365
|
+
expect(enSpec.action).toBe(koSpec.action);
|
|
366
|
+
expect(enSpec.url).toBe(koSpec.url);
|
|
367
|
+
expect(enSpec.responseFormat).toBe(koSpec.responseFormat);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ===========================================================================
|
|
372
|
+
// Chinese (SVO)
|
|
373
|
+
// ===========================================================================
|
|
374
|
+
|
|
375
|
+
describe('Chinese (SVO)', () => {
|
|
376
|
+
it('should parse fetch in Chinese', () => {
|
|
377
|
+
const node = flow.parse('获取 /api/users 以 json 到 #user-list', 'zh');
|
|
378
|
+
expect(node.action).toBe('fetch');
|
|
379
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
380
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
381
|
+
expect(extractRoleValue(node, 'destination')).toBe('#user-list');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should parse poll in Chinese', () => {
|
|
385
|
+
const node = flow.parse('轮询 /api/status 每 5s', 'zh');
|
|
386
|
+
expect(node.action).toBe('poll');
|
|
387
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/status');
|
|
388
|
+
expect(extractRoleValue(node, 'duration')).toBe('5s');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should parse stream in Chinese', () => {
|
|
392
|
+
const node = flow.parse('流式 /api/events 以 sse 到 #event-log', 'zh');
|
|
393
|
+
expect(node.action).toBe('stream');
|
|
394
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/events');
|
|
395
|
+
expect(extractRoleValue(node, 'style')).toBe('sse');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should parse transform in Chinese', () => {
|
|
399
|
+
const node = flow.parse('转换 data 用 uppercase', 'zh');
|
|
400
|
+
expect(node.action).toBe('transform');
|
|
401
|
+
expect(extractRoleValue(node, 'patient')).toBe('data');
|
|
402
|
+
expect(extractRoleValue(node, 'instrument')).toBe('uppercase');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should compile Chinese fetch to same JS as English', () => {
|
|
406
|
+
const enResult = flow.compile('fetch /api/users as json into #user-list', 'en');
|
|
407
|
+
const zhResult = flow.compile('获取 /api/users 以 json 到 #user-list', 'zh');
|
|
408
|
+
expect(enResult.ok).toBe(true);
|
|
409
|
+
expect(zhResult.ok).toBe(true);
|
|
410
|
+
expect(enResult.code).toBe(zhResult.code);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// ===========================================================================
|
|
415
|
+
// Turkish (SOV)
|
|
416
|
+
// ===========================================================================
|
|
417
|
+
|
|
418
|
+
describe('Turkish (SOV)', () => {
|
|
419
|
+
it('should parse fetch in Turkish SOV order', () => {
|
|
420
|
+
const node = flow.parse('/api/users json olarak getir', 'tr');
|
|
421
|
+
expect(node.action).toBe('fetch');
|
|
422
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
423
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should parse poll in Turkish', () => {
|
|
427
|
+
// SOV: marker comes after value (postposition)
|
|
428
|
+
const node = flow.parse('/api/status 5s her yokla', 'tr');
|
|
429
|
+
expect(node.action).toBe('poll');
|
|
430
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/status');
|
|
431
|
+
expect(extractRoleValue(node, 'duration')).toBe('5s');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should parse stream in Turkish', () => {
|
|
435
|
+
const node = flow.parse('/api/events sse olarak aktar', 'tr');
|
|
436
|
+
expect(node.action).toBe('stream');
|
|
437
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/events');
|
|
438
|
+
expect(extractRoleValue(node, 'style')).toBe('sse');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should parse transform in Turkish', () => {
|
|
442
|
+
// SOV: instrument(sovPos:2) before patient(sovPos:1), marker after value
|
|
443
|
+
const node = flow.parse('uppercase ile data dönüştür', 'tr');
|
|
444
|
+
expect(node.action).toBe('transform');
|
|
445
|
+
expect(extractRoleValue(node, 'patient')).toBe('data');
|
|
446
|
+
expect(extractRoleValue(node, 'instrument')).toBe('uppercase');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should produce same FlowSpec as English', () => {
|
|
450
|
+
const enNode = flow.parse('fetch /api/users as json', 'en');
|
|
451
|
+
const trNode = flow.parse('/api/users json olarak getir', 'tr');
|
|
452
|
+
const enSpec = toFlowSpec(enNode, 'en');
|
|
453
|
+
const trSpec = toFlowSpec(trNode, 'tr');
|
|
454
|
+
expect(enSpec.action).toBe(trSpec.action);
|
|
455
|
+
expect(enSpec.url).toBe(trSpec.url);
|
|
456
|
+
expect(enSpec.responseFormat).toBe(trSpec.responseFormat);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ===========================================================================
|
|
461
|
+
// French (SVO)
|
|
462
|
+
// ===========================================================================
|
|
463
|
+
|
|
464
|
+
describe('French (SVO)', () => {
|
|
465
|
+
it('should parse fetch in French', () => {
|
|
466
|
+
const node = flow.parse('récupérer /api/users comme json dans #user-list', 'fr');
|
|
467
|
+
expect(node.action).toBe('fetch');
|
|
468
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users');
|
|
469
|
+
expect(extractRoleValue(node, 'style')).toBe('json');
|
|
470
|
+
expect(extractRoleValue(node, 'destination')).toBe('#user-list');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should parse poll in French', () => {
|
|
474
|
+
const node = flow.parse('interroger /api/status chaque 5s', 'fr');
|
|
475
|
+
expect(node.action).toBe('poll');
|
|
476
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/status');
|
|
477
|
+
expect(extractRoleValue(node, 'duration')).toBe('5s');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should parse stream in French', () => {
|
|
481
|
+
const node = flow.parse('diffuser /api/events comme sse dans #event-log', 'fr');
|
|
482
|
+
expect(node.action).toBe('stream');
|
|
483
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/events');
|
|
484
|
+
expect(extractRoleValue(node, 'style')).toBe('sse');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should parse transform in French', () => {
|
|
488
|
+
const node = flow.parse('transformer data avec uppercase', 'fr');
|
|
489
|
+
expect(node.action).toBe('transform');
|
|
490
|
+
expect(extractRoleValue(node, 'patient')).toBe('data');
|
|
491
|
+
expect(extractRoleValue(node, 'instrument')).toBe('uppercase');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should compile French fetch to same JS as English', () => {
|
|
495
|
+
const enResult = flow.compile('fetch /api/users as json into #user-list', 'en');
|
|
496
|
+
const frResult = flow.compile('récupérer /api/users comme json dans #user-list', 'fr');
|
|
497
|
+
expect(enResult.ok).toBe(true);
|
|
498
|
+
expect(frResult.ok).toBe(true);
|
|
499
|
+
expect(enResult.code).toBe(frResult.code);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ===========================================================================
|
|
504
|
+
// Semantic Equivalence (all 8 languages)
|
|
505
|
+
// ===========================================================================
|
|
506
|
+
|
|
507
|
+
describe('Semantic Equivalence', () => {
|
|
508
|
+
it('should produce identical FlowSpec for fetch across all languages', () => {
|
|
509
|
+
const inputs: [string, string][] = [
|
|
510
|
+
['fetch /api/users as json into #user-list', 'en'],
|
|
511
|
+
['obtener /api/users como json en #user-list', 'es'],
|
|
512
|
+
['جلب /api/users ك json في #user-list', 'ar'],
|
|
513
|
+
['获取 /api/users 以 json 到 #user-list', 'zh'],
|
|
514
|
+
['récupérer /api/users comme json dans #user-list', 'fr'],
|
|
515
|
+
];
|
|
516
|
+
|
|
517
|
+
const specs = inputs.map(([input, lang]) => {
|
|
518
|
+
const node = flow.parse(input, lang);
|
|
519
|
+
return toFlowSpec(node, lang);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
for (const spec of specs) {
|
|
523
|
+
expect(spec.action).toBe('fetch');
|
|
524
|
+
expect(spec.url).toBe('/api/users');
|
|
525
|
+
expect(spec.responseFormat).toBe('json');
|
|
526
|
+
expect(spec.target).toBe('#user-list');
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// ===========================================================================
|
|
532
|
+
// Validation / Error Handling
|
|
533
|
+
// ===========================================================================
|
|
534
|
+
|
|
535
|
+
describe('Validation', () => {
|
|
536
|
+
it('should validate correct fetch syntax', () => {
|
|
537
|
+
const result = flow.validate('fetch /api/users as json', 'en');
|
|
538
|
+
expect(result.valid).toBe(true);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('should return ok=false for invalid input', () => {
|
|
542
|
+
const result = flow.compile('gobbledygook nonsense', 'en');
|
|
543
|
+
expect(result.ok).toBe(false);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should handle URLs with path params', () => {
|
|
547
|
+
const node = flow.parse('fetch /api/users/{id}', 'en');
|
|
548
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/users/{id}');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should handle URLs with query params', () => {
|
|
552
|
+
const node = flow.parse('fetch /api/search?q=hello', 'en');
|
|
553
|
+
expect(extractRoleValue(node, 'source')).toBe('/api/search?q=hello');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('should validate in all 8 languages', () => {
|
|
557
|
+
const validInputs: [string, string][] = [
|
|
558
|
+
['fetch /api/users as json', 'en'],
|
|
559
|
+
['obtener /api/users como json', 'es'],
|
|
560
|
+
['/api/users json で 取得', 'ja'],
|
|
561
|
+
['جلب /api/users ك json', 'ar'],
|
|
562
|
+
['/api/users json 로 가져오기', 'ko'],
|
|
563
|
+
['获取 /api/users 以 json', 'zh'],
|
|
564
|
+
['/api/users json olarak getir', 'tr'],
|
|
565
|
+
['récupérer /api/users comme json', 'fr'],
|
|
566
|
+
];
|
|
567
|
+
|
|
568
|
+
for (const [input, lang] of validInputs) {
|
|
569
|
+
const result = flow.validate(input, lang);
|
|
570
|
+
expect(result.valid).toBe(true);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// ===========================================================================
|
|
576
|
+
// Natural Language Renderer
|
|
577
|
+
// ===========================================================================
|
|
578
|
+
|
|
579
|
+
describe('Renderer', () => {
|
|
580
|
+
it('should render fetch to English', () => {
|
|
581
|
+
const node = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
582
|
+
const rendered = renderFlow(node, 'en');
|
|
583
|
+
expect(rendered).toContain('fetch');
|
|
584
|
+
expect(rendered).toContain('/api/users');
|
|
585
|
+
expect(rendered).toContain('json');
|
|
586
|
+
expect(rendered).toContain('#user-list');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should render fetch to Japanese SOV', () => {
|
|
590
|
+
const node = flow.parse('fetch /api/users as json', 'en');
|
|
591
|
+
const rendered = renderFlow(node, 'ja');
|
|
592
|
+
expect(rendered).toContain('取得');
|
|
593
|
+
expect(rendered).toContain('/api/users');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('should render poll to Spanish', () => {
|
|
597
|
+
const node = flow.parse('poll /api/status every 5s into #dashboard', 'en');
|
|
598
|
+
const rendered = renderFlow(node, 'es');
|
|
599
|
+
expect(rendered).toContain('sondear');
|
|
600
|
+
expect(rendered).toContain('cada');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should render fetch to Arabic VSO', () => {
|
|
604
|
+
const node = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
605
|
+
const rendered = renderFlow(node, 'ar');
|
|
606
|
+
expect(rendered).toContain('جلب');
|
|
607
|
+
expect(rendered).toContain('/api/users');
|
|
608
|
+
expect(rendered).toContain('ك');
|
|
609
|
+
expect(rendered).toContain('في');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should render submit to Arabic VSO', () => {
|
|
613
|
+
const node = flow.parse('submit #checkout to /api/order as json', 'en');
|
|
614
|
+
const rendered = renderFlow(node, 'ar');
|
|
615
|
+
expect(rendered).toContain('أرسل');
|
|
616
|
+
expect(rendered).toContain('إلى');
|
|
617
|
+
expect(rendered).toContain('/api/order');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should render transform to Arabic', () => {
|
|
621
|
+
const node = flow.parse('transform data with uppercase', 'en');
|
|
622
|
+
const rendered = renderFlow(node, 'ar');
|
|
623
|
+
expect(rendered).toContain('حوّل');
|
|
624
|
+
expect(rendered).toContain('ب');
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('should render stream to Arabic VSO', () => {
|
|
628
|
+
const node = flow.parse('stream /api/events as sse into #event-log', 'en');
|
|
629
|
+
const rendered = renderFlow(node, 'ar');
|
|
630
|
+
expect(rendered).toContain('بث');
|
|
631
|
+
expect(rendered).toContain('/api/events');
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should render fetch to Korean SOV', () => {
|
|
635
|
+
const node = flow.parse('fetch /api/users as json', 'en');
|
|
636
|
+
const rendered = renderFlow(node, 'ko');
|
|
637
|
+
expect(rendered).toContain('가져오기');
|
|
638
|
+
expect(rendered).toContain('/api/users');
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('should render fetch to Chinese', () => {
|
|
642
|
+
const node = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
643
|
+
const rendered = renderFlow(node, 'zh');
|
|
644
|
+
expect(rendered).toContain('获取');
|
|
645
|
+
expect(rendered).toContain('/api/users');
|
|
646
|
+
expect(rendered).toContain('以');
|
|
647
|
+
expect(rendered).toContain('到');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should render fetch to Turkish SOV', () => {
|
|
651
|
+
const node = flow.parse('fetch /api/users as json', 'en');
|
|
652
|
+
const rendered = renderFlow(node, 'tr');
|
|
653
|
+
expect(rendered).toContain('getir');
|
|
654
|
+
expect(rendered).toContain('/api/users');
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('should render fetch to French', () => {
|
|
658
|
+
const node = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
659
|
+
const rendered = renderFlow(node, 'fr');
|
|
660
|
+
expect(rendered).toContain('récupérer');
|
|
661
|
+
expect(rendered).toContain('/api/users');
|
|
662
|
+
expect(rendered).toContain('comme');
|
|
663
|
+
expect(rendered).toContain('dans');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('should round-trip Arabic render through parser', () => {
|
|
667
|
+
const enNode = flow.parse('fetch /api/users as json into #user-list', 'en');
|
|
668
|
+
const arText = renderFlow(enNode, 'ar');
|
|
669
|
+
const arNode = flow.parse(arText, 'ar');
|
|
670
|
+
expect(arNode.action).toBe('fetch');
|
|
671
|
+
expect(extractRoleValue(arNode, 'source')).toBe('/api/users');
|
|
672
|
+
expect(extractRoleValue(arNode, 'style')).toBe('json');
|
|
673
|
+
expect(extractRoleValue(arNode, 'destination')).toBe('#user-list');
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// ===========================================================================
|
|
678
|
+
// Poll with Format
|
|
679
|
+
// ===========================================================================
|
|
680
|
+
|
|
681
|
+
describe('Poll with responseFormat', () => {
|
|
682
|
+
it('should compile poll with json format', () => {
|
|
683
|
+
const result = flow.compile('poll /api/status every 5s as json into #dashboard', 'en');
|
|
684
|
+
expect(result.ok).toBe(true);
|
|
685
|
+
expect(result.code).toContain('.json()');
|
|
686
|
+
expect(result.code).toContain('JSON.stringify');
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should compile poll without format as text', () => {
|
|
690
|
+
const result = flow.compile('poll /api/status every 5s into #dashboard', 'en');
|
|
691
|
+
expect(result.ok).toBe(true);
|
|
692
|
+
expect(result.code).toContain('.text()');
|
|
693
|
+
expect(result.code).not.toContain('.json()');
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
});
|