@vibe-lang/runtime 0.2.5

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 (250) hide show
  1. package/package.json +46 -0
  2. package/src/ast/index.ts +375 -0
  3. package/src/ast.ts +2 -0
  4. package/src/debug/advanced-features.ts +482 -0
  5. package/src/debug/bun-inspector.ts +424 -0
  6. package/src/debug/handoff-manager.ts +283 -0
  7. package/src/debug/index.ts +150 -0
  8. package/src/debug/runner.ts +365 -0
  9. package/src/debug/server.ts +565 -0
  10. package/src/debug/stack-merger.ts +267 -0
  11. package/src/debug/state.ts +581 -0
  12. package/src/debug/test/advanced-features.test.ts +300 -0
  13. package/src/debug/test/e2e.test.ts +218 -0
  14. package/src/debug/test/handoff-manager.test.ts +256 -0
  15. package/src/debug/test/runner.test.ts +256 -0
  16. package/src/debug/test/stack-merger.test.ts +163 -0
  17. package/src/debug/test/state.test.ts +400 -0
  18. package/src/debug/test/ts-debug-integration.test.ts +374 -0
  19. package/src/debug/test/ts-import-tracker.test.ts +125 -0
  20. package/src/debug/test/ts-source-map.test.ts +169 -0
  21. package/src/debug/ts-import-tracker.ts +151 -0
  22. package/src/debug/ts-source-map.ts +171 -0
  23. package/src/errors/index.ts +124 -0
  24. package/src/index.ts +358 -0
  25. package/src/lexer/index.ts +348 -0
  26. package/src/lexer.ts +2 -0
  27. package/src/parser/index.ts +792 -0
  28. package/src/parser/parse.ts +45 -0
  29. package/src/parser/test/async.test.ts +248 -0
  30. package/src/parser/test/destructuring.test.ts +167 -0
  31. package/src/parser/test/do-expression.test.ts +486 -0
  32. package/src/parser/test/errors/do-expression.test.ts +95 -0
  33. package/src/parser/test/errors/error-locations.test.ts +230 -0
  34. package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
  35. package/src/parser/test/errors/missing-tokens.test.ts +126 -0
  36. package/src/parser/test/errors/model-declaration.test.ts +185 -0
  37. package/src/parser/test/errors/nested-blocks.test.ts +226 -0
  38. package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
  39. package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
  40. package/src/parser/test/import-export.test.ts +143 -0
  41. package/src/parser/test/literals.test.ts +404 -0
  42. package/src/parser/test/model-declaration.test.ts +161 -0
  43. package/src/parser/test/nested-blocks.test.ts +402 -0
  44. package/src/parser/test/parser.test.ts +743 -0
  45. package/src/parser/test/private.test.ts +136 -0
  46. package/src/parser/test/template-literal.test.ts +127 -0
  47. package/src/parser/test/tool-declaration.test.ts +302 -0
  48. package/src/parser/test/ts-block.test.ts +252 -0
  49. package/src/parser/test/type-annotations.test.ts +254 -0
  50. package/src/parser/visitor/helpers.ts +330 -0
  51. package/src/parser/visitor.ts +794 -0
  52. package/src/parser.ts +2 -0
  53. package/src/runtime/ai/cache-chunking.test.ts +69 -0
  54. package/src/runtime/ai/cache-chunking.ts +73 -0
  55. package/src/runtime/ai/client.ts +109 -0
  56. package/src/runtime/ai/context.ts +168 -0
  57. package/src/runtime/ai/formatters.ts +316 -0
  58. package/src/runtime/ai/index.ts +38 -0
  59. package/src/runtime/ai/language-ref.ts +38 -0
  60. package/src/runtime/ai/providers/anthropic.ts +253 -0
  61. package/src/runtime/ai/providers/google.ts +201 -0
  62. package/src/runtime/ai/providers/openai.ts +156 -0
  63. package/src/runtime/ai/retry.ts +100 -0
  64. package/src/runtime/ai/return-tools.ts +301 -0
  65. package/src/runtime/ai/test/client.test.ts +83 -0
  66. package/src/runtime/ai/test/formatters.test.ts +485 -0
  67. package/src/runtime/ai/test/retry.test.ts +137 -0
  68. package/src/runtime/ai/test/return-tools.test.ts +450 -0
  69. package/src/runtime/ai/test/tool-loop.test.ts +319 -0
  70. package/src/runtime/ai/test/tool-schema.test.ts +241 -0
  71. package/src/runtime/ai/tool-loop.ts +203 -0
  72. package/src/runtime/ai/tool-schema.ts +151 -0
  73. package/src/runtime/ai/types.ts +113 -0
  74. package/src/runtime/ai-logger.ts +255 -0
  75. package/src/runtime/ai-provider.ts +347 -0
  76. package/src/runtime/async/dependencies.ts +276 -0
  77. package/src/runtime/async/executor.ts +293 -0
  78. package/src/runtime/async/index.ts +43 -0
  79. package/src/runtime/async/scheduling.ts +163 -0
  80. package/src/runtime/async/test/dependencies.test.ts +284 -0
  81. package/src/runtime/async/test/executor.test.ts +388 -0
  82. package/src/runtime/context.ts +357 -0
  83. package/src/runtime/exec/ai.ts +139 -0
  84. package/src/runtime/exec/expressions.ts +475 -0
  85. package/src/runtime/exec/frames.ts +26 -0
  86. package/src/runtime/exec/functions.ts +305 -0
  87. package/src/runtime/exec/interpolation.ts +312 -0
  88. package/src/runtime/exec/statements.ts +604 -0
  89. package/src/runtime/exec/tools.ts +129 -0
  90. package/src/runtime/exec/typescript.ts +215 -0
  91. package/src/runtime/exec/variables.ts +279 -0
  92. package/src/runtime/index.ts +975 -0
  93. package/src/runtime/modules.ts +452 -0
  94. package/src/runtime/serialize.ts +103 -0
  95. package/src/runtime/state.ts +489 -0
  96. package/src/runtime/stdlib/core.ts +45 -0
  97. package/src/runtime/stdlib/directory.test.ts +156 -0
  98. package/src/runtime/stdlib/edit.test.ts +154 -0
  99. package/src/runtime/stdlib/fastEdit.test.ts +201 -0
  100. package/src/runtime/stdlib/glob.test.ts +106 -0
  101. package/src/runtime/stdlib/grep.test.ts +144 -0
  102. package/src/runtime/stdlib/index.ts +16 -0
  103. package/src/runtime/stdlib/readFile.test.ts +123 -0
  104. package/src/runtime/stdlib/tools/index.ts +707 -0
  105. package/src/runtime/stdlib/writeFile.test.ts +157 -0
  106. package/src/runtime/step.ts +969 -0
  107. package/src/runtime/test/ai-context.test.ts +1086 -0
  108. package/src/runtime/test/ai-result-object.test.ts +419 -0
  109. package/src/runtime/test/ai-tool-flow.test.ts +859 -0
  110. package/src/runtime/test/async-execution-order.test.ts +618 -0
  111. package/src/runtime/test/async-execution.test.ts +344 -0
  112. package/src/runtime/test/async-nested.test.ts +660 -0
  113. package/src/runtime/test/async-parallel-timing.test.ts +546 -0
  114. package/src/runtime/test/basic1.test.ts +154 -0
  115. package/src/runtime/test/binary-operators.test.ts +431 -0
  116. package/src/runtime/test/break-statement.test.ts +257 -0
  117. package/src/runtime/test/context-modes.test.ts +650 -0
  118. package/src/runtime/test/context.test.ts +466 -0
  119. package/src/runtime/test/core-functions.test.ts +228 -0
  120. package/src/runtime/test/e2e.test.ts +88 -0
  121. package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
  122. package/src/runtime/test/error-locations/main-error.vibe +4 -0
  123. package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
  124. package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
  125. package/src/runtime/test/for-in.test.ts +312 -0
  126. package/src/runtime/test/helpers.ts +69 -0
  127. package/src/runtime/test/imports.test.ts +334 -0
  128. package/src/runtime/test/json-expressions.test.ts +232 -0
  129. package/src/runtime/test/literals.test.ts +372 -0
  130. package/src/runtime/test/logical-indexing.test.ts +478 -0
  131. package/src/runtime/test/member-methods.test.ts +324 -0
  132. package/src/runtime/test/model-config.test.ts +338 -0
  133. package/src/runtime/test/null-handling.test.ts +342 -0
  134. package/src/runtime/test/private-visibility.test.ts +332 -0
  135. package/src/runtime/test/runtime-state.test.ts +514 -0
  136. package/src/runtime/test/scoping.test.ts +370 -0
  137. package/src/runtime/test/string-interpolation.test.ts +354 -0
  138. package/src/runtime/test/template-literal.test.ts +181 -0
  139. package/src/runtime/test/tool-execution.test.ts +467 -0
  140. package/src/runtime/test/tool-schema-generation.test.ts +477 -0
  141. package/src/runtime/test/tostring.test.ts +210 -0
  142. package/src/runtime/test/ts-block.test.ts +594 -0
  143. package/src/runtime/test/ts-error-location.test.ts +231 -0
  144. package/src/runtime/test/types.test.ts +732 -0
  145. package/src/runtime/test/verbose-logger.test.ts +710 -0
  146. package/src/runtime/test/vibe-expression.test.ts +54 -0
  147. package/src/runtime/test/vibe-value-errors.test.ts +541 -0
  148. package/src/runtime/test/while.test.ts +232 -0
  149. package/src/runtime/tools/builtin.ts +30 -0
  150. package/src/runtime/tools/directory-tools.ts +70 -0
  151. package/src/runtime/tools/file-tools.ts +228 -0
  152. package/src/runtime/tools/index.ts +5 -0
  153. package/src/runtime/tools/registry.ts +48 -0
  154. package/src/runtime/tools/search-tools.ts +134 -0
  155. package/src/runtime/tools/security.ts +36 -0
  156. package/src/runtime/tools/system-tools.ts +312 -0
  157. package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
  158. package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
  159. package/src/runtime/tools/test/registry.test.ts +713 -0
  160. package/src/runtime/tools/test/security.test.ts +86 -0
  161. package/src/runtime/tools/test/system-tools.test.ts +679 -0
  162. package/src/runtime/tools/test/ts-schema.test.ts +357 -0
  163. package/src/runtime/tools/ts-schema.ts +341 -0
  164. package/src/runtime/tools/types.ts +89 -0
  165. package/src/runtime/tools/utility-tools.ts +198 -0
  166. package/src/runtime/ts-eval.ts +126 -0
  167. package/src/runtime/types.ts +797 -0
  168. package/src/runtime/validation.ts +160 -0
  169. package/src/runtime/verbose-logger.ts +459 -0
  170. package/src/runtime.ts +2 -0
  171. package/src/semantic/analyzer-context.ts +62 -0
  172. package/src/semantic/analyzer-validators.ts +575 -0
  173. package/src/semantic/analyzer-visitors.ts +534 -0
  174. package/src/semantic/analyzer.ts +83 -0
  175. package/src/semantic/index.ts +11 -0
  176. package/src/semantic/symbol-table.ts +58 -0
  177. package/src/semantic/test/async-validation.test.ts +301 -0
  178. package/src/semantic/test/compress-validation.test.ts +179 -0
  179. package/src/semantic/test/const-reassignment.test.ts +111 -0
  180. package/src/semantic/test/control-flow.test.ts +346 -0
  181. package/src/semantic/test/destructuring.test.ts +185 -0
  182. package/src/semantic/test/duplicate-declarations.test.ts +168 -0
  183. package/src/semantic/test/export-validation.test.ts +111 -0
  184. package/src/semantic/test/fixtures/math.ts +31 -0
  185. package/src/semantic/test/imports.test.ts +148 -0
  186. package/src/semantic/test/json-type.test.ts +68 -0
  187. package/src/semantic/test/literals.test.ts +127 -0
  188. package/src/semantic/test/model-validation.test.ts +179 -0
  189. package/src/semantic/test/prompt-validation.test.ts +343 -0
  190. package/src/semantic/test/scoping.test.ts +312 -0
  191. package/src/semantic/test/tool-validation.test.ts +306 -0
  192. package/src/semantic/test/ts-type-checking.test.ts +563 -0
  193. package/src/semantic/test/type-constraints.test.ts +111 -0
  194. package/src/semantic/test/type-inference.test.ts +87 -0
  195. package/src/semantic/test/type-validation.test.ts +552 -0
  196. package/src/semantic/test/undefined-variables.test.ts +163 -0
  197. package/src/semantic/ts-block-checker.ts +204 -0
  198. package/src/semantic/ts-signatures.ts +194 -0
  199. package/src/semantic/ts-types.ts +170 -0
  200. package/src/semantic/types.ts +58 -0
  201. package/tests/fixtures/conditional-logic.vibe +14 -0
  202. package/tests/fixtures/function-call.vibe +16 -0
  203. package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
  204. package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
  205. package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
  206. package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
  207. package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
  208. package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
  209. package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
  210. package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
  211. package/tests/fixtures/imports/nested-import/main.vibe +3 -0
  212. package/tests/fixtures/imports/nested-import/utils.ts +3 -0
  213. package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
  214. package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
  215. package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
  216. package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
  217. package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
  218. package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
  219. package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
  220. package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
  221. package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
  222. package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
  223. package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
  224. package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
  225. package/tests/fixtures/imports/ts-import/main.vibe +4 -0
  226. package/tests/fixtures/imports/ts-import/math.ts +9 -0
  227. package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
  228. package/tests/fixtures/imports/ts-variables/data.ts +10 -0
  229. package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
  230. package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
  231. package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
  232. package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
  233. package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
  234. package/tests/fixtures/multiple-ai-calls.vibe +10 -0
  235. package/tests/fixtures/simple-greeting.vibe +6 -0
  236. package/tests/fixtures/template-literals.vibe +11 -0
  237. package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
  238. package/tests/integration/basic-ai/basic-ai.vibe +12 -0
  239. package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
  240. package/tests/integration/bug-fix/buggy-code.ts +22 -0
  241. package/tests/integration/bug-fix/fix-bug.vibe +21 -0
  242. package/tests/integration/compress/compress.integration.test.ts +206 -0
  243. package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
  244. package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
  245. package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
  246. package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
  247. package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
  248. package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
  249. package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
  250. package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
@@ -0,0 +1,713 @@
1
+ import { describe, expect, test, beforeEach, beforeAll, afterAll } from 'bun:test';
2
+ import { createToolRegistry, createToolRegistryWithBuiltins } from '../registry';
3
+ import { builtinTools } from '../builtin';
4
+ import type { RegisteredTool, ToolRegistry } from '../types';
5
+
6
+ // Use project-relative temp directory for test files (gitignored)
7
+ const TEST_TMP_DIR = '.test-tmp';
8
+
9
+ describe('Tool Registry', () => {
10
+ let registry: ToolRegistry;
11
+
12
+ beforeEach(() => {
13
+ registry = createToolRegistry();
14
+ });
15
+
16
+ // ============================================================================
17
+ // Basic registration and retrieval
18
+ // ============================================================================
19
+
20
+ test('registers and retrieves a tool', () => {
21
+ const tool: RegisteredTool = {
22
+ name: 'testTool',
23
+ kind: 'user',
24
+ schema: {
25
+ name: 'testTool',
26
+ description: 'A test tool',
27
+ parameters: [{ name: 'input', type: { type: 'string' }, required: true }],
28
+ },
29
+ executor: async () => 'result',
30
+ };
31
+
32
+ registry.register(tool);
33
+ const retrieved = registry.get('testTool');
34
+
35
+ expect(retrieved).toBeDefined();
36
+ expect(retrieved?.name).toBe('testTool');
37
+ expect(retrieved?.kind).toBe('user');
38
+ });
39
+
40
+ test('returns undefined for non-existent tool', () => {
41
+ expect(registry.get('nonExistent')).toBeUndefined();
42
+ });
43
+
44
+ test('has returns true for registered tool', () => {
45
+ const tool: RegisteredTool = {
46
+ name: 'existingTool',
47
+ kind: 'user',
48
+ schema: {
49
+ name: 'existingTool',
50
+ parameters: [],
51
+ },
52
+ executor: async () => null,
53
+ };
54
+
55
+ registry.register(tool);
56
+ expect(registry.has('existingTool')).toBe(true);
57
+ expect(registry.has('missingTool')).toBe(false);
58
+ });
59
+
60
+ // ============================================================================
61
+ // Listing tools
62
+ // ============================================================================
63
+
64
+ test('lists all registered tools', () => {
65
+ const tool1: RegisteredTool = {
66
+ name: 'tool1',
67
+ kind: 'user',
68
+ schema: { name: 'tool1', parameters: [] },
69
+ executor: async () => null,
70
+ };
71
+
72
+ const tool2: RegisteredTool = {
73
+ name: 'tool2',
74
+ kind: 'builtin',
75
+ schema: { name: 'tool2', parameters: [] },
76
+ executor: async () => null,
77
+ };
78
+
79
+ registry.register(tool1);
80
+ registry.register(tool2);
81
+
82
+ const tools = registry.list();
83
+ expect(tools).toHaveLength(2);
84
+ expect(tools.map((t) => t.name)).toContain('tool1');
85
+ expect(tools.map((t) => t.name)).toContain('tool2');
86
+ });
87
+
88
+ test('getSchemas returns all tool schemas', () => {
89
+ const tool: RegisteredTool = {
90
+ name: 'schemaTest',
91
+ kind: 'user',
92
+ schema: {
93
+ name: 'schemaTest',
94
+ description: 'Test schema',
95
+ parameters: [
96
+ { name: 'param1', type: { type: 'string' }, required: true },
97
+ { name: 'param2', type: { type: 'number' }, required: false },
98
+ ],
99
+ },
100
+ executor: async () => null,
101
+ };
102
+
103
+ registry.register(tool);
104
+ const schemas = registry.getSchemas();
105
+
106
+ expect(schemas).toHaveLength(1);
107
+ expect(schemas[0].name).toBe('schemaTest');
108
+ expect(schemas[0].parameters).toHaveLength(2);
109
+ });
110
+
111
+ // ============================================================================
112
+ // Registry with builtins
113
+ // ============================================================================
114
+
115
+ test('createToolRegistryWithBuiltins includes all builtin tools', () => {
116
+ const registryWithBuiltins = createToolRegistryWithBuiltins();
117
+ const tools = registryWithBuiltins.list();
118
+
119
+ // Check that we have builtin tools
120
+ expect(tools.length).toBeGreaterThan(0);
121
+
122
+ // Verify specific builtin tools are present
123
+ expect(registryWithBuiltins.has('sleep')).toBe(true);
124
+ expect(registryWithBuiltins.has('now')).toBe(true);
125
+ expect(registryWithBuiltins.has('jsonParse')).toBe(true);
126
+ expect(registryWithBuiltins.has('jsonStringify')).toBe(true);
127
+ expect(registryWithBuiltins.has('env')).toBe(true);
128
+ });
129
+
130
+ test('builtin tools have kind "builtin"', () => {
131
+ const registryWithBuiltins = createToolRegistryWithBuiltins();
132
+ const sleepTool = registryWithBuiltins.get('sleep');
133
+
134
+ expect(sleepTool?.kind).toBe('builtin');
135
+ });
136
+
137
+ // ============================================================================
138
+ // Tool overwriting
139
+ // ============================================================================
140
+
141
+ test('registering tool with same name overwrites', () => {
142
+ const tool1: RegisteredTool = {
143
+ name: 'duplicateTool',
144
+ kind: 'user',
145
+ schema: { name: 'duplicateTool', description: 'first', parameters: [] },
146
+ executor: async () => 'first',
147
+ };
148
+
149
+ const tool2: RegisteredTool = {
150
+ name: 'duplicateTool',
151
+ kind: 'user',
152
+ schema: { name: 'duplicateTool', description: 'second', parameters: [] },
153
+ executor: async () => 'second',
154
+ };
155
+
156
+ registry.register(tool1);
157
+ registry.register(tool2);
158
+
159
+ const retrieved = registry.get('duplicateTool');
160
+ expect(retrieved?.schema.description).toBe('second');
161
+ });
162
+ });
163
+
164
+ describe('Builtin Tools', () => {
165
+ // ============================================================================
166
+ // Verify builtin tool definitions
167
+ // ============================================================================
168
+
169
+ test('all builtin tools have required properties', () => {
170
+ for (const tool of builtinTools) {
171
+ expect(tool.name).toBeDefined();
172
+ expect(tool.kind).toBe('builtin');
173
+ expect(tool.schema).toBeDefined();
174
+ expect(tool.executor).toBeInstanceOf(Function);
175
+ }
176
+ });
177
+
178
+ test('builtin tools have proper schemas', () => {
179
+ const readFileTool = builtinTools.find((t) => t.name === 'readFile');
180
+ expect(readFileTool).toBeDefined();
181
+ expect(readFileTool?.schema.name).toBe('readFile');
182
+ expect(readFileTool?.schema.description).toBeDefined();
183
+ expect(readFileTool?.schema.parameters.length).toBeGreaterThan(0);
184
+ });
185
+
186
+ // ============================================================================
187
+ // Builtin tool execution
188
+ // ============================================================================
189
+
190
+ test('sleep tool waits specified time', async () => {
191
+ const sleepTool = builtinTools.find((t) => t.name === 'sleep');
192
+ expect(sleepTool).toBeDefined();
193
+
194
+ const start = Date.now();
195
+ await sleepTool!.executor({ ms: 50 });
196
+ const elapsed = Date.now() - start;
197
+
198
+ expect(elapsed).toBeGreaterThanOrEqual(40); // Allow some tolerance
199
+ });
200
+
201
+ test('now tool returns current timestamp', async () => {
202
+ const nowTool = builtinTools.find((t) => t.name === 'now');
203
+ expect(nowTool).toBeDefined();
204
+
205
+ const before = Date.now();
206
+ const result = await nowTool!.executor({});
207
+ const after = Date.now();
208
+
209
+ expect(typeof result).toBe('number');
210
+ expect(result as number).toBeGreaterThanOrEqual(before);
211
+ expect(result as number).toBeLessThanOrEqual(after);
212
+ });
213
+
214
+ test('jsonParse tool parses JSON string', async () => {
215
+ const jsonParseTool = builtinTools.find((t) => t.name === 'jsonParse');
216
+ expect(jsonParseTool).toBeDefined();
217
+
218
+ const result = await jsonParseTool!.executor({ text: '{"name":"test","value":42}' });
219
+ expect(result).toEqual({ name: 'test', value: 42 });
220
+ });
221
+
222
+ test('jsonStringify tool converts to JSON string', async () => {
223
+ const jsonStringifyTool = builtinTools.find((t) => t.name === 'jsonStringify');
224
+ expect(jsonStringifyTool).toBeDefined();
225
+
226
+ const result = await jsonStringifyTool!.executor({ value: { name: 'test', value: 42 } });
227
+ expect(result).toBe('{"name":"test","value":42}');
228
+ });
229
+
230
+ test('env tool returns environment variable', async () => {
231
+ const envTool = builtinTools.find((t) => t.name === 'env');
232
+ expect(envTool).toBeDefined();
233
+
234
+ // Set a test env var
235
+ process.env.TEST_VAR = 'test_value';
236
+ const result = await envTool!.executor({ name: 'TEST_VAR' });
237
+ expect(result).toBe('test_value');
238
+
239
+ // Cleanup
240
+ delete process.env.TEST_VAR;
241
+ });
242
+
243
+ test('env tool returns empty string for missing variable without default', async () => {
244
+ const envTool = builtinTools.find((t) => t.name === 'env');
245
+ expect(envTool).toBeDefined();
246
+
247
+ const result = await envTool!.executor({ name: 'NONEXISTENT_VAR_12345' });
248
+ expect(result).toBe('');
249
+ });
250
+
251
+ test('env tool returns default value for missing variable', async () => {
252
+ const envTool = builtinTools.find((t) => t.name === 'env');
253
+ expect(envTool).toBeDefined();
254
+
255
+ const result = await envTool!.executor({ name: 'NONEXISTENT_VAR_12345', defaultValue: 'fallback' });
256
+ expect(result).toBe('fallback');
257
+ });
258
+
259
+ // ============================================================================
260
+ // Enhanced readFile with line ranges
261
+ // ============================================================================
262
+
263
+ describe('readFile with line ranges', () => {
264
+ const fs = require('fs');
265
+ const path = require('path');
266
+ let testDir: string;
267
+ let testFile: string;
268
+
269
+ beforeAll(() => {
270
+ fs.mkdirSync(TEST_TMP_DIR, { recursive: true });
271
+ testDir = fs.mkdtempSync(path.join(TEST_TMP_DIR, 'vibe-test-'));
272
+ testFile = path.join(testDir, 'test.txt');
273
+ // Create a 10-line test file
274
+ fs.writeFileSync(testFile, 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10');
275
+ });
276
+
277
+ afterAll(() => {
278
+ fs.rmSync(testDir, { recursive: true });
279
+ });
280
+
281
+ test('reads entire file without line parameters', async () => {
282
+ const readFileTool = builtinTools.find((t) => t.name === 'readFile')!;
283
+ const result = await readFileTool.executor({ path: testFile });
284
+ expect(result).toBe('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10');
285
+ });
286
+
287
+ test('reads from startLine to end', async () => {
288
+ const readFileTool = builtinTools.find((t) => t.name === 'readFile')!;
289
+ const result = await readFileTool.executor({ path: testFile, startLine: 8 });
290
+ expect(result).toBe('Line 8\nLine 9\nLine 10');
291
+ });
292
+
293
+ test('reads from beginning to endLine', async () => {
294
+ const readFileTool = builtinTools.find((t) => t.name === 'readFile')!;
295
+ const result = await readFileTool.executor({ path: testFile, endLine: 3 });
296
+ expect(result).toBe('Line 1\nLine 2\nLine 3');
297
+ });
298
+
299
+ test('reads specific line range', async () => {
300
+ const readFileTool = builtinTools.find((t) => t.name === 'readFile')!;
301
+ const result = await readFileTool.executor({ path: testFile, startLine: 3, endLine: 5 });
302
+ expect(result).toBe('Line 3\nLine 4\nLine 5');
303
+ });
304
+
305
+ test('handles startLine beyond file length', async () => {
306
+ const readFileTool = builtinTools.find((t) => t.name === 'readFile')!;
307
+ const result = await readFileTool.executor({ path: testFile, startLine: 100 });
308
+ expect(result).toBe('');
309
+ });
310
+
311
+ test('handles endLine beyond file length', async () => {
312
+ const readFileTool = builtinTools.find((t) => t.name === 'readFile')!;
313
+ const result = await readFileTool.executor({ path: testFile, startLine: 9, endLine: 100 });
314
+ expect(result).toBe('Line 9\nLine 10');
315
+ });
316
+
317
+ test('handles single line (startLine equals endLine)', async () => {
318
+ const readFileTool = builtinTools.find((t) => t.name === 'readFile')!;
319
+ const result = await readFileTool.executor({ path: testFile, startLine: 5, endLine: 5 });
320
+ expect(result).toBe('Line 5');
321
+ });
322
+ });
323
+
324
+ // ============================================================================
325
+ // New tool tests
326
+ // ============================================================================
327
+
328
+ describe('edit tool', () => {
329
+ const fs = require('fs');
330
+ const path = require('path');
331
+ let testDir: string;
332
+
333
+ beforeAll(() => {
334
+ fs.mkdirSync(TEST_TMP_DIR, { recursive: true });
335
+ testDir = fs.mkdtempSync(path.join(TEST_TMP_DIR, 'vibe-edit-test-'));
336
+ });
337
+
338
+ afterAll(() => {
339
+ fs.rmSync(testDir, { recursive: true });
340
+ });
341
+
342
+ test('replaces text when exactly one match', async () => {
343
+ const editTool = builtinTools.find((t) => t.name === 'edit')!;
344
+ const testFile = path.join(testDir, 'edit-test1.txt');
345
+ fs.writeFileSync(testFile, 'foo bar baz');
346
+
347
+ const result = await editTool.executor({ path: testFile, oldText: 'bar', newText: 'qux' });
348
+ expect(result).toBe(true);
349
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('foo qux baz');
350
+ });
351
+
352
+ test('throws error when no match found', async () => {
353
+ const editTool = builtinTools.find((t) => t.name === 'edit')!;
354
+ const testFile = path.join(testDir, 'edit-test2.txt');
355
+ fs.writeFileSync(testFile, 'foo bar baz');
356
+
357
+ await expect(
358
+ editTool.executor({ path: testFile, oldText: 'qux', newText: 'xxx' })
359
+ ).rejects.toThrow('edit failed: oldText not found in file');
360
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('foo bar baz');
361
+ });
362
+
363
+ test('throws error when multiple matches found', async () => {
364
+ const editTool = builtinTools.find((t) => t.name === 'edit')!;
365
+ const testFile = path.join(testDir, 'edit-test3.txt');
366
+ fs.writeFileSync(testFile, 'foo bar foo baz');
367
+
368
+ await expect(
369
+ editTool.executor({ path: testFile, oldText: 'foo', newText: 'qux' })
370
+ ).rejects.toThrow('edit failed: oldText matches 2 times, must match exactly once');
371
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('foo bar foo baz');
372
+ });
373
+ });
374
+
375
+ describe('directory tools', () => {
376
+ const fs = require('fs');
377
+ const path = require('path');
378
+ let testDir: string;
379
+
380
+ beforeAll(() => {
381
+ fs.mkdirSync(TEST_TMP_DIR, { recursive: true });
382
+ testDir = fs.mkdtempSync(path.join(TEST_TMP_DIR, 'vibe-dir-test-'));
383
+ });
384
+
385
+ afterAll(() => {
386
+ fs.rmSync(testDir, { recursive: true });
387
+ });
388
+
389
+ test('mkdir creates directory', async () => {
390
+ const mkdirTool = builtinTools.find((t) => t.name === 'mkdir')!;
391
+ const newDir = path.join(testDir, 'new-dir');
392
+
393
+ const result = await mkdirTool.executor({ path: newDir });
394
+ expect(result).toBe(true);
395
+ expect(fs.existsSync(newDir)).toBe(true);
396
+ });
397
+
398
+ test('mkdir creates nested directories with recursive=true', async () => {
399
+ const mkdirTool = builtinTools.find((t) => t.name === 'mkdir')!;
400
+ const nestedDir = path.join(testDir, 'a', 'b', 'c');
401
+
402
+ const result = await mkdirTool.executor({ path: nestedDir, recursive: true });
403
+ expect(result).toBe(true);
404
+ expect(fs.existsSync(nestedDir)).toBe(true);
405
+ });
406
+
407
+ test('dirExists returns true for existing directory', async () => {
408
+ const dirExistsTool = builtinTools.find((t) => t.name === 'dirExists')!;
409
+ const result = await dirExistsTool.executor({ path: testDir });
410
+ expect(result).toBe(true);
411
+ });
412
+
413
+ test('dirExists returns false for non-existent directory', async () => {
414
+ const dirExistsTool = builtinTools.find((t) => t.name === 'dirExists')!;
415
+ const result = await dirExistsTool.executor({ path: path.join(testDir, 'nonexistent') });
416
+ expect(result).toBe(false);
417
+ });
418
+ });
419
+
420
+ describe('utility tools', () => {
421
+ test('random returns float between 0 and 1 without args', async () => {
422
+ const randomTool = builtinTools.find((t) => t.name === 'random')!;
423
+ const result = await randomTool.executor({}) as number;
424
+ expect(result).toBeGreaterThanOrEqual(0);
425
+ expect(result).toBeLessThan(1);
426
+ });
427
+
428
+ test('random returns integer in range with min/max', async () => {
429
+ const randomTool = builtinTools.find((t) => t.name === 'random')!;
430
+ for (let i = 0; i < 10; i++) {
431
+ const result = await randomTool.executor({ min: 5, max: 10 }) as number;
432
+ expect(result).toBeGreaterThanOrEqual(5);
433
+ expect(result).toBeLessThanOrEqual(10);
434
+ expect(Number.isInteger(result)).toBe(true);
435
+ }
436
+ });
437
+
438
+ test('uuid returns valid UUID v4 format', async () => {
439
+ const uuidTool = builtinTools.find((t) => t.name === 'uuid')!;
440
+ const result = await uuidTool.executor({}) as string;
441
+ expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
442
+ });
443
+
444
+ test('uuid returns different values on each call', async () => {
445
+ const uuidTool = builtinTools.find((t) => t.name === 'uuid')!;
446
+ const uuid1 = await uuidTool.executor({});
447
+ const uuid2 = await uuidTool.executor({});
448
+ expect(uuid1).not.toBe(uuid2);
449
+ });
450
+ });
451
+
452
+ // ============================================================================
453
+ // File tools: writeFile, appendFile, fileExists, listDir
454
+ // ============================================================================
455
+
456
+ describe('file tools', () => {
457
+ const fs = require('fs');
458
+ const path = require('path');
459
+ let testDir: string;
460
+
461
+ beforeAll(() => {
462
+ fs.mkdirSync(TEST_TMP_DIR, { recursive: true });
463
+ testDir = fs.mkdtempSync(path.join(TEST_TMP_DIR, 'vibe-file-test-'));
464
+ });
465
+
466
+ afterAll(() => {
467
+ fs.rmSync(testDir, { recursive: true });
468
+ });
469
+
470
+ describe('writeFile', () => {
471
+ test('writes content to new file', async () => {
472
+ const writeFileTool = builtinTools.find((t) => t.name === 'writeFile')!;
473
+ const testFile = path.join(testDir, 'write-test1.txt');
474
+
475
+ const result = await writeFileTool.executor({ path: testFile, content: 'Hello, World!' });
476
+ expect(result).toBe(true);
477
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('Hello, World!');
478
+ });
479
+
480
+ test('overwrites existing file', async () => {
481
+ const writeFileTool = builtinTools.find((t) => t.name === 'writeFile')!;
482
+ const testFile = path.join(testDir, 'write-test2.txt');
483
+ fs.writeFileSync(testFile, 'original content');
484
+
485
+ const result = await writeFileTool.executor({ path: testFile, content: 'new content' });
486
+ expect(result).toBe(true);
487
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('new content');
488
+ });
489
+
490
+ test('writes empty content', async () => {
491
+ const writeFileTool = builtinTools.find((t) => t.name === 'writeFile')!;
492
+ const testFile = path.join(testDir, 'write-test3.txt');
493
+
494
+ const result = await writeFileTool.executor({ path: testFile, content: '' });
495
+ expect(result).toBe(true);
496
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('');
497
+ });
498
+ });
499
+
500
+ describe('appendFile', () => {
501
+ test('appends content to existing file', async () => {
502
+ const appendFileTool = builtinTools.find((t) => t.name === 'appendFile')!;
503
+ const testFile = path.join(testDir, 'append-test1.txt');
504
+ fs.writeFileSync(testFile, 'Hello');
505
+
506
+ const result = await appendFileTool.executor({ path: testFile, content: ', World!' });
507
+ expect(result).toBe(true);
508
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('Hello, World!');
509
+ });
510
+
511
+ test('creates new file if it does not exist', async () => {
512
+ const appendFileTool = builtinTools.find((t) => t.name === 'appendFile')!;
513
+ const testFile = path.join(testDir, 'append-test2.txt');
514
+
515
+ const result = await appendFileTool.executor({ path: testFile, content: 'New content' });
516
+ expect(result).toBe(true);
517
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('New content');
518
+ });
519
+
520
+ test('appends multiple times', async () => {
521
+ const appendFileTool = builtinTools.find((t) => t.name === 'appendFile')!;
522
+ const testFile = path.join(testDir, 'append-test3.txt');
523
+ fs.writeFileSync(testFile, 'Line 1');
524
+
525
+ await appendFileTool.executor({ path: testFile, content: '\nLine 2' });
526
+ await appendFileTool.executor({ path: testFile, content: '\nLine 3' });
527
+
528
+ expect(fs.readFileSync(testFile, 'utf8')).toBe('Line 1\nLine 2\nLine 3');
529
+ });
530
+ });
531
+
532
+ describe('fileExists', () => {
533
+ test('returns true for existing file', async () => {
534
+ const fileExistsTool = builtinTools.find((t) => t.name === 'fileExists')!;
535
+ const testFile = path.join(testDir, 'exists-test1.txt');
536
+ fs.writeFileSync(testFile, 'content');
537
+
538
+ const result = await fileExistsTool.executor({ path: testFile });
539
+ expect(result).toBe(true);
540
+ });
541
+
542
+ test('returns false for non-existent file', async () => {
543
+ const fileExistsTool = builtinTools.find((t) => t.name === 'fileExists')!;
544
+ const testFile = path.join(testDir, 'nonexistent.txt');
545
+
546
+ const result = await fileExistsTool.executor({ path: testFile });
547
+ expect(result).toBe(false);
548
+ });
549
+
550
+ test('returns false for directory (Bun.file behavior)', async () => {
551
+ const fileExistsTool = builtinTools.find((t) => t.name === 'fileExists')!;
552
+ // Note: Bun.file().exists() returns false for directories
553
+ const result = await fileExistsTool.executor({ path: testDir });
554
+ expect(result).toBe(false);
555
+ });
556
+ });
557
+
558
+ describe('listDir', () => {
559
+ test('lists files in directory', async () => {
560
+ const listDirTool = builtinTools.find((t) => t.name === 'listDir')!;
561
+ const subDir = path.join(testDir, 'listdir-test');
562
+ fs.mkdirSync(subDir);
563
+ fs.writeFileSync(path.join(subDir, 'file1.txt'), 'a');
564
+ fs.writeFileSync(path.join(subDir, 'file2.txt'), 'b');
565
+ fs.writeFileSync(path.join(subDir, 'file3.txt'), 'c');
566
+
567
+ const result = (await listDirTool.executor({ path: subDir })) as string[];
568
+ expect(result.sort()).toEqual(['file1.txt', 'file2.txt', 'file3.txt']);
569
+ });
570
+
571
+ test('returns empty array for empty directory', async () => {
572
+ const listDirTool = builtinTools.find((t) => t.name === 'listDir')!;
573
+ const emptyDir = path.join(testDir, 'empty-dir');
574
+ fs.mkdirSync(emptyDir);
575
+
576
+ const result = await listDirTool.executor({ path: emptyDir });
577
+ expect(result).toEqual([]);
578
+ });
579
+
580
+ test('includes subdirectories in listing', async () => {
581
+ const listDirTool = builtinTools.find((t) => t.name === 'listDir')!;
582
+ const subDir = path.join(testDir, 'listdir-mixed');
583
+ fs.mkdirSync(subDir);
584
+ fs.writeFileSync(path.join(subDir, 'file.txt'), 'a');
585
+ fs.mkdirSync(path.join(subDir, 'subdir'));
586
+
587
+ const result = (await listDirTool.executor({ path: subDir })) as string[];
588
+ expect(result.sort()).toEqual(['file.txt', 'subdir']);
589
+ });
590
+ });
591
+ });
592
+
593
+ // ============================================================================
594
+ // Search tools: glob, grep
595
+ // ============================================================================
596
+
597
+ describe('search tools', () => {
598
+ const fs = require('fs');
599
+ const path = require('path');
600
+ let testDir: string;
601
+
602
+ beforeAll(() => {
603
+ fs.mkdirSync(TEST_TMP_DIR, { recursive: true });
604
+ testDir = fs.mkdtempSync(path.join(TEST_TMP_DIR, 'vibe-search-test-'));
605
+
606
+ // Create test file structure
607
+ fs.mkdirSync(path.join(testDir, 'src'));
608
+ fs.mkdirSync(path.join(testDir, 'src', 'utils'));
609
+ fs.writeFileSync(path.join(testDir, 'src', 'index.ts'), 'export const main = () => console.log("hello");');
610
+ fs.writeFileSync(path.join(testDir, 'src', 'app.ts'), 'import { main } from "./index";\nmain();');
611
+ fs.writeFileSync(path.join(testDir, 'src', 'utils', 'helper.ts'), 'export function helper() { return 42; }');
612
+ fs.writeFileSync(path.join(testDir, 'README.md'), '# Test Project\n\nThis is a test.');
613
+ fs.writeFileSync(path.join(testDir, 'config.json'), '{"name": "test"}');
614
+ });
615
+
616
+ afterAll(() => {
617
+ fs.rmSync(testDir, { recursive: true });
618
+ });
619
+
620
+ describe('glob', () => {
621
+ // Helper to normalize paths for cross-platform comparison
622
+ const normalizePaths = (paths: string[]) => paths.map((p) => p.replace(/\\/g, '/')).sort();
623
+
624
+ test('finds files matching pattern', async () => {
625
+ const globTool = builtinTools.find((t) => t.name === 'glob')!;
626
+ const result = (await globTool.executor({ pattern: '**/*.ts', cwd: testDir })) as string[];
627
+ expect(normalizePaths(result)).toEqual(['src/app.ts', 'src/index.ts', 'src/utils/helper.ts']);
628
+ });
629
+
630
+ test('finds files in specific directory', async () => {
631
+ const globTool = builtinTools.find((t) => t.name === 'glob')!;
632
+ const result = (await globTool.executor({ pattern: '*.ts', cwd: path.join(testDir, 'src') })) as string[];
633
+ expect(normalizePaths(result)).toEqual(['app.ts', 'index.ts']);
634
+ });
635
+
636
+ test('finds files with multiple extensions', async () => {
637
+ const globTool = builtinTools.find((t) => t.name === 'glob')!;
638
+ const result = (await globTool.executor({ pattern: '*.{md,json}', cwd: testDir })) as string[];
639
+ expect(normalizePaths(result)).toEqual(['README.md', 'config.json']);
640
+ });
641
+
642
+ test('returns empty array when no matches', async () => {
643
+ const globTool = builtinTools.find((t) => t.name === 'glob')!;
644
+ const result = await globTool.executor({ pattern: '*.xyz', cwd: testDir });
645
+ expect(result).toEqual([]);
646
+ });
647
+ });
648
+
649
+ describe('grep', () => {
650
+ // Helper to check if a file path ends with expected suffix (cross-platform)
651
+ const fileEndsWith = (file: string, suffix: string) =>
652
+ file.replace(/\\/g, '/').endsWith(suffix.replace(/\\/g, '/'));
653
+
654
+ test('finds matches in single file', async () => {
655
+ const grepTool = builtinTools.find((t) => t.name === 'grep')!;
656
+ const result = (await grepTool.executor({
657
+ pattern: 'console',
658
+ path: path.join(testDir, 'src', 'index.ts'),
659
+ })) as Array<{ file: string; line: number; match: string }>;
660
+
661
+ expect(result).toHaveLength(1);
662
+ expect(result[0].line).toBe(1);
663
+ expect(result[0].match).toBe('console');
664
+ });
665
+
666
+ test('finds matches across directory', async () => {
667
+ const grepTool = builtinTools.find((t) => t.name === 'grep')!;
668
+ const result = (await grepTool.executor({
669
+ pattern: 'export',
670
+ path: path.join(testDir, 'src'),
671
+ })) as Array<{ file: string; line: number; match: string }>;
672
+
673
+ expect(result.length).toBeGreaterThanOrEqual(2);
674
+ const files = [...new Set(result.map((r) => r.file))];
675
+ expect(files.some((f) => fileEndsWith(f, 'src/index.ts'))).toBe(true);
676
+ expect(files.some((f) => fileEndsWith(f, 'src/utils/helper.ts'))).toBe(true);
677
+ });
678
+
679
+ test('supports case-insensitive search', async () => {
680
+ const grepTool = builtinTools.find((t) => t.name === 'grep')!;
681
+ const result = (await grepTool.executor({
682
+ pattern: 'CONSOLE',
683
+ path: path.join(testDir, 'src', 'index.ts'),
684
+ ignoreCase: true,
685
+ })) as Array<{ file: string; line: number; match: string }>;
686
+
687
+ expect(result).toHaveLength(1);
688
+ expect(result[0].match).toBe('console');
689
+ });
690
+
691
+ test('returns empty array when no matches', async () => {
692
+ const grepTool = builtinTools.find((t) => t.name === 'grep')!;
693
+ const result = await grepTool.executor({
694
+ pattern: 'nonexistent',
695
+ path: testDir,
696
+ });
697
+
698
+ expect(result).toEqual([]);
699
+ });
700
+
701
+ test('supports regex patterns', async () => {
702
+ const grepTool = builtinTools.find((t) => t.name === 'grep')!;
703
+ const result = (await grepTool.executor({
704
+ pattern: 'function\\s+\\w+',
705
+ path: path.join(testDir, 'src', 'utils', 'helper.ts'),
706
+ })) as Array<{ file: string; line: number; match: string }>;
707
+
708
+ expect(result).toHaveLength(1);
709
+ expect(result[0].match).toBe('function helper');
710
+ });
711
+ });
712
+ });
713
+ });