aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-agent-loop-ceiling.js
blob: 9a00e2bfa3ada8f0e8f4453f61ab8505561e94ec (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
// modules/test/test-agent-loop-ceiling.js — run: node
// MAINT-006: the hard stop used to be reachable only when EVERY call in a batch was blocked, so a
// turn that mixed one repeated call with one novel call ran to maxTurns. This drives the real
// agenticLoop with a deterministic model stub and asserts the mixed batch ends at the ceiling.
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 load = (file, cls) => new Function(fsMod.readFileSync(path.join(ROOT, file), 'utf8') + '\nreturn ' + cls + ';')();
const AI = load('module-aiassist.js', 'WebX11AIAssistantModule');
const VFS = load('virtual-fs.js', 'VirtualFileSystem');

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

async function runLoop(shape, maxTurns) {
    const out = [];
    const vfs = new VFS();
    vfs.saveToStorage = () => true;
    vfs.writeFile('/probe.txt', 'alpha\nbeta\ngamma');
    const ai = new AI();          // real constructor: the loop reads _commandFreq/_toolCallLog
    let turn = 0;
    ai.terminal = {
        fs: vfs,
        print: (m) => out.push(String(m)),
        log: () => {},
        moduleManager: { modules: {}, getAllCommands: () => ({ read_file: 'AI', list_files: 'AI' }) },
        ollamaManager: {
            chatWithTools: async () => {
                turn++;
                const calls = shape(turn);
                return { message: { role: 'assistant', content: '', tool_calls: calls }, tool_calls: calls };
            },
        },
    };
    ai._debugPanelEnabled = false;
    let res = null, err = null;
    try { res = await ai.agenticLoop([{ role: 'user', content: 'loop ceiling probe' }], { maxTurns, agent: 'probe' }); }
    catch (e) { err = e.message; }
    return { out, res, err, turn };
}

// A. mixed batch: one identical call every turn + one novel call every turn
(async () => {
    const { out, res, err, turn } = await runLoop((t) => ([
        { id: 'rep-' + t, type: 'function', function: { name: 'read_file', arguments: JSON.stringify({ path: '/probe.txt' }) } },
        { id: 'nov-' + t, type: 'function', function: { name: 'list_files', arguments: JSON.stringify({ path: '/novel-' + t }) } },
    ]), 20);
    const joined = out.join('\n');
    ok('A1 no exception', err === null, String(err));
    ok('A2 the ceiling fires', /no progress after \d+ identical calls/.test(joined), joined.slice(-200));
    ok('A3 it stops well before maxTurns', turn < 20, 'ran ' + turn + ' turns');
    ok('A4 it returns a partial answer', !!res && typeof res.finalAnswer === 'string', JSON.stringify(res && Object.keys(res)));
})();

// B. all-identical batch still stops (the original path must not regress)
setTimeout(async () => {
    const { out, err, turn } = await runLoop((t) => ([
        { id: 'r' + t, type: 'function', function: { name: 'read_file', arguments: JSON.stringify({ path: '/probe.txt' }) } },
    ]), 20);
    const joined = out.join('\n');
    ok('B1 all-blocked batch has no exception', err === null, String(err));
    ok('B2 all-blocked batch stops early', turn < 20, 'ran ' + turn + ' turns');
    ok('B3 all-blocked batch reports the ceiling', /no progress after \d+ identical calls/.test(joined), joined.slice(-200));

    // C. a genuinely productive loop is NOT stopped early: every call differs
    const third = await runLoop((t) => ([
        { id: 'u' + t, type: 'function', function: { name: 'list_files', arguments: JSON.stringify({ path: '/dir-' + t }) } },
    ]), 3);
    ok('C1 a productive loop runs to maxTurns', third.turn >= 3, 'ran ' + third.turn + ' turns');
    ok('C2 no ceiling message on a productive loop', !/no progress after/.test(third.out.join('\n')), third.out.join('|').slice(-160));

    if (fails === 0) console.log('test-agent-loop-ceiling: ' + passes + ' passed, 0 failed');
    else { console.log('test-agent-loop-ceiling: ' + passes + ' passed, ' + fails + ' FAILED'); process.exit(1); }
}, 50);