@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.
- package/package.json +46 -0
- package/src/ast/index.ts +375 -0
- package/src/ast.ts +2 -0
- package/src/debug/advanced-features.ts +482 -0
- package/src/debug/bun-inspector.ts +424 -0
- package/src/debug/handoff-manager.ts +283 -0
- package/src/debug/index.ts +150 -0
- package/src/debug/runner.ts +365 -0
- package/src/debug/server.ts +565 -0
- package/src/debug/stack-merger.ts +267 -0
- package/src/debug/state.ts +581 -0
- package/src/debug/test/advanced-features.test.ts +300 -0
- package/src/debug/test/e2e.test.ts +218 -0
- package/src/debug/test/handoff-manager.test.ts +256 -0
- package/src/debug/test/runner.test.ts +256 -0
- package/src/debug/test/stack-merger.test.ts +163 -0
- package/src/debug/test/state.test.ts +400 -0
- package/src/debug/test/ts-debug-integration.test.ts +374 -0
- package/src/debug/test/ts-import-tracker.test.ts +125 -0
- package/src/debug/test/ts-source-map.test.ts +169 -0
- package/src/debug/ts-import-tracker.ts +151 -0
- package/src/debug/ts-source-map.ts +171 -0
- package/src/errors/index.ts +124 -0
- package/src/index.ts +358 -0
- package/src/lexer/index.ts +348 -0
- package/src/lexer.ts +2 -0
- package/src/parser/index.ts +792 -0
- package/src/parser/parse.ts +45 -0
- package/src/parser/test/async.test.ts +248 -0
- package/src/parser/test/destructuring.test.ts +167 -0
- package/src/parser/test/do-expression.test.ts +486 -0
- package/src/parser/test/errors/do-expression.test.ts +95 -0
- package/src/parser/test/errors/error-locations.test.ts +230 -0
- package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
- package/src/parser/test/errors/missing-tokens.test.ts +126 -0
- package/src/parser/test/errors/model-declaration.test.ts +185 -0
- package/src/parser/test/errors/nested-blocks.test.ts +226 -0
- package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
- package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
- package/src/parser/test/import-export.test.ts +143 -0
- package/src/parser/test/literals.test.ts +404 -0
- package/src/parser/test/model-declaration.test.ts +161 -0
- package/src/parser/test/nested-blocks.test.ts +402 -0
- package/src/parser/test/parser.test.ts +743 -0
- package/src/parser/test/private.test.ts +136 -0
- package/src/parser/test/template-literal.test.ts +127 -0
- package/src/parser/test/tool-declaration.test.ts +302 -0
- package/src/parser/test/ts-block.test.ts +252 -0
- package/src/parser/test/type-annotations.test.ts +254 -0
- package/src/parser/visitor/helpers.ts +330 -0
- package/src/parser/visitor.ts +794 -0
- package/src/parser.ts +2 -0
- package/src/runtime/ai/cache-chunking.test.ts +69 -0
- package/src/runtime/ai/cache-chunking.ts +73 -0
- package/src/runtime/ai/client.ts +109 -0
- package/src/runtime/ai/context.ts +168 -0
- package/src/runtime/ai/formatters.ts +316 -0
- package/src/runtime/ai/index.ts +38 -0
- package/src/runtime/ai/language-ref.ts +38 -0
- package/src/runtime/ai/providers/anthropic.ts +253 -0
- package/src/runtime/ai/providers/google.ts +201 -0
- package/src/runtime/ai/providers/openai.ts +156 -0
- package/src/runtime/ai/retry.ts +100 -0
- package/src/runtime/ai/return-tools.ts +301 -0
- package/src/runtime/ai/test/client.test.ts +83 -0
- package/src/runtime/ai/test/formatters.test.ts +485 -0
- package/src/runtime/ai/test/retry.test.ts +137 -0
- package/src/runtime/ai/test/return-tools.test.ts +450 -0
- package/src/runtime/ai/test/tool-loop.test.ts +319 -0
- package/src/runtime/ai/test/tool-schema.test.ts +241 -0
- package/src/runtime/ai/tool-loop.ts +203 -0
- package/src/runtime/ai/tool-schema.ts +151 -0
- package/src/runtime/ai/types.ts +113 -0
- package/src/runtime/ai-logger.ts +255 -0
- package/src/runtime/ai-provider.ts +347 -0
- package/src/runtime/async/dependencies.ts +276 -0
- package/src/runtime/async/executor.ts +293 -0
- package/src/runtime/async/index.ts +43 -0
- package/src/runtime/async/scheduling.ts +163 -0
- package/src/runtime/async/test/dependencies.test.ts +284 -0
- package/src/runtime/async/test/executor.test.ts +388 -0
- package/src/runtime/context.ts +357 -0
- package/src/runtime/exec/ai.ts +139 -0
- package/src/runtime/exec/expressions.ts +475 -0
- package/src/runtime/exec/frames.ts +26 -0
- package/src/runtime/exec/functions.ts +305 -0
- package/src/runtime/exec/interpolation.ts +312 -0
- package/src/runtime/exec/statements.ts +604 -0
- package/src/runtime/exec/tools.ts +129 -0
- package/src/runtime/exec/typescript.ts +215 -0
- package/src/runtime/exec/variables.ts +279 -0
- package/src/runtime/index.ts +975 -0
- package/src/runtime/modules.ts +452 -0
- package/src/runtime/serialize.ts +103 -0
- package/src/runtime/state.ts +489 -0
- package/src/runtime/stdlib/core.ts +45 -0
- package/src/runtime/stdlib/directory.test.ts +156 -0
- package/src/runtime/stdlib/edit.test.ts +154 -0
- package/src/runtime/stdlib/fastEdit.test.ts +201 -0
- package/src/runtime/stdlib/glob.test.ts +106 -0
- package/src/runtime/stdlib/grep.test.ts +144 -0
- package/src/runtime/stdlib/index.ts +16 -0
- package/src/runtime/stdlib/readFile.test.ts +123 -0
- package/src/runtime/stdlib/tools/index.ts +707 -0
- package/src/runtime/stdlib/writeFile.test.ts +157 -0
- package/src/runtime/step.ts +969 -0
- package/src/runtime/test/ai-context.test.ts +1086 -0
- package/src/runtime/test/ai-result-object.test.ts +419 -0
- package/src/runtime/test/ai-tool-flow.test.ts +859 -0
- package/src/runtime/test/async-execution-order.test.ts +618 -0
- package/src/runtime/test/async-execution.test.ts +344 -0
- package/src/runtime/test/async-nested.test.ts +660 -0
- package/src/runtime/test/async-parallel-timing.test.ts +546 -0
- package/src/runtime/test/basic1.test.ts +154 -0
- package/src/runtime/test/binary-operators.test.ts +431 -0
- package/src/runtime/test/break-statement.test.ts +257 -0
- package/src/runtime/test/context-modes.test.ts +650 -0
- package/src/runtime/test/context.test.ts +466 -0
- package/src/runtime/test/core-functions.test.ts +228 -0
- package/src/runtime/test/e2e.test.ts +88 -0
- package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
- package/src/runtime/test/error-locations/main-error.vibe +4 -0
- package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
- package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
- package/src/runtime/test/for-in.test.ts +312 -0
- package/src/runtime/test/helpers.ts +69 -0
- package/src/runtime/test/imports.test.ts +334 -0
- package/src/runtime/test/json-expressions.test.ts +232 -0
- package/src/runtime/test/literals.test.ts +372 -0
- package/src/runtime/test/logical-indexing.test.ts +478 -0
- package/src/runtime/test/member-methods.test.ts +324 -0
- package/src/runtime/test/model-config.test.ts +338 -0
- package/src/runtime/test/null-handling.test.ts +342 -0
- package/src/runtime/test/private-visibility.test.ts +332 -0
- package/src/runtime/test/runtime-state.test.ts +514 -0
- package/src/runtime/test/scoping.test.ts +370 -0
- package/src/runtime/test/string-interpolation.test.ts +354 -0
- package/src/runtime/test/template-literal.test.ts +181 -0
- package/src/runtime/test/tool-execution.test.ts +467 -0
- package/src/runtime/test/tool-schema-generation.test.ts +477 -0
- package/src/runtime/test/tostring.test.ts +210 -0
- package/src/runtime/test/ts-block.test.ts +594 -0
- package/src/runtime/test/ts-error-location.test.ts +231 -0
- package/src/runtime/test/types.test.ts +732 -0
- package/src/runtime/test/verbose-logger.test.ts +710 -0
- package/src/runtime/test/vibe-expression.test.ts +54 -0
- package/src/runtime/test/vibe-value-errors.test.ts +541 -0
- package/src/runtime/test/while.test.ts +232 -0
- package/src/runtime/tools/builtin.ts +30 -0
- package/src/runtime/tools/directory-tools.ts +70 -0
- package/src/runtime/tools/file-tools.ts +228 -0
- package/src/runtime/tools/index.ts +5 -0
- package/src/runtime/tools/registry.ts +48 -0
- package/src/runtime/tools/search-tools.ts +134 -0
- package/src/runtime/tools/security.ts +36 -0
- package/src/runtime/tools/system-tools.ts +312 -0
- package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
- package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
- package/src/runtime/tools/test/registry.test.ts +713 -0
- package/src/runtime/tools/test/security.test.ts +86 -0
- package/src/runtime/tools/test/system-tools.test.ts +679 -0
- package/src/runtime/tools/test/ts-schema.test.ts +357 -0
- package/src/runtime/tools/ts-schema.ts +341 -0
- package/src/runtime/tools/types.ts +89 -0
- package/src/runtime/tools/utility-tools.ts +198 -0
- package/src/runtime/ts-eval.ts +126 -0
- package/src/runtime/types.ts +797 -0
- package/src/runtime/validation.ts +160 -0
- package/src/runtime/verbose-logger.ts +459 -0
- package/src/runtime.ts +2 -0
- package/src/semantic/analyzer-context.ts +62 -0
- package/src/semantic/analyzer-validators.ts +575 -0
- package/src/semantic/analyzer-visitors.ts +534 -0
- package/src/semantic/analyzer.ts +83 -0
- package/src/semantic/index.ts +11 -0
- package/src/semantic/symbol-table.ts +58 -0
- package/src/semantic/test/async-validation.test.ts +301 -0
- package/src/semantic/test/compress-validation.test.ts +179 -0
- package/src/semantic/test/const-reassignment.test.ts +111 -0
- package/src/semantic/test/control-flow.test.ts +346 -0
- package/src/semantic/test/destructuring.test.ts +185 -0
- package/src/semantic/test/duplicate-declarations.test.ts +168 -0
- package/src/semantic/test/export-validation.test.ts +111 -0
- package/src/semantic/test/fixtures/math.ts +31 -0
- package/src/semantic/test/imports.test.ts +148 -0
- package/src/semantic/test/json-type.test.ts +68 -0
- package/src/semantic/test/literals.test.ts +127 -0
- package/src/semantic/test/model-validation.test.ts +179 -0
- package/src/semantic/test/prompt-validation.test.ts +343 -0
- package/src/semantic/test/scoping.test.ts +312 -0
- package/src/semantic/test/tool-validation.test.ts +306 -0
- package/src/semantic/test/ts-type-checking.test.ts +563 -0
- package/src/semantic/test/type-constraints.test.ts +111 -0
- package/src/semantic/test/type-inference.test.ts +87 -0
- package/src/semantic/test/type-validation.test.ts +552 -0
- package/src/semantic/test/undefined-variables.test.ts +163 -0
- package/src/semantic/ts-block-checker.ts +204 -0
- package/src/semantic/ts-signatures.ts +194 -0
- package/src/semantic/ts-types.ts +170 -0
- package/src/semantic/types.ts +58 -0
- package/tests/fixtures/conditional-logic.vibe +14 -0
- package/tests/fixtures/function-call.vibe +16 -0
- package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
- package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
- package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
- package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
- package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
- package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
- package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
- package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
- package/tests/fixtures/imports/nested-import/main.vibe +3 -0
- package/tests/fixtures/imports/nested-import/utils.ts +3 -0
- package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
- package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
- package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
- package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
- package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
- package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
- package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
- package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
- package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
- package/tests/fixtures/imports/ts-import/main.vibe +4 -0
- package/tests/fixtures/imports/ts-import/math.ts +9 -0
- package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/data.ts +10 -0
- package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
- package/tests/fixtures/multiple-ai-calls.vibe +10 -0
- package/tests/fixtures/simple-greeting.vibe +6 -0
- package/tests/fixtures/template-literals.vibe +11 -0
- package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
- package/tests/integration/basic-ai/basic-ai.vibe +12 -0
- package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
- package/tests/integration/bug-fix/buggy-code.ts +22 -0
- package/tests/integration/bug-fix/fix-bug.vibe +21 -0
- package/tests/integration/compress/compress.integration.test.ts +206 -0
- package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
- package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
- package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
- package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
- package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
- package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
- package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
- package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
// Formatter tests
|
|
2
|
+
|
|
3
|
+
import { describe, test, expect } from 'bun:test';
|
|
4
|
+
import {
|
|
5
|
+
buildSystemMessage,
|
|
6
|
+
buildContextMessage,
|
|
7
|
+
buildPromptMessage,
|
|
8
|
+
buildMessages,
|
|
9
|
+
buildToolSystemMessage,
|
|
10
|
+
extractTextContent,
|
|
11
|
+
extractUsage,
|
|
12
|
+
} from '../formatters';
|
|
13
|
+
import type { ToolSchema } from '../../tools/types';
|
|
14
|
+
|
|
15
|
+
describe('buildSystemMessage', () => {
|
|
16
|
+
test('returns a system message string', () => {
|
|
17
|
+
const message = buildSystemMessage();
|
|
18
|
+
expect(typeof message).toBe('string');
|
|
19
|
+
expect(message.length).toBeGreaterThan(0);
|
|
20
|
+
expect(message).toContain('Vibe');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('buildContextMessage', () => {
|
|
25
|
+
test('returns null for empty context', () => {
|
|
26
|
+
expect(buildContextMessage('')).toBeNull();
|
|
27
|
+
expect(buildContextMessage(' ')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('wraps context with header', () => {
|
|
31
|
+
const message = buildContextMessage('Variable x = 5');
|
|
32
|
+
expect(message).toContain('context');
|
|
33
|
+
expect(message).toContain('Variable x = 5');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('buildPromptMessage', () => {
|
|
38
|
+
test('returns prompt as-is', () => {
|
|
39
|
+
// Type instructions are now handled by return tools, not prompt modification
|
|
40
|
+
expect(buildPromptMessage('Hello')).toBe('Hello');
|
|
41
|
+
expect(buildPromptMessage('Calculate 2+2')).toBe('Calculate 2+2');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('buildToolSystemMessage', () => {
|
|
46
|
+
test('returns null for empty tools array', () => {
|
|
47
|
+
expect(buildToolSystemMessage([])).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('formats single tool with description', () => {
|
|
51
|
+
const tools: ToolSchema[] = [
|
|
52
|
+
{
|
|
53
|
+
name: 'getWeather',
|
|
54
|
+
description: 'Get weather for a city',
|
|
55
|
+
parameters: [
|
|
56
|
+
{ name: 'city', type: { type: 'string' }, required: true },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const message = buildToolSystemMessage(tools);
|
|
62
|
+
|
|
63
|
+
expect(message).toBe(
|
|
64
|
+
`You have access to the following tools:
|
|
65
|
+
- getWeather(city: string)
|
|
66
|
+
Get weather for a city
|
|
67
|
+
|
|
68
|
+
Call tools when needed to complete the task.`
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('full system message with multiple user-defined tools', () => {
|
|
73
|
+
// This represents what tools defined in Vibe would look like:
|
|
74
|
+
// tool add(a: number, b: number): number @description "Add two numbers" @param a "First number" @param b "Second number"
|
|
75
|
+
// tool multiply(x: number, y: number): number @description "Multiply two numbers"
|
|
76
|
+
// tool fetchData(url: text): json @description "Fetch JSON from URL" @param url "The URL to fetch"
|
|
77
|
+
const tools: ToolSchema[] = [
|
|
78
|
+
{
|
|
79
|
+
name: 'add',
|
|
80
|
+
description: 'Add two numbers',
|
|
81
|
+
parameters: [
|
|
82
|
+
{ name: 'a', type: { type: 'number' }, description: 'First number', required: true },
|
|
83
|
+
{ name: 'b', type: { type: 'number' }, description: 'Second number', required: true },
|
|
84
|
+
],
|
|
85
|
+
returns: { type: 'number' },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'multiply',
|
|
89
|
+
description: 'Multiply two numbers',
|
|
90
|
+
parameters: [
|
|
91
|
+
{ name: 'x', type: { type: 'number' }, required: true },
|
|
92
|
+
{ name: 'y', type: { type: 'number' }, required: true },
|
|
93
|
+
],
|
|
94
|
+
returns: { type: 'number' },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'fetchData',
|
|
98
|
+
description: 'Fetch JSON from URL',
|
|
99
|
+
parameters: [
|
|
100
|
+
{ name: 'url', type: { type: 'string' }, description: 'The URL to fetch', required: true },
|
|
101
|
+
],
|
|
102
|
+
returns: { type: 'object' },
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const message = buildToolSystemMessage(tools);
|
|
107
|
+
|
|
108
|
+
expect(message).toBe(
|
|
109
|
+
`You have access to the following tools:
|
|
110
|
+
- add(a: number, b: number)
|
|
111
|
+
Add two numbers
|
|
112
|
+
Parameters:
|
|
113
|
+
a: First number
|
|
114
|
+
b: Second number
|
|
115
|
+
- multiply(x: number, y: number)
|
|
116
|
+
Multiply two numbers
|
|
117
|
+
- fetchData(url: string)
|
|
118
|
+
Fetch JSON from URL
|
|
119
|
+
Parameters:
|
|
120
|
+
url: The URL to fetch
|
|
121
|
+
|
|
122
|
+
Call tools when needed to complete the task.`
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('full message array with tools, context, and prompt', () => {
|
|
127
|
+
const tools: ToolSchema[] = [
|
|
128
|
+
{
|
|
129
|
+
name: 'calculate',
|
|
130
|
+
description: 'Perform arithmetic',
|
|
131
|
+
parameters: [
|
|
132
|
+
{ name: 'expression', type: { type: 'string' }, description: 'Math expression', required: true },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'storeResult',
|
|
137
|
+
description: 'Store a calculation result',
|
|
138
|
+
parameters: [
|
|
139
|
+
{ name: 'key', type: { type: 'string' }, required: true },
|
|
140
|
+
{ name: 'value', type: { type: 'number' }, required: true },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const contextText = `<entry> (current scope)
|
|
146
|
+
- x (number): 10
|
|
147
|
+
- y (number): 20`;
|
|
148
|
+
|
|
149
|
+
const messages = buildMessages(
|
|
150
|
+
'Calculate x + y and store it',
|
|
151
|
+
contextText,
|
|
152
|
+
tools
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Verify complete message structure
|
|
156
|
+
expect(messages).toHaveLength(4);
|
|
157
|
+
|
|
158
|
+
// Message 0: Base system message
|
|
159
|
+
expect(messages[0]).toEqual({
|
|
160
|
+
role: 'system',
|
|
161
|
+
content: `You are an AI assistant integrated into the Vibe programming language runtime.
|
|
162
|
+
Your responses will be used programmatically in the execution flow.
|
|
163
|
+
Be concise and precise. Follow any type constraints exactly.
|
|
164
|
+
When context is provided, use it to inform your response.`,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Message 1: Tool system message
|
|
168
|
+
expect(messages[1]).toEqual({
|
|
169
|
+
role: 'system',
|
|
170
|
+
content: `You have access to the following tools:
|
|
171
|
+
- calculate(expression: string)
|
|
172
|
+
Perform arithmetic
|
|
173
|
+
Parameters:
|
|
174
|
+
expression: Math expression
|
|
175
|
+
- storeResult(key: string, value: number)
|
|
176
|
+
Store a calculation result
|
|
177
|
+
|
|
178
|
+
Call tools when needed to complete the task.`,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Message 2: Context message
|
|
182
|
+
expect(messages[2]).toEqual({
|
|
183
|
+
role: 'user',
|
|
184
|
+
content: `Here is the current program context:
|
|
185
|
+
|
|
186
|
+
<entry> (current scope)
|
|
187
|
+
- x (number): 10
|
|
188
|
+
- y (number): 20`,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Message 3: Prompt
|
|
192
|
+
expect(messages[3]).toEqual({
|
|
193
|
+
role: 'user',
|
|
194
|
+
content: 'Calculate x + y and store it',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('formats tool without description', () => {
|
|
199
|
+
const tools: ToolSchema[] = [
|
|
200
|
+
{
|
|
201
|
+
name: 'now',
|
|
202
|
+
parameters: [],
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const message = buildToolSystemMessage(tools);
|
|
207
|
+
|
|
208
|
+
expect(message).toBe(
|
|
209
|
+
`You have access to the following tools:
|
|
210
|
+
- now()
|
|
211
|
+
|
|
212
|
+
Call tools when needed to complete the task.`
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('formats multiple tools with various parameters', () => {
|
|
217
|
+
const tools: ToolSchema[] = [
|
|
218
|
+
{
|
|
219
|
+
name: 'add',
|
|
220
|
+
description: 'Add two numbers',
|
|
221
|
+
parameters: [
|
|
222
|
+
{ name: 'a', type: { type: 'number' }, required: true },
|
|
223
|
+
{ name: 'b', type: { type: 'number' }, required: true },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'greet',
|
|
228
|
+
description: 'Greet someone',
|
|
229
|
+
parameters: [
|
|
230
|
+
{ name: 'name', type: { type: 'string' }, required: true },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'now',
|
|
235
|
+
description: 'Get current timestamp',
|
|
236
|
+
parameters: [],
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
const message = buildToolSystemMessage(tools);
|
|
241
|
+
|
|
242
|
+
expect(message).toBe(
|
|
243
|
+
`You have access to the following tools:
|
|
244
|
+
- add(a: number, b: number)
|
|
245
|
+
Add two numbers
|
|
246
|
+
- greet(name: string)
|
|
247
|
+
Greet someone
|
|
248
|
+
- now()
|
|
249
|
+
Get current timestamp
|
|
250
|
+
|
|
251
|
+
Call tools when needed to complete the task.`
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('formats tool with many parameters', () => {
|
|
256
|
+
const tools: ToolSchema[] = [
|
|
257
|
+
{
|
|
258
|
+
name: 'sendEmail',
|
|
259
|
+
description: 'Send an email',
|
|
260
|
+
parameters: [
|
|
261
|
+
{ name: 'to', type: { type: 'string' }, required: true },
|
|
262
|
+
{ name: 'subject', type: { type: 'string' }, required: true },
|
|
263
|
+
{ name: 'body', type: { type: 'string' }, required: true },
|
|
264
|
+
{ name: 'cc', type: { type: 'string' }, required: false },
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
const message = buildToolSystemMessage(tools);
|
|
270
|
+
|
|
271
|
+
expect(message).toBe(
|
|
272
|
+
`You have access to the following tools:
|
|
273
|
+
- sendEmail(to: string, subject: string, body: string, cc: string)
|
|
274
|
+
Send an email
|
|
275
|
+
|
|
276
|
+
Call tools when needed to complete the task.`
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('formats tool with complex nested object parameter', () => {
|
|
281
|
+
const tools: ToolSchema[] = [
|
|
282
|
+
{
|
|
283
|
+
name: 'createOrder',
|
|
284
|
+
description: 'Create a new order',
|
|
285
|
+
parameters: [
|
|
286
|
+
{
|
|
287
|
+
name: 'customer',
|
|
288
|
+
type: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: {
|
|
291
|
+
name: { type: 'string' },
|
|
292
|
+
email: { type: 'string' },
|
|
293
|
+
address: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: {
|
|
296
|
+
street: { type: 'string' },
|
|
297
|
+
city: { type: 'string' },
|
|
298
|
+
zip: { type: 'string' },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
description: 'Customer information',
|
|
304
|
+
required: true,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'items',
|
|
308
|
+
type: {
|
|
309
|
+
type: 'array',
|
|
310
|
+
items: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: {
|
|
313
|
+
productId: { type: 'string' },
|
|
314
|
+
quantity: { type: 'number' },
|
|
315
|
+
price: { type: 'number' },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
description: 'Order items',
|
|
320
|
+
required: true,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
const message = buildToolSystemMessage(tools);
|
|
327
|
+
|
|
328
|
+
expect(message).toBe(
|
|
329
|
+
`You have access to the following tools:
|
|
330
|
+
- createOrder(customer: {name: string, email: string, address: {street: string, city: string, zip: string}}, items: {productId: string, quantity: number, price: number}[])
|
|
331
|
+
Create a new order
|
|
332
|
+
Parameters:
|
|
333
|
+
customer: Customer information
|
|
334
|
+
items: Order items
|
|
335
|
+
|
|
336
|
+
Call tools when needed to complete the task.`
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('buildMessages', () => {
|
|
342
|
+
test('builds messages with system and prompt', () => {
|
|
343
|
+
const messages = buildMessages('Hello', '');
|
|
344
|
+
|
|
345
|
+
expect(messages).toHaveLength(2);
|
|
346
|
+
expect(messages[0].role).toBe('system');
|
|
347
|
+
expect(messages[1].role).toBe('user');
|
|
348
|
+
expect(messages[1].content).toBe('Hello');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('includes context message when provided', () => {
|
|
352
|
+
const messages = buildMessages('Hello', 'x = 5');
|
|
353
|
+
|
|
354
|
+
expect(messages).toHaveLength(3);
|
|
355
|
+
expect(messages[0].role).toBe('system');
|
|
356
|
+
expect(messages[1].role).toBe('user');
|
|
357
|
+
expect(messages[1].content).toContain('x = 5');
|
|
358
|
+
expect(messages[2].role).toBe('user');
|
|
359
|
+
expect(messages[2].content).toBe('Hello');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('includes tool system message when tools provided', () => {
|
|
363
|
+
const tools: ToolSchema[] = [
|
|
364
|
+
{
|
|
365
|
+
name: 'add',
|
|
366
|
+
description: 'Add two numbers',
|
|
367
|
+
parameters: [
|
|
368
|
+
{ name: 'a', type: { type: 'number' }, required: true },
|
|
369
|
+
{ name: 'b', type: { type: 'number' }, required: true },
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
const messages = buildMessages('Calculate 2+3', '', tools);
|
|
375
|
+
|
|
376
|
+
// Should have: system, tool system, prompt
|
|
377
|
+
expect(messages).toHaveLength(3);
|
|
378
|
+
expect(messages[0].role).toBe('system');
|
|
379
|
+
expect(messages[1].role).toBe('system');
|
|
380
|
+
expect(messages[1].content).toContain('You have access to the following tools:');
|
|
381
|
+
expect(messages[1].content).toContain('- add(a: number, b: number)');
|
|
382
|
+
expect(messages[1].content).toContain('Add two numbers');
|
|
383
|
+
expect(messages[2].role).toBe('user');
|
|
384
|
+
expect(messages[2].content).toBe('Calculate 2+3');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('includes all message types: system, tools, context, prompt', () => {
|
|
388
|
+
const tools: ToolSchema[] = [
|
|
389
|
+
{
|
|
390
|
+
name: 'getWeather',
|
|
391
|
+
description: 'Get weather',
|
|
392
|
+
parameters: [{ name: 'city', type: { type: 'string' }, required: true }],
|
|
393
|
+
},
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
const messages = buildMessages(
|
|
397
|
+
'What is the weather?',
|
|
398
|
+
'location = Seattle',
|
|
399
|
+
tools
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// Should have: system, tool system, context, prompt
|
|
403
|
+
expect(messages).toHaveLength(4);
|
|
404
|
+
expect(messages[0].role).toBe('system');
|
|
405
|
+
expect(messages[0].content).toContain('Vibe');
|
|
406
|
+
expect(messages[1].role).toBe('system');
|
|
407
|
+
expect(messages[1].content).toContain('getWeather(city: string)');
|
|
408
|
+
expect(messages[1].content).toContain('Get weather');
|
|
409
|
+
expect(messages[2].role).toBe('user');
|
|
410
|
+
expect(messages[2].content).toContain('location = Seattle');
|
|
411
|
+
expect(messages[3].role).toBe('user');
|
|
412
|
+
expect(messages[3].content).toBe('What is the weather?');
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('extractTextContent', () => {
|
|
417
|
+
test('extracts from Anthropic format', () => {
|
|
418
|
+
const response = {
|
|
419
|
+
content: [{ type: 'text', text: 'Hello from Claude' }],
|
|
420
|
+
};
|
|
421
|
+
expect(extractTextContent(response)).toBe('Hello from Claude');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('extracts from OpenAI format', () => {
|
|
425
|
+
const response = {
|
|
426
|
+
choices: [{ message: { content: 'Hello from GPT' } }],
|
|
427
|
+
};
|
|
428
|
+
expect(extractTextContent(response)).toBe('Hello from GPT');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('extracts from Google format', () => {
|
|
432
|
+
const response = {
|
|
433
|
+
candidates: [
|
|
434
|
+
{
|
|
435
|
+
content: {
|
|
436
|
+
parts: [{ text: 'Hello from Gemini' }],
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
expect(extractTextContent(response)).toBe('Hello from Gemini');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('throws for unknown format', () => {
|
|
445
|
+
expect(() => extractTextContent({})).toThrow();
|
|
446
|
+
expect(() => extractTextContent({ unknown: 'format' })).toThrow();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe('extractUsage', () => {
|
|
451
|
+
test('extracts from Anthropic format', () => {
|
|
452
|
+
const response = {
|
|
453
|
+
usage: { input_tokens: 10, output_tokens: 20 },
|
|
454
|
+
};
|
|
455
|
+
expect(extractUsage(response)).toEqual({
|
|
456
|
+
inputTokens: 10,
|
|
457
|
+
outputTokens: 20,
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('extracts from OpenAI format', () => {
|
|
462
|
+
const response = {
|
|
463
|
+
usage: { prompt_tokens: 15, completion_tokens: 25 },
|
|
464
|
+
};
|
|
465
|
+
expect(extractUsage(response)).toEqual({
|
|
466
|
+
inputTokens: 15,
|
|
467
|
+
outputTokens: 25,
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test('extracts from Google format', () => {
|
|
472
|
+
const response = {
|
|
473
|
+
usageMetadata: { promptTokenCount: 8, candidatesTokenCount: 12 },
|
|
474
|
+
};
|
|
475
|
+
expect(extractUsage(response)).toEqual({
|
|
476
|
+
inputTokens: 8,
|
|
477
|
+
outputTokens: 12,
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test('returns undefined for missing usage', () => {
|
|
482
|
+
expect(extractUsage({})).toBeUndefined();
|
|
483
|
+
expect(extractUsage({ content: 'text' })).toBeUndefined();
|
|
484
|
+
});
|
|
485
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Retry logic tests
|
|
2
|
+
|
|
3
|
+
import { describe, test, expect, mock } from 'bun:test';
|
|
4
|
+
import {
|
|
5
|
+
isRetryableError,
|
|
6
|
+
calculateDelay,
|
|
7
|
+
withRetry,
|
|
8
|
+
createAIErrorFromResponse,
|
|
9
|
+
} from '../retry';
|
|
10
|
+
import { AIError } from '../types';
|
|
11
|
+
|
|
12
|
+
describe('isRetryableError', () => {
|
|
13
|
+
test('returns true for AIError with isRetryable flag', () => {
|
|
14
|
+
const error = new AIError('Rate limited', 429, true);
|
|
15
|
+
expect(isRetryableError(error)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('returns false for AIError without isRetryable flag', () => {
|
|
19
|
+
const error = new AIError('Bad request', 400, false);
|
|
20
|
+
expect(isRetryableError(error)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('returns true for network-related errors', () => {
|
|
24
|
+
expect(isRetryableError(new Error('network error'))).toBe(true);
|
|
25
|
+
expect(isRetryableError(new Error('ECONNRESET'))).toBe(true);
|
|
26
|
+
expect(isRetryableError(new Error('timeout'))).toBe(true);
|
|
27
|
+
expect(isRetryableError(new Error('socket hang up'))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('returns false for non-retryable errors', () => {
|
|
31
|
+
expect(isRetryableError(new Error('Unknown error'))).toBe(false);
|
|
32
|
+
expect(isRetryableError(new Error('Invalid API key'))).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('calculateDelay', () => {
|
|
37
|
+
test('returns delay with exponential growth', () => {
|
|
38
|
+
const delay0 = calculateDelay(0, 1000, 30000);
|
|
39
|
+
const delay1 = calculateDelay(1, 1000, 30000);
|
|
40
|
+
const delay2 = calculateDelay(2, 1000, 30000);
|
|
41
|
+
|
|
42
|
+
// Each should be roughly double the previous (with jitter)
|
|
43
|
+
// delay0 is base * 2^0 * jitter = 1000 * 1 * (0.5-1.0) = 500-1000
|
|
44
|
+
expect(delay0).toBeGreaterThanOrEqual(500);
|
|
45
|
+
expect(delay0).toBeLessThanOrEqual(1000);
|
|
46
|
+
|
|
47
|
+
// delay1 is base * 2^1 * jitter = 1000 * 2 * (0.5-1.0) = 1000-2000
|
|
48
|
+
expect(delay1).toBeGreaterThanOrEqual(1000);
|
|
49
|
+
expect(delay1).toBeLessThanOrEqual(2000);
|
|
50
|
+
|
|
51
|
+
// delay2 is base * 2^2 * jitter = 1000 * 4 * (0.5-1.0) = 2000-4000
|
|
52
|
+
expect(delay2).toBeGreaterThanOrEqual(2000);
|
|
53
|
+
expect(delay2).toBeLessThanOrEqual(4000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('caps delay at maxDelay', () => {
|
|
57
|
+
const delay = calculateDelay(10, 1000, 5000);
|
|
58
|
+
// At attempt 10, base delay would be 1000 * 2^10 = 1024000
|
|
59
|
+
// But capped at 5000 with jitter = 2500-5000
|
|
60
|
+
expect(delay).toBeGreaterThanOrEqual(2500);
|
|
61
|
+
expect(delay).toBeLessThanOrEqual(5000);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('createAIErrorFromResponse', () => {
|
|
66
|
+
test('creates retryable error for 429', () => {
|
|
67
|
+
const error = createAIErrorFromResponse(429, 'Rate limited');
|
|
68
|
+
expect(error.statusCode).toBe(429);
|
|
69
|
+
expect(error.isRetryable).toBe(true);
|
|
70
|
+
expect(error.message).toContain('Rate limited');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('creates retryable error for 5xx', () => {
|
|
74
|
+
const error500 = createAIErrorFromResponse(500, 'Internal error');
|
|
75
|
+
expect(error500.isRetryable).toBe(true);
|
|
76
|
+
|
|
77
|
+
const error503 = createAIErrorFromResponse(503, 'Service unavailable');
|
|
78
|
+
expect(error503.isRetryable).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('creates non-retryable error for 4xx (except 429)', () => {
|
|
82
|
+
const error400 = createAIErrorFromResponse(400, 'Bad request');
|
|
83
|
+
expect(error400.isRetryable).toBe(false);
|
|
84
|
+
|
|
85
|
+
const error401 = createAIErrorFromResponse(401, 'Unauthorized');
|
|
86
|
+
expect(error401.isRetryable).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('withRetry', () => {
|
|
91
|
+
test('returns result on first success', async () => {
|
|
92
|
+
const fn = mock().mockResolvedValue('success');
|
|
93
|
+
const result = await withRetry(fn, { maxRetries: 3 });
|
|
94
|
+
|
|
95
|
+
expect(result).toBe('success');
|
|
96
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('retries on retryable error', async () => {
|
|
100
|
+
const fn = mock()
|
|
101
|
+
.mockRejectedValueOnce(new AIError('Rate limited', 429, true))
|
|
102
|
+
.mockResolvedValue('success');
|
|
103
|
+
|
|
104
|
+
const result = await withRetry(fn, { maxRetries: 3, baseDelayMs: 10 });
|
|
105
|
+
|
|
106
|
+
expect(result).toBe('success');
|
|
107
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('does not retry on non-retryable error', async () => {
|
|
111
|
+
const fn = mock().mockRejectedValue(new AIError('Bad request', 400, false));
|
|
112
|
+
|
|
113
|
+
await expect(withRetry(fn, { maxRetries: 3 })).rejects.toThrow('Bad request');
|
|
114
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('throws after max retries', async () => {
|
|
118
|
+
const fn = mock().mockRejectedValue(new AIError('Rate limited', 429, true));
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
withRetry(fn, { maxRetries: 2, baseDelayMs: 10 })
|
|
122
|
+
).rejects.toThrow('Rate limited');
|
|
123
|
+
|
|
124
|
+
expect(fn).toHaveBeenCalledTimes(3); // Initial + 2 retries
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('retries on network errors', async () => {
|
|
128
|
+
const fn = mock()
|
|
129
|
+
.mockRejectedValueOnce(new Error('network error'))
|
|
130
|
+
.mockResolvedValue('success');
|
|
131
|
+
|
|
132
|
+
const result = await withRetry(fn, { maxRetries: 3, baseDelayMs: 10 });
|
|
133
|
+
|
|
134
|
+
expect(result).toBe('success');
|
|
135
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
136
|
+
});
|
|
137
|
+
});
|