diff options
| author | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-09 23:58:35 +0100 |
|---|---|---|
| committer | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-09 23:58:35 +0100 |
| commit | 9f6744dd5cb99b904f9a23fce6f2d98e8332bbf8 (patch) | |
| tree | d3f3a525e8594db010bbf0e2aa6e4c2590445339 /modules/test/test-ai-test-module-commands.js | |
| parent | 8e3a6155f6a5a1266ff1fa801c8864484bcdbda9 (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.js | 105 |
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); +})(); |