aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-tool-context-bound.js
blob: 8cf2a5e04cc871369c9348233a57df38d646d6be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// modules/test/test-tool-context-bound.js — run: node
// MAINT-023: one agentic turn must not grow without limit. Measures the same growth the
// reviewer measured (6 -> 112 705 tokens across 8 model calls) against the new bounds.
const fsMod = require('fs'), path = require('path');
const ROOT = path.join(__dirname, '..', 'core');
const store = {};
global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } };
global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } };
global.window = global;

const AI = new Function(fsMod.readFileSync(path.join(ROOT, 'module-aiassist.js'), 'utf8') + '\nreturn WebX11AIAssistantModule;')();

function mk() {
    const ai = Object.create(AI.prototype);
    ai.terminal = { print: () => {}, log: () => {}, fs: null };
    return ai;
}
const approxTokens = s => Math.ceil(s / 4);

let fails = 0, passes = 0;
function ok(name, cond, detail) {
    if (cond) { passes++; return; }
    fails++;
    console.log('FAIL ' + name + (detail ? ' — ' + detail : ''));
}

// A. a single oversized observation is clipped with a note
{
    const ai = mk();
    const big = 'x'.repeat(20000);
    const clipped = ai._clipToolResult(big);
    ok('A1 oversized result is clipped', clipped.length < 7000, 'len=' + clipped.length);
    ok('A2 the model is told it was clipped', /clipped:/.test(clipped) && /offset\/limit/.test(clipped), clipped.slice(-90));
    ok('A3 short results pass through untouched', ai._clipToolResult('ok') === 'ok');
    ok('A4 non-string results are stringified', ai._clipToolResult({ a: 1 }) === '{"a":1}');
}

// B. the running total of tool bodies stays inside the budget
{
    const ai = mk();
    const messages = [{ role: 'user', content: 'go' }];
    let worst = 0;
    for (let turn = 0; turn < 8; turn++) {
        messages.push({ role: 'assistant', content: '', tool_calls: [{ id: 'c' + turn }] });
        // the reviewer's shape: each read_file page arrives at 16 000 chars
        messages.push({ role: 'tool', tool_call_id: 'c' + turn, content: ai._clipToolResult('y'.repeat(16000)) });
        ai._boundToolContext(messages);
        const toolChars = messages.filter(m => m.role === 'tool').reduce((s, m) => s + String(m.content).length, 0);
        worst = Math.max(worst, toolChars);
    }
    // The ceiling is keepVerbatim clipped results plus tiny stubs — it must not grow with turns.
    ok('B1 total tool context stays within the ceiling', worst <= 27000, 'worst=' + worst);
    ok('B2 8 turns stay far below the old 112k-token blowup', approxTokens(worst) < 8000, 'worst≈' + approxTokens(worst) + ' tokens');
    ok('B3 the newest 4 results are verbatim', messages.filter(m => m.role === 'tool').slice(-4).every(m => !/elided/.test(m.content)));
    ok('B4 older results carry an elision stub', messages.filter(m => m.role === 'tool').slice(0, 4).every(m => /elided/.test(m.content)));
}

// B2. the ceiling is turn-independent: 20 turns cost the same as 8
{
    const ai = mk();
    const messages = [{ role: 'user', content: 'go' }];
    for (let turn = 0; turn < 20; turn++) {
        messages.push({ role: 'assistant', content: '', tool_calls: [{ id: 'c' + turn }] });
        messages.push({ role: 'tool', tool_call_id: 'c' + turn, content: ai._clipToolResult('y'.repeat(16000)) });
        ai._boundToolContext(messages);
    }
    const toolChars = messages.filter(m => m.role === 'tool').reduce((s, m) => s + String(m.content).length, 0);
    ok('B5 20 turns cost the same ceiling as 8', toolChars <= 27000, 'toolChars=' + toolChars);
}

// C. no tool messages -> nothing is touched
{
    const ai = mk();
    const messages = [{ role: 'user', content: 'hi' }, { role: 'assistant', content: 'hello' }];
    const before = JSON.stringify(messages);
    ai._boundToolContext(messages);
    ok('C1 non-tool messages are untouched', JSON.stringify(messages) === before);
}

// D. a small turn is never rewritten
{
    const ai = mk();
    const messages = [{ role: 'user', content: 'go' }, { role: 'tool', tool_call_id: 'c1', content: 'small result' }];
    ai._boundToolContext(messages);
    ok('D1 small tool results stay verbatim', messages[1].content === 'small result', messages[1].content);
}

if (fails === 0) {
    console.log('test-tool-context-bound: ' + passes + ' passed, 0 failed');
} else {
    console.log('test-tool-context-bound: ' + passes + ' passed, ' + fails + ' FAILED');
    process.exit(1);
}