diff options
Diffstat (limited to 'modules/test/test-debug-args-preview.js')
| -rw-r--r-- | modules/test/test-debug-args-preview.js | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/modules/test/test-debug-args-preview.js b/modules/test/test-debug-args-preview.js new file mode 100644 index 0000000..12292eb --- /dev/null +++ b/modules/test/test-debug-args-preview.js @@ -0,0 +1,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); |