@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,1086 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { parse } from '../../parser/parse';
|
|
3
|
+
import {
|
|
4
|
+
createInitialState,
|
|
5
|
+
runUntilPause,
|
|
6
|
+
resumeWithAIResponse,
|
|
7
|
+
buildLocalContext,
|
|
8
|
+
type ContextVariable,
|
|
9
|
+
} from '../index';
|
|
10
|
+
import { formatContextForAI } from '../context';
|
|
11
|
+
import { runWithMockAI } from './helpers';
|
|
12
|
+
|
|
13
|
+
describe('AI Context Tests', () => {
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Prompt type filtering in AI calls
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
test('prompt typed variables filtered from context during do call', () => {
|
|
19
|
+
const ast = parse(`
|
|
20
|
+
const SYSTEM: prompt = "You are a helpful assistant"
|
|
21
|
+
let userQuestion: prompt = "What is 2+2?"
|
|
22
|
+
let userData = "some user data"
|
|
23
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
24
|
+
let result = vibe userQuestion m default
|
|
25
|
+
`);
|
|
26
|
+
|
|
27
|
+
let state = createInitialState(ast);
|
|
28
|
+
state = runUntilPause(state);
|
|
29
|
+
|
|
30
|
+
expect(state.status).toBe('awaiting_ai');
|
|
31
|
+
expect(state.pendingAI?.prompt).toBe('What is 2+2?');
|
|
32
|
+
|
|
33
|
+
// Prompt typed variables (SYSTEM, userQuestion) and model should be filtered out
|
|
34
|
+
// Only userData should remain
|
|
35
|
+
expect(state.localContext).toEqual([
|
|
36
|
+
{ kind: 'variable', name: 'userData', value: 'some user data', type: 'text', isConst: false, frameName: '<entry>', frameDepth: 0, source: null },
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
expect(state.globalContext).toEqual(state.localContext);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('prompt typed variables filtered in nested function context', () => {
|
|
43
|
+
const ast = parse(`
|
|
44
|
+
const GLOBAL_PROMPT: prompt = "Global system prompt"
|
|
45
|
+
const GLOBAL_DATA = "global data value"
|
|
46
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
47
|
+
|
|
48
|
+
function processQuery(input: text): text {
|
|
49
|
+
const LOCAL_PROMPT: prompt = "Process this: {input}"
|
|
50
|
+
let localData = "local data"
|
|
51
|
+
return vibe LOCAL_PROMPT m default
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let result = processQuery("user query")
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
let state = createInitialState(ast);
|
|
58
|
+
state = runUntilPause(state);
|
|
59
|
+
|
|
60
|
+
expect(state.status).toBe('awaiting_ai');
|
|
61
|
+
// With unified interpolation, {input} is left as reference in prompt-typed variables
|
|
62
|
+
expect(state.pendingAI?.prompt).toBe('Process this: {input}');
|
|
63
|
+
|
|
64
|
+
// Local context: function frame only, LOCAL_PROMPT filtered out
|
|
65
|
+
// Note: function parameters now have explicit type annotations
|
|
66
|
+
expect(state.localContext).toEqual([
|
|
67
|
+
{ kind: 'variable', name: 'input', value: 'user query', type: 'text', isConst: false, frameName: 'processQuery', frameDepth: 1, source: null },
|
|
68
|
+
{ kind: 'variable', name: 'localData', value: 'local data', type: 'text', isConst: false, frameName: 'processQuery', frameDepth: 1, source: null },
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
// Global context: entry frame + function frame, all prompts and model filtered
|
|
72
|
+
expect(state.globalContext).toEqual([
|
|
73
|
+
{ kind: 'variable', name: 'GLOBAL_DATA', value: 'global data value', type: 'text', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
74
|
+
{ kind: 'variable', name: 'input', value: 'user query', type: 'text', isConst: false, frameName: 'processQuery', frameDepth: 1, source: null },
|
|
75
|
+
{ kind: 'variable', name: 'localData', value: 'local data', type: 'text', isConst: false, frameName: 'processQuery', frameDepth: 1, source: null },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('prompt filtering with multiple do calls and result assignment', () => {
|
|
80
|
+
// Assignment order: inputData, model, ANALYZE_PROMPT, analyzed, SUMMARIZE_PROMPT, summary
|
|
81
|
+
// Context should show visible variables in assignment order (prompts/model filtered)
|
|
82
|
+
const ast = parse(`
|
|
83
|
+
let inputData = "raw data"
|
|
84
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
85
|
+
const ANALYZE_PROMPT: prompt = "Analyze this"
|
|
86
|
+
let analyzed = vibe ANALYZE_PROMPT m default
|
|
87
|
+
const SUMMARIZE_PROMPT: prompt = "Summarize this"
|
|
88
|
+
let summary = vibe SUMMARIZE_PROMPT m default
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
let state = createInitialState(ast);
|
|
92
|
+
|
|
93
|
+
// First do call - after inputData, model, ANALYZE_PROMPT assigned
|
|
94
|
+
state = runUntilPause(state);
|
|
95
|
+
expect(state.status).toBe('awaiting_ai');
|
|
96
|
+
expect(state.pendingAI?.prompt).toBe('Analyze this');
|
|
97
|
+
|
|
98
|
+
// Context at first AI call: only inputData visible (model, ANALYZE_PROMPT filtered)
|
|
99
|
+
expect(state.localContext).toEqual([
|
|
100
|
+
{ kind: 'variable', name: 'inputData', value: 'raw data', type: 'text', isConst: false, frameName: '<entry>', frameDepth: 0, source: null },
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// Verify formatted text context at first pause
|
|
104
|
+
const formatted1 = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
105
|
+
expect(formatted1.text).toBe(
|
|
106
|
+
` <entry> (current scope)
|
|
107
|
+
- inputData (text): raw data`
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Resume - analyzed gets assigned, then SUMMARIZE_PROMPT, then pause at second do
|
|
111
|
+
state = resumeWithAIResponse(state, 'analysis result');
|
|
112
|
+
state = runUntilPause(state);
|
|
113
|
+
expect(state.status).toBe('awaiting_ai');
|
|
114
|
+
expect(state.pendingAI?.prompt).toBe('Summarize this');
|
|
115
|
+
|
|
116
|
+
// Context at second AI call: inputData, do prompt with response, analyzed (in execution order)
|
|
117
|
+
expect(state.localContext).toEqual([
|
|
118
|
+
{ kind: 'variable', name: 'inputData', value: 'raw data', type: 'text', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
119
|
+
{ kind: 'prompt', aiType: 'vibe', prompt: 'Analyze this', response: 'analysis result', frameName: '<entry>', frameDepth: 0 },
|
|
120
|
+
{ kind: 'variable', name: 'analyzed', value: 'analysis result', type: 'text', isConst: false, source: 'ai', frameName: '<entry>', frameDepth: 0 },
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
// Verify formatted text context at second pause - shows execution order with prompt and response
|
|
124
|
+
// Response is shown via variable assignment (not duplicated with prompt)
|
|
125
|
+
const formatted2 = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
126
|
+
expect(formatted2.text).toBe(
|
|
127
|
+
` <entry> (current scope)
|
|
128
|
+
- inputData (text): raw data
|
|
129
|
+
--> vibe: "Analyze this"
|
|
130
|
+
<-- analyzed (text): analysis result`
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Complete execution
|
|
134
|
+
state = resumeWithAIResponse(state, 'summary result');
|
|
135
|
+
state = runUntilPause(state);
|
|
136
|
+
expect(state.status).toBe('completed');
|
|
137
|
+
|
|
138
|
+
// Verify locals have values in assignment order (including filtered types)
|
|
139
|
+
const locals = state.callStack[0].locals;
|
|
140
|
+
expect(locals['inputData'].value).toBe('raw data');
|
|
141
|
+
// model filtered from context
|
|
142
|
+
expect(locals['ANALYZE_PROMPT'].value).toBe('Analyze this');
|
|
143
|
+
// VibeValue: value is directly in .value (not .value.value)
|
|
144
|
+
expect(locals['analyzed'].value).toBe('analysis result');
|
|
145
|
+
expect(locals['SUMMARIZE_PROMPT'].value).toBe('Summarize this');
|
|
146
|
+
expect(locals['summary'].value).toBe('summary result');
|
|
147
|
+
|
|
148
|
+
// Context at completion - includes all variables and prompts (with responses) in execution order
|
|
149
|
+
expect(state.localContext).toEqual([
|
|
150
|
+
{ kind: 'variable', name: 'inputData', value: 'raw data', type: 'text', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
151
|
+
{ kind: 'prompt', aiType: 'vibe', prompt: 'Analyze this', response: 'analysis result', frameName: '<entry>', frameDepth: 0 },
|
|
152
|
+
{ kind: 'variable', name: 'analyzed', value: 'analysis result', type: 'text', isConst: false, source: 'ai', frameName: '<entry>', frameDepth: 0 },
|
|
153
|
+
{ kind: 'prompt', aiType: 'vibe', prompt: 'Summarize this', response: 'summary result', frameName: '<entry>', frameDepth: 0 },
|
|
154
|
+
{ kind: 'variable', name: 'summary', value: 'summary result', type: 'text', isConst: false, source: 'ai', frameName: '<entry>', frameDepth: 0 },
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
// Verify formatted text context at completion - shows all entries in execution order
|
|
158
|
+
// Responses shown via variable assignments (not duplicated with prompts)
|
|
159
|
+
const formatted3 = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
160
|
+
expect(formatted3.text).toBe(
|
|
161
|
+
` <entry> (current scope)
|
|
162
|
+
- inputData (text): raw data
|
|
163
|
+
--> vibe: "Analyze this"
|
|
164
|
+
<-- analyzed (text): analysis result
|
|
165
|
+
--> vibe: "Summarize this"
|
|
166
|
+
<-- summary (text): summary result`
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('deeply nested functions with prompt variables at each level', () => {
|
|
171
|
+
const ast = parse(`
|
|
172
|
+
const ROOT_PROMPT: prompt = "Root prompt"
|
|
173
|
+
const ROOT_DATA = "root data"
|
|
174
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
175
|
+
|
|
176
|
+
function level1(input: text): text {
|
|
177
|
+
const L1_PROMPT: prompt = "Level 1 prompt"
|
|
178
|
+
let l1Data = "level 1 data"
|
|
179
|
+
return level2(input)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function level2(input: text): text {
|
|
183
|
+
const L2_PROMPT: prompt = "Level 2 prompt"
|
|
184
|
+
let l2Data = "level 2 data"
|
|
185
|
+
return vibe "Process {input}" m default
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let result = level1("deep input")
|
|
189
|
+
`);
|
|
190
|
+
|
|
191
|
+
let state = createInitialState(ast);
|
|
192
|
+
state = runUntilPause(state);
|
|
193
|
+
|
|
194
|
+
expect(state.status).toBe('awaiting_ai');
|
|
195
|
+
// With unified interpolation, {input} is left as reference in vibe expressions
|
|
196
|
+
expect(state.pendingAI?.prompt).toBe('Process {input}');
|
|
197
|
+
|
|
198
|
+
// Local context: level2 frame only, L2_PROMPT filtered
|
|
199
|
+
// Note: function parameters now have explicit type annotations
|
|
200
|
+
expect(state.localContext).toEqual([
|
|
201
|
+
{ kind: 'variable', name: 'input', value: 'deep input', type: 'text', isConst: false, frameName: 'level2', frameDepth: 2, source: null },
|
|
202
|
+
{ kind: 'variable', name: 'l2Data', value: 'level 2 data', type: 'text', isConst: false, frameName: 'level2', frameDepth: 2, source: null },
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
// Global context: all frames, all prompts filtered, all models filtered
|
|
206
|
+
expect(state.globalContext).toEqual([
|
|
207
|
+
{ kind: 'variable', name: 'ROOT_DATA', value: 'root data', type: 'text', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
208
|
+
{ kind: 'variable', name: 'input', value: 'deep input', type: 'text', isConst: false, frameName: 'level1', frameDepth: 1, source: null },
|
|
209
|
+
{ kind: 'variable', name: 'l1Data', value: 'level 1 data', type: 'text', isConst: false, frameName: 'level1', frameDepth: 1, source: null },
|
|
210
|
+
{ kind: 'variable', name: 'input', value: 'deep input', type: 'text', isConst: false, frameName: 'level2', frameDepth: 2, source: null },
|
|
211
|
+
{ kind: 'variable', name: 'l2Data', value: 'level 2 data', type: 'text', isConst: false, frameName: 'level2', frameDepth: 2, source: null },
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
// Verify formatted output shows proper nesting without any prompts
|
|
215
|
+
const formatted = formatContextForAI(state.globalContext, { includeInstructions: false });
|
|
216
|
+
expect(formatted.text).toBe(
|
|
217
|
+
` <entry> (entry)
|
|
218
|
+
- ROOT_DATA (text): root data
|
|
219
|
+
|
|
220
|
+
level1 (depth 1)
|
|
221
|
+
- input (text): deep input
|
|
222
|
+
- l1Data (text): level 1 data
|
|
223
|
+
|
|
224
|
+
level2 (current scope)
|
|
225
|
+
- input (text): deep input
|
|
226
|
+
- l2Data (text): level 2 data`
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Context captured before AI calls
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
test('context captured before do call', () => {
|
|
235
|
+
const ast = parse(`
|
|
236
|
+
const API_KEY = "secret"
|
|
237
|
+
let counter = "0"
|
|
238
|
+
model m = { name: "test", apiKey: "key123", url: "http://test" }
|
|
239
|
+
let result = vibe "process data" m default
|
|
240
|
+
`);
|
|
241
|
+
|
|
242
|
+
let state = createInitialState(ast);
|
|
243
|
+
state = runUntilPause(state);
|
|
244
|
+
|
|
245
|
+
expect(state.status).toBe('awaiting_ai');
|
|
246
|
+
|
|
247
|
+
// Verify complete local context before AI call (models filtered out)
|
|
248
|
+
expect(state.localContext).toEqual([
|
|
249
|
+
{ kind: 'variable', name: 'API_KEY', value: 'secret', type: 'text', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
250
|
+
{ kind: 'variable', name: 'counter', value: '0', type: 'text', isConst: false, frameName: '<entry>', frameDepth: 0, source: null },
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
// Global context same as local at top level
|
|
254
|
+
expect(state.globalContext).toEqual(state.localContext);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('context includes function parameters when inside function', () => {
|
|
258
|
+
const ast = parse(`
|
|
259
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
260
|
+
function process(input: text): text {
|
|
261
|
+
let localVar = "local value"
|
|
262
|
+
return vibe "process {input}" m default
|
|
263
|
+
}
|
|
264
|
+
let result = process("my input")
|
|
265
|
+
`);
|
|
266
|
+
|
|
267
|
+
let state = createInitialState(ast);
|
|
268
|
+
state = runUntilPause(state);
|
|
269
|
+
|
|
270
|
+
expect(state.status).toBe('awaiting_ai');
|
|
271
|
+
|
|
272
|
+
// Local context should have function params and locals only (depth 1 = called from entry)
|
|
273
|
+
// Note: function parameters now have explicit type annotations
|
|
274
|
+
expect(state.localContext).toEqual([
|
|
275
|
+
{ kind: 'variable', name: 'input', value: 'my input', type: 'text', isConst: false, frameName: 'process', frameDepth: 1, source: null },
|
|
276
|
+
{ kind: 'variable', name: 'localVar', value: 'local value', type: 'text', isConst: false, frameName: 'process', frameDepth: 1, source: null },
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
// Global context has entry frame (depth 0, model filtered out) + function frame (depth 1)
|
|
280
|
+
expect(state.globalContext).toEqual([
|
|
281
|
+
{ kind: 'variable', name: 'input', value: 'my input', type: 'text', isConst: false, frameName: 'process', frameDepth: 1, source: null },
|
|
282
|
+
{ kind: 'variable', name: 'localVar', value: 'local value', type: 'text', isConst: false, frameName: 'process', frameDepth: 1, source: null },
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Context formatter tests
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
test('context formatter formats variables in declaration order', () => {
|
|
291
|
+
// Note: models are filtered out before reaching formatter
|
|
292
|
+
const context: ContextVariable[] = [
|
|
293
|
+
{ kind: 'variable', name: 'mutableVar', value: 'changing', type: null, isConst: false, frameName: '<entry>', frameDepth: 0 },
|
|
294
|
+
{ kind: 'variable', name: 'CONFIG', value: { key: 'value' }, type: 'json', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
295
|
+
{ kind: 'variable', name: 'anotherLet', value: 'also changing', type: null, isConst: false, frameName: '<entry>', frameDepth: 0 },
|
|
296
|
+
{ kind: 'variable', name: 'SYSTEM_PROMPT', value: 'be helpful', type: 'text', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
const formatted = formatContextForAI(context);
|
|
300
|
+
|
|
301
|
+
// Variables remain in original order (no sorting)
|
|
302
|
+
expect(formatted.variables).toEqual(context);
|
|
303
|
+
|
|
304
|
+
// Verify formatted text - all variables together in declaration order
|
|
305
|
+
expect(formatted.text).toBe(
|
|
306
|
+
`## VIBE Program Context
|
|
307
|
+
Variables from the VIBE language call stack.
|
|
308
|
+
|
|
309
|
+
<entry> (current scope)
|
|
310
|
+
- mutableVar: changing
|
|
311
|
+
- CONFIG (json): {"key":"value"}
|
|
312
|
+
- anotherLet: also changing
|
|
313
|
+
- SYSTEM_PROMPT (text): be helpful`
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('context formatter preserves declaration order', () => {
|
|
318
|
+
const context: ContextVariable[] = [
|
|
319
|
+
{ kind: 'variable', name: 'z_const', value: 'z', type: null, isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
320
|
+
{ kind: 'variable', name: 'a_let', value: 'a', type: null, isConst: false, frameName: '<entry>', frameDepth: 0 },
|
|
321
|
+
{ kind: 'variable', name: 'a_const', value: 'a', type: null, isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
322
|
+
{ kind: 'variable', name: 'z_let', value: 'z', type: null, isConst: false, frameName: '<entry>', frameDepth: 0 },
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
const formatted = formatContextForAI(context);
|
|
326
|
+
|
|
327
|
+
// Variables remain in original declaration order
|
|
328
|
+
expect(formatted.variables).toEqual(context);
|
|
329
|
+
|
|
330
|
+
// Verify formatted text preserves order (no instructions for clarity)
|
|
331
|
+
const noInstructions = formatContextForAI(context, { includeInstructions: false });
|
|
332
|
+
expect(noInstructions.text).toBe(
|
|
333
|
+
` <entry> (current scope)
|
|
334
|
+
- z_const: z
|
|
335
|
+
- a_let: a
|
|
336
|
+
- a_const: a
|
|
337
|
+
- z_let: z`
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('context with all type annotations formats correctly', () => {
|
|
342
|
+
// Note: models are filtered out before reaching formatter, so only text/json/null types
|
|
343
|
+
const context: ContextVariable[] = [
|
|
344
|
+
{ kind: 'variable', name: 'jsonVar', value: { key: 'value' }, type: 'json', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
345
|
+
{ kind: 'variable', name: 'textVar', value: 'text value', type: 'text', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
346
|
+
{ kind: 'variable', name: 'untypedConst', value: 'constant', type: null, isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
347
|
+
{ kind: 'variable', name: 'untypedLet', value: 'mutable', type: null, isConst: false, frameName: '<entry>', frameDepth: 0 },
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
const formatted = formatContextForAI(context);
|
|
351
|
+
|
|
352
|
+
// Verify formatted output - all variables together in declaration order
|
|
353
|
+
expect(formatted.text).toBe(
|
|
354
|
+
`## VIBE Program Context
|
|
355
|
+
Variables from the VIBE language call stack.
|
|
356
|
+
|
|
357
|
+
<entry> (current scope)
|
|
358
|
+
- jsonVar (json): {"key":"value"}
|
|
359
|
+
- textVar (text): text value
|
|
360
|
+
- untypedConst: constant
|
|
361
|
+
- untypedLet: mutable`
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Verify variables array matches input
|
|
365
|
+
expect(formatted.variables).toEqual(context);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('context without instructions outputs variables only', () => {
|
|
369
|
+
const context: ContextVariable[] = [
|
|
370
|
+
{ kind: 'variable', name: 'API_KEY', value: 'secret123', type: null, isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
371
|
+
{ kind: 'variable', name: 'counter', value: '42', type: null, isConst: false, frameName: '<entry>', frameDepth: 0 },
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
const formatted = formatContextForAI(context, { includeInstructions: false });
|
|
375
|
+
|
|
376
|
+
expect(formatted.text).toBe(
|
|
377
|
+
` <entry> (current scope)
|
|
378
|
+
- API_KEY: secret123
|
|
379
|
+
- counter: 42`
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// Full program tests with mock AI
|
|
385
|
+
// ============================================================================
|
|
386
|
+
|
|
387
|
+
test('full program with mock AI response', () => {
|
|
388
|
+
const ast = parse(`
|
|
389
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
390
|
+
let input = "hello"
|
|
391
|
+
let result = vibe "transform {input}" m default
|
|
392
|
+
`);
|
|
393
|
+
|
|
394
|
+
let state = createInitialState(ast);
|
|
395
|
+
state = runWithMockAI(state, 'TRANSFORMED');
|
|
396
|
+
|
|
397
|
+
expect(state.status).toBe('completed');
|
|
398
|
+
// VibeValue: value is directly in .value
|
|
399
|
+
expect(state.callStack[0].locals['result'].value).toBe('TRANSFORMED');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('multiple do calls with different mock responses', () => {
|
|
403
|
+
const ast = parse(`
|
|
404
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
405
|
+
let first = vibe "first prompt" m default
|
|
406
|
+
let second = vibe "second prompt" m default
|
|
407
|
+
`);
|
|
408
|
+
|
|
409
|
+
let state = createInitialState(ast);
|
|
410
|
+
state = runWithMockAI(state, {
|
|
411
|
+
'first prompt': 'FIRST_RESPONSE',
|
|
412
|
+
'second prompt': 'SECOND_RESPONSE',
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(state.status).toBe('completed');
|
|
416
|
+
// VibeValue: value is directly in .value
|
|
417
|
+
expect(state.callStack[0].locals['first'].value).toBe('FIRST_RESPONSE');
|
|
418
|
+
expect(state.callStack[0].locals['second'].value).toBe('SECOND_RESPONSE');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('context state correct after mock AI response', () => {
|
|
422
|
+
const ast = parse(`
|
|
423
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
424
|
+
const SYSTEM = "system prompt"
|
|
425
|
+
let result = vibe "query" m default
|
|
426
|
+
`);
|
|
427
|
+
|
|
428
|
+
let state = createInitialState(ast);
|
|
429
|
+
state = runWithMockAI(state, 'AI_RESPONSE');
|
|
430
|
+
|
|
431
|
+
expect(state.status).toBe('completed');
|
|
432
|
+
|
|
433
|
+
// Verify final variables have correct values and isConst
|
|
434
|
+
const locals = state.callStack[0].locals;
|
|
435
|
+
expect(locals['m'].isConst).toBe(true);
|
|
436
|
+
expect(locals['SYSTEM'].isConst).toBe(true);
|
|
437
|
+
expect(locals['result'].isConst).toBe(false);
|
|
438
|
+
// VibeValue: value is directly in .value
|
|
439
|
+
expect(locals['result'].value).toBe('AI_RESPONSE');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// Complex program context tests
|
|
444
|
+
// ============================================================================
|
|
445
|
+
|
|
446
|
+
test('complex program with mix of const, let, models - global scope', () => {
|
|
447
|
+
// Complex program with multiple models, constants, and variables
|
|
448
|
+
const ast = parse(`
|
|
449
|
+
const API_BASE = "https://api.example.com"
|
|
450
|
+
const CONFIG: json = { timeout: "30", retries: "3" }
|
|
451
|
+
model gpt = { name: "gpt-4", apiKey: "key1", url: "http://gpt" }
|
|
452
|
+
model claude = { name: "claude", apiKey: "key2", url: "http://claude" }
|
|
453
|
+
let userInput: text = "hello world"
|
|
454
|
+
let counter = "0"
|
|
455
|
+
let metadata: json = { version: "1.0" }
|
|
456
|
+
let result = vibe "process {userInput}" gpt default
|
|
457
|
+
`);
|
|
458
|
+
|
|
459
|
+
let state = createInitialState(ast);
|
|
460
|
+
state = runUntilPause(state);
|
|
461
|
+
|
|
462
|
+
expect(state.status).toBe('awaiting_ai');
|
|
463
|
+
|
|
464
|
+
// Models (gpt, claude) should be filtered out of context
|
|
465
|
+
// Verify complete local context with correct order and types (all in entry frame, depth 0)
|
|
466
|
+
expect(state.localContext).toEqual([
|
|
467
|
+
{ kind: 'variable', name: 'API_BASE', value: 'https://api.example.com', type: 'text', isConst: true, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
468
|
+
{ kind: 'variable', name: 'CONFIG', value: { timeout: '30', retries: '3' }, type: 'json', isConst: true, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
469
|
+
{ kind: 'variable', name: 'userInput', value: 'hello world', type: 'text', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
470
|
+
{ kind: 'variable', name: 'counter', value: '0', type: 'text', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
471
|
+
{ kind: 'variable', name: 'metadata', value: { version: '1.0' }, type: 'json', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
472
|
+
]);
|
|
473
|
+
|
|
474
|
+
// Global context same at top level
|
|
475
|
+
expect(state.globalContext).toEqual(state.localContext);
|
|
476
|
+
|
|
477
|
+
// Verify formatted context sorts const first
|
|
478
|
+
const formatted = formatContextForAI(state.localContext);
|
|
479
|
+
expect(formatted.variables).toEqual([
|
|
480
|
+
{ kind: 'variable', name: 'API_BASE', value: 'https://api.example.com', type: 'text', isConst: true, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
481
|
+
{ kind: 'variable', name: 'CONFIG', value: { timeout: '30', retries: '3' }, type: 'json', isConst: true, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
482
|
+
{ kind: 'variable', name: 'userInput', value: 'hello world', type: 'text', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
483
|
+
{ kind: 'variable', name: 'counter', value: '0', type: 'text', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
484
|
+
{ kind: 'variable', name: 'metadata', value: { version: '1.0' }, type: 'json', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
485
|
+
]);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('complex function with params, locals, and nested block', () => {
|
|
489
|
+
// Function with multiple parameters, local variables, and AI call inside nested block
|
|
490
|
+
const ast = parse(`
|
|
491
|
+
const SYSTEM_PROMPT = "You are a helpful assistant"
|
|
492
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
493
|
+
|
|
494
|
+
function processData(inputText: text, options: text) {
|
|
495
|
+
const FUNC_CONST = "function constant"
|
|
496
|
+
let normalized: text = "normalized"
|
|
497
|
+
let result: json = { status: "pending" }
|
|
498
|
+
|
|
499
|
+
if true {
|
|
500
|
+
let blockVar = "inside block"
|
|
501
|
+
let response = vibe "analyze {inputText}" m default
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let output = processData("test input", "opts")
|
|
506
|
+
`);
|
|
507
|
+
|
|
508
|
+
let state = createInitialState(ast);
|
|
509
|
+
state = runUntilPause(state);
|
|
510
|
+
|
|
511
|
+
expect(state.status).toBe('awaiting_ai');
|
|
512
|
+
|
|
513
|
+
// Local context should only have current frame (function scope + block scope)
|
|
514
|
+
// Should NOT include model m (filtered out) or outer SYSTEM_PROMPT (different frame)
|
|
515
|
+
// Depth 1 = called from entry
|
|
516
|
+
// Note: function parameters now have explicit type annotations
|
|
517
|
+
expect(state.localContext).toEqual([
|
|
518
|
+
{ kind: 'variable', name: 'inputText', value: 'test input', type: 'text', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
519
|
+
{ kind: 'variable', name: 'options', value: 'opts', type: 'text', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
520
|
+
{ kind: 'variable', name: 'FUNC_CONST', value: 'function constant', type: 'text', isConst: true, frameName: 'processData', frameDepth: 1, source: null },
|
|
521
|
+
{ kind: 'variable', name: 'normalized', value: 'normalized', type: 'text', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
522
|
+
{ kind: 'variable', name: 'result', value: { status: 'pending' }, type: 'json', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
523
|
+
{ kind: 'variable', name: 'blockVar', value: 'inside block', type: 'text', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
524
|
+
]);
|
|
525
|
+
|
|
526
|
+
// Global context includes all frames: <entry> (depth 0) + function (depth 1), models filtered out
|
|
527
|
+
expect(state.globalContext).toEqual([
|
|
528
|
+
{ kind: 'variable', name: 'SYSTEM_PROMPT', value: 'You are a helpful assistant', type: 'text', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
529
|
+
{ kind: 'variable', name: 'inputText', value: 'test input', type: 'text', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
530
|
+
{ kind: 'variable', name: 'options', value: 'opts', type: 'text', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
531
|
+
{ kind: 'variable', name: 'FUNC_CONST', value: 'function constant', type: 'text', isConst: true, frameName: 'processData', frameDepth: 1, source: null },
|
|
532
|
+
{ kind: 'variable', name: 'normalized', value: 'normalized', type: 'text', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
533
|
+
{ kind: 'variable', name: 'result', value: { status: 'pending' }, type: 'json', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
534
|
+
{ kind: 'variable', name: 'blockVar', value: 'inside block', type: 'text', isConst: false, frameName: 'processData', frameDepth: 1, source: null },
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
// Verify formatted global context preserves declaration order
|
|
538
|
+
const formatted = formatContextForAI(state.globalContext);
|
|
539
|
+
expect(formatted.variables).toEqual(state.globalContext);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('context at multiple call depths via sequential do calls', () => {
|
|
543
|
+
// Multi-checkpoint test: verify context at each do call as we traverse the call stack
|
|
544
|
+
const ast = parse(`
|
|
545
|
+
const GLOBAL_CONST = "global"
|
|
546
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
547
|
+
|
|
548
|
+
function helper(value: text): text {
|
|
549
|
+
const HELPER_CONST = "helper const"
|
|
550
|
+
let helperVar = "helper value"
|
|
551
|
+
return vibe "helper work with {value}" m default
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function main(input: text): text {
|
|
555
|
+
const MAIN_CONST = "main const"
|
|
556
|
+
let mainVar = "main value"
|
|
557
|
+
let mainResult = vibe "main work with {input}" m default
|
|
558
|
+
return helper(input)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let result = main("test")
|
|
562
|
+
`);
|
|
563
|
+
|
|
564
|
+
let state = createInitialState(ast);
|
|
565
|
+
|
|
566
|
+
// === Checkpoint 1: Inside main(), at first do call ===
|
|
567
|
+
state = runUntilPause(state);
|
|
568
|
+
expect(state.status).toBe('awaiting_ai');
|
|
569
|
+
// With unified interpolation, {input} is left as reference in prompt strings
|
|
570
|
+
// The AI sees the literal {input} and gets the value through context
|
|
571
|
+
expect(state.pendingAI?.prompt).toBe('main work with {input}');
|
|
572
|
+
|
|
573
|
+
// Local context: main's frame only (depth 1 = called from entry)
|
|
574
|
+
// Note: function parameters now have explicit type annotations
|
|
575
|
+
expect(state.localContext).toEqual([
|
|
576
|
+
{ kind: 'variable', name: 'input', value: 'test', type: 'text', isConst: false, frameName: 'main', frameDepth: 1, source: null },
|
|
577
|
+
{ kind: 'variable', name: 'MAIN_CONST', value: 'main const', type: 'text', isConst: true, frameName: 'main', frameDepth: 1, source: null },
|
|
578
|
+
{ kind: 'variable', name: 'mainVar', value: 'main value', type: 'text', isConst: false, frameName: 'main', frameDepth: 1, source: null },
|
|
579
|
+
]);
|
|
580
|
+
|
|
581
|
+
// Global context: <entry> (depth 0) + main function (depth 1), models filtered
|
|
582
|
+
expect(state.globalContext).toEqual([
|
|
583
|
+
{ kind: 'variable', name: 'GLOBAL_CONST', value: 'global', type: 'text', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
584
|
+
{ kind: 'variable', name: 'input', value: 'test', type: 'text', isConst: false, frameName: 'main', frameDepth: 1, source: null },
|
|
585
|
+
{ kind: 'variable', name: 'MAIN_CONST', value: 'main const', type: 'text', isConst: true, frameName: 'main', frameDepth: 1, source: null },
|
|
586
|
+
{ kind: 'variable', name: 'mainVar', value: 'main value', type: 'text', isConst: false, frameName: 'main', frameDepth: 1, source: null },
|
|
587
|
+
]);
|
|
588
|
+
|
|
589
|
+
// Verify formatted context preserves declaration order at checkpoint 1
|
|
590
|
+
const formatted1 = formatContextForAI(state.globalContext);
|
|
591
|
+
expect(formatted1.variables).toEqual(state.globalContext);
|
|
592
|
+
|
|
593
|
+
// Resume and continue to next pause
|
|
594
|
+
state = resumeWithAIResponse(state, 'main response');
|
|
595
|
+
|
|
596
|
+
// === Checkpoint 2: Inside helper(), at second do call ===
|
|
597
|
+
state = runUntilPause(state);
|
|
598
|
+
expect(state.status).toBe('awaiting_ai');
|
|
599
|
+
// With unified interpolation, {value} is left as reference in prompt strings
|
|
600
|
+
expect(state.pendingAI?.prompt).toBe('helper work with {value}');
|
|
601
|
+
|
|
602
|
+
// Local context: helper's frame only (depth 2 = called from main which is called from entry)
|
|
603
|
+
// Note: function parameters now have explicit type annotations
|
|
604
|
+
expect(state.localContext).toEqual([
|
|
605
|
+
{ kind: 'variable', name: 'value', value: 'test', type: 'text', isConst: false, frameName: 'helper', frameDepth: 2, source: null },
|
|
606
|
+
{ kind: 'variable', name: 'HELPER_CONST', value: 'helper const', type: 'text', isConst: true, frameName: 'helper', frameDepth: 2, source: null },
|
|
607
|
+
{ kind: 'variable', name: 'helperVar', value: 'helper value', type: 'text', isConst: false, frameName: 'helper', frameDepth: 2, source: null },
|
|
608
|
+
]);
|
|
609
|
+
|
|
610
|
+
// Global context: <entry> (depth 0) + main (depth 1) + helper (depth 2), models filtered
|
|
611
|
+
// Note: mainResult now has the response from checkpoint 1, and prompt is included
|
|
612
|
+
expect(state.globalContext).toEqual([
|
|
613
|
+
{ kind: 'variable', name: 'GLOBAL_CONST', value: 'global', type: 'text', isConst: true, frameName: '<entry>', frameDepth: 0, source: null },
|
|
614
|
+
{ kind: 'variable', name: 'input', value: 'test', type: 'text', isConst: false, frameName: 'main', frameDepth: 1, source: null },
|
|
615
|
+
{ kind: 'variable', name: 'MAIN_CONST', value: 'main const', type: 'text', isConst: true, frameName: 'main', frameDepth: 1, source: null },
|
|
616
|
+
{ kind: 'variable', name: 'mainVar', value: 'main value', type: 'text', isConst: false, frameName: 'main', frameDepth: 1, source: null },
|
|
617
|
+
{ kind: 'prompt', aiType: 'vibe', prompt: 'main work with {input}', response: 'main response', frameName: 'main', frameDepth: 1 },
|
|
618
|
+
{ kind: 'variable', name: 'mainResult', value: 'main response', type: 'text', isConst: false, source: 'ai', frameName: 'main', frameDepth: 1 },
|
|
619
|
+
{ kind: 'variable', name: 'value', value: 'test', type: 'text', isConst: false, frameName: 'helper', frameDepth: 2, source: null },
|
|
620
|
+
{ kind: 'variable', name: 'HELPER_CONST', value: 'helper const', type: 'text', isConst: true, frameName: 'helper', frameDepth: 2, source: null },
|
|
621
|
+
{ kind: 'variable', name: 'helperVar', value: 'helper value', type: 'text', isConst: false, frameName: 'helper', frameDepth: 2, source: null },
|
|
622
|
+
]);
|
|
623
|
+
|
|
624
|
+
// Verify formatted context preserves declaration order at checkpoint 2
|
|
625
|
+
const formatted2 = formatContextForAI(state.globalContext);
|
|
626
|
+
expect(formatted2.variables).toEqual(state.globalContext);
|
|
627
|
+
|
|
628
|
+
// Verify formatted text with nested call stack (3 frames: entry=0, main=1, helper=2)
|
|
629
|
+
// All entries together, grouped by frame with indentation
|
|
630
|
+
// Entry is leftmost (least indented), deeper calls are more indented
|
|
631
|
+
// Response shown via variable assignment (not duplicated with prompt)
|
|
632
|
+
expect(formatted2.text).toBe(
|
|
633
|
+
`## VIBE Program Context
|
|
634
|
+
Variables from the VIBE language call stack.
|
|
635
|
+
|
|
636
|
+
<entry> (entry)
|
|
637
|
+
- GLOBAL_CONST (text): global
|
|
638
|
+
|
|
639
|
+
main (depth 1)
|
|
640
|
+
- input (text): test
|
|
641
|
+
- MAIN_CONST (text): main const
|
|
642
|
+
- mainVar (text): main value
|
|
643
|
+
--> vibe: "main work with {input}"
|
|
644
|
+
<-- mainResult (text): main response
|
|
645
|
+
|
|
646
|
+
helper (current scope)
|
|
647
|
+
- value (text): test
|
|
648
|
+
- HELPER_CONST (text): helper const
|
|
649
|
+
- helperVar (text): helper value`
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
// Resume and complete
|
|
653
|
+
state = resumeWithAIResponse(state, 'helper response');
|
|
654
|
+
state = runUntilPause(state);
|
|
655
|
+
expect(state.status).toBe('completed');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test('context with all type annotations and complex values', () => {
|
|
659
|
+
const ast = parse(`
|
|
660
|
+
const PROMPT: text = "analyze this data"
|
|
661
|
+
const CONFIG: json = { modelName: "gpt-4", temperature: "high" }
|
|
662
|
+
model ai = { name: "test", apiKey: "key", url: "http://test" }
|
|
663
|
+
let userMessage: text = "user says hello"
|
|
664
|
+
let data: json = { items: ["a", "b", "c"], count: "3" }
|
|
665
|
+
let untypedVar = "plain string"
|
|
666
|
+
let result = vibe "process" ai default
|
|
667
|
+
`);
|
|
668
|
+
|
|
669
|
+
let state = createInitialState(ast);
|
|
670
|
+
state = runUntilPause(state);
|
|
671
|
+
|
|
672
|
+
expect(state.status).toBe('awaiting_ai');
|
|
673
|
+
|
|
674
|
+
// Verify all variables with their types (model 'ai' filtered out)
|
|
675
|
+
expect(state.localContext).toEqual([
|
|
676
|
+
{ kind: 'variable', name: 'PROMPT', value: 'analyze this data', type: 'text', isConst: true, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
677
|
+
{ kind: 'variable', name: 'CONFIG', value: { modelName: 'gpt-4', temperature: 'high' }, type: 'json', isConst: true, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
678
|
+
{ kind: 'variable', name: 'userMessage', value: 'user says hello', type: 'text', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
679
|
+
{ kind: 'variable', name: 'data', value: { items: ['a', 'b', 'c'], count: '3' }, type: 'json', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
680
|
+
{ kind: 'variable', name: 'untypedVar', value: 'plain string', type: 'text', isConst: false, source: null, frameName: '<entry>', frameDepth: 0 },
|
|
681
|
+
]);
|
|
682
|
+
|
|
683
|
+
// Verify formatted output - all variables together in declaration order
|
|
684
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
685
|
+
expect(formatted.text).toBe(
|
|
686
|
+
` <entry> (current scope)
|
|
687
|
+
- PROMPT (text): analyze this data
|
|
688
|
+
- CONFIG (json): {"modelName":"gpt-4","temperature":"high"}
|
|
689
|
+
- userMessage (text): user says hello
|
|
690
|
+
- data (json): {"items":["a","b","c"],"count":"3"}
|
|
691
|
+
- untypedVar (text): plain string`
|
|
692
|
+
);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test('variable source changes from ai to undefined when reassigned', () => {
|
|
696
|
+
const ast = parse(`
|
|
697
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
698
|
+
let result: text = vibe "get initial value" m default
|
|
699
|
+
result = "overwritten by code"
|
|
700
|
+
`);
|
|
701
|
+
|
|
702
|
+
// Run until AI pause
|
|
703
|
+
let state = createInitialState(ast);
|
|
704
|
+
state = runUntilPause(state);
|
|
705
|
+
expect(state.status).toBe('awaiting_ai');
|
|
706
|
+
|
|
707
|
+
// Resume with AI response
|
|
708
|
+
state = resumeWithAIResponse(state, 'ai generated value');
|
|
709
|
+
state = runUntilPause(state);
|
|
710
|
+
|
|
711
|
+
// At this point, result should have source: 'ai'
|
|
712
|
+
// But execution continues and reassigns result
|
|
713
|
+
expect(state.status).toBe('completed');
|
|
714
|
+
|
|
715
|
+
// Check the variable's source is now null (reassigned by code)
|
|
716
|
+
const frame = state.callStack[0];
|
|
717
|
+
expect(frame.locals['result'].value).toBe('overwritten by code');
|
|
718
|
+
expect(frame.locals['result'].source).toBe(null);
|
|
719
|
+
|
|
720
|
+
// Verify context shows history: first AI-sourced entry, then code-sourced entry
|
|
721
|
+
// With snapshotting, both entries are preserved
|
|
722
|
+
const resultEntries = state.localContext.filter(
|
|
723
|
+
(e): e is ContextVariable => e.kind === 'variable' && e.name === 'result'
|
|
724
|
+
);
|
|
725
|
+
// First entry is AI-sourced
|
|
726
|
+
expect(resultEntries[0]?.source).toBe('ai');
|
|
727
|
+
expect(resultEntries[0]?.value).toBe('ai generated value');
|
|
728
|
+
// Second entry is code-sourced (no AI attribution)
|
|
729
|
+
expect(resultEntries[1]?.source).toBe(null);
|
|
730
|
+
expect(resultEntries[1]?.value).toBe('overwritten by code');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test('variable source is ai immediately after AI response assignment', () => {
|
|
734
|
+
const ast = parse(`
|
|
735
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
736
|
+
let result: text = vibe "get value" m default
|
|
737
|
+
`);
|
|
738
|
+
|
|
739
|
+
let state = createInitialState(ast);
|
|
740
|
+
state = runUntilPause(state);
|
|
741
|
+
expect(state.status).toBe('awaiting_ai');
|
|
742
|
+
|
|
743
|
+
// Resume with AI response
|
|
744
|
+
state = resumeWithAIResponse(state, 'ai response');
|
|
745
|
+
state = runUntilPause(state);
|
|
746
|
+
expect(state.status).toBe('completed');
|
|
747
|
+
|
|
748
|
+
// Check the variable's source is 'ai'
|
|
749
|
+
const frame = state.callStack[0];
|
|
750
|
+
expect(frame.locals['result'].value).toBe('ai response');
|
|
751
|
+
expect(frame.locals['result'].source).toBe('ai');
|
|
752
|
+
|
|
753
|
+
// Verify context shows AI attribution
|
|
754
|
+
const resultEntry = state.localContext.find(
|
|
755
|
+
(e): e is ContextVariable => e.kind === 'variable' && e.name === 'result'
|
|
756
|
+
);
|
|
757
|
+
expect(resultEntry?.source).toBe('ai');
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe('Tool Call Context Formatting', () => {
|
|
762
|
+
// These are unit tests for formatContextForAI - they directly set up orderedEntries
|
|
763
|
+
// to test the formatter in isolation. For full integration tests that actually
|
|
764
|
+
// execute tools through the AI provider, see ai-tool-integration.test.ts
|
|
765
|
+
|
|
766
|
+
test('tool call with result is formatted correctly', () => {
|
|
767
|
+
const ast = parse('let x = "test"');
|
|
768
|
+
let state = createInitialState(ast);
|
|
769
|
+
state = runUntilPause(state);
|
|
770
|
+
|
|
771
|
+
// Set up tool call entry directly to test formatter
|
|
772
|
+
const frame = state.callStack[state.callStack.length - 1];
|
|
773
|
+
frame.orderedEntries.push({
|
|
774
|
+
kind: 'tool-call',
|
|
775
|
+
toolName: 'getWeather',
|
|
776
|
+
args: { city: 'Seattle' },
|
|
777
|
+
result: { temp: 55, condition: 'rainy' },
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Rebuild context
|
|
781
|
+
state = { ...state, localContext: buildLocalContext(state) };
|
|
782
|
+
|
|
783
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
784
|
+
expect(formatted.text).toBe(
|
|
785
|
+
` <entry> (current scope)
|
|
786
|
+
- x (text): test
|
|
787
|
+
[tool] getWeather({"city":"Seattle"})
|
|
788
|
+
[result] {"temp":55,"condition":"rainy"}`
|
|
789
|
+
);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test('tool call with error is formatted correctly', () => {
|
|
793
|
+
const ast = parse('let x = "test"');
|
|
794
|
+
let state = createInitialState(ast);
|
|
795
|
+
state = runUntilPause(state);
|
|
796
|
+
|
|
797
|
+
// Add a failed tool call entry
|
|
798
|
+
const frame = state.callStack[state.callStack.length - 1];
|
|
799
|
+
frame.orderedEntries.push({
|
|
800
|
+
kind: 'tool-call',
|
|
801
|
+
toolName: 'readFile',
|
|
802
|
+
args: { path: '/nonexistent.txt' },
|
|
803
|
+
error: 'File not found',
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Rebuild context
|
|
807
|
+
state = { ...state, localContext: buildLocalContext(state) };
|
|
808
|
+
|
|
809
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
810
|
+
expect(formatted.text).toBe(
|
|
811
|
+
` <entry> (current scope)
|
|
812
|
+
- x (text): test
|
|
813
|
+
[tool] readFile({"path":"/nonexistent.txt"})
|
|
814
|
+
[error] File not found`
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test('multiple tool calls are formatted in order', () => {
|
|
819
|
+
const ast = parse('let x = "test"');
|
|
820
|
+
let state = createInitialState(ast);
|
|
821
|
+
state = runUntilPause(state);
|
|
822
|
+
|
|
823
|
+
// Add multiple tool call entries
|
|
824
|
+
const frame = state.callStack[state.callStack.length - 1];
|
|
825
|
+
frame.orderedEntries.push(
|
|
826
|
+
{
|
|
827
|
+
kind: 'tool-call',
|
|
828
|
+
toolName: 'fetch',
|
|
829
|
+
args: { url: 'https://api.example.com/data' },
|
|
830
|
+
result: { status: 'ok' },
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
kind: 'tool-call',
|
|
834
|
+
toolName: 'jsonParse',
|
|
835
|
+
args: { text: '{"key":"value"}' },
|
|
836
|
+
result: { key: 'value' },
|
|
837
|
+
}
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
// Rebuild context
|
|
841
|
+
state = { ...state, localContext: buildLocalContext(state) };
|
|
842
|
+
|
|
843
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
844
|
+
expect(formatted.text).toBe(
|
|
845
|
+
` <entry> (current scope)
|
|
846
|
+
- x (text): test
|
|
847
|
+
[tool] fetch({"url":"https://api.example.com/data"})
|
|
848
|
+
[result] {"status":"ok"}
|
|
849
|
+
[tool] jsonParse({"text":"{\\"key\\":\\"value\\"}"})
|
|
850
|
+
[result] {"key":"value"}`
|
|
851
|
+
);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test('tool calls mixed with prompts are formatted correctly', () => {
|
|
855
|
+
const ast = parse('let x = "test"');
|
|
856
|
+
let state = createInitialState(ast);
|
|
857
|
+
state = runUntilPause(state);
|
|
858
|
+
|
|
859
|
+
// Add a tool call followed by a prompt and its result variable
|
|
860
|
+
const frame = state.callStack[state.callStack.length - 1];
|
|
861
|
+
frame.orderedEntries.push(
|
|
862
|
+
{
|
|
863
|
+
kind: 'tool-call',
|
|
864
|
+
toolName: 'getWeather',
|
|
865
|
+
args: { city: 'Seattle' },
|
|
866
|
+
result: { temp: 55 },
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
kind: 'prompt',
|
|
870
|
+
aiType: 'vibe' as const,
|
|
871
|
+
prompt: 'Summarize the weather',
|
|
872
|
+
response: 'It is 55 degrees in Seattle',
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
kind: 'variable',
|
|
876
|
+
name: 'summary',
|
|
877
|
+
value: 'It is 55 degrees in Seattle',
|
|
878
|
+
type: 'text',
|
|
879
|
+
isConst: false,
|
|
880
|
+
source: 'ai' as const,
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
// Rebuild context
|
|
885
|
+
state = { ...state, localContext: buildLocalContext(state) };
|
|
886
|
+
|
|
887
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
888
|
+
expect(formatted.text).toBe(
|
|
889
|
+
` <entry> (current scope)
|
|
890
|
+
- x (text): test
|
|
891
|
+
[tool] getWeather({"city":"Seattle"})
|
|
892
|
+
[result] {"temp":55}
|
|
893
|
+
--> vibe: "Summarize the weather"
|
|
894
|
+
<-- summary (text): It is 55 degrees in Seattle`
|
|
895
|
+
);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test('tool call without result (pending) is formatted correctly', () => {
|
|
899
|
+
const ast = parse('let x = "test"');
|
|
900
|
+
let state = createInitialState(ast);
|
|
901
|
+
state = runUntilPause(state);
|
|
902
|
+
|
|
903
|
+
// Add a tool call without result (simulating pending state)
|
|
904
|
+
const frame = state.callStack[state.callStack.length - 1];
|
|
905
|
+
frame.orderedEntries.push({
|
|
906
|
+
kind: 'tool-call',
|
|
907
|
+
toolName: 'longRunningTask',
|
|
908
|
+
args: { input: 'data' },
|
|
909
|
+
// No result or error - pending
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Rebuild context
|
|
913
|
+
state = { ...state, localContext: buildLocalContext(state) };
|
|
914
|
+
|
|
915
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
916
|
+
expect(formatted.text).toBe(
|
|
917
|
+
` <entry> (current scope)
|
|
918
|
+
- x (text): test
|
|
919
|
+
[tool] longRunningTask({"input":"data"})`
|
|
920
|
+
);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
test('full AI call flow with tool calls shows complete context', () => {
|
|
924
|
+
// Simulates: vibe "What's the weather in Seattle and SF?" -> AI calls tools -> final response
|
|
925
|
+
// Note: model variables are filtered from context (they are config, not data)
|
|
926
|
+
const ast = parse(`
|
|
927
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
928
|
+
let weather: text = vibe "What's the weather in Seattle and San Francisco?" m default
|
|
929
|
+
`);
|
|
930
|
+
|
|
931
|
+
let state = createInitialState(ast);
|
|
932
|
+
state = runUntilPause(state);
|
|
933
|
+
expect(state.status).toBe('awaiting_ai');
|
|
934
|
+
|
|
935
|
+
// Simulate the AI response with tool rounds
|
|
936
|
+
// Round 1: AI calls two tools in parallel
|
|
937
|
+
const toolRounds = [
|
|
938
|
+
{
|
|
939
|
+
toolCalls: [
|
|
940
|
+
{ id: 'call_1', toolName: 'getWeather', args: { city: 'Seattle' } },
|
|
941
|
+
{ id: 'call_2', toolName: 'getWeather', args: { city: 'San Francisco' } },
|
|
942
|
+
],
|
|
943
|
+
results: [
|
|
944
|
+
{ toolCallId: 'call_1', result: { temp: 55, condition: 'rainy' } },
|
|
945
|
+
{ toolCallId: 'call_2', result: { temp: 68, condition: 'sunny' } },
|
|
946
|
+
],
|
|
947
|
+
},
|
|
948
|
+
];
|
|
949
|
+
|
|
950
|
+
// Resume with AI response (after tool calls completed)
|
|
951
|
+
state = resumeWithAIResponse(
|
|
952
|
+
state,
|
|
953
|
+
'Seattle is 55°F and rainy. San Francisco is 68°F and sunny.',
|
|
954
|
+
undefined, // no interaction log
|
|
955
|
+
toolRounds
|
|
956
|
+
);
|
|
957
|
+
state = runUntilPause(state);
|
|
958
|
+
expect(state.status).toBe('completed');
|
|
959
|
+
|
|
960
|
+
// Verify the variable was assigned
|
|
961
|
+
const frame = state.callStack[0];
|
|
962
|
+
expect(frame.locals['weather'].value).toBe('Seattle is 55°F and rainy. San Francisco is 68°F and sunny.');
|
|
963
|
+
|
|
964
|
+
// Verify the context shows: AI call → tool calls → response (via variable assignment)
|
|
965
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
966
|
+
expect(formatted.text).toBe(
|
|
967
|
+
` <entry> (current scope)
|
|
968
|
+
--> vibe: "What's the weather in Seattle and San Francisco?"
|
|
969
|
+
[tool] getWeather({"city":"Seattle"})
|
|
970
|
+
[result] {"temp":55,"condition":"rainy"}
|
|
971
|
+
[tool] getWeather({"city":"San Francisco"})
|
|
972
|
+
[result] {"temp":68,"condition":"sunny"}
|
|
973
|
+
<-- weather (text): Seattle is 55°F and rainy. San Francisco is 68°F and sunny.`
|
|
974
|
+
);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test('multiple rounds of tool calls show in context', () => {
|
|
978
|
+
// Simulates: do -> AI calls tool -> AI calls another tool -> final response
|
|
979
|
+
// Note: model variables are filtered from context
|
|
980
|
+
const ast = parse(`
|
|
981
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
982
|
+
let result: text = vibe "Find user 123 and get their orders" m default
|
|
983
|
+
`);
|
|
984
|
+
|
|
985
|
+
let state = createInitialState(ast);
|
|
986
|
+
state = runUntilPause(state);
|
|
987
|
+
expect(state.status).toBe('awaiting_ai');
|
|
988
|
+
|
|
989
|
+
// Simulate multiple rounds of tool calls
|
|
990
|
+
const toolRounds = [
|
|
991
|
+
// Round 1: Get user info
|
|
992
|
+
{
|
|
993
|
+
toolCalls: [
|
|
994
|
+
{ id: 'call_1', toolName: 'getUser', args: { id: 123 } },
|
|
995
|
+
],
|
|
996
|
+
results: [
|
|
997
|
+
{ toolCallId: 'call_1', result: { name: 'Alice', email: 'alice@example.com' } },
|
|
998
|
+
],
|
|
999
|
+
},
|
|
1000
|
+
// Round 2: Get orders for that user
|
|
1001
|
+
{
|
|
1002
|
+
toolCalls: [
|
|
1003
|
+
{ id: 'call_2', toolName: 'getOrders', args: { userId: 123 } },
|
|
1004
|
+
],
|
|
1005
|
+
results: [
|
|
1006
|
+
// Note: JSON.stringify removes trailing zeros (149.50 -> 149.5)
|
|
1007
|
+
{ toolCallId: 'call_2', result: [{ orderId: 'A1', total: 99.99 }, { orderId: 'A2', total: 149.5 }] },
|
|
1008
|
+
],
|
|
1009
|
+
},
|
|
1010
|
+
];
|
|
1011
|
+
|
|
1012
|
+
state = resumeWithAIResponse(
|
|
1013
|
+
state,
|
|
1014
|
+
'Alice (alice@example.com) has 2 orders totaling $249.49.',
|
|
1015
|
+
undefined,
|
|
1016
|
+
toolRounds
|
|
1017
|
+
);
|
|
1018
|
+
state = runUntilPause(state);
|
|
1019
|
+
expect(state.status).toBe('completed');
|
|
1020
|
+
|
|
1021
|
+
// Verify context shows: AI call → tool calls → response (via variable assignment)
|
|
1022
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
1023
|
+
expect(formatted.text).toBe(
|
|
1024
|
+
` <entry> (current scope)
|
|
1025
|
+
--> vibe: "Find user 123 and get their orders"
|
|
1026
|
+
[tool] getUser({"id":123})
|
|
1027
|
+
[result] {"name":"Alice","email":"alice@example.com"}
|
|
1028
|
+
[tool] getOrders({"userId":123})
|
|
1029
|
+
[result] [{"orderId":"A1","total":99.99},{"orderId":"A2","total":149.5}]
|
|
1030
|
+
<-- result (text): Alice (alice@example.com) has 2 orders totaling $249.49.`
|
|
1031
|
+
);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
test('tool call with error followed by retry shows in context', () => {
|
|
1035
|
+
// Note: model variables are filtered from context
|
|
1036
|
+
const ast = parse(`
|
|
1037
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
1038
|
+
let data: text = vibe "Read the config file" m default
|
|
1039
|
+
`);
|
|
1040
|
+
|
|
1041
|
+
let state = createInitialState(ast);
|
|
1042
|
+
state = runUntilPause(state);
|
|
1043
|
+
expect(state.status).toBe('awaiting_ai');
|
|
1044
|
+
|
|
1045
|
+
// Simulate: first tool call fails, AI retries with different path
|
|
1046
|
+
const toolRounds = [
|
|
1047
|
+
{
|
|
1048
|
+
toolCalls: [
|
|
1049
|
+
{ id: 'call_1', toolName: 'readFile', args: { path: '/etc/config.json' } },
|
|
1050
|
+
],
|
|
1051
|
+
results: [
|
|
1052
|
+
{ toolCallId: 'call_1', error: 'Permission denied' },
|
|
1053
|
+
],
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
toolCalls: [
|
|
1057
|
+
{ id: 'call_2', toolName: 'readFile', args: { path: './config.json' } },
|
|
1058
|
+
],
|
|
1059
|
+
results: [
|
|
1060
|
+
{ toolCallId: 'call_2', result: '{"setting": "value"}' },
|
|
1061
|
+
],
|
|
1062
|
+
},
|
|
1063
|
+
];
|
|
1064
|
+
|
|
1065
|
+
state = resumeWithAIResponse(
|
|
1066
|
+
state,
|
|
1067
|
+
'Found config with setting=value',
|
|
1068
|
+
undefined,
|
|
1069
|
+
toolRounds
|
|
1070
|
+
);
|
|
1071
|
+
state = runUntilPause(state);
|
|
1072
|
+
expect(state.status).toBe('completed');
|
|
1073
|
+
|
|
1074
|
+
// Verify context shows: AI call → error → retry → response (via variable assignment)
|
|
1075
|
+
const formatted = formatContextForAI(state.localContext, { includeInstructions: false });
|
|
1076
|
+
expect(formatted.text).toBe(
|
|
1077
|
+
` <entry> (current scope)
|
|
1078
|
+
--> vibe: "Read the config file"
|
|
1079
|
+
[tool] readFile({"path":"/etc/config.json"})
|
|
1080
|
+
[error] Permission denied
|
|
1081
|
+
[tool] readFile({"path":"./config.json"})
|
|
1082
|
+
[result] {"setting": "value"}
|
|
1083
|
+
<-- data (text): Found config with setting=value`
|
|
1084
|
+
);
|
|
1085
|
+
});
|
|
1086
|
+
});
|