@mmapp/player-core 0.1.0-alpha.1

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.
Files changed (63) hide show
  1. package/dist/index.d.mts +1436 -0
  2. package/dist/index.d.ts +1436 -0
  3. package/dist/index.js +4828 -0
  4. package/dist/index.mjs +4762 -0
  5. package/package.json +35 -0
  6. package/package.json.backup +35 -0
  7. package/src/__tests__/actions.test.ts +187 -0
  8. package/src/__tests__/blueprint-e2e.test.ts +706 -0
  9. package/src/__tests__/blueprint-test-runner.test.ts +680 -0
  10. package/src/__tests__/core-functions.test.ts +78 -0
  11. package/src/__tests__/dsl-compiler.test.ts +1382 -0
  12. package/src/__tests__/dsl-grammar.test.ts +1682 -0
  13. package/src/__tests__/events.test.ts +200 -0
  14. package/src/__tests__/expression.test.ts +296 -0
  15. package/src/__tests__/failure-policies.test.ts +110 -0
  16. package/src/__tests__/frontend-context.test.ts +182 -0
  17. package/src/__tests__/integration.test.ts +256 -0
  18. package/src/__tests__/security.test.ts +190 -0
  19. package/src/__tests__/state-machine.test.ts +450 -0
  20. package/src/__tests__/testing-engine.test.ts +671 -0
  21. package/src/actions/dispatcher.ts +80 -0
  22. package/src/actions/index.ts +7 -0
  23. package/src/actions/types.ts +25 -0
  24. package/src/dsl/compiler/component-mapper.ts +289 -0
  25. package/src/dsl/compiler/field-mapper.ts +187 -0
  26. package/src/dsl/compiler/index.ts +82 -0
  27. package/src/dsl/compiler/manifest-compiler.ts +76 -0
  28. package/src/dsl/compiler/symbol-table.ts +214 -0
  29. package/src/dsl/compiler/utils.ts +48 -0
  30. package/src/dsl/compiler/view-compiler.ts +286 -0
  31. package/src/dsl/compiler/workflow-compiler.ts +600 -0
  32. package/src/dsl/index.ts +66 -0
  33. package/src/dsl/ir-migration.ts +221 -0
  34. package/src/dsl/ir-types.ts +416 -0
  35. package/src/dsl/lexer.ts +579 -0
  36. package/src/dsl/parser.ts +115 -0
  37. package/src/dsl/types.ts +256 -0
  38. package/src/events/event-bus.ts +68 -0
  39. package/src/events/index.ts +9 -0
  40. package/src/events/pattern-matcher.ts +61 -0
  41. package/src/events/types.ts +27 -0
  42. package/src/expression/evaluator.ts +676 -0
  43. package/src/expression/functions.ts +214 -0
  44. package/src/expression/index.ts +13 -0
  45. package/src/expression/types.ts +64 -0
  46. package/src/index.ts +61 -0
  47. package/src/state-machine/index.ts +16 -0
  48. package/src/state-machine/interpreter.ts +319 -0
  49. package/src/state-machine/types.ts +89 -0
  50. package/src/testing/action-trace.ts +209 -0
  51. package/src/testing/blueprint-test-runner.ts +214 -0
  52. package/src/testing/graph-walker.ts +249 -0
  53. package/src/testing/index.ts +69 -0
  54. package/src/testing/nrt-comparator.ts +199 -0
  55. package/src/testing/nrt-types.ts +230 -0
  56. package/src/testing/test-actions.ts +645 -0
  57. package/src/testing/test-compiler.ts +278 -0
  58. package/src/testing/test-runner.ts +444 -0
  59. package/src/testing/types.ts +231 -0
  60. package/src/validation/definition-validator.ts +812 -0
  61. package/src/validation/index.ts +13 -0
  62. package/tsconfig.json +26 -0
  63. package/vitest.config.ts +8 -0
@@ -0,0 +1,450 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { StateMachine } from '../state-machine/interpreter';
3
+ import { createEvaluator, WEB_FAILURE_POLICIES, clearExpressionCache } from '../expression';
4
+ import type { PlayerWorkflowDefinition, PlayerAction } from '../state-machine/types';
5
+ import type { Evaluator, ExpressionContext } from '../expression/types';
6
+
7
+ function makeDefinition(overrides: Partial<PlayerWorkflowDefinition> = {}): PlayerWorkflowDefinition {
8
+ return {
9
+ id: 'test-def-1',
10
+ slug: 'test-workflow',
11
+ states: [
12
+ { name: 'start', type: 'START' },
13
+ { name: 'step1', type: 'REGULAR' },
14
+ { name: 'step2', type: 'REGULAR' },
15
+ { name: 'done', type: 'END' },
16
+ ],
17
+ transitions: [
18
+ { name: 'begin', from: ['start'], to: 'step1' },
19
+ { name: 'advance', from: ['step1'], to: 'step2' },
20
+ { name: 'finish', from: ['step2'], to: 'done' },
21
+ ],
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ describe('StateMachine', () => {
27
+ let evaluator: Evaluator;
28
+
29
+ beforeEach(() => {
30
+ clearExpressionCache();
31
+ evaluator = createEvaluator({
32
+ functions: [],
33
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
34
+ });
35
+ });
36
+
37
+ describe('Initialization', () => {
38
+ it('starts in the START state', () => {
39
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
40
+ expect(sm.currentState).toBe('start');
41
+ expect(sm.status).toBe('ACTIVE');
42
+ });
43
+
44
+ it('throws if no START state found', () => {
45
+ expect(() => {
46
+ new StateMachine(
47
+ makeDefinition({ states: [{ name: 'x', type: 'REGULAR' }] }),
48
+ {},
49
+ { evaluator },
50
+ );
51
+ }).toThrow('No START state');
52
+ });
53
+
54
+ it('accepts initial data', () => {
55
+ const sm = new StateMachine(makeDefinition(), { key: 'value' }, { evaluator });
56
+ expect(sm.stateData.key).toBe('value');
57
+ });
58
+ });
59
+
60
+ describe('Transitions', () => {
61
+ it('executes a valid transition', async () => {
62
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
63
+ const result = await sm.transition('begin');
64
+ expect(result.success).toBe(true);
65
+ expect(result.from_state).toBe('start');
66
+ expect(result.to_state).toBe('step1');
67
+ expect(sm.currentState).toBe('step1');
68
+ });
69
+
70
+ it('rejects invalid transition from current state', async () => {
71
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
72
+ const result = await sm.transition('advance'); // not valid from 'start'
73
+ expect(result.success).toBe(false);
74
+ expect(result.error).toContain('not valid from state');
75
+ });
76
+
77
+ it('rejects transition on non-ACTIVE instance', async () => {
78
+ const def = makeDefinition({
79
+ transitions: [{ name: 'end_now', from: ['start'], to: 'done' }],
80
+ });
81
+ const sm = new StateMachine(def, {}, { evaluator });
82
+ await sm.transition('end_now'); // moves to 'done' (END state)
83
+ expect(sm.status).toBe('COMPLETED');
84
+
85
+ const result = await sm.transition('begin');
86
+ expect(result.success).toBe(false);
87
+ expect(result.error).toContain('COMPLETED');
88
+ });
89
+
90
+ it('merges transition data into state_data', async () => {
91
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
92
+ await sm.transition('begin', { extra: 42 });
93
+ expect(sm.stateData.extra).toBe(42);
94
+ });
95
+
96
+ it('chains through multiple transitions', async () => {
97
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
98
+ await sm.transition('begin');
99
+ await sm.transition('advance');
100
+ await sm.transition('finish');
101
+ expect(sm.currentState).toBe('done');
102
+ expect(sm.status).toBe('COMPLETED');
103
+ });
104
+ });
105
+
106
+ describe('Conditions', () => {
107
+ it('blocks transition when condition fails', async () => {
108
+ const def = makeDefinition({
109
+ transitions: [
110
+ { name: 'guarded', from: ['start'], to: 'step1', conditions: ['gt(count, 10)'] },
111
+ ],
112
+ });
113
+ const sm = new StateMachine(def, { count: 5 }, { evaluator });
114
+ const result = await sm.transition('guarded');
115
+ expect(result.success).toBe(false);
116
+ expect(result.error).toContain('condition not met');
117
+ });
118
+
119
+ it('allows transition when condition passes', async () => {
120
+ const def = makeDefinition({
121
+ transitions: [
122
+ { name: 'guarded', from: ['start'], to: 'step1', conditions: ['gt(count, 3)'] },
123
+ ],
124
+ });
125
+ const sm = new StateMachine(def, { count: 5 }, { evaluator });
126
+ const result = await sm.transition('guarded');
127
+ expect(result.success).toBe(true);
128
+ });
129
+ });
130
+
131
+ describe('Auto-transitions', () => {
132
+ it('drains auto-transitions after a manual transition', async () => {
133
+ const def = makeDefinition({
134
+ transitions: [
135
+ { name: 'begin', from: ['start'], to: 'step1' },
136
+ { name: 'auto_advance', from: ['step1'], to: 'step2', auto: true },
137
+ ],
138
+ });
139
+ const sm = new StateMachine(def, {}, { evaluator });
140
+ await sm.transition('begin');
141
+ expect(sm.currentState).toBe('step2'); // auto-transitioned
142
+ });
143
+
144
+ it('chains multiple auto-transitions', async () => {
145
+ const def = makeDefinition({
146
+ transitions: [
147
+ { name: 'begin', from: ['start'], to: 'step1' },
148
+ { name: 'auto1', from: ['step1'], to: 'step2', auto: true },
149
+ { name: 'auto2', from: ['step2'], to: 'done', auto: true },
150
+ ],
151
+ });
152
+ const sm = new StateMachine(def, {}, { evaluator });
153
+ await sm.transition('begin');
154
+ expect(sm.currentState).toBe('done');
155
+ expect(sm.status).toBe('COMPLETED');
156
+ });
157
+
158
+ it('evaluates auto-transition conditions', async () => {
159
+ const def = makeDefinition({
160
+ transitions: [
161
+ { name: 'begin', from: ['start'], to: 'step1' },
162
+ { name: 'auto_guarded', from: ['step1'], to: 'step2', auto: true, conditions: ['gt(count, 10)'] },
163
+ ],
164
+ });
165
+ const sm = new StateMachine(def, { count: 5 }, { evaluator });
166
+ await sm.transition('begin');
167
+ expect(sm.currentState).toBe('step1'); // condition not met, stays
168
+ });
169
+
170
+ it('stops at MAX_AUTO_CHAIN depth', async () => {
171
+ // Create a circular auto-transition (A→B→A→B→...)
172
+ const def: PlayerWorkflowDefinition = {
173
+ id: 'loop',
174
+ slug: 'loop-workflow',
175
+ states: [
176
+ { name: 'A', type: 'START' },
177
+ { name: 'B', type: 'REGULAR' },
178
+ ],
179
+ transitions: [
180
+ { name: 'start_loop', from: ['A'], to: 'B' },
181
+ { name: 'loop_back', from: ['B'], to: 'A', auto: true },
182
+ { name: 'loop_fwd', from: ['A'], to: 'B', auto: true },
183
+ ],
184
+ };
185
+ const sm = new StateMachine(def, {}, { evaluator });
186
+ await sm.transition('start_loop');
187
+ // Should stop after 10 auto-transitions, not infinite loop
188
+ expect(['A', 'B']).toContain(sm.currentState);
189
+ });
190
+ });
191
+
192
+ describe('Terminal States', () => {
193
+ it('sets COMPLETED on END state', async () => {
194
+ const sm = new StateMachine(
195
+ makeDefinition({ transitions: [{ name: 'end', from: ['start'], to: 'done' }] }),
196
+ {},
197
+ { evaluator },
198
+ );
199
+ await sm.transition('end');
200
+ expect(sm.status).toBe('COMPLETED');
201
+ });
202
+
203
+ it('sets CANCELLED on CANCELLED state', async () => {
204
+ const def = makeDefinition({
205
+ states: [
206
+ { name: 'start', type: 'START' },
207
+ { name: 'cancelled', type: 'CANCELLED' },
208
+ ],
209
+ transitions: [{ name: 'cancel', from: ['start'], to: 'cancelled' }],
210
+ });
211
+ const sm = new StateMachine(def, {}, { evaluator });
212
+ await sm.transition('cancel');
213
+ expect(sm.status).toBe('CANCELLED');
214
+ });
215
+ });
216
+
217
+ describe('Actions', () => {
218
+ it('executes on_enter actions', async () => {
219
+ const actions: PlayerAction[] = [];
220
+ const def = makeDefinition({
221
+ states: [
222
+ { name: 'start', type: 'START' },
223
+ { name: 'step1', type: 'REGULAR', on_enter: [{ type: 'log', config: { message: 'entered' } }] },
224
+ { name: 'done', type: 'END' },
225
+ ],
226
+ });
227
+
228
+ const sm = new StateMachine(def, {}, {
229
+ evaluator,
230
+ actionHandlers: new Map([['log', (action) => { actions.push(action); }]]),
231
+ });
232
+
233
+ const result = await sm.transition('begin');
234
+ expect(result.actions_executed).toHaveLength(1);
235
+ expect(result.actions_executed[0].type).toBe('log');
236
+ expect(actions).toHaveLength(1);
237
+ });
238
+
239
+ it('executes on_exit actions', async () => {
240
+ const exitCalled = vi.fn();
241
+ const def = makeDefinition({
242
+ states: [
243
+ { name: 'start', type: 'START', on_exit: [{ type: 'cleanup', config: {} }] },
244
+ { name: 'step1', type: 'REGULAR' },
245
+ { name: 'done', type: 'END' },
246
+ ],
247
+ });
248
+
249
+ const sm = new StateMachine(def, {}, {
250
+ evaluator,
251
+ actionHandlers: new Map([['cleanup', exitCalled]]),
252
+ });
253
+
254
+ await sm.transition('begin');
255
+ expect(exitCalled).toHaveBeenCalledOnce();
256
+ });
257
+
258
+ it('executes transition actions', async () => {
259
+ const def = makeDefinition({
260
+ transitions: [
261
+ { name: 'begin', from: ['start'], to: 'step1', actions: [{ type: 'notify', config: { msg: 'hi' } }] },
262
+ ],
263
+ });
264
+ const notified = vi.fn();
265
+ const sm = new StateMachine(def, {}, {
266
+ evaluator,
267
+ actionHandlers: new Map([['notify', notified]]),
268
+ });
269
+
270
+ await sm.transition('begin');
271
+ expect(notified).toHaveBeenCalledOnce();
272
+ });
273
+
274
+ it('skips actions whose condition fails', async () => {
275
+ const handler = vi.fn();
276
+ const def = makeDefinition({
277
+ states: [
278
+ { name: 'start', type: 'START' },
279
+ {
280
+ name: 'step1', type: 'REGULAR',
281
+ on_enter: [{ type: 'conditional', config: {}, condition: 'eq(count, 999)' }],
282
+ },
283
+ { name: 'done', type: 'END' },
284
+ ],
285
+ });
286
+
287
+ const sm = new StateMachine(def, { count: 1 }, {
288
+ evaluator,
289
+ actionHandlers: new Map([['conditional', handler]]),
290
+ });
291
+
292
+ await sm.transition('begin');
293
+ expect(handler).not.toHaveBeenCalled();
294
+ });
295
+
296
+ it('continues after action errors', async () => {
297
+ const def = makeDefinition({
298
+ states: [
299
+ { name: 'start', type: 'START' },
300
+ {
301
+ name: 'step1', type: 'REGULAR',
302
+ on_enter: [
303
+ { type: 'failing', config: {} },
304
+ { type: 'succeeding', config: {} },
305
+ ],
306
+ },
307
+ { name: 'done', type: 'END' },
308
+ ],
309
+ });
310
+
311
+ const succeeding = vi.fn();
312
+ const sm = new StateMachine(def, {}, {
313
+ evaluator,
314
+ actionHandlers: new Map([
315
+ ['failing', () => { throw new Error('boom'); }],
316
+ ['succeeding', succeeding],
317
+ ]),
318
+ });
319
+
320
+ await sm.transition('begin');
321
+ expect(succeeding).toHaveBeenCalledOnce();
322
+ expect(sm.currentState).toBe('step1');
323
+ });
324
+ });
325
+
326
+ describe('Events', () => {
327
+ it('emits state_exit, state_enter, and transition events', async () => {
328
+ const events: string[] = [];
329
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
330
+ sm.on((e) => events.push(e.type));
331
+
332
+ await sm.transition('begin');
333
+ expect(events).toEqual(['state_exit', 'state_enter', 'transition']);
334
+ });
335
+
336
+ it('emits action_executed event', async () => {
337
+ const events: string[] = [];
338
+ const def = makeDefinition({
339
+ states: [
340
+ { name: 'start', type: 'START' },
341
+ { name: 'step1', type: 'REGULAR', on_enter: [{ type: 'log', config: {} }] },
342
+ { name: 'done', type: 'END' },
343
+ ],
344
+ });
345
+ const sm = new StateMachine(def, {}, {
346
+ evaluator,
347
+ actionHandlers: new Map([['log', () => {}]]),
348
+ });
349
+ sm.on((e) => events.push(e.type));
350
+
351
+ await sm.transition('begin');
352
+ expect(events).toContain('action_executed');
353
+ });
354
+
355
+ it('emits error event on action failure', async () => {
356
+ const errors: string[] = [];
357
+ const def = makeDefinition({
358
+ states: [
359
+ { name: 'start', type: 'START' },
360
+ { name: 'step1', type: 'REGULAR', on_enter: [{ type: 'bad', config: {} }] },
361
+ { name: 'done', type: 'END' },
362
+ ],
363
+ });
364
+ const sm = new StateMachine(def, {}, {
365
+ evaluator,
366
+ actionHandlers: new Map([['bad', () => { throw new Error('oops'); }]]),
367
+ });
368
+ sm.on((e) => { if (e.type === 'error' && e.error) errors.push(e.error); });
369
+
370
+ await sm.transition('begin');
371
+ expect(errors).toEqual(['oops']);
372
+ });
373
+
374
+ it('unsubscribe removes listener', async () => {
375
+ const events: string[] = [];
376
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
377
+ const unsub = sm.on((e) => events.push(e.type));
378
+ unsub();
379
+
380
+ await sm.transition('begin');
381
+ expect(events).toHaveLength(0);
382
+ });
383
+
384
+ it('listener errors do not break state machine', async () => {
385
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
386
+ sm.on(() => { throw new Error('listener boom'); });
387
+
388
+ const result = await sm.transition('begin');
389
+ expect(result.success).toBe(true);
390
+ });
391
+ });
392
+
393
+ describe('Snapshot', () => {
394
+ it('returns an immutable snapshot', async () => {
395
+ const sm = new StateMachine(makeDefinition(), { x: 1 }, { evaluator });
396
+ const snap = sm.getSnapshot();
397
+ expect(snap.current_state).toBe('start');
398
+ expect(snap.state_data.x).toBe(1);
399
+
400
+ // Mutating snapshot should not affect state machine
401
+ (snap.state_data as Record<string, unknown>).x = 999;
402
+ expect(sm.stateData.x).toBe(1);
403
+ });
404
+ });
405
+
406
+ describe('Utility Methods', () => {
407
+ it('getAvailableTransitions returns non-auto transitions from current state', () => {
408
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
409
+ const available = sm.getAvailableTransitions();
410
+ expect(available).toHaveLength(1);
411
+ expect(available[0].name).toBe('begin');
412
+ });
413
+
414
+ it('getCurrentStateDefinition returns the current state', () => {
415
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
416
+ const state = sm.getCurrentStateDefinition();
417
+ expect(state?.name).toBe('start');
418
+ expect(state?.type).toBe('START');
419
+ });
420
+
421
+ it('setField updates state_data', () => {
422
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
423
+ sm.setField('newField', 'value');
424
+ expect(sm.stateData.newField).toBe('value');
425
+ });
426
+
427
+ it('setMemory updates memory', () => {
428
+ const sm = new StateMachine(makeDefinition(), {}, { evaluator });
429
+ sm.setMemory('key', 'val');
430
+ const snap = sm.getSnapshot();
431
+ expect(snap.memory.key).toBe('val');
432
+ });
433
+
434
+ it('registerAction adds a handler at runtime', async () => {
435
+ const handler = vi.fn();
436
+ const def = makeDefinition({
437
+ states: [
438
+ { name: 'start', type: 'START' },
439
+ { name: 'step1', type: 'REGULAR', on_enter: [{ type: 'dynamic', config: {} }] },
440
+ { name: 'done', type: 'END' },
441
+ ],
442
+ });
443
+ const sm = new StateMachine(def, {}, { evaluator });
444
+ sm.registerAction('dynamic', handler);
445
+
446
+ await sm.transition('begin');
447
+ expect(handler).toHaveBeenCalledOnce();
448
+ });
449
+ });
450
+ });