aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-debug-args-preview.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/test/test-debug-args-preview.js')
-rw-r--r--modules/test/test-debug-args-preview.js112
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);