summaryrefslogtreecommitdiff
path: root/modules/test/test-debug-args-preview.js
blob: 12292eb253bc1da2f34e229b0cc417badad08e12 (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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// node modules/test/test-debug-args-preview.js
// Regression for the filed report:
//   "Error in WebX11AIAssistantModule.debug-ai: Cannot read properties of undefined (reading 'substring')"
// Cause: _toolCallLog entries carry `args: undefined` (a tool call with no
// arguments) or no `args` key at all (_recordToolCall never sets one), and both
// the debug panel and the tool-call log did `JSON.stringify(args).substring(...)`.
// JSON.stringify(undefined) === undefined, so .substring threw inside the render
// path and the panel died before printing anything.
const fsMod = require('fs'), path = require('path');
const ROOT = path.join(__dirname, '..', 'core');
const SRC = process.env.AIASSIST_SRC || path.join(ROOT, 'module-aiassist.js');

// --- browser stubs (the class is a browser module) -------------------------
const store = {};
global.localStorage = {
    getItem: k => (k in store ? store[k] : null),
    setItem: (k, v) => { store[k] = String(v); },
    removeItem: k => { delete store[k]; },
    clear: () => { for (const k of Object.keys(store)) delete store[k]; }
};
global.window = global;
const panelEl = {
    attrs: {}, innerHTML: '',
    getAttribute(k) { return (k in this.attrs) ? this.attrs[k] : null; },
    setAttribute(k, v) { this.attrs[k] = String(v); }
};
global.document = {
    getElementById: id => (id === 'debug-ai-panel' ? panelEl : null),
    createElement: () => ({ style: {}, click() {}, setAttribute() {} }),
    body: { appendChild() {}, removeChild() {} }
};

const src = fsMod.readFileSync(SRC, 'utf8');
const AI = new Function(src + '\nreturn WebX11AIAssistantModule;')();

// Bypass the constructor (it builds prompts / touches DOM) but keep the real
// prototype methods under test.
function mk() {
    const out = [];
    const ai = Object.create(AI.prototype);
    ai._toolCallLog = [];
    ai._debugPanelEnabled = true;
    ai._lastRawResponse = null;
    ai._lastRawMessages = null;
    ai._errorLog = [];
    ai.terminal = {
        print: (m, t) => out.push({ text: String(m), type: t || '' }),
        log: () => {}, printHTML: () => {}
    };
    return { ai, out };
}

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

// 1-4: helper contract (guarded so a missing helper reports FAIL instead of
// aborting the run — this is what proves the test catches the old code).
{
    const { ai } = mk();
    const fmt = (v, m) => (typeof ai._formatToolArgs === 'function' ? ai._formatToolArgs(v, m) : '<no _formatToolArgs>');
    ok('1 _formatToolArgs(undefined) === "null"', fmt(undefined, 60) === 'null', JSON.stringify(fmt(undefined, 60)));
    ok('2 _formatToolArgs(null) === "null"', fmt(null, 60) === 'null', JSON.stringify(fmt(null, 60)));
    ok('3 _formatToolArgs({a:1}) === \'{"a":1}\'', fmt({ a: 1 }, 60) === '{"a":1}', fmt({ a: 1 }, 60));
    const long = 'x'.repeat(200);
    const cut = fmt(long, 10);
    ok('4 truncates to max', cut === 'xxxxxxxxxx', JSON.stringify(cut));
}

// 5: the debug panel renders an entry whose args is undefined (the live shape
// pushed at the tool-call site) and one with no args key at all (_recordToolCall).
{
    const { ai } = mk();
    ai._toolCallLog.push({ turn: 1, name: 'list_files', args: undefined, duration: 0.2, error: false });
    ai._toolCallLog.push({ turn: 2, name: 'get_status', duration: 0.1, ok: true });
    panelEl.attrs['data-tab'] = 'tools';
    panelEl.innerHTML = '';
    let threw = null;
    try { ai._updateDebugPanel(); } catch (e) { threw = e; }
    ok('5 _updateDebugPanel does not throw on args:undefined', threw === null, threw && threw.message);
    ok('5b panel rendered the "null" preview', !threw && /debug-args">null</.test(panelEl.innerHTML),
        String(panelEl.innerHTML).slice(0, 400));
}

// 6: the terminal tool-call log (var e = this._toolCallLog[i]) does not throw.
{
    const { ai, out } = mk();
    ai._toolCallLog.push({ turn: 1, name: 'list_files', args: undefined, duration: 0.2, error: false });
    ai._toolCallLog.push({ turn: 2, name: 'read_file', args: '{"path":"/a.txt"}', duration: 0.1 });
    let threw = null;
    try { ai._showToolCallLog(); } catch (e) { threw = e; }
    const printed = out.map(o => o.text).join('\n');
    ok('6 _showToolCallLog does not throw on args:undefined', threw === null, threw && threw.message);
    ok('6b log shows null for the argument-less call', /list_files\(null\)/.test(printed), printed.slice(0, 300));
    ok('6c real args still shown', /read_file\(\{"path":"\/a\.txt"\}\)/.test(printed), printed.slice(0, 300));
}

// 7: a circular argument object must not throw either (JSON.stringify throws there).
{
    const { ai } = mk();
    const circ = {}; circ.self = circ;
    let threw = null, got = '';
    try { got = ai._formatToolArgs(circ, 40); } catch (e) { threw = e; }
    ok('7 circular args degrade instead of throwing', threw === null && typeof got === 'string' && got.length > 0,
        threw ? threw.message : got);
}

console.log('test-debug-args-preview: ' + passes + ' passed, ' + fails + ' failed');
process.exit(fails ? 1 : 0);