@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,200 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { EventBus } from '../events/event-bus';
3
+ import { compilePattern, matchTopic, clearPatternCache } from '../events/pattern-matcher';
4
+
5
+ describe('Pattern Matcher', () => {
6
+ beforeEach(() => {
7
+ clearPatternCache();
8
+ });
9
+
10
+ describe('compilePattern', () => {
11
+ it('compiles exact match patterns', () => {
12
+ const regex = compilePattern('workflow.order:state_enter');
13
+ expect(matchTopic(regex, 'workflow.order:state_enter')).toBe(true);
14
+ expect(matchTopic(regex, 'workflow.order:state_exit')).toBe(false);
15
+ });
16
+
17
+ it('compiles single wildcard (*) patterns', () => {
18
+ const regex = compilePattern('workflow.*:state_enter');
19
+ expect(matchTopic(regex, 'workflow.order:state_enter')).toBe(true);
20
+ expect(matchTopic(regex, 'workflow.task:state_enter')).toBe(true);
21
+ expect(matchTopic(regex, 'workflow.order:state_exit')).toBe(false);
22
+ });
23
+
24
+ it('wildcard does not cross segment boundaries', () => {
25
+ const regex = compilePattern('*:completed');
26
+ expect(matchTopic(regex, 'workflow:completed')).toBe(true);
27
+ expect(matchTopic(regex, 'workflow.order:completed')).toBe(false); // * doesn't cross .
28
+ });
29
+
30
+ it('compiles double wildcard (**) patterns', () => {
31
+ const regex = compilePattern('workflow.**');
32
+ expect(matchTopic(regex, 'workflow.order')).toBe(true);
33
+ expect(matchTopic(regex, 'workflow.order:state_enter')).toBe(true);
34
+ expect(matchTopic(regex, 'workflow.a.b.c:deep')).toBe(true);
35
+ expect(matchTopic(regex, 'other.thing')).toBe(false);
36
+ });
37
+
38
+ it('handles multiple wildcards', () => {
39
+ const regex = compilePattern('*:*:completed');
40
+ expect(matchTopic(regex, 'workflow:order:completed')).toBe(true);
41
+ expect(matchTopic(regex, 'a:b:completed')).toBe(true);
42
+ expect(matchTopic(regex, 'a:b:failed')).toBe(false);
43
+ });
44
+
45
+ it('escapes regex special characters', () => {
46
+ const regex = compilePattern('user+name.test');
47
+ expect(matchTopic(regex, 'user+name.test')).toBe(true);
48
+ expect(matchTopic(regex, 'userXname.test')).toBe(false);
49
+ });
50
+
51
+ it('caches compiled patterns', () => {
52
+ const r1 = compilePattern('test.pattern');
53
+ const r2 = compilePattern('test.pattern');
54
+ expect(r1).toBe(r2); // Same reference from cache
55
+ });
56
+ });
57
+
58
+ describe('matchTopic', () => {
59
+ it('matches exact topics', () => {
60
+ const regex = compilePattern('events:order:created');
61
+ expect(matchTopic(regex, 'events:order:created')).toBe(true);
62
+ expect(matchTopic(regex, 'events:order:updated')).toBe(false);
63
+ });
64
+
65
+ it('matches real-world on_event patterns', () => {
66
+ // Pattern from PWE on_event: "*:*:#vote:instance.completed"
67
+ const regex = compilePattern('*:*:#vote:instance.completed');
68
+ expect(matchTopic(regex, 'workflow:def1:#vote:instance.completed')).toBe(true);
69
+ expect(matchTopic(regex, 'x:y:#vote:instance.completed')).toBe(true);
70
+ expect(matchTopic(regex, 'x:y:#vote:instance.failed')).toBe(false);
71
+ });
72
+ });
73
+ });
74
+
75
+ describe('EventBus', () => {
76
+ let bus: EventBus;
77
+
78
+ beforeEach(() => {
79
+ clearPatternCache();
80
+ bus = new EventBus();
81
+ });
82
+
83
+ describe('subscribe/publish', () => {
84
+ it('delivers events to matching subscribers', async () => {
85
+ const handler = vi.fn();
86
+ bus.subscribe('test:event', handler);
87
+
88
+ await bus.publish('test:event', { data: 1 });
89
+ expect(handler).toHaveBeenCalledWith({ topic: 'test:event', payload: { data: 1 } });
90
+ });
91
+
92
+ it('does not deliver to non-matching subscribers', async () => {
93
+ const handler = vi.fn();
94
+ bus.subscribe('other:event', handler);
95
+
96
+ await bus.publish('test:event', { data: 1 });
97
+ expect(handler).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('delivers to multiple matching subscribers', async () => {
101
+ const h1 = vi.fn();
102
+ const h2 = vi.fn();
103
+ bus.subscribe('test:event', h1);
104
+ bus.subscribe('test:event', h2);
105
+
106
+ await bus.publish('test:event');
107
+ expect(h1).toHaveBeenCalledOnce();
108
+ expect(h2).toHaveBeenCalledOnce();
109
+ });
110
+
111
+ it('supports wildcard subscriptions', async () => {
112
+ const handler = vi.fn();
113
+ bus.subscribe('test:*', handler);
114
+
115
+ await bus.publish('test:event1');
116
+ await bus.publish('test:event2');
117
+ expect(handler).toHaveBeenCalledTimes(2);
118
+ });
119
+
120
+ it('supports double-wildcard subscriptions', async () => {
121
+ const handler = vi.fn();
122
+ bus.subscribe('**', handler);
123
+
124
+ await bus.publish('any:topic:here');
125
+ expect(handler).toHaveBeenCalledOnce();
126
+ });
127
+
128
+ it('defaults payload to empty object', async () => {
129
+ const handler = vi.fn();
130
+ bus.subscribe('test', handler);
131
+
132
+ await bus.publish('test');
133
+ expect(handler).toHaveBeenCalledWith({ topic: 'test', payload: {} });
134
+ });
135
+ });
136
+
137
+ describe('unsubscribe', () => {
138
+ it('removes subscription when unsubscribe is called', async () => {
139
+ const handler = vi.fn();
140
+ const unsub = bus.subscribe('test:event', handler);
141
+
142
+ unsub();
143
+ await bus.publish('test:event');
144
+ expect(handler).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it('only removes the specific subscription', async () => {
148
+ const h1 = vi.fn();
149
+ const h2 = vi.fn();
150
+ const unsub1 = bus.subscribe('test:event', h1);
151
+ bus.subscribe('test:event', h2);
152
+
153
+ unsub1();
154
+ await bus.publish('test:event');
155
+ expect(h1).not.toHaveBeenCalled();
156
+ expect(h2).toHaveBeenCalledOnce();
157
+ });
158
+ });
159
+
160
+ describe('error handling', () => {
161
+ it('catches and logs handler errors without propagating', async () => {
162
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
163
+ const good = vi.fn();
164
+ bus.subscribe('test', () => { throw new Error('boom'); });
165
+ bus.subscribe('test', good);
166
+
167
+ await bus.publish('test');
168
+ expect(good).toHaveBeenCalledOnce();
169
+ expect(warn).toHaveBeenCalled();
170
+ warn.mockRestore();
171
+ });
172
+ });
173
+
174
+ describe('emit (fire-and-forget)', () => {
175
+ it('publishes without awaiting', () => {
176
+ const handler = vi.fn();
177
+ bus.subscribe('test', handler);
178
+
179
+ bus.emit('test', { x: 1 });
180
+ // Handler may or may not have been called yet (async)
181
+ // Just verify it doesn't throw
182
+ });
183
+ });
184
+
185
+ describe('size and clear', () => {
186
+ it('tracks subscription count', () => {
187
+ expect(bus.size).toBe(0);
188
+ bus.subscribe('a', () => {});
189
+ bus.subscribe('b', () => {});
190
+ expect(bus.size).toBe(2);
191
+ });
192
+
193
+ it('clears all subscriptions', () => {
194
+ bus.subscribe('a', () => {});
195
+ bus.subscribe('b', () => {});
196
+ bus.clear();
197
+ expect(bus.size).toBe(0);
198
+ });
199
+ });
200
+ });
@@ -0,0 +1,296 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import {
3
+ createEvaluator,
4
+ CORE_FUNCTIONS,
5
+ buildFunctionMap,
6
+ WEB_FAILURE_POLICIES,
7
+ clearExpressionCache,
8
+ } from '../expression';
9
+ import type { Evaluator, ExpressionContext } from '../expression';
10
+
11
+ describe('Expression Engine', () => {
12
+ let evaluator: Evaluator;
13
+ let ctx: ExpressionContext;
14
+
15
+ beforeEach(() => {
16
+ clearExpressionCache();
17
+ evaluator = createEvaluator({
18
+ functions: [],
19
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
20
+ });
21
+ ctx = {
22
+ state_data: { title: 'Test', count: 5, name: 'Alice', items: [1, 2, 3] },
23
+ memory: { visited: true },
24
+ current_state: 'active',
25
+ status: 'ACTIVE',
26
+ title: 'Test',
27
+ count: 5,
28
+ name: 'Alice',
29
+ items: [1, 2, 3],
30
+ };
31
+ });
32
+
33
+ describe('Literals', () => {
34
+ it('evaluates numeric literals', () => {
35
+ expect(evaluator.evaluate('42', ctx).value).toBe(42);
36
+ expect(evaluator.evaluate('3.14', ctx).value).toBe(3.14);
37
+ expect(evaluator.evaluate('0', ctx).value).toBe(0);
38
+ });
39
+
40
+ it('evaluates boolean literals', () => {
41
+ expect(evaluator.evaluate('true', ctx).value).toBe(true);
42
+ expect(evaluator.evaluate('false', ctx).value).toBe(false);
43
+ });
44
+
45
+ it('evaluates null/undefined', () => {
46
+ expect(evaluator.evaluate('null', ctx).value).toBe(null);
47
+ expect(evaluator.evaluate('undefined', ctx).value).toBe(undefined);
48
+ });
49
+
50
+ it('evaluates string literals', () => {
51
+ expect(evaluator.evaluate("'hello'", ctx).value).toBe('hello');
52
+ expect(evaluator.evaluate('"world"', ctx).value).toBe('world');
53
+ });
54
+ });
55
+
56
+ describe('Path Resolution', () => {
57
+ it('resolves direct field access', () => {
58
+ expect(evaluator.evaluate('title', ctx).value).toBe('Test');
59
+ expect(evaluator.evaluate('count', ctx).value).toBe(5);
60
+ });
61
+
62
+ it('resolves nested paths', () => {
63
+ expect(evaluator.evaluate('state_data.title', ctx).value).toBe('Test');
64
+ expect(evaluator.evaluate('memory.visited', ctx).value).toBe(true);
65
+ });
66
+
67
+ it('returns undefined for missing paths', () => {
68
+ expect(evaluator.evaluate('nonexistent', ctx).value).toBeUndefined();
69
+ });
70
+
71
+ it('resolves current_state and status', () => {
72
+ expect(evaluator.evaluate('current_state', ctx).value).toBe('active');
73
+ expect(evaluator.evaluate('status', ctx).value).toBe('ACTIVE');
74
+ });
75
+ });
76
+
77
+ describe('Math Functions', () => {
78
+ it('add', () => {
79
+ expect(evaluator.evaluate('add(2, 3)', ctx).value).toBe(5);
80
+ expect(evaluator.evaluate('add(count, 10)', ctx).value).toBe(15);
81
+ });
82
+
83
+ it('subtract', () => {
84
+ expect(evaluator.evaluate('subtract(10, 3)', ctx).value).toBe(7);
85
+ });
86
+
87
+ it('multiply', () => {
88
+ expect(evaluator.evaluate('multiply(4, 5)', ctx).value).toBe(20);
89
+ });
90
+
91
+ it('divide', () => {
92
+ expect(evaluator.evaluate('divide(10, 2)', ctx).value).toBe(5);
93
+ expect(evaluator.evaluate('divide(10, 0)', ctx).value).toBe(0); // safe divide by zero
94
+ });
95
+
96
+ it('abs', () => {
97
+ expect(evaluator.evaluate('abs(-5)', ctx).value).toBe(5);
98
+ });
99
+
100
+ it('round', () => {
101
+ expect(evaluator.evaluate('round(3.7)', ctx).value).toBe(4);
102
+ expect(evaluator.evaluate('round(3.456, 2)', ctx).value).toBe(3.46);
103
+ });
104
+
105
+ it('min/max', () => {
106
+ expect(evaluator.evaluate('min(1, 2, 3)', ctx).value).toBe(1);
107
+ expect(evaluator.evaluate('max(1, 2, 3)', ctx).value).toBe(3);
108
+ });
109
+ });
110
+
111
+ describe('Comparison Functions', () => {
112
+ it('eq/neq', () => {
113
+ expect(evaluator.evaluate('eq(count, 5)', ctx).value).toBe(true);
114
+ expect(evaluator.evaluate('neq(count, 3)', ctx).value).toBe(true);
115
+ });
116
+
117
+ it('gt/gte/lt/lte', () => {
118
+ expect(evaluator.evaluate('gt(count, 3)', ctx).value).toBe(true);
119
+ expect(evaluator.evaluate('gte(count, 5)', ctx).value).toBe(true);
120
+ expect(evaluator.evaluate('lt(count, 10)', ctx).value).toBe(true);
121
+ expect(evaluator.evaluate('lte(count, 5)', ctx).value).toBe(true);
122
+ });
123
+ });
124
+
125
+ describe('Logic Functions', () => {
126
+ it('if', () => {
127
+ expect(evaluator.evaluate("if(eq(count, 5), 'yes', 'no')", ctx).value).toBe('yes');
128
+ expect(evaluator.evaluate("if(eq(count, 3), 'yes', 'no')", ctx).value).toBe('no');
129
+ });
130
+
131
+ it('and/or/not', () => {
132
+ expect(evaluator.evaluate('and(true, true)', ctx).value).toBe(true);
133
+ expect(evaluator.evaluate('and(true, false)', ctx).value).toBe(false);
134
+ expect(evaluator.evaluate('or(false, true)', ctx).value).toBe(true);
135
+ expect(evaluator.evaluate('not(false)', ctx).value).toBe(true);
136
+ });
137
+
138
+ it('coalesce', () => {
139
+ expect(evaluator.evaluate('coalesce(null, undefined, 42)', ctx).value).toBe(42);
140
+ expect(evaluator.evaluate("coalesce('first', 'second')", ctx).value).toBe('first');
141
+ });
142
+ });
143
+
144
+ describe('String Functions', () => {
145
+ it('concat', () => {
146
+ expect(evaluator.evaluate("concat('Hello', ' ', 'World')", ctx).value).toBe('Hello World');
147
+ });
148
+
149
+ it('upper/lower', () => {
150
+ expect(evaluator.evaluate("upper('hello')", ctx).value).toBe('HELLO');
151
+ expect(evaluator.evaluate("lower('HELLO')", ctx).value).toBe('hello');
152
+ });
153
+
154
+ it('trim', () => {
155
+ expect(evaluator.evaluate("trim(' hello ')", ctx).value).toBe('hello');
156
+ });
157
+
158
+ it('length', () => {
159
+ expect(evaluator.evaluate("length('hello')", ctx).value).toBe(5);
160
+ expect(evaluator.evaluate('length(items)', ctx).value).toBe(3);
161
+ });
162
+ });
163
+
164
+ describe('Path Functions', () => {
165
+ it('get', () => {
166
+ expect(evaluator.evaluate("get(state_data, 'title')", ctx).value).toBe('Test');
167
+ });
168
+
169
+ it('includes', () => {
170
+ expect(evaluator.evaluate("includes('hello world', 'world')", ctx).value).toBe(true);
171
+ });
172
+
173
+ it('is_defined', () => {
174
+ expect(evaluator.evaluate('is_defined(title)', ctx).value).toBe(true);
175
+ // Nonexistent vars in with() throw ReferenceError → fallback
176
+ // Use is_defined with a path that resolves to null/undefined instead
177
+ expect(evaluator.evaluate('is_defined(null)', ctx).value).toBe(false);
178
+ expect(evaluator.evaluate('is_defined(state_data.missing)', ctx).value).toBe(false);
179
+ });
180
+ });
181
+
182
+ describe('Type Functions', () => {
183
+ it('is_empty', () => {
184
+ expect(evaluator.evaluate("is_empty('')", ctx).value).toBe(true);
185
+ expect(evaluator.evaluate('is_empty(null)', ctx).value).toBe(true);
186
+ expect(evaluator.evaluate("is_empty('hello')", ctx).value).toBe(false);
187
+ });
188
+
189
+ it('is_null', () => {
190
+ expect(evaluator.evaluate('is_null(null)', ctx).value).toBe(true);
191
+ expect(evaluator.evaluate('is_null(title)', ctx).value).toBe(false);
192
+ });
193
+
194
+ it('to_string', () => {
195
+ expect(evaluator.evaluate('to_string(42)', ctx).value).toBe('42');
196
+ expect(evaluator.evaluate('to_string(null)', ctx).value).toBe('');
197
+ });
198
+ });
199
+
200
+ describe('Template Interpolation', () => {
201
+ it('interpolates expressions in templates', () => {
202
+ const result = evaluator.evaluateTemplate('Hello {{name}}, you have {{count}} items', ctx);
203
+ expect(result.value).toBe('Hello Alice, you have 5 items');
204
+ });
205
+
206
+ it('returns plain strings unchanged', () => {
207
+ const result = evaluator.evaluateTemplate('no interpolation here', ctx);
208
+ expect(result.value).toBe('no interpolation here');
209
+ });
210
+
211
+ it('handles missing values as empty string', () => {
212
+ const result = evaluator.evaluateTemplate('Value: {{nonexistent}}', ctx);
213
+ expect(result.value).toBe('Value: ');
214
+ });
215
+ });
216
+
217
+ describe('Complex Expressions', () => {
218
+ it('evaluates operator expressions', () => {
219
+ expect(evaluator.evaluate('count > 3', ctx).value).toBe(true);
220
+ expect(evaluator.evaluate('count == 5', ctx).value).toBe(true);
221
+ expect(evaluator.evaluate('count != 10', ctx).value).toBe(true);
222
+ });
223
+
224
+ it('evaluates ternary expressions', () => {
225
+ const result = evaluator.evaluate("count > 3 ? 'many' : 'few'", ctx);
226
+ expect(result.value).toBe('many');
227
+ });
228
+
229
+ it('evaluates nested function calls', () => {
230
+ const result = evaluator.evaluate('add(multiply(2, 3), 4)', ctx);
231
+ expect(result.value).toBe(10);
232
+ });
233
+ });
234
+
235
+ describe('Validation', () => {
236
+ it('rejects invalid syntax', () => {
237
+ const result = evaluator.validate('add(1, ');
238
+ expect(result.valid).toBe(false);
239
+ });
240
+
241
+ it('accepts valid expressions', () => {
242
+ const result = evaluator.validate('add(1, 2)');
243
+ expect(result.valid).toBe(true);
244
+ });
245
+
246
+ it('unknown functions are valid syntax (just return undefined)', () => {
247
+ const result = evaluator.validate('eval("bad")');
248
+ expect(result.valid).toBe(true);
249
+ // eval is not in fnMap, so it safely returns undefined
250
+ const evalResult = evaluator.evaluate('eval("test")', ctx);
251
+ expect(evalResult.status).toBe('ok');
252
+ expect(evalResult.value).toBeUndefined();
253
+ });
254
+ });
255
+
256
+ describe('Failure Policy', () => {
257
+ it('VIEW_BINDING returns empty string fallback on syntax error', () => {
258
+ const viewEvaluator = createEvaluator({
259
+ functions: [],
260
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
261
+ });
262
+ const result = viewEvaluator.evaluate('add(1, ', ctx);
263
+ expect(result.status).toBe('fallback');
264
+ expect(result.value).toBe('');
265
+ });
266
+
267
+ it('CONDITIONAL_VISIBILITY returns true fallback on syntax error', () => {
268
+ const visEvaluator = createEvaluator({
269
+ functions: [],
270
+ failurePolicy: WEB_FAILURE_POLICIES.CONDITIONAL_VISIBILITY,
271
+ });
272
+ const result = visEvaluator.evaluate('add(1, ', ctx);
273
+ expect(result.status).toBe('fallback');
274
+ expect(result.value).toBe(true);
275
+ });
276
+ });
277
+
278
+ describe('Custom Functions', () => {
279
+ it('registers and uses custom functions', () => {
280
+ const custom = createEvaluator({
281
+ functions: [{ name: 'double', fn: (x: unknown) => Number(x) * 2, arity: 1 }],
282
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
283
+ });
284
+ expect(custom.evaluate('double(21)', ctx).value).toBe(42);
285
+ });
286
+ });
287
+
288
+ describe('buildFunctionMap', () => {
289
+ it('builds map from function array', () => {
290
+ const map = buildFunctionMap(CORE_FUNCTIONS);
291
+ expect(map.size).toBe(31);
292
+ expect(map.has('add')).toBe(true);
293
+ expect(map.has('eq')).toBe(true);
294
+ });
295
+ });
296
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createEvaluator, WEB_FAILURE_POLICIES, clearExpressionCache } from '../expression';
3
+ import type { ExpressionContext } from '../expression';
4
+
5
+ describe('Web Failure Policies', () => {
6
+ beforeEach(() => {
7
+ clearExpressionCache();
8
+ });
9
+
10
+ const ctx: ExpressionContext = {
11
+ state_data: { title: 'Test' },
12
+ memory: {},
13
+ current_state: 'active',
14
+ status: 'ACTIVE',
15
+ title: 'Test',
16
+ };
17
+
18
+ describe('VIEW_BINDING', () => {
19
+ it('returns empty string fallback on missing path', () => {
20
+ const evaluator = createEvaluator({
21
+ functions: [],
22
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
23
+ });
24
+ // This triggers a parse error (incomplete expression)
25
+ const result = evaluator.evaluate('add(1,', ctx);
26
+ expect(result.value).toBe('');
27
+ expect(result.status).toBe('fallback');
28
+ });
29
+
30
+ it('logs at warn level', () => {
31
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
32
+ const evaluator = createEvaluator({
33
+ functions: [],
34
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
35
+ });
36
+ evaluator.evaluate('add(1,', ctx);
37
+ expect(warn).toHaveBeenCalled();
38
+ warn.mockRestore();
39
+ });
40
+ });
41
+
42
+ describe('EVENT_REACTION', () => {
43
+ it('logs diagnostic and continues on error', () => {
44
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
45
+ const evaluator = createEvaluator({
46
+ functions: [],
47
+ failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION,
48
+ });
49
+ const result = evaluator.evaluate('add(1,', ctx);
50
+ expect(result.status).toBe('error');
51
+ expect(result.value).toBeUndefined();
52
+ expect(result.error).toBeDefined();
53
+ expect(error).toHaveBeenCalled();
54
+ error.mockRestore();
55
+ });
56
+
57
+ it('returns undefined fallback value', () => {
58
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
59
+ const evaluator = createEvaluator({
60
+ functions: [],
61
+ failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION,
62
+ });
63
+ const result = evaluator.evaluate('add(1,', ctx);
64
+ expect(result.value).toBeUndefined();
65
+ error.mockRestore();
66
+ });
67
+ });
68
+
69
+ describe('DURING_ACTION', () => {
70
+ it('logs and skips on error', () => {
71
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
72
+ const evaluator = createEvaluator({
73
+ functions: [],
74
+ failurePolicy: WEB_FAILURE_POLICIES.DURING_ACTION,
75
+ });
76
+ const result = evaluator.evaluate('add(1,', ctx);
77
+ expect(result.status).toBe('error');
78
+ expect(result.value).toBeUndefined();
79
+ error.mockRestore();
80
+ });
81
+ });
82
+
83
+ describe('CONDITIONAL_VISIBILITY', () => {
84
+ it('returns true on expression error (show, do not hide)', () => {
85
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
86
+ const evaluator = createEvaluator({
87
+ functions: [],
88
+ failurePolicy: WEB_FAILURE_POLICIES.CONDITIONAL_VISIBILITY,
89
+ });
90
+ const result = evaluator.evaluate('add(1,', ctx);
91
+ expect(result.value).toBe(true);
92
+ expect(result.status).toBe('fallback');
93
+ warn.mockRestore();
94
+ });
95
+
96
+ it('logs at warn level, not error', () => {
97
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
98
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
99
+ const evaluator = createEvaluator({
100
+ functions: [],
101
+ failurePolicy: WEB_FAILURE_POLICIES.CONDITIONAL_VISIBILITY,
102
+ });
103
+ evaluator.evaluate('add(1,', ctx);
104
+ expect(warn).toHaveBeenCalled();
105
+ expect(error).not.toHaveBeenCalled();
106
+ warn.mockRestore();
107
+ error.mockRestore();
108
+ });
109
+ });
110
+ });