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);
|