@variojs/core 0.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.
Files changed (209) hide show
  1. package/__tests__/README.md +101 -0
  2. package/__tests__/errors.test.ts +139 -0
  3. package/__tests__/expression/cache.test.ts +118 -0
  4. package/__tests__/expression/compiler.test.ts +111 -0
  5. package/__tests__/expression/evaluate.test.ts +95 -0
  6. package/__tests__/expression/parser.test.ts +57 -0
  7. package/__tests__/expression/whitelist.test.ts +59 -0
  8. package/__tests__/performance.test.ts +379 -0
  9. package/__tests__/runtime/create-context.test.ts +78 -0
  10. package/__tests__/runtime/loop-context-pool.test.ts +74 -0
  11. package/__tests__/runtime/path.test.ts +128 -0
  12. package/__tests__/vm/executor-timeout.test.ts +117 -0
  13. package/__tests__/vm/executor.test.ts +173 -0
  14. package/__tests__/vm/handlers/array.test.ts +113 -0
  15. package/__tests__/vm/handlers/call.test.ts +93 -0
  16. package/dist/errors.d.ts +100 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +132 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/expression/cache.d.ts +53 -0
  21. package/dist/expression/cache.d.ts.map +1 -0
  22. package/dist/expression/cache.js +158 -0
  23. package/dist/expression/cache.js.map +1 -0
  24. package/dist/expression/compiler.d.ts +34 -0
  25. package/dist/expression/compiler.d.ts.map +1 -0
  26. package/dist/expression/compiler.js +123 -0
  27. package/dist/expression/compiler.js.map +1 -0
  28. package/dist/expression/dependencies.d.ts +17 -0
  29. package/dist/expression/dependencies.d.ts.map +1 -0
  30. package/dist/expression/dependencies.js +106 -0
  31. package/dist/expression/dependencies.js.map +1 -0
  32. package/dist/expression/evaluate.d.ts +22 -0
  33. package/dist/expression/evaluate.d.ts.map +1 -0
  34. package/dist/expression/evaluate.js +75 -0
  35. package/dist/expression/evaluate.js.map +1 -0
  36. package/dist/expression/evaluator.d.ts +22 -0
  37. package/dist/expression/evaluator.d.ts.map +1 -0
  38. package/dist/expression/evaluator.js +506 -0
  39. package/dist/expression/evaluator.js.map +1 -0
  40. package/dist/expression/index.d.ts +12 -0
  41. package/dist/expression/index.d.ts.map +1 -0
  42. package/dist/expression/index.js +12 -0
  43. package/dist/expression/index.js.map +1 -0
  44. package/dist/expression/parser.d.ts +15 -0
  45. package/dist/expression/parser.d.ts.map +1 -0
  46. package/dist/expression/parser.js +42 -0
  47. package/dist/expression/parser.js.map +1 -0
  48. package/dist/expression/utils.d.ts +46 -0
  49. package/dist/expression/utils.d.ts.map +1 -0
  50. package/dist/expression/utils.js +78 -0
  51. package/dist/expression/utils.js.map +1 -0
  52. package/dist/expression/whitelist.d.ts +24 -0
  53. package/dist/expression/whitelist.d.ts.map +1 -0
  54. package/dist/expression/whitelist.js +198 -0
  55. package/dist/expression/whitelist.js.map +1 -0
  56. package/dist/index.d.ts +19 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/runtime/context.d.ts +8 -0
  61. package/dist/runtime/context.d.ts.map +1 -0
  62. package/dist/runtime/context.js +7 -0
  63. package/dist/runtime/context.js.map +1 -0
  64. package/dist/runtime/create-context.d.ts +50 -0
  65. package/dist/runtime/create-context.d.ts.map +1 -0
  66. package/dist/runtime/create-context.js +73 -0
  67. package/dist/runtime/create-context.js.map +1 -0
  68. package/dist/runtime/index.d.ts +10 -0
  69. package/dist/runtime/index.d.ts.map +1 -0
  70. package/dist/runtime/index.js +10 -0
  71. package/dist/runtime/index.js.map +1 -0
  72. package/dist/runtime/loop-context-pool.d.ts +58 -0
  73. package/dist/runtime/loop-context-pool.d.ts.map +1 -0
  74. package/dist/runtime/loop-context-pool.js +114 -0
  75. package/dist/runtime/loop-context-pool.js.map +1 -0
  76. package/dist/runtime/path.d.ts +114 -0
  77. package/dist/runtime/path.d.ts.map +1 -0
  78. package/dist/runtime/path.js +302 -0
  79. package/dist/runtime/path.js.map +1 -0
  80. package/dist/runtime/proxy.d.ts +18 -0
  81. package/dist/runtime/proxy.d.ts.map +1 -0
  82. package/dist/runtime/proxy.js +53 -0
  83. package/dist/runtime/proxy.js.map +1 -0
  84. package/dist/runtime/sandbox.d.ts +20 -0
  85. package/dist/runtime/sandbox.d.ts.map +1 -0
  86. package/dist/runtime/sandbox.js +32 -0
  87. package/dist/runtime/sandbox.js.map +1 -0
  88. package/dist/types.d.ts +175 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +22 -0
  91. package/dist/types.js.map +1 -0
  92. package/dist/vm/action.d.ts +11 -0
  93. package/dist/vm/action.d.ts.map +1 -0
  94. package/dist/vm/action.js +10 -0
  95. package/dist/vm/action.js.map +1 -0
  96. package/dist/vm/errors.d.ts +5 -0
  97. package/dist/vm/errors.d.ts.map +1 -0
  98. package/dist/vm/errors.js +5 -0
  99. package/dist/vm/errors.js.map +1 -0
  100. package/dist/vm/executor.d.ts +35 -0
  101. package/dist/vm/executor.d.ts.map +1 -0
  102. package/dist/vm/executor.js +137 -0
  103. package/dist/vm/executor.js.map +1 -0
  104. package/dist/vm/handlers/array/pop.d.ts +12 -0
  105. package/dist/vm/handlers/array/pop.d.ts.map +1 -0
  106. package/dist/vm/handlers/array/pop.js +28 -0
  107. package/dist/vm/handlers/array/pop.js.map +1 -0
  108. package/dist/vm/handlers/array/push.d.ts +13 -0
  109. package/dist/vm/handlers/array/push.d.ts.map +1 -0
  110. package/dist/vm/handlers/array/push.js +42 -0
  111. package/dist/vm/handlers/array/push.js.map +1 -0
  112. package/dist/vm/handlers/array/shift.d.ts +12 -0
  113. package/dist/vm/handlers/array/shift.d.ts.map +1 -0
  114. package/dist/vm/handlers/array/shift.js +28 -0
  115. package/dist/vm/handlers/array/shift.js.map +1 -0
  116. package/dist/vm/handlers/array/splice.d.ts +12 -0
  117. package/dist/vm/handlers/array/splice.d.ts.map +1 -0
  118. package/dist/vm/handlers/array/splice.js +59 -0
  119. package/dist/vm/handlers/array/splice.js.map +1 -0
  120. package/dist/vm/handlers/array/unshift.d.ts +13 -0
  121. package/dist/vm/handlers/array/unshift.d.ts.map +1 -0
  122. package/dist/vm/handlers/array/unshift.js +42 -0
  123. package/dist/vm/handlers/array/unshift.js.map +1 -0
  124. package/dist/vm/handlers/array/utils.d.ts +10 -0
  125. package/dist/vm/handlers/array/utils.d.ts.map +1 -0
  126. package/dist/vm/handlers/array/utils.js +33 -0
  127. package/dist/vm/handlers/array/utils.js.map +1 -0
  128. package/dist/vm/handlers/batch.d.ts +12 -0
  129. package/dist/vm/handlers/batch.d.ts.map +1 -0
  130. package/dist/vm/handlers/batch.js +40 -0
  131. package/dist/vm/handlers/batch.js.map +1 -0
  132. package/dist/vm/handlers/call.d.ts +14 -0
  133. package/dist/vm/handlers/call.d.ts.map +1 -0
  134. package/dist/vm/handlers/call.js +65 -0
  135. package/dist/vm/handlers/call.js.map +1 -0
  136. package/dist/vm/handlers/emit.d.ts +12 -0
  137. package/dist/vm/handlers/emit.d.ts.map +1 -0
  138. package/dist/vm/handlers/emit.js +26 -0
  139. package/dist/vm/handlers/emit.js.map +1 -0
  140. package/dist/vm/handlers/if.d.ts +13 -0
  141. package/dist/vm/handlers/if.d.ts.map +1 -0
  142. package/dist/vm/handlers/if.js +35 -0
  143. package/dist/vm/handlers/if.js.map +1 -0
  144. package/dist/vm/handlers/index.d.ts +11 -0
  145. package/dist/vm/handlers/index.d.ts.map +1 -0
  146. package/dist/vm/handlers/index.js +46 -0
  147. package/dist/vm/handlers/index.js.map +1 -0
  148. package/dist/vm/handlers/log.d.ts +12 -0
  149. package/dist/vm/handlers/log.d.ts.map +1 -0
  150. package/dist/vm/handlers/log.js +41 -0
  151. package/dist/vm/handlers/log.js.map +1 -0
  152. package/dist/vm/handlers/loop.d.ts +12 -0
  153. package/dist/vm/handlers/loop.d.ts.map +1 -0
  154. package/dist/vm/handlers/loop.js +71 -0
  155. package/dist/vm/handlers/loop.js.map +1 -0
  156. package/dist/vm/handlers/navigate.d.ts +12 -0
  157. package/dist/vm/handlers/navigate.d.ts.map +1 -0
  158. package/dist/vm/handlers/navigate.js +43 -0
  159. package/dist/vm/handlers/navigate.js.map +1 -0
  160. package/dist/vm/handlers/set.d.ts +15 -0
  161. package/dist/vm/handlers/set.d.ts.map +1 -0
  162. package/dist/vm/handlers/set.js +30 -0
  163. package/dist/vm/handlers/set.js.map +1 -0
  164. package/dist/vm/index.d.ts +8 -0
  165. package/dist/vm/index.d.ts.map +1 -0
  166. package/dist/vm/index.js +7 -0
  167. package/dist/vm/index.js.map +1 -0
  168. package/package.json +34 -0
  169. package/src/errors.ts +194 -0
  170. package/src/expression/README.md +192 -0
  171. package/src/expression/cache.ts +199 -0
  172. package/src/expression/compiler.ts +144 -0
  173. package/src/expression/dependencies.ts +116 -0
  174. package/src/expression/evaluate.ts +95 -0
  175. package/src/expression/evaluator.ts +640 -0
  176. package/src/expression/index.ts +27 -0
  177. package/src/expression/parser.ts +54 -0
  178. package/src/expression/utils.ts +89 -0
  179. package/src/expression/whitelist.ts +224 -0
  180. package/src/globals.d.ts +10 -0
  181. package/src/index.ts +72 -0
  182. package/src/runtime/context.ts +8 -0
  183. package/src/runtime/create-context.ts +133 -0
  184. package/src/runtime/index.ts +28 -0
  185. package/src/runtime/loop-context-pool.ts +134 -0
  186. package/src/runtime/path.ts +372 -0
  187. package/src/runtime/proxy.ts +66 -0
  188. package/src/runtime/sandbox.ts +43 -0
  189. package/src/types.ts +177 -0
  190. package/src/vm/errors.ts +10 -0
  191. package/src/vm/executor.ts +210 -0
  192. package/src/vm/handlers/array/pop.ts +47 -0
  193. package/src/vm/handlers/array/push.ts +68 -0
  194. package/src/vm/handlers/array/shift.ts +47 -0
  195. package/src/vm/handlers/array/splice.ts +78 -0
  196. package/src/vm/handlers/array/unshift.ts +68 -0
  197. package/src/vm/handlers/array/utils.ts +41 -0
  198. package/src/vm/handlers/batch.ts +57 -0
  199. package/src/vm/handlers/call.ts +92 -0
  200. package/src/vm/handlers/emit.ts +39 -0
  201. package/src/vm/handlers/if.ts +48 -0
  202. package/src/vm/handlers/index.ts +52 -0
  203. package/src/vm/handlers/log.ts +54 -0
  204. package/src/vm/handlers/loop.ts +102 -0
  205. package/src/vm/handlers/navigate.ts +64 -0
  206. package/src/vm/handlers/set.ts +43 -0
  207. package/src/vm/index.ts +8 -0
  208. package/tsconfig.json +17 -0
  209. package/vitest.config.ts +30 -0
@@ -0,0 +1,101 @@
1
+ # Vario Core 测试说明
2
+
3
+ ## 快速开始
4
+
5
+ ```bash
6
+ # 运行所有测试
7
+ pnpm test
8
+
9
+ # 监听模式
10
+ pnpm test:watch
11
+
12
+ # 生成覆盖率
13
+ pnpm test -- --coverage
14
+ ```
15
+
16
+ ## 测试结构
17
+
18
+ ```
19
+ __tests__/
20
+ ├── runtime/ # RuntimeContext 相关测试
21
+ │ └── create-context.test.ts
22
+ ├── expression/ # 表达式系统测试
23
+ │ ├── evaluate.test.ts # 表达式求值
24
+ │ ├── parser.test.ts # 表达式解析
25
+ │ ├── whitelist.test.ts # AST 白名单验证
26
+ │ └── cache.test.ts # 表达式缓存
27
+ └── vm/ # Instruction VM 测试
28
+ ├── executor.test.ts # 指令执行器
29
+ └── handlers/
30
+ └── call.test.ts # call 指令处理器
31
+ ```
32
+
33
+ ## 测试编写要点
34
+
35
+ ### 1. 测试命名
36
+ - 使用中文描述测试行为(符合项目规范)
37
+ - 格式:`应该 + 行为描述`
38
+
39
+ ### 2. 测试组织
40
+ - 使用 `describe` 组织相关测试
41
+ - 使用 `beforeEach` 共享测试设置
42
+ - 每个测试独立,不依赖其他测试
43
+
44
+ ### 3. 常见测试模式
45
+
46
+ #### 正常流程测试
47
+ ```typescript
48
+ it('应该执行 set 指令', async () => {
49
+ await execute([{ op: 'set', path: 'count', value: 10 }], ctx)
50
+ expect(ctx.count).toBe(10)
51
+ })
52
+ ```
53
+
54
+ #### 错误处理测试
55
+ ```typescript
56
+ it('应该在未知指令时抛出错误', async () => {
57
+ await expect(
58
+ execute([{ op: 'unknown' }], ctx)
59
+ ).rejects.toThrow('Unknown op')
60
+ })
61
+ ```
62
+
63
+ #### 边界条件测试
64
+ ```typescript
65
+ it('应该处理空数组', () => {
66
+ const result = evaluate('items.length', ctx)
67
+ expect(result).toBe(0)
68
+ })
69
+ ```
70
+
71
+ ## 运行特定测试
72
+
73
+ ```bash
74
+ # 运行单个文件
75
+ pnpm test __tests__/runtime/create-context.test.ts
76
+
77
+ # 运行匹配的测试
78
+ pnpm test -- -t "应该创建基本的运行时上下文"
79
+
80
+ # 详细输出
81
+ pnpm test -- --reporter=verbose
82
+ ```
83
+
84
+ ## 调试测试
85
+
86
+ 在测试中使用 `console.log` 或断点调试:
87
+
88
+ ```typescript
89
+ it('调试测试', () => {
90
+ console.log('调试信息:', ctx)
91
+ debugger // 在支持的环境中会暂停
92
+ expect(ctx.user).toBeDefined()
93
+ })
94
+ ```
95
+
96
+ ## 注意事项
97
+
98
+ 1. **测试隔离**: 每个测试应该独立运行,不依赖执行顺序
99
+ 2. **清理状态**: 使用 `beforeEach` 重置状态,避免测试间污染
100
+ 3. **异步测试**: 使用 `async/await` 处理异步操作
101
+ 4. **类型安全**: 测试代码也应该遵循 TypeScript 类型检查
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 错误处理体系测试
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import {
7
+ VarioError,
8
+ ActionError,
9
+ ExpressionError,
10
+ ServiceError,
11
+ BatchError,
12
+ ErrorCodes
13
+ } from '../src/errors.js'
14
+ import type { Action } from '../src/types.js'
15
+
16
+ describe('错误处理体系', () => {
17
+ describe('VarioError', () => {
18
+ it('应该创建基础错误', () => {
19
+ const error = new VarioError('Test error', 'TEST_ERROR', {
20
+ metadata: { key: 'value' }
21
+ })
22
+
23
+ expect(error.message).toBe('Test error')
24
+ expect(error.code).toBe('TEST_ERROR')
25
+ expect(error.context.metadata).toEqual({ key: 'value' })
26
+ expect(error.name).toBe('VarioError')
27
+ })
28
+
29
+ it('应该生成友好的错误消息', () => {
30
+ const error = new VarioError('Test error', 'TEST_ERROR', {
31
+ schemaPath: 'events.click[0]',
32
+ expression: 'user.name',
33
+ action: { type: 'set' } as Action
34
+ })
35
+
36
+ const friendly = error.getFriendlyMessage()
37
+ expect(friendly).toContain('Test error')
38
+ expect(friendly).toContain('Schema 路径: events.click[0]')
39
+ expect(friendly).toContain('表达式: user.name')
40
+ expect(friendly).toContain('动作类型: set')
41
+ })
42
+
43
+ it('应该转换为 JSON', () => {
44
+ const error = new VarioError('Test error', 'TEST_ERROR', {
45
+ metadata: { key: 'value' }
46
+ })
47
+
48
+ const json = error.toJSON()
49
+ expect(json.name).toBe('VarioError')
50
+ expect(json.message).toBe('Test error')
51
+ expect(json.code).toBe('TEST_ERROR')
52
+ expect(json.context).toBeDefined()
53
+ })
54
+ })
55
+
56
+ describe('ActionError', () => {
57
+ it('应该创建动作错误', () => {
58
+ const action: Action = { type: 'set', path: 'count' }
59
+ const error = new ActionError(
60
+ action,
61
+ 'Action failed',
62
+ ErrorCodes.ACTION_EXECUTION_ERROR,
63
+ { metadata: { param: 'path' } }
64
+ )
65
+
66
+ expect(error.message).toBe('Action failed')
67
+ expect(error.code).toBe(ErrorCodes.ACTION_EXECUTION_ERROR)
68
+ expect(error.context.action).toBe(action)
69
+ expect(error.context.metadata).toEqual({ param: 'path' })
70
+ })
71
+ })
72
+
73
+ describe('ExpressionError', () => {
74
+ it('应该创建表达式错误', () => {
75
+ const error = new ExpressionError(
76
+ 'user.name',
77
+ 'Expression failed',
78
+ ErrorCodes.EXPRESSION_EVALUATION_ERROR,
79
+ { metadata: { nodeType: 'Identifier' } }
80
+ )
81
+
82
+ expect(error.message).toBe('Expression failed')
83
+ expect(error.code).toBe(ErrorCodes.EXPRESSION_EVALUATION_ERROR)
84
+ expect(error.context.expression).toBe('user.name')
85
+ expect(error.context.metadata).toEqual({ nodeType: 'Identifier' })
86
+ })
87
+ })
88
+
89
+ describe('ServiceError', () => {
90
+ it('应该创建服务错误', () => {
91
+ const originalError = new Error('Original error')
92
+ const error = new ServiceError(
93
+ 'test.service',
94
+ 'Service call failed',
95
+ originalError,
96
+ { metadata: { method: 'test.service' } }
97
+ )
98
+
99
+ expect(error.service).toBe('test.service')
100
+ expect(error.message).toBe('Service call failed')
101
+ expect(error.originalError).toBe(originalError)
102
+ expect(error.context.metadata).toEqual({ method: 'test.service' })
103
+ })
104
+ })
105
+
106
+ describe('BatchError', () => {
107
+ it('应该创建批量错误', () => {
108
+ const action1: Action = { type: 'set', path: 'a' }
109
+ const action2: Action = { type: 'set', path: 'b' }
110
+ const error1 = new Error('Error 1')
111
+ const error2 = new Error('Error 2')
112
+
113
+ const error = new BatchError(
114
+ [
115
+ { action: action1, error: error1 },
116
+ { action: action2, error: error2 }
117
+ ],
118
+ 'Batch failed',
119
+ { metadata: { failedCount: 2 } }
120
+ )
121
+
122
+ expect(error.message).toBe('Batch failed')
123
+ expect(error.failedActions).toHaveLength(2)
124
+ expect(error.failedActions[0].action).toBe(action1)
125
+ expect(error.failedActions[1].action).toBe(action2)
126
+ })
127
+ })
128
+
129
+ describe('ErrorCodes', () => {
130
+ it('应该包含所有错误码', () => {
131
+ expect(ErrorCodes.ACTION_UNKNOWN_TYPE).toBe('ACTION_UNKNOWN_TYPE')
132
+ expect(ErrorCodes.ACTION_EXECUTION_ERROR).toBe('ACTION_EXECUTION_ERROR')
133
+ expect(ErrorCodes.EXPRESSION_PARSE_ERROR).toBe('EXPRESSION_PARSE_ERROR')
134
+ expect(ErrorCodes.EXPRESSION_VALIDATION_ERROR).toBe('EXPRESSION_VALIDATION_ERROR')
135
+ expect(ErrorCodes.SERVICE_NOT_FOUND).toBe('SERVICE_NOT_FOUND')
136
+ expect(ErrorCodes.BATCH_ERROR).toBe('BATCH_ERROR')
137
+ })
138
+ })
139
+ })
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 表达式缓存系统测试
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest'
6
+ import { createRuntimeContext } from '../../src/runtime/create-context'
7
+ import { evaluate } from '../../src/expression/evaluate'
8
+ import {
9
+ getCachedExpression,
10
+ setCachedExpression,
11
+ invalidateCache,
12
+ clearCache
13
+ } from '../../src/expression/cache'
14
+ import { registerBuiltinMethods } from '../../src/vm/handlers'
15
+
16
+ describe('表达式缓存系统', () => {
17
+ let ctx: ReturnType<typeof createRuntimeContext>
18
+
19
+ beforeEach(() => {
20
+ ctx = createRuntimeContext({
21
+ user: { name: 'John', age: 30 },
22
+ items: [1, 2, 3],
23
+ count: 0
24
+ })
25
+ registerBuiltinMethods(ctx)
26
+ })
27
+
28
+ it('应该缓存表达式结果', () => {
29
+ const expr = 'user.age + 10'
30
+ const result = evaluate(expr, ctx)
31
+
32
+ // 第二次求值应该使用缓存
33
+ const cached = getCachedExpression(expr, ctx)
34
+ expect(cached).toBe(result)
35
+ expect(cached).toBe(40)
36
+ })
37
+
38
+ it('应该手动设置缓存', () => {
39
+ const expr = 'test.expression'
40
+ const result = 123
41
+ const dependencies = ['user.name']
42
+
43
+ setCachedExpression(expr, result, dependencies, ctx)
44
+
45
+ const cached = getCachedExpression(expr, ctx)
46
+ expect(cached).toBe(123)
47
+ })
48
+
49
+ it('应该在依赖变化时使缓存失效', () => {
50
+ const expr = 'user.name'
51
+ const result1 = evaluate(expr, ctx)
52
+ expect(result1).toBe('John')
53
+
54
+ // 验证缓存存在
55
+ const cached1 = getCachedExpression(expr, ctx)
56
+ expect(cached1).toBe('John')
57
+
58
+ // 修改依赖的状态
59
+ ctx._set('user.name', 'Jane')
60
+ invalidateCache('user.name', ctx)
61
+
62
+ // 缓存应该失效
63
+ const cached2 = getCachedExpression(expr, ctx)
64
+ expect(cached2).toBeNull()
65
+ })
66
+
67
+ it('应该支持通配符依赖失效', () => {
68
+ const expr = 'items[0]'
69
+ evaluate(expr, ctx) // 创建缓存,依赖 items.*
70
+
71
+ // 验证缓存存在
72
+ const cached1 = getCachedExpression(expr, ctx)
73
+ expect(cached1).toBe(1)
74
+
75
+ // 修改数组元素
76
+ ctx._set('items.0', 999)
77
+ invalidateCache('items.0', ctx)
78
+
79
+ // 缓存应该失效
80
+ const cached2 = getCachedExpression(expr, ctx)
81
+ expect(cached2).toBeNull()
82
+ })
83
+
84
+ it('应该清除所有缓存', () => {
85
+ const expr1 = 'user.name'
86
+ const expr2 = 'user.age'
87
+
88
+ evaluate(expr1, ctx)
89
+ evaluate(expr2, ctx)
90
+
91
+ // 验证缓存存在
92
+ expect(getCachedExpression(expr1, ctx)).not.toBeNull()
93
+ expect(getCachedExpression(expr2, ctx)).not.toBeNull()
94
+
95
+ // 清除所有缓存
96
+ clearCache(ctx)
97
+
98
+ // 缓存应该都被清除
99
+ expect(getCachedExpression(expr1, ctx)).toBeNull()
100
+ expect(getCachedExpression(expr2, ctx)).toBeNull()
101
+ })
102
+
103
+ it('应该在缓存已满时使用 LRU 淘汰', () => {
104
+ // 创建超过 MAX_CACHE_SIZE (100) 的缓存
105
+ for (let i = 0; i < 110; i++) {
106
+ const expr = `test.${i}`
107
+ setCachedExpression(expr, i, [], ctx)
108
+ }
109
+
110
+ // 最旧的缓存应该被淘汰
111
+ const oldest = getCachedExpression('test.0', ctx)
112
+ expect(oldest).toBeNull()
113
+
114
+ // 最新的缓存应该存在
115
+ const newest = getCachedExpression('test.109', ctx)
116
+ expect(newest).toBe(109)
117
+ })
118
+ })
@@ -0,0 +1,111 @@
1
+ /**
2
+ * 表达式编译器测试
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { parseExpression } from '../../src/expression/parser.js'
7
+ import {
8
+ compileSimpleExpression,
9
+ getCompiledExpression,
10
+ clearCompiledCache
11
+ } from '../../src/expression/compiler.js'
12
+ import { createRuntimeContext } from '../../src/runtime/create-context.js'
13
+
14
+ describe('表达式编译器', () => {
15
+ beforeEach(() => {
16
+ clearCompiledCache()
17
+ })
18
+
19
+ describe('compileSimpleExpression', () => {
20
+ it('应该编译字面量表达式', () => {
21
+ const ast = parseExpression('42')
22
+ const compiled = compileSimpleExpression(ast)
23
+
24
+ expect(compiled).not.toBeNull()
25
+ const ctx = createRuntimeContext()
26
+ expect(compiled!(ctx)).toBe(42)
27
+ })
28
+
29
+ it('应该编译字符串字面量', () => {
30
+ const ast = parseExpression('"hello"')
31
+ const compiled = compileSimpleExpression(ast)
32
+
33
+ expect(compiled).not.toBeNull()
34
+ const ctx = createRuntimeContext()
35
+ expect(compiled!(ctx)).toBe('hello')
36
+ })
37
+
38
+ it('应该编译标识符表达式', () => {
39
+ const ast = parseExpression('user')
40
+ const compiled = compileSimpleExpression(ast)
41
+
42
+ expect(compiled).not.toBeNull()
43
+ const ctx = createRuntimeContext({ user: { name: 'John' } })
44
+ expect(compiled!(ctx)).toEqual({ name: 'John' })
45
+ })
46
+
47
+ it('应该编译静态成员访问', () => {
48
+ const ast = parseExpression('user.name')
49
+ const compiled = compileSimpleExpression(ast)
50
+
51
+ expect(compiled).not.toBeNull()
52
+ const ctx = createRuntimeContext({ user: { name: 'John' } })
53
+ expect(compiled!(ctx)).toBe('John')
54
+ })
55
+
56
+ it('应该编译嵌套成员访问', () => {
57
+ const ast = parseExpression('user.profile.avatar')
58
+ const compiled = compileSimpleExpression(ast)
59
+
60
+ expect(compiled).not.toBeNull()
61
+ const ctx = createRuntimeContext({
62
+ user: { profile: { avatar: 'url' } }
63
+ })
64
+ expect(compiled!(ctx)).toBe('url')
65
+ })
66
+
67
+ it('应该编译数组索引访问', () => {
68
+ // 使用方括号语法,因为 items.0 在 JavaScript 中不是有效的属性访问
69
+ const ast = parseExpression('items[0]')
70
+ const compiled = compileSimpleExpression(ast)
71
+
72
+ expect(compiled).not.toBeNull()
73
+ const ctx = createRuntimeContext({ items: ['a', 'b', 'c'] })
74
+ expect(compiled!(ctx)).toBe('a')
75
+ })
76
+
77
+ it('应该拒绝编译复杂表达式', () => {
78
+ const ast = parseExpression('user.name + 1')
79
+ const compiled = compileSimpleExpression(ast)
80
+
81
+ expect(compiled).toBeNull()
82
+ })
83
+
84
+ it('应该拒绝编译函数调用', () => {
85
+ const ast = parseExpression('Math.max(1, 2)')
86
+ const compiled = compileSimpleExpression(ast)
87
+
88
+ expect(compiled).toBeNull()
89
+ })
90
+ })
91
+
92
+ describe('getCompiledExpression', () => {
93
+ it('应该缓存编译结果', () => {
94
+ const expr = 'user.name'
95
+ const ast = parseExpression(expr)
96
+
97
+ const compiled1 = getCompiledExpression(expr, ast)
98
+ const compiled2 = getCompiledExpression(expr, ast)
99
+
100
+ expect(compiled1).toBe(compiled2)
101
+ })
102
+
103
+ it('应该为无法编译的表达式返回 null', () => {
104
+ const expr = 'user.name + 1'
105
+ const ast = parseExpression(expr)
106
+
107
+ const compiled = getCompiledExpression(expr, ast)
108
+ expect(compiled).toBeNull()
109
+ })
110
+ })
111
+ })
@@ -0,0 +1,95 @@
1
+ /**
2
+ * 表达式求值测试
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest'
6
+ import { createRuntimeContext } from '../../src/runtime/create-context'
7
+ import { evaluate } from '../../src/expression/evaluate'
8
+ import { registerBuiltinMethods } from '../../src/vm/handlers'
9
+
10
+ describe('表达式求值', () => {
11
+ let ctx: ReturnType<typeof createRuntimeContext>
12
+
13
+ beforeEach(() => {
14
+ ctx = createRuntimeContext({
15
+ user: { name: 'John', age: 30 },
16
+ items: [1, 2, 3],
17
+ count: 0
18
+ })
19
+ registerBuiltinMethods(ctx)
20
+ })
21
+
22
+ it('应该求值字面量', () => {
23
+ expect(evaluate('123', ctx)).toBe(123)
24
+ expect(evaluate('"hello"', ctx)).toBe('hello')
25
+ expect(evaluate('true', ctx)).toBe(true)
26
+ })
27
+
28
+ it('应该求值标识符(直接属性访问)', () => {
29
+ expect(evaluate('user.name', ctx)).toBe('John')
30
+ expect(evaluate('user.age', ctx)).toBe(30)
31
+ expect(evaluate('count', ctx)).toBe(0)
32
+ })
33
+
34
+ it('应该求值成员访问', () => {
35
+ expect(evaluate('user.name', ctx)).toBe('John')
36
+ expect(evaluate('items[0]', ctx)).toBe(1)
37
+ })
38
+
39
+ it('应该求值二元运算', () => {
40
+ expect(evaluate('1 + 2', ctx)).toBe(3)
41
+ expect(evaluate('user.age > 18', ctx)).toBe(true)
42
+ expect(evaluate('user.age === 30', ctx)).toBe(true)
43
+ })
44
+
45
+ it('应该求值逻辑运算', () => {
46
+ expect(evaluate('true && false', ctx)).toBe(false)
47
+ expect(evaluate('true || false', ctx)).toBe(true)
48
+ expect(evaluate('user.age > 18 && user.name === "John"', ctx)).toBe(true)
49
+ })
50
+
51
+ it('应该求值三元表达式', () => {
52
+ expect(evaluate('user.age > 18 ? "adult" : "minor"', ctx)).toBe('adult')
53
+ })
54
+
55
+ it('应该求值空值合并', () => {
56
+ expect(evaluate('user.name ?? "unknown"', ctx)).toBe('John')
57
+ expect(evaluate('user.email ?? "no-email"', ctx)).toBe('no-email')
58
+ })
59
+
60
+ it('应该求值函数调用(白名单)', () => {
61
+ expect(evaluate('Math.max(1, 2, 3)', ctx)).toBe(3)
62
+ expect(evaluate('Array.isArray(items)', ctx)).toBe(true)
63
+ expect(evaluate('String(user.age)', ctx)).toBe('30')
64
+ })
65
+
66
+ it('应该拒绝禁止的语法', () => {
67
+ expect(() => evaluate('user.name = "Jane"', ctx)).toThrow() // 赋值
68
+ expect(() => evaluate('user.age++', ctx)).toThrow() // 自增
69
+ expect(() => evaluate('() => {}', ctx)).toThrow() // 箭头函数
70
+ })
71
+
72
+ it('应该拒绝访问全局对象', () => {
73
+ expect(() => evaluate('window', ctx)).toThrow()
74
+ expect(() => evaluate('document', ctx)).toThrow()
75
+ })
76
+
77
+ it('默认应拒绝未白名单的全局函数调用', () => {
78
+ ;(globalThis as any).__testFn = () => 42
79
+ expect(() => evaluate('__testFn()', ctx)).toThrow()
80
+ delete (globalThis as any).__testFn
81
+ })
82
+
83
+ it('开启 allowGlobals 时应允许任意全局函数调用', () => {
84
+ ;(globalThis as any).__testFn = () => 42
85
+ expect(evaluate('__testFn()', ctx, { allowGlobals: true })).toBe(42)
86
+ delete (globalThis as any).__testFn
87
+ })
88
+
89
+ it('开启 allowGlobals 时允许访问全局对象属性', () => {
90
+ ;(globalThis as any).__testVal = 7
91
+ expect(() => evaluate('globalThis.__testVal', ctx)).toThrow()
92
+ expect(evaluate('globalThis.__testVal', ctx, { allowGlobals: true })).toBe(7)
93
+ delete (globalThis as any).__testVal
94
+ })
95
+ })
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 表达式解析器测试
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { parseExpression } from '../../src/expression/parser'
7
+ import { ExpressionError } from '../../src/types'
8
+
9
+ describe('表达式解析器', () => {
10
+ it('应该解析简单表达式', () => {
11
+ const ast = parseExpression('1 + 2')
12
+ expect(ast).toBeDefined()
13
+ expect(ast.type).toBe('BinaryExpression')
14
+ })
15
+
16
+ it('应该解析成员访问表达式', () => {
17
+ const ast = parseExpression('user.name')
18
+ expect(ast).toBeDefined()
19
+ expect(ast.type).toBe('MemberExpression')
20
+ })
21
+
22
+ it('应该解析函数调用表达式', () => {
23
+ const ast = parseExpression('Math.max(1, 2, 3)')
24
+ expect(ast).toBeDefined()
25
+ expect(ast.type).toBe('CallExpression')
26
+ })
27
+
28
+ it('应该解析三元表达式', () => {
29
+ const ast = parseExpression('age > 18 ? "adult" : "minor"')
30
+ expect(ast).toBeDefined()
31
+ expect(ast.type).toBe('ConditionalExpression')
32
+ })
33
+
34
+ it('应该在解析失败时抛出错误', () => {
35
+ expect(() => {
36
+ parseExpression('invalid syntax {')
37
+ }).toThrow()
38
+ })
39
+
40
+ it('应该解析逻辑表达式', () => {
41
+ const ast = parseExpression('a && b || c')
42
+ expect(ast).toBeDefined()
43
+ expect(ast.type).toBe('LogicalExpression')
44
+ })
45
+
46
+ it('应该解析数组表达式', () => {
47
+ const ast = parseExpression('[1, 2, 3]')
48
+ expect(ast).toBeDefined()
49
+ expect(ast.type).toBe('ArrayExpression')
50
+ })
51
+
52
+ it('应该解析对象表达式', () => {
53
+ const ast = parseExpression('{ a: 1, b: 2 }')
54
+ expect(ast).toBeDefined()
55
+ expect(ast.type).toBe('ObjectExpression')
56
+ })
57
+ })
@@ -0,0 +1,59 @@
1
+ /**
2
+ * AST 白名单验证器测试
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { parseExpression } from '../../src/expression/parser'
7
+ import { validateAST } from '../../src/expression/whitelist'
8
+ import { ExpressionError } from '../../src/types'
9
+
10
+ describe('AST 白名单验证', () => {
11
+ it('应该允许安全的表达式', () => {
12
+ const safeExpressions = [
13
+ '1 + 2',
14
+ 'user.name',
15
+ 'items[0]',
16
+ 'age > 18',
17
+ 'user.name ?? "unknown"',
18
+ 'Math.max(1, 2)',
19
+ '[1, 2, 3]',
20
+ '{ a: 1, b: 2 }'
21
+ ]
22
+
23
+ safeExpressions.forEach(expr => {
24
+ const ast = parseExpression(expr)
25
+ expect(() => validateAST(ast)).not.toThrow()
26
+ })
27
+ })
28
+
29
+ it('应该拒绝赋值表达式', () => {
30
+ const ast = parseExpression('user.name = "Jane"')
31
+ expect(() => validateAST(ast)).toThrow(ExpressionError)
32
+ })
33
+
34
+ it('应该拒绝自增/自减表达式', () => {
35
+ const ast = parseExpression('count++')
36
+ expect(() => validateAST(ast)).toThrow(ExpressionError)
37
+ })
38
+
39
+ it('应该拒绝箭头函数', () => {
40
+ const ast = parseExpression('() => {}')
41
+ expect(() => validateAST(ast)).toThrow(ExpressionError)
42
+ })
43
+
44
+ it('应该拒绝函数表达式', () => {
45
+ const ast = parseExpression('function() {}')
46
+ expect(() => validateAST(ast)).toThrow(ExpressionError)
47
+ })
48
+
49
+ it('应该拒绝 new 表达式', () => {
50
+ const ast = parseExpression('new Date()')
51
+ expect(() => validateAST(ast)).toThrow(ExpressionError)
52
+ })
53
+
54
+ it('应该拒绝 this 表达式', () => {
55
+ // this 在表达式上下文中可能无法解析,但如果有,应该被拒绝
56
+ // 注意:实际测试中,this 可能无法作为独立表达式解析
57
+ // 这里主要验证白名单机制工作正常
58
+ })
59
+ })