summaryrefslogtreecommitdiff
path: root/modules/test/test-ai-test-module-commands.js
diff options
context:
space:
mode:
authorHermes SysOp <sysop@webx11.duckdns.org>2026-11-09 23:58:35 +0100
committerHermes SysOp <sysop@webx11.duckdns.org>2026-11-09 23:58:35 +0100
commit9f6744dd5cb99b904f9a23fce6f2d98e8332bbf8 (patch)
treed3f3a525e8594db010bbf0e2aa6e4c2590445339 /modules/test/test-ai-test-module-commands.js
parent8e3a6155f6a5a1266ff1fa801c8864484bcdbda9 (diff)
fix(newos4): six crash classes from the feedback queue (v4.6.158)
Each item is symptom -> cause -> fix, re-verified against the served bytes. 1. debug-ai panel threw "Cannot read properties of undefined (reading 'substring')" (RPT-20261108-231622). _updateDebugPanel and two sibling previews called JSON.stringify(entry.args).substring(); JSON.stringify(undefined) returns undefined, and _toolCallLog entries carry args: undefined for any tool call with no arguments. All three previews now go through _formatToolArgs(), which renders "null" and never throws. 2. test_module threw "Cannot convert undefined or null to object" (RPT-20261108-232127). Object.keys(instance.commands) assumed every module initialises a command map. The throw landed after loadModuleFromCode had already succeeded, so the rolledBack:true branches were skipped and an unverified module stayed loaded. Both sites now require instance.commands. 3. list_files threw "originalPath.replace is not a function" (RPT-20261109-172742, 3 occurrences in errors.json). A model can send a non-string path despite the schema. The path family (list_files/read_file/write_file/make_directory/ delete_file) now coerces through _pathArg() and answers the tool's own honest error instead of a TypeError. 4. wpm search failed for EVERY keyword (RPT-20261109-123907): packages.json entry `kanban` used `description` while the other 31 use `desc`, and search() called pkg.desc.toLowerCase() mid-loop. Catalog reads now go through _pkg() / _packageMap() (desc || description, values coerced to strings), and the kanban entry carries name/version/desc/commands. Re-running the original loop for pdf/git/zzz is clean. 5. wx/webxai/webx turned a chat-lock timeout into an unhandled rejection (RPT-20261107-150726): forwardToAI() called chat(args) in a synchronous function without returning or catching the promise, so the 10 s busy throw never reached dispatchCommand's catch. It now returns the promise with a .catch that prints "AI error: ..." through terminal.print — console.error is hooked by the debug supervisor and would re-file the same report. 6. CryptoGzip._base64ToBytes threw a raw DOMException on non-base64 input (RPT-20261102-203616). It now validates first and throws Error("not valid base64 input (<reason>)"). The duplicate shadowing definitions at the bottom of the class were deleted — the later copy is the one that carried the unguarded atob(). Also regenerated modules/core/core-bundle.js (generated, never hand-edited) and bumped the build v4.6.157 -> v4.6.158 (main.html x3 + module-loader.js). Verified: 35/35 node tests, including 6 new regression tests each proven red against the pre-fix source; an independent orchestrator harness 36/36; and newos4-maintain.py --fix all PASS after regenerating the stale changelog page. Served bytes are byte-identical for the bundle, main.html and both ext modules, and a live headless-Chromium run against v4.6.158 shows WpmModule.search('pdf') returning 1 result, the debug panel rendering a no-arg entry as "null", _base64ToBytes('grüße') raising a plain Error that names base64, and 0 JS errors on load. m.pull (RPT-20261102-211438) is closed as a usage error: no module in the tree defines pull(); the real interfaces are the webdav-pull and model-pull commands.
Diffstat (limited to 'modules/test/test-ai-test-module-commands.js')
-rw-r--r--modules/test/test-ai-test-module-commands.js105
1 files changed, 105 insertions, 0 deletions
diff --git a/modules/test/test-ai-test-module-commands.js b/modules/test/test-ai-test-module-commands.js
new file mode 100644
index 0000000..9683011
--- /dev/null
+++ b/modules/test/test-ai-test-module-commands.js
@@ -0,0 +1,105 @@
+// node modules/test/test-ai-test-module-commands.js
+// test_module used `oldInstance ? Object.keys(oldInstance.commands) : []`. A module
+// that has no command map (or whose reloaded instance does not re-register one)
+// made Object.keys throw "Cannot convert undefined or null to object". The throw
+// happened AFTER loadModuleFromCode() succeeded, so runTool's catch returned an
+// error and the rollback branches were skipped: the unverified module stayed
+// loaded. The guards keep the rollback path reachable.
+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');
+
+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;
+global.document = { getElementById: () => null, createElement: () => ({ style: {} }), body: { appendChild() {}, removeChild() {} } };
+global.navigator = global.navigator || {};
+
+const AI = new Function(fsMod.readFileSync(SRC, 'utf8') + '\nreturn WebX11AIAssistantModule;')();
+
+// `next` is what loadModuleFromCode installs as the reloaded instance.
+function mk(oldInstance, next) {
+ const out = [];
+ const modules = { Demo: oldInstance };
+ const ai = Object.create(AI.prototype);
+ ai.terminal = {
+ print: (m, t) => out.push({ text: String(m), type: t || '' }),
+ log: () => {}, printHTML: () => {},
+ fs: { readFile: () => '// demo module source', currentPath: [] },
+ moduleManager: {
+ modules,
+ loadModuleFromCode: async (code, url) => { modules.Demo = next; return true; }
+ }
+ };
+ ai.checkGuardrail = () => null;
+ ai._errorLog = [];
+ ai._toolCallLog = [];
+ ai._captureConsoleErrors = async () => [];
+ ai._captureCommandOutput = async () => 'ok';
+ ai._captureShellCommand = async () => 'ok';
+ return { ai, modules, out };
+}
+
+let fails = 0, passes = 0;
+function ok(name, cond, detail) {
+ if (cond) { passes++; return; }
+ fails++;
+ console.log('FAIL ' + name + (detail ? ' — ' + detail : ''));
+}
+
+(async () => {
+ // A. reloaded instance lost its command map: must report + roll back,
+ // not throw and leave the bad instance installed.
+ {
+ const oldInstance = { commands: { 'demo-a': {}, 'demo-b': {} } };
+ const next = { /* no commands property at all */ };
+ const { ai, modules } = mk(oldInstance, next);
+ const raw = await ai.runTool('test_module', JSON.stringify({ name: 'Demo' }));
+ let res = null;
+ try { res = JSON.parse(raw); } catch (e) {}
+ ok('A1 result is JSON', res !== null, String(raw).slice(0, 200));
+ ok('A2 reports Commands lost after edit', !!res && res.error === 'Commands lost after edit',
+ String(raw).slice(0, 200));
+ ok('A3 lists both lost commands', !!res && Array.isArray(res.lost) && res.lost.length === 2,
+ JSON.stringify(res && res.lost));
+ ok('A4 marks rolledBack', !!res && res.rolledBack === true, String(raw).slice(0, 200));
+ ok('A5 old instance restored', modules.Demo === oldInstance, 'modules.Demo is ' + (modules.Demo === next ? 'the new instance' : 'neither'));
+ ok('A6 no Object.keys TypeError', !/Cannot convert undefined or null to object/.test(String(raw)),
+ String(raw).slice(0, 200));
+ }
+
+ // B. the OLD instance itself has no command map: Object.keys(old.commands)
+ // used to throw before the new module was even loaded.
+ {
+ const oldInstance = { /* no commands */ };
+ const next = { commands: { 'demo-a': {} } };
+ const { ai } = mk(oldInstance, next);
+ const raw = await ai.runTool('test_module', JSON.stringify({ name: 'Demo' }));
+ let res = null;
+ try { res = JSON.parse(raw); } catch (e) {}
+ ok('B1 does not throw on a command-less old instance', res !== null, String(raw).slice(0, 200));
+ ok('B2 reports success (nothing was lost)', !!res && res.success === true, String(raw).slice(0, 200));
+ ok('B3 no Object.keys TypeError', !/Cannot convert undefined or null to object/.test(String(raw)),
+ String(raw).slice(0, 200));
+ }
+
+ // C. control: unchanged command map still passes the verification
+ {
+ const oldInstance = { commands: { 'demo-a': {} } };
+ const next = { commands: { 'demo-a': {} } };
+ const { ai, modules } = mk(oldInstance, next);
+ const raw = await ai.runTool('test_module', JSON.stringify({ name: 'Demo' }));
+ let res = null;
+ try { res = JSON.parse(raw); } catch (e) {}
+ ok('C1 success', !!res && res.success === true, String(raw).slice(0, 200));
+ ok('C2 new instance stays loaded', modules.Demo === next);
+ }
+
+ console.log('test-ai-test-module-commands: ' + passes + ' passed, ' + fails + ' failed');
+ process.exit(fails ? 1 : 0);
+})();