@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,450 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import {
3
+ getReturnTools,
4
+ isReturnToolCall,
5
+ shouldUseReturnTool,
6
+ collectAndValidateFieldResults,
7
+ buildReturnInstruction,
8
+ isFieldReturnResult,
9
+ RETURN_FIELD_TOOL,
10
+ } from '../return-tools';
11
+ import { executeWithTools } from '../tool-loop';
12
+ import type { AIRequest, AIResponse } from '../types';
13
+
14
+ const TEST_ROOT_DIR = process.cwd();
15
+
16
+ describe('return-tools', () => {
17
+ describe('isReturnToolCall', () => {
18
+ it('should return true for return field tool', () => {
19
+ expect(isReturnToolCall(RETURN_FIELD_TOOL)).toBe(true);
20
+ });
21
+
22
+ it('should return false for other tools', () => {
23
+ expect(isReturnToolCall('getWeather')).toBe(false);
24
+ expect(isReturnToolCall('calculator')).toBe(false);
25
+ });
26
+ });
27
+
28
+ describe('shouldUseReturnTool', () => {
29
+ it('should return true for number type', () => {
30
+ expect(shouldUseReturnTool('number')).toBe(true);
31
+ });
32
+
33
+ it('should return true for boolean type', () => {
34
+ expect(shouldUseReturnTool('boolean')).toBe(true);
35
+ });
36
+
37
+ it('should return true for number[] type', () => {
38
+ expect(shouldUseReturnTool('number[]')).toBe(true);
39
+ });
40
+
41
+ it('should return true for boolean[] type', () => {
42
+ expect(shouldUseReturnTool('boolean[]')).toBe(true);
43
+ });
44
+
45
+ it('should return true for text[] type', () => {
46
+ expect(shouldUseReturnTool('text[]')).toBe(true);
47
+ });
48
+
49
+ it('should return true for json type', () => {
50
+ expect(shouldUseReturnTool('json')).toBe(true);
51
+ });
52
+
53
+ it('should return true for json[] type', () => {
54
+ expect(shouldUseReturnTool('json[]')).toBe(true);
55
+ });
56
+
57
+ it('should return true for text type', () => {
58
+ expect(shouldUseReturnTool('text')).toBe(true);
59
+ });
60
+
61
+ it('should return false for null type', () => {
62
+ expect(shouldUseReturnTool(null)).toBe(false);
63
+ });
64
+ });
65
+
66
+ describe('getReturnTools', () => {
67
+ it('should return array with single return field tool', () => {
68
+ const tools = getReturnTools();
69
+ expect(tools).toHaveLength(1);
70
+ expect(tools[0].name).toBe(RETURN_FIELD_TOOL);
71
+ });
72
+
73
+ it('should return tool with valid schema', () => {
74
+ const tools = getReturnTools();
75
+ const tool = tools[0];
76
+
77
+ expect(tool.__vibeTool).toBe(true);
78
+ expect(tool.schema.name).toBe(RETURN_FIELD_TOOL);
79
+ expect(tool.schema.parameters).toHaveLength(2);
80
+ expect(tool.schema.parameters[0].name).toBe('field');
81
+ expect(tool.schema.parameters[0].required).toBe(true);
82
+ expect(tool.schema.parameters[1].name).toBe('value');
83
+ expect(tool.schema.parameters[1].required).toBe(true);
84
+ });
85
+ });
86
+
87
+ describe('return field tool executor', () => {
88
+ it('should return field return result object', async () => {
89
+ const tools = getReturnTools();
90
+ const fieldTool = tools[0];
91
+
92
+ const result = await fieldTool.executor({ field: 'value', value: 42 });
93
+ expect(result).toEqual({ __fieldReturn: true, field: 'value', value: 42 });
94
+ });
95
+
96
+ it('should pass through any value type', async () => {
97
+ const tools = getReturnTools();
98
+ const fieldTool = tools[0];
99
+
100
+ // Numbers
101
+ const num = await fieldTool.executor({ field: 'n', value: 123 });
102
+ expect(num).toEqual({ __fieldReturn: true, field: 'n', value: 123 });
103
+
104
+ // Strings
105
+ const str = await fieldTool.executor({ field: 's', value: 'hello' });
106
+ expect(str).toEqual({ __fieldReturn: true, field: 's', value: 'hello' });
107
+
108
+ // Booleans
109
+ const bool = await fieldTool.executor({ field: 'b', value: true });
110
+ expect(bool).toEqual({ __fieldReturn: true, field: 'b', value: true });
111
+
112
+ // Arrays
113
+ const arr = await fieldTool.executor({ field: 'a', value: [1, 2, 3] });
114
+ expect(arr).toEqual({ __fieldReturn: true, field: 'a', value: [1, 2, 3] });
115
+
116
+ // Objects
117
+ const obj = await fieldTool.executor({ field: 'o', value: { name: 'Alice' } });
118
+ expect(obj).toEqual({ __fieldReturn: true, field: 'o', value: { name: 'Alice' } });
119
+ });
120
+ });
121
+
122
+ describe('isFieldReturnResult', () => {
123
+ it('should return true for valid field return result', () => {
124
+ expect(isFieldReturnResult({ __fieldReturn: true, field: 'value', value: 42 })).toBe(true);
125
+ });
126
+
127
+ it('should return false for non-objects', () => {
128
+ expect(isFieldReturnResult(null)).toBe(false);
129
+ expect(isFieldReturnResult(undefined)).toBe(false);
130
+ expect(isFieldReturnResult(42)).toBe(false);
131
+ expect(isFieldReturnResult('string')).toBe(false);
132
+ });
133
+
134
+ it('should return false for objects without __fieldReturn', () => {
135
+ expect(isFieldReturnResult({ field: 'value', value: 42 })).toBe(false);
136
+ expect(isFieldReturnResult({ __fieldReturn: false, field: 'value', value: 42 })).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe('collectAndValidateFieldResults', () => {
141
+ describe('single field validation', () => {
142
+ it('should validate number field', () => {
143
+ const results = [{ __fieldReturn: true as const, field: 'value', value: 42 }];
144
+ const expected = [{ name: 'value', type: 'number' }];
145
+
146
+ const collected = collectAndValidateFieldResults(results, expected);
147
+ expect(collected).toEqual({ value: 42 });
148
+ });
149
+
150
+ it('should validate boolean field', () => {
151
+ const results = [{ __fieldReturn: true as const, field: 'value', value: true }];
152
+ const expected = [{ name: 'value', type: 'boolean' }];
153
+
154
+ const collected = collectAndValidateFieldResults(results, expected);
155
+ expect(collected).toEqual({ value: true });
156
+ });
157
+
158
+ it('should validate text field', () => {
159
+ const results = [{ __fieldReturn: true as const, field: 'value', value: 'hello' }];
160
+ const expected = [{ name: 'value', type: 'text' }];
161
+
162
+ const collected = collectAndValidateFieldResults(results, expected);
163
+ expect(collected).toEqual({ value: 'hello' });
164
+ });
165
+
166
+ it('should validate json field', () => {
167
+ const results = [{ __fieldReturn: true as const, field: 'value', value: { name: 'Alice' } }];
168
+ const expected = [{ name: 'value', type: 'json' }];
169
+
170
+ const collected = collectAndValidateFieldResults(results, expected);
171
+ expect(collected).toEqual({ value: { name: 'Alice' } });
172
+ });
173
+
174
+ it('should throw for wrong number type', () => {
175
+ const results = [{ __fieldReturn: true as const, field: 'value', value: 'not a number' }];
176
+ const expected = [{ name: 'value', type: 'number' }];
177
+
178
+ expect(() => collectAndValidateFieldResults(results, expected)).toThrow(/expected number/);
179
+ });
180
+
181
+ it('should throw for wrong boolean type', () => {
182
+ const results = [{ __fieldReturn: true as const, field: 'value', value: 'yes' }];
183
+ const expected = [{ name: 'value', type: 'boolean' }];
184
+
185
+ expect(() => collectAndValidateFieldResults(results, expected)).toThrow(/expected boolean/);
186
+ });
187
+
188
+ it('should throw for NaN as number', () => {
189
+ const results = [{ __fieldReturn: true as const, field: 'value', value: NaN }];
190
+ const expected = [{ name: 'value', type: 'number' }];
191
+
192
+ expect(() => collectAndValidateFieldResults(results, expected)).toThrow(/expected number/);
193
+ });
194
+ });
195
+
196
+ describe('array type validation', () => {
197
+ it('should validate number[] field', () => {
198
+ const results = [{ __fieldReturn: true as const, field: 'nums', value: [1, 2, 3] }];
199
+ const expected = [{ name: 'nums', type: 'number[]' }];
200
+
201
+ const collected = collectAndValidateFieldResults(results, expected);
202
+ expect(collected).toEqual({ nums: [1, 2, 3] });
203
+ });
204
+
205
+ it('should validate text[] field', () => {
206
+ const results = [{ __fieldReturn: true as const, field: 'items', value: ['a', 'b', 'c'] }];
207
+ const expected = [{ name: 'items', type: 'text[]' }];
208
+
209
+ const collected = collectAndValidateFieldResults(results, expected);
210
+ expect(collected).toEqual({ items: ['a', 'b', 'c'] });
211
+ });
212
+
213
+ it('should validate boolean[] field', () => {
214
+ const results = [{ __fieldReturn: true as const, field: 'flags', value: [true, false, true] }];
215
+ const expected = [{ name: 'flags', type: 'boolean[]' }];
216
+
217
+ const collected = collectAndValidateFieldResults(results, expected);
218
+ expect(collected).toEqual({ flags: [true, false, true] });
219
+ });
220
+
221
+ it('should validate json[] field', () => {
222
+ const results = [{ __fieldReturn: true as const, field: 'objects', value: [{ a: 1 }, { b: 2 }] }];
223
+ const expected = [{ name: 'objects', type: 'json[]' }];
224
+
225
+ const collected = collectAndValidateFieldResults(results, expected);
226
+ expect(collected).toEqual({ objects: [{ a: 1 }, { b: 2 }] });
227
+ });
228
+
229
+ it('should throw for non-array when expecting array', () => {
230
+ const results = [{ __fieldReturn: true as const, field: 'nums', value: 42 }];
231
+ const expected = [{ name: 'nums', type: 'number[]' }];
232
+
233
+ expect(() => collectAndValidateFieldResults(results, expected)).toThrow(/expected number\[\]/);
234
+ });
235
+
236
+ it('should throw for array with wrong element types', () => {
237
+ const results = [{ __fieldReturn: true as const, field: 'nums', value: [1, 'two', 3] }];
238
+ const expected = [{ name: 'nums', type: 'number[]' }];
239
+
240
+ expect(() => collectAndValidateFieldResults(results, expected)).toThrow(/element 1/);
241
+ });
242
+ });
243
+
244
+ describe('multi-field validation', () => {
245
+ it('should validate multiple fields', () => {
246
+ const results = [
247
+ { __fieldReturn: true as const, field: 'name', value: 'Alice' },
248
+ { __fieldReturn: true as const, field: 'age', value: 30 },
249
+ ];
250
+ const expected = [
251
+ { name: 'name', type: 'text' },
252
+ { name: 'age', type: 'number' },
253
+ ];
254
+
255
+ const collected = collectAndValidateFieldResults(results, expected);
256
+ expect(collected).toEqual({ name: 'Alice', age: 30 });
257
+ });
258
+
259
+ it('should throw for missing field', () => {
260
+ const results = [{ __fieldReturn: true as const, field: 'name', value: 'Alice' }];
261
+ const expected = [
262
+ { name: 'name', type: 'text' },
263
+ { name: 'age', type: 'number' },
264
+ ];
265
+
266
+ expect(() => collectAndValidateFieldResults(results, expected)).toThrow(/Missing field 'age'/);
267
+ });
268
+
269
+ it('should throw for unexpected field', () => {
270
+ const results = [
271
+ { __fieldReturn: true as const, field: 'name', value: 'Alice' },
272
+ { __fieldReturn: true as const, field: 'extra', value: 'unexpected' },
273
+ ];
274
+ const expected = [{ name: 'name', type: 'text' }];
275
+
276
+ expect(() => collectAndValidateFieldResults(results, expected)).toThrow(/Unexpected field 'extra'/);
277
+ });
278
+ });
279
+ });
280
+
281
+ describe('buildReturnInstruction', () => {
282
+ it('should return empty string for no fields', () => {
283
+ expect(buildReturnInstruction([])).toBe('');
284
+ });
285
+
286
+ it('should build instruction for single field', () => {
287
+ const instruction = buildReturnInstruction([{ name: 'value', type: 'number' }]);
288
+ expect(instruction).toContain('__vibe_return_field');
289
+ expect(instruction).toContain('"value" (number)');
290
+ });
291
+
292
+ it('should build instruction for multiple fields', () => {
293
+ const instruction = buildReturnInstruction([
294
+ { name: 'name', type: 'text' },
295
+ { name: 'age', type: 'number' },
296
+ ]);
297
+ expect(instruction).toContain('"name" (text)');
298
+ expect(instruction).toContain('"age" (number)');
299
+ });
300
+ });
301
+ });
302
+
303
+ describe('executeWithTools with return tools', () => {
304
+ it('should extract value when return tool called successfully', async () => {
305
+ const tools = getReturnTools();
306
+
307
+ const request: AIRequest = {
308
+ operationType: 'do',
309
+ prompt: 'Return 42',
310
+ contextText: '',
311
+ targetType: null,
312
+ model: { name: 'test', apiKey: 'key', url: null },
313
+ };
314
+
315
+ // AI calls return tool with correct value
316
+ const executeProvider = async (): Promise<AIResponse> => ({
317
+ content: '',
318
+ parsedValue: '',
319
+ toolCalls: [{ id: 'call_1', toolName: RETURN_FIELD_TOOL, args: { field: 'value', value: 42 } }],
320
+ stopReason: 'tool_use',
321
+ });
322
+
323
+ const { returnFieldResults, completedViaReturnTool, rounds } = await executeWithTools(
324
+ request,
325
+ tools,
326
+ TEST_ROOT_DIR,
327
+ executeProvider,
328
+ { expectedReturnTool: RETURN_FIELD_TOOL }
329
+ );
330
+
331
+ expect(completedViaReturnTool).toBe(true);
332
+ expect(returnFieldResults).toHaveLength(1);
333
+ expect(returnFieldResults![0]).toEqual({ __fieldReturn: true, field: 'value', value: 42 });
334
+ expect(rounds).toHaveLength(1);
335
+ });
336
+
337
+ it('should collect multiple field results', async () => {
338
+ const tools = getReturnTools();
339
+
340
+ const request: AIRequest = {
341
+ operationType: 'do',
342
+ prompt: 'Return name and age',
343
+ contextText: '',
344
+ targetType: null,
345
+ model: { name: 'test', apiKey: 'key', url: null },
346
+ };
347
+
348
+ // AI calls return tool twice in one response
349
+ const executeProvider = async (): Promise<AIResponse> => ({
350
+ content: '',
351
+ parsedValue: '',
352
+ toolCalls: [
353
+ { id: 'call_1', toolName: RETURN_FIELD_TOOL, args: { field: 'name', value: 'Alice' } },
354
+ { id: 'call_2', toolName: RETURN_FIELD_TOOL, args: { field: 'age', value: 30 } },
355
+ ],
356
+ stopReason: 'tool_use',
357
+ });
358
+
359
+ const { returnFieldResults, completedViaReturnTool } = await executeWithTools(
360
+ request,
361
+ tools,
362
+ TEST_ROOT_DIR,
363
+ executeProvider,
364
+ { expectedReturnTool: RETURN_FIELD_TOOL }
365
+ );
366
+
367
+ expect(completedViaReturnTool).toBe(true);
368
+ expect(returnFieldResults).toHaveLength(2);
369
+ expect(returnFieldResults![0]).toEqual({ __fieldReturn: true, field: 'name', value: 'Alice' });
370
+ expect(returnFieldResults![1]).toEqual({ __fieldReturn: true, field: 'age', value: 30 });
371
+ });
372
+
373
+ it('should retry when AI responds with text instead of tool call', async () => {
374
+ const tools = getReturnTools();
375
+
376
+ const request: AIRequest = {
377
+ operationType: 'do',
378
+ prompt: 'Return 42',
379
+ contextText: '',
380
+ targetType: null,
381
+ model: { name: 'test', apiKey: 'key', url: null },
382
+ };
383
+
384
+ let callCount = 0;
385
+ const executeProvider = async (): Promise<AIResponse> => {
386
+ callCount++;
387
+ if (callCount === 1) {
388
+ // First call: AI responds with text (wrong)
389
+ return {
390
+ content: 'The answer is 42',
391
+ parsedValue: 'The answer is 42',
392
+ stopReason: 'end',
393
+ };
394
+ }
395
+ // Second call: AI calls return tool correctly
396
+ return {
397
+ content: '',
398
+ parsedValue: '',
399
+ toolCalls: [{ id: 'call_1', toolName: RETURN_FIELD_TOOL, args: { field: 'value', value: 42 } }],
400
+ stopReason: 'tool_use',
401
+ };
402
+ };
403
+
404
+ const { returnFieldResults, completedViaReturnTool, rounds } = await executeWithTools(
405
+ request,
406
+ tools,
407
+ TEST_ROOT_DIR,
408
+ executeProvider,
409
+ { expectedReturnTool: RETURN_FIELD_TOOL, maxRounds: 3 }
410
+ );
411
+
412
+ expect(callCount).toBe(2);
413
+ expect(completedViaReturnTool).toBe(true);
414
+ expect(returnFieldResults).toHaveLength(1);
415
+ // First round has synthesized error, second round has successful tool call
416
+ expect(rounds).toHaveLength(2);
417
+ expect(rounds[0].results[0].error).toContain('must call');
418
+ });
419
+
420
+ it('should not complete via return tool when not expected', async () => {
421
+ const tools = getReturnTools();
422
+
423
+ const request: AIRequest = {
424
+ operationType: 'do',
425
+ prompt: 'Hello',
426
+ contextText: '',
427
+ targetType: null,
428
+ model: { name: 'test', apiKey: 'key', url: null },
429
+ };
430
+
431
+ // No expectedReturnTool, so text response is fine
432
+ const executeProvider = async (): Promise<AIResponse> => ({
433
+ content: 'Hello back!',
434
+ parsedValue: 'Hello back!',
435
+ stopReason: 'end',
436
+ });
437
+
438
+ const { returnFieldResults, completedViaReturnTool, rounds } = await executeWithTools(
439
+ request,
440
+ tools,
441
+ TEST_ROOT_DIR,
442
+ executeProvider
443
+ // No expectedReturnTool
444
+ );
445
+
446
+ expect(completedViaReturnTool).toBe(false);
447
+ expect(returnFieldResults).toBeUndefined();
448
+ expect(rounds).toHaveLength(0);
449
+ });
450
+ });