// 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) : ''); 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 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);