Skip to content

Commit b24a9a9

Browse files
test: 24 tests for context manager and tool resilience (all passing)
1 parent 8a96419 commit b24a9a9

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/**
2+
* Tests for context-manager.js and tool-resilience.js
3+
* Run: node --test src/agent/__tests__/context-manager.test.js
4+
*/
5+
import { describe, it } from 'node:test';
6+
import assert from 'node:assert/strict';
7+
import {
8+
estimateTokens,
9+
estimateHistoryTokens,
10+
truncateToolResult,
11+
truncateString,
12+
pruneHistory,
13+
safeToolResultContent,
14+
sanitizeToolInput,
15+
CONFIG,
16+
} from '../context-manager.js';
17+
import {
18+
classifyError,
19+
createResilientCaller,
20+
getSuggestion,
21+
} from '../tool-resilience.js';
22+
23+
// ─── Token Estimation ─────────────────────────────────────────────────────────
24+
25+
describe('estimateTokens', () => {
26+
it('estimates string tokens conservatively', () => {
27+
const text = 'Hello world, this is a test string for token estimation.';
28+
const tokens = estimateTokens(text);
29+
// ~55 chars / 3.5 ≈ 16 tokens
30+
assert.ok(tokens > 10 && tokens < 25, `Got ${tokens}`);
31+
});
32+
33+
it('handles null/undefined', () => {
34+
assert.equal(estimateTokens(null), 0);
35+
assert.equal(estimateTokens(undefined), 0);
36+
assert.equal(estimateTokens(''), 0);
37+
});
38+
39+
it('estimates array content blocks', () => {
40+
const content = [
41+
{ type: 'text', text: 'Hello world' },
42+
{ type: 'tool_use', name: 'quote_get', input: { symbol: 'AAPL' }, id: '123' },
43+
];
44+
const tokens = estimateTokens(content);
45+
assert.ok(tokens > 5, `Expected >5 tokens, got ${tokens}`);
46+
});
47+
});
48+
49+
describe('estimateHistoryTokens', () => {
50+
it('estimates message array', () => {
51+
const messages = [
52+
{ role: 'user', content: 'Analyze my chart' },
53+
{ role: 'assistant', content: 'I will analyze your chart now.' },
54+
];
55+
const tokens = estimateHistoryTokens(messages);
56+
assert.ok(tokens > 10, `Expected >10 tokens, got ${tokens}`);
57+
});
58+
});
59+
60+
// ─── Truncation ───────────────────────────────────────────────────────────────
61+
62+
describe('truncateString', () => {
63+
it('does not truncate short strings', () => {
64+
const short = 'Hello world';
65+
assert.equal(truncateString(short, 1000), short);
66+
});
67+
68+
it('truncates long strings with marker', () => {
69+
const long = 'x'.repeat(10_000);
70+
const truncated = truncateString(long, 1_000);
71+
assert.ok(truncated.length < 1_200, `Got length ${truncated.length}`);
72+
assert.ok(truncated.includes('TRUNCATED'));
73+
});
74+
75+
it('handles non-string input', () => {
76+
const obj = { key: 'value', nested: { deep: true } };
77+
const result = truncateString(obj, 1000);
78+
assert.ok(typeof result === 'string');
79+
});
80+
});
81+
82+
describe('truncateToolResult', () => {
83+
it('truncates pine_get_source', () => {
84+
const hugeSource = Array.from({ length: 500 }, (_, i) => `line ${i}: var x = ${i}`).join('\n');
85+
const result = truncateToolResult('pine_get_source', hugeSource);
86+
assert.ok(result.length < hugeSource.length, 'Should be shorter');
87+
assert.ok(result.includes('truncated'), 'Should mention truncation');
88+
});
89+
90+
it('truncates large OHLCV data', () => {
91+
const bars = Array.from({ length: 500 }, (_, i) => ({
92+
time: 1700000000 + i * 60,
93+
open: 100 + i,
94+
high: 101 + i,
95+
low: 99 + i,
96+
close: 100.5 + i,
97+
volume: 1000,
98+
}));
99+
const data = JSON.stringify({ bars, symbol: 'AAPL' });
100+
const result = truncateToolResult('data_get_ohlcv', data);
101+
const parsed = JSON.parse(result);
102+
assert.ok(parsed.bars.length <= CONFIG.MAX_OHLCV_BARS, `Got ${parsed.bars.length} bars`);
103+
assert.ok(parsed._truncated === true);
104+
});
105+
106+
it('leaves small results untouched', () => {
107+
const small = JSON.stringify({ success: true, price: 150.25 });
108+
const result = truncateToolResult('quote_get', small);
109+
assert.equal(result, small);
110+
});
111+
});
112+
113+
// ─── Input Sanitization ───────────────────────────────────────────────────────
114+
115+
describe('sanitizeToolInput', () => {
116+
it('caps OHLCV count at 200', () => {
117+
const input = { count: 500, summary: false };
118+
const sanitized = sanitizeToolInput('data_get_ohlcv', input);
119+
assert.equal(sanitized.count, 200);
120+
});
121+
122+
it('leaves normal OHLCV requests alone', () => {
123+
const input = { count: 100, summary: true };
124+
const sanitized = sanitizeToolInput('data_get_ohlcv', input);
125+
assert.equal(sanitized.count, 100);
126+
});
127+
128+
it('caps pine label requests', () => {
129+
const input = { max_labels: 200 };
130+
const sanitized = sanitizeToolInput('data_get_pine_labels', input);
131+
assert.equal(sanitized.max_labels, 50);
132+
});
133+
134+
it('caps batch_run symbols', () => {
135+
const symbols = Array.from({ length: 20 }, (_, i) => `SYM${i}`);
136+
const input = { symbols, action: 'screenshot' };
137+
const sanitized = sanitizeToolInput('batch_run', input);
138+
assert.equal(sanitized.symbols.length, 10);
139+
});
140+
});
141+
142+
// ─── History Pruning ──────────────────────────────────────────────────────────
143+
144+
describe('pruneHistory', () => {
145+
it('does not prune small histories', () => {
146+
const messages = [
147+
{ role: 'user', content: 'Hello' },
148+
{ role: 'assistant', content: 'Hi there!' },
149+
];
150+
const pruned = pruneHistory(messages);
151+
assert.equal(pruned.length, 2);
152+
});
153+
154+
it('prunes large histories', () => {
155+
// Create a history that exceeds the threshold
156+
const messages = Array.from({ length: 100 }, (_, i) => ({
157+
role: i % 2 === 0 ? 'user' : 'assistant',
158+
content: 'x'.repeat(10_000), // ~10K chars each = ~2.8K tokens × 100 = ~280K tokens
159+
}));
160+
const pruned = pruneHistory(messages);
161+
assert.ok(pruned.length < messages.length, `Pruned from ${messages.length} to ${pruned.length}`);
162+
assert.ok(pruned.length >= CONFIG.MIN_MESSAGES_TO_KEEP);
163+
});
164+
165+
it('keeps minimum messages', () => {
166+
const messages = Array.from({ length: 200 }, (_, i) => ({
167+
role: i % 2 === 0 ? 'user' : 'assistant',
168+
content: 'x'.repeat(50_000),
169+
}));
170+
const pruned = pruneHistory(messages);
171+
assert.ok(pruned.length >= CONFIG.MIN_MESSAGES_TO_KEEP);
172+
});
173+
});
174+
175+
// ─── Error Classification ─────────────────────────────────────────────────────
176+
177+
describe('classifyError', () => {
178+
it('classifies undefined as UI_NOT_READY', () => {
179+
const result = classifyError('ui_open_panel', 'undefined');
180+
assert.equal(result.type, 'UI_NOT_READY');
181+
assert.ok(result.retryable);
182+
});
183+
184+
it('classifies connection errors', () => {
185+
const result = classifyError('chart_get_state', 'CDP connection refused');
186+
assert.equal(result.type, 'CONNECTION_ERROR');
187+
assert.ok(result.retryable);
188+
});
189+
190+
it('classifies Pine Script errors as non-retryable', () => {
191+
const result = classifyError('pine_smart_compile', 'Compilation error: undeclared identifier');
192+
assert.equal(result.type, 'PINE_ERROR');
193+
assert.ok(!result.retryable);
194+
});
195+
196+
it('classifies protected indicator errors', () => {
197+
const result = classifyError('data_get_indicator', 'This indicator is protected');
198+
assert.equal(result.type, 'PROTECTED_INDICATOR');
199+
assert.ok(!result.retryable);
200+
});
201+
});
202+
203+
// ─── Resilient Caller ─────────────────────────────────────────────────────────
204+
205+
describe('createResilientCaller', () => {
206+
it('passes through on success', async () => {
207+
const mockCallTool = async () => ({ success: true, price: 150 });
208+
const resilientCall = createResilientCaller(mockCallTool);
209+
const result = await resilientCall('quote_get', { symbol: 'AAPL' });
210+
assert.deepEqual(result, { success: true, price: 150 });
211+
});
212+
213+
it('returns error object for undefined results', async () => {
214+
let calls = 0;
215+
const mockCallTool = async () => {
216+
calls++;
217+
return undefined;
218+
};
219+
const resilientCall = createResilientCaller(mockCallTool);
220+
const result = await resilientCall('ui_open_panel', { panel: 'pine-editor' });
221+
assert.equal(result.success, false);
222+
assert.ok(result.error.includes('undefined'));
223+
assert.ok(calls > 1, `Expected retries, got ${calls} calls`);
224+
});
225+
226+
it('provides suggestions for known tools', () => {
227+
const suggestion = getSuggestion('pine_set_source');
228+
assert.ok(suggestion.includes('ui_open_panel'));
229+
});
230+
});
231+
232+
console.log('All context manager tests passed ✓');

0 commit comments

Comments
 (0)