@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,182 @@
1
+ /**
2
+ * Frontend Expression Context Tests — verifies evaluator resolves
3
+ * event, local, viewport, and $domain() context fields.
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach } from 'vitest';
7
+ import { createEvaluator, WEB_FAILURE_POLICIES, clearExpressionCache } from '../expression';
8
+ import type { Evaluator, ExpressionContext } from '../expression';
9
+
10
+ describe('Frontend Expression Context', () => {
11
+ let evaluator: Evaluator;
12
+
13
+ beforeEach(() => {
14
+ clearExpressionCache();
15
+ });
16
+
17
+ it('resolves event payload fields', () => {
18
+ evaluator = createEvaluator({
19
+ functions: [],
20
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
21
+ });
22
+
23
+ const ctx: ExpressionContext = {
24
+ state_data: {},
25
+ memory: {},
26
+ current_state: 'listening',
27
+ status: 'ACTIVE',
28
+ event: {
29
+ instance_id: 'i-123',
30
+ from_state: 'pending',
31
+ to_state: 'completed',
32
+ changed_fields: ['status', 'result'],
33
+ },
34
+ };
35
+
36
+ expect(evaluator.evaluate('event.to_state', ctx).value).toBe('completed');
37
+ expect(evaluator.evaluate('event.instance_id', ctx).value).toBe('i-123');
38
+ });
39
+
40
+ it('resolves local state fields', () => {
41
+ evaluator = createEvaluator({
42
+ functions: [],
43
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
44
+ });
45
+
46
+ const ctx: ExpressionContext = {
47
+ state_data: {},
48
+ memory: {},
49
+ current_state: 'active',
50
+ status: 'ACTIVE',
51
+ local: {
52
+ selected_tab: 'details',
53
+ filter_active: true,
54
+ },
55
+ };
56
+
57
+ expect(evaluator.evaluate('local.selected_tab', ctx).value).toBe('details');
58
+ expect(evaluator.evaluate('local.filter_active', ctx).value).toBe(true);
59
+ });
60
+
61
+ it('resolves viewport dimensions via custom functions', () => {
62
+ evaluator = createEvaluator({
63
+ functions: [
64
+ { name: 'viewport_width', fn: () => 1920, arity: 0 },
65
+ { name: 'viewport_height', fn: () => 1080, arity: 0 },
66
+ { name: 'is_online', fn: () => true, arity: 0 },
67
+ ],
68
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
69
+ });
70
+
71
+ const ctx: ExpressionContext = {
72
+ state_data: {},
73
+ memory: {},
74
+ current_state: 'active',
75
+ status: 'ACTIVE',
76
+ };
77
+
78
+ expect(evaluator.evaluate('viewport_width()', ctx).value).toBe(1920);
79
+ expect(evaluator.evaluate('viewport_height()', ctx).value).toBe(1080);
80
+ expect(evaluator.evaluate('is_online()', ctx).value).toBe(true);
81
+ });
82
+
83
+ it('resolves $domain() via custom function', () => {
84
+ evaluator = createEvaluator({
85
+ functions: [
86
+ {
87
+ name: '$domain',
88
+ fn: (slug: unknown, field: unknown) => {
89
+ if (slug === 'user-stats' && field === 'points') return 42;
90
+ if (slug === 'user-stats' && field === 'level') return 5;
91
+ return undefined;
92
+ },
93
+ arity: 2,
94
+ },
95
+ ],
96
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
97
+ });
98
+
99
+ const ctx: ExpressionContext = {
100
+ state_data: {},
101
+ memory: {},
102
+ current_state: 'dashboard',
103
+ status: 'ACTIVE',
104
+ };
105
+
106
+ expect(evaluator.evaluate("$domain('user-stats', 'points')", ctx).value).toBe(42);
107
+ expect(evaluator.evaluate("$domain('user-stats', 'level')", ctx).value).toBe(5);
108
+ });
109
+
110
+ it('resolves $fe_ref() via custom function', () => {
111
+ evaluator = createEvaluator({
112
+ functions: [
113
+ {
114
+ name: '$fe_ref',
115
+ fn: (instanceId: unknown, path: unknown) => {
116
+ if (instanceId === 'nav-1' && path === 'active_page') return '/dashboard';
117
+ return undefined;
118
+ },
119
+ arity: 2,
120
+ },
121
+ ],
122
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
123
+ });
124
+
125
+ const ctx: ExpressionContext = {
126
+ state_data: {},
127
+ memory: {},
128
+ current_state: 'nav',
129
+ status: 'ACTIVE',
130
+ };
131
+
132
+ expect(evaluator.evaluate("$fe_ref('nav-1', 'active_page')", ctx).value).toBe('/dashboard');
133
+ });
134
+
135
+ it('resolves $local() via custom function', () => {
136
+ evaluator = createEvaluator({
137
+ functions: [
138
+ {
139
+ name: '$local',
140
+ fn: (key: unknown) => {
141
+ const store: Record<string, unknown> = { theme: 'dark', lang: 'en' };
142
+ return store[key as string];
143
+ },
144
+ arity: 1,
145
+ },
146
+ ],
147
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
148
+ });
149
+
150
+ const ctx: ExpressionContext = {
151
+ state_data: {},
152
+ memory: {},
153
+ current_state: 'settings',
154
+ status: 'ACTIVE',
155
+ };
156
+
157
+ expect(evaluator.evaluate("$local('theme')", ctx).value).toBe('dark');
158
+ expect(evaluator.evaluate("$local('lang')", ctx).value).toBe('en');
159
+ });
160
+
161
+ it('combines event + state_data + functions in one context', () => {
162
+ evaluator = createEvaluator({
163
+ functions: [
164
+ { name: 'viewport_width', fn: () => 800, arity: 0 },
165
+ ],
166
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
167
+ });
168
+
169
+ const ctx: ExpressionContext = {
170
+ state_data: { total: 100 },
171
+ memory: { last_seen: 'yesterday' },
172
+ current_state: 'dashboard',
173
+ status: 'ACTIVE',
174
+ total: 100,
175
+ event: { new_points: 10 },
176
+ };
177
+
178
+ // Expression using state_data field + event field + function
179
+ expect(evaluator.evaluate('add(total, event.new_points)', ctx).value).toBe(110);
180
+ expect(evaluator.evaluate('gt(viewport_width(), 768)', ctx).value).toBe(true);
181
+ });
182
+ });
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Integration Tests — full chain from domain event through browser engine to action dispatch.
3
+ *
4
+ * Tests the complete pipeline:
5
+ * 1. Create state machine with on_event subscription
6
+ * 2. Wire event bus to action handler
7
+ * 3. Publish domain event matching the pattern
8
+ * 4. Verify action fires with correct context
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+ import { StateMachine } from '../state-machine/interpreter';
13
+ import { EventBus } from '../events/event-bus';
14
+ import { ActionDispatcher } from '../actions/dispatcher';
15
+ import { createEvaluator, WEB_FAILURE_POLICIES, clearExpressionCache } from '../expression';
16
+ import type { PlayerWorkflowDefinition, PlayerAction } from '../state-machine/types';
17
+ import type { Evaluator, ExpressionContext } from '../expression/types';
18
+
19
+ describe('Integration: Domain Event → Engine → Action', () => {
20
+ let evaluator: Evaluator;
21
+ let eventBus: EventBus;
22
+ let dispatcher: ActionDispatcher;
23
+
24
+ beforeEach(() => {
25
+ clearExpressionCache();
26
+ evaluator = createEvaluator({
27
+ functions: [],
28
+ failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION,
29
+ });
30
+ eventBus = new EventBus();
31
+ dispatcher = new ActionDispatcher();
32
+ });
33
+
34
+ it('full chain: domain event → pattern match → toast action fires', async () => {
35
+ // 1. Define workflow with on_event subscription
36
+ const definition: PlayerWorkflowDefinition = {
37
+ id: 'dashboard-experience',
38
+ slug: 'dashboard',
39
+ states: [
40
+ {
41
+ name: 'active',
42
+ type: 'START',
43
+ on_event: [
44
+ {
45
+ match: 'project:*:*:complete_task.transitioned',
46
+ actions: [
47
+ {
48
+ type: 'toast',
49
+ config: { variant: 'success', message: 'Task completed!' },
50
+ },
51
+ ],
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ transitions: [],
57
+ };
58
+
59
+ // 2. Create state machine
60
+ const sm = new StateMachine(definition, {}, {
61
+ evaluator,
62
+ actionHandlers: new Map(),
63
+ });
64
+
65
+ // 3. Register toast handler
66
+ const toastHandler = vi.fn();
67
+ dispatcher.register('toast', toastHandler);
68
+
69
+ // 4. Wire on_event subscriptions to event bus → action dispatcher
70
+ const stateDef = sm.getCurrentStateDefinition();
71
+ if (stateDef?.on_event) {
72
+ for (const sub of stateDef.on_event) {
73
+ eventBus.subscribe(sub.match, async (event) => {
74
+ const ctx: ExpressionContext = {
75
+ ...sm.stateData,
76
+ state_data: sm.stateData,
77
+ memory: sm.getSnapshot().memory,
78
+ current_state: sm.currentState,
79
+ status: sm.status,
80
+ event: event.payload,
81
+ };
82
+ await dispatcher.execute(sub.actions, ctx, evaluator);
83
+ });
84
+ }
85
+ }
86
+
87
+ // 5. Publish domain event (simulating WebSocket delivery)
88
+ await eventBus.publish('project:stakeholder:s1:complete_task.transitioned', {
89
+ instance_id: 'i-999',
90
+ from_state: 'in_progress',
91
+ to_state: 'done',
92
+ trigger: 'user',
93
+ });
94
+
95
+ // 6. Verify toast action was called
96
+ expect(toastHandler).toHaveBeenCalledOnce();
97
+ expect(toastHandler).toHaveBeenCalledWith(
98
+ { variant: 'success', message: 'Task completed!' },
99
+ expect.objectContaining({
100
+ current_state: 'active',
101
+ status: 'ACTIVE',
102
+ }),
103
+ );
104
+ });
105
+
106
+ it('full chain: domain event with conditions — action fires only when condition met', async () => {
107
+ const definition: PlayerWorkflowDefinition = {
108
+ id: 'vote-watcher',
109
+ slug: 'vote-experience',
110
+ states: [
111
+ {
112
+ name: 'watching',
113
+ type: 'START',
114
+ on_event: [
115
+ {
116
+ match: 'investment:*:*:vote.transitioned',
117
+ conditions: ['gte(vote_count, threshold)'],
118
+ actions: [
119
+ {
120
+ type: 'toast',
121
+ config: { variant: 'info', message: 'Funding threshold reached!' },
122
+ },
123
+ ],
124
+ },
125
+ ],
126
+ },
127
+ ],
128
+ transitions: [],
129
+ };
130
+
131
+ const sm = new StateMachine(definition, { vote_count: 5, threshold: 10 }, {
132
+ evaluator,
133
+ actionHandlers: new Map(),
134
+ });
135
+
136
+ const toastHandler = vi.fn();
137
+ dispatcher.register('toast', toastHandler);
138
+
139
+ // Wire subscriptions
140
+ const stateDef = sm.getCurrentStateDefinition()!;
141
+ for (const sub of stateDef.on_event!) {
142
+ eventBus.subscribe(sub.match, async (event) => {
143
+ const ctx: ExpressionContext = {
144
+ ...sm.stateData,
145
+ state_data: sm.stateData,
146
+ memory: sm.getSnapshot().memory,
147
+ current_state: sm.currentState,
148
+ status: sm.status,
149
+ event: event.payload,
150
+ };
151
+
152
+ // Check conditions
153
+ if (sub.conditions) {
154
+ for (const condition of sub.conditions) {
155
+ const result = evaluator.evaluate<boolean>(condition, ctx);
156
+ if (!result.value) return; // Skip
157
+ }
158
+ }
159
+
160
+ await dispatcher.execute(sub.actions, ctx, evaluator);
161
+ });
162
+ }
163
+
164
+ // Publish event — vote_count (5) < threshold (10), should NOT fire
165
+ await eventBus.publish('investment:stakeholder:s1:vote.transitioned', {});
166
+ expect(toastHandler).not.toHaveBeenCalled();
167
+
168
+ // Update vote_count past threshold
169
+ sm.setField('vote_count', 15);
170
+
171
+ // Publish again — now vote_count (15) >= threshold (10), should fire
172
+ await eventBus.publish('investment:stakeholder:s1:vote.transitioned', {});
173
+ expect(toastHandler).toHaveBeenCalledOnce();
174
+ });
175
+
176
+ it('full chain: multiple subscriptions, only matching patterns fire', async () => {
177
+ const definition: PlayerWorkflowDefinition = {
178
+ id: 'multi-sub',
179
+ slug: 'multi',
180
+ states: [
181
+ {
182
+ name: 'active',
183
+ type: 'START',
184
+ on_event: [
185
+ {
186
+ match: 'project:*:*:created.transitioned',
187
+ actions: [{ type: 'action_a', config: {} }],
188
+ },
189
+ {
190
+ match: 'task:*:*:completed.transitioned',
191
+ actions: [{ type: 'action_b', config: {} }],
192
+ },
193
+ ],
194
+ },
195
+ ],
196
+ transitions: [],
197
+ };
198
+
199
+ const sm = new StateMachine(definition, {}, { evaluator, actionHandlers: new Map() });
200
+ const handlerA = vi.fn();
201
+ const handlerB = vi.fn();
202
+ dispatcher.register('action_a', handlerA);
203
+ dispatcher.register('action_b', handlerB);
204
+
205
+ const stateDef = sm.getCurrentStateDefinition()!;
206
+ for (const sub of stateDef.on_event!) {
207
+ eventBus.subscribe(sub.match, async (event) => {
208
+ const ctx: ExpressionContext = {
209
+ ...sm.stateData, state_data: sm.stateData, memory: {},
210
+ current_state: sm.currentState, status: sm.status,
211
+ };
212
+ await dispatcher.execute(sub.actions, ctx, evaluator);
213
+ });
214
+ }
215
+
216
+ // Only task event should fire action_b
217
+ await eventBus.publish('task:s:1:completed.transitioned', {});
218
+ expect(handlerA).not.toHaveBeenCalled();
219
+ expect(handlerB).toHaveBeenCalledOnce();
220
+
221
+ // Only project event should fire action_a
222
+ await eventBus.publish('project:s:1:created.transitioned', {});
223
+ expect(handlerA).toHaveBeenCalledOnce();
224
+ });
225
+
226
+ it('full chain: auto-transition + on_enter actions', async () => {
227
+ const onEnterHandler = vi.fn();
228
+ const definition: PlayerWorkflowDefinition = {
229
+ id: 'auto-chain',
230
+ slug: 'auto',
231
+ states: [
232
+ { name: 'init', type: 'START' },
233
+ { name: 'processing', type: 'REGULAR', on_enter: [{ type: 'notify', config: { step: 'processing' } }] },
234
+ { name: 'done', type: 'END', on_enter: [{ type: 'notify', config: { step: 'done' } }] },
235
+ ],
236
+ transitions: [
237
+ { name: 'start', from: ['init'], to: 'processing' },
238
+ { name: 'auto_complete', from: ['processing'], to: 'done', auto: true },
239
+ ],
240
+ };
241
+
242
+ const sm = new StateMachine(definition, {}, {
243
+ evaluator,
244
+ actionHandlers: new Map([['notify', onEnterHandler]]),
245
+ });
246
+
247
+ // Should auto-chain: init → processing (on_enter fires) → done (on_enter fires)
248
+ const result = await sm.transition('start');
249
+ expect(result.success).toBe(true);
250
+ expect(sm.currentState).toBe('done');
251
+ expect(sm.status).toBe('COMPLETED');
252
+ expect(onEnterHandler).toHaveBeenCalledTimes(2);
253
+ expect(onEnterHandler.mock.calls[0][0]).toEqual({ type: 'notify', config: { step: 'processing' } });
254
+ expect(onEnterHandler.mock.calls[1][0]).toEqual({ type: 'notify', config: { step: 'done' } });
255
+ });
256
+ });
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Security Tests — Verify that the recursive descent parser
3
+ * blocks all known sandbox escape vectors.
4
+ *
5
+ * These tests confirm that the old new Function() + with() RCE
6
+ * vulnerabilities are no longer exploitable.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach } from 'vitest';
10
+ import {
11
+ createEvaluator,
12
+ WEB_FAILURE_POLICIES,
13
+ clearExpressionCache,
14
+ } from '../expression';
15
+ import type { Evaluator, ExpressionContext } from '../expression';
16
+
17
+ describe('Security: Expression Evaluator RCE Prevention', () => {
18
+ let evaluator: Evaluator;
19
+ let ctx: ExpressionContext;
20
+
21
+ beforeEach(() => {
22
+ clearExpressionCache();
23
+ evaluator = createEvaluator({
24
+ functions: [],
25
+ failurePolicy: WEB_FAILURE_POLICIES.VIEW_BINDING,
26
+ });
27
+ ctx = {
28
+ state_data: { title: 'Test' },
29
+ memory: {},
30
+ current_state: 'active',
31
+ status: 'ACTIVE',
32
+ };
33
+ });
34
+
35
+ describe('Prototype chain escape prevention', () => {
36
+ it('constructor.constructor cannot access global scope', () => {
37
+ // Old vulnerability: constructor.constructor("return this")() gave globalThis
38
+ const result = evaluator.evaluate('constructor.constructor("return this")()', ctx);
39
+ // Should NOT return global/window — should be undefined or error
40
+ expect(result.value).not.toBe(globalThis);
41
+ // In the safe parser, "constructor" is just an identifier lookup
42
+ // that resolves from context (undefined), so member access fails
43
+ });
44
+
45
+ it('__proto__ resolves from context but cannot execute code', () => {
46
+ const result = evaluator.evaluate('__proto__', ctx);
47
+ // __proto__ may resolve from the context object's prototype,
48
+ // but it's safe — there's no way to call constructors or execute code
49
+ expect(result.status).toBe('ok');
50
+ });
51
+
52
+ it('prototype chain traversal is blocked', () => {
53
+ const result = evaluator.evaluate('state_data.__proto__.constructor', ctx);
54
+ // The parser resolves this as member access, but __proto__ should
55
+ // NOT give access to Object constructor in a meaningful way
56
+ expect(result.status).toBe('ok');
57
+ // Even if it resolves, it cannot execute arbitrary code
58
+ });
59
+ });
60
+
61
+ describe('Code injection prevention', () => {
62
+ it('eval() is not available (returns undefined)', () => {
63
+ const result = evaluator.evaluate('eval("1+1")', ctx);
64
+ // eval is just an unknown function — returns undefined
65
+ expect(result.value).toBeUndefined();
66
+ });
67
+
68
+ it('Function() constructor is not available', () => {
69
+ // In old code: Function("return process")() could access Node globals
70
+ // Now: Function("return 42") is a function call on "Function" (an identifier)
71
+ // that resolves from context. The result of that call cannot be invoked
72
+ // because the parser sees Function(...)() as a double-call which fails
73
+ const result = evaluator.evaluate('Function("return 42")()', ctx);
74
+ // Hits "Cannot call non-function" → fallback ('' for VIEW_BINDING)
75
+ expect(result.status).toBe('fallback');
76
+ });
77
+
78
+ it('import() is not executable', () => {
79
+ // import("malicious-module") should fail to parse or be inert
80
+ const result = evaluator.evaluate('import("fs")', ctx);
81
+ expect(result.value).toBeUndefined();
82
+ });
83
+
84
+ it('require() is not available', () => {
85
+ const result = evaluator.evaluate('require("child_process")', ctx);
86
+ expect(result.value).toBeUndefined();
87
+ });
88
+
89
+ it('globalThis access returns undefined', () => {
90
+ const result = evaluator.evaluate('globalThis', ctx);
91
+ expect(result.value).toBeUndefined();
92
+ });
93
+
94
+ it('window access returns undefined', () => {
95
+ const result = evaluator.evaluate('window', ctx);
96
+ expect(result.value).toBeUndefined();
97
+ });
98
+
99
+ it('document access returns undefined', () => {
100
+ const result = evaluator.evaluate('document', ctx);
101
+ expect(result.value).toBeUndefined();
102
+ });
103
+
104
+ it('process access returns undefined', () => {
105
+ const result = evaluator.evaluate('process', ctx);
106
+ expect(result.value).toBeUndefined();
107
+ });
108
+ });
109
+
110
+ describe('No arbitrary JavaScript execution', () => {
111
+ it('assignment operators are not supported (parse error)', () => {
112
+ const result = evaluator.evaluate('x = 42', ctx);
113
+ // This should either fail to parse or return the identifier value
114
+ // It should NOT actually assign anything
115
+ expect(ctx['x']).toBeUndefined();
116
+ });
117
+
118
+ it('semicolons cannot chain statements', () => {
119
+ // Semicolons should cause a parse error
120
+ const result = evaluator.validate('a; b');
121
+ expect(result.valid).toBe(false);
122
+ });
123
+
124
+ it('template literals (backticks) are not supported', () => {
125
+ const result = evaluator.validate('`${process.env}`');
126
+ expect(result.valid).toBe(false);
127
+ });
128
+
129
+ it('arrow functions cannot be created', () => {
130
+ const result = evaluator.validate('() => 42');
131
+ expect(result.valid).toBe(false);
132
+ });
133
+
134
+ it('for/while/if statements cannot be injected', () => {
135
+ expect(evaluator.validate('for(;;){}').valid).toBe(false);
136
+ expect(evaluator.validate('while(1){}').valid).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe('Depth limit protection', () => {
141
+ it('deeply nested expressions are rejected', () => {
142
+ // Create an expression nested 60 levels deep
143
+ const nested = '(' .repeat(60) + '1' + ')'.repeat(60);
144
+ const result = evaluator.evaluate(nested, ctx);
145
+ // Should fail with depth limit
146
+ expect(result.status).toBe('fallback');
147
+ });
148
+ });
149
+
150
+ describe('Legitimate expressions still work', () => {
151
+ it('function calls work normally', () => {
152
+ expect(evaluator.evaluate('add(1, 2)', ctx).value).toBe(3);
153
+ expect(evaluator.evaluate('multiply(3, 4)', ctx).value).toBe(12);
154
+ });
155
+
156
+ it('comparisons work normally', () => {
157
+ expect(evaluator.evaluate('5 > 3', ctx).value).toBe(true);
158
+ expect(evaluator.evaluate('1 == 1', ctx).value).toBe(true);
159
+ });
160
+
161
+ it('ternary works normally', () => {
162
+ expect(evaluator.evaluate("true ? 'yes' : 'no'", ctx).value).toBe('yes');
163
+ });
164
+
165
+ it('nested function calls work', () => {
166
+ expect(evaluator.evaluate('add(multiply(2, 3), 4)', ctx).value).toBe(10);
167
+ });
168
+
169
+ it('logical operators work', () => {
170
+ expect(evaluator.evaluate('true && false', ctx).value).toBe(false);
171
+ expect(evaluator.evaluate('true || false', ctx).value).toBe(true);
172
+ expect(evaluator.evaluate('!false', ctx).value).toBe(true);
173
+ });
174
+
175
+ it('path resolution works', () => {
176
+ expect(evaluator.evaluate('state_data.title', ctx).value).toBe('Test');
177
+ expect(evaluator.evaluate('current_state', ctx).value).toBe('active');
178
+ });
179
+
180
+ it('string literals work', () => {
181
+ expect(evaluator.evaluate("'hello'", ctx).value).toBe('hello');
182
+ expect(evaluator.evaluate('"world"', ctx).value).toBe('world');
183
+ });
184
+
185
+ it('templates still work', () => {
186
+ const r = evaluator.evaluateTemplate('State: {{current_state}}', ctx);
187
+ expect(r.value).toBe('State: active');
188
+ });
189
+ });
190
+ });