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-aiasm-rejection.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-aiasm-rejection.js')
| -rw-r--r-- | modules/test/test-aiasm-rejection.js | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/modules/test/test-aiasm-rejection.js b/modules/test/test-aiasm-rejection.js new file mode 100644 index 0000000..d6e18f6 --- /dev/null +++ b/modules/test/test-aiasm-rejection.js @@ -0,0 +1,95 @@ +// node modules/test/test-aiasm-rejection.js +// test-aiasm-rejection — the wx / webxai / webx aliases must not turn the AI chat-lock timeout +// into an unhandled promise rejection (RPT-20261107-150726-bf727247). +// +// Why this file exists: forwardToAI() used to call the assistant's chat(args) with no await and no +// .catch() inside a non-async function. When the assistant was already mid-turn, chat() waited +// _chatLockTimeoutMs (10s) and then rejected with "AI is still busy with another turn…". Nothing +// was attached to that promise, so it surfaced on window.unhandledrejection and the debug +// supervisor auto-filed a bug report for what is normal "the user typed twice" UX. +const fs = require('fs'); +const path = require('path'); + +const SRC = path.join(__dirname, '..', 'core', 'module-aiasm.js'); +const Alias = new Function(fs.readFileSync(SRC, 'utf8') + '\nreturn WebXAIAliasModule;')(); + +const BUSY = 'AI is still busy with another turn (waited 10s). Wait for it to finish, or run ' + + "'ai-stop' and retry — the running turn's lock is left alone."; + +const unhandled = []; +process.on('unhandledRejection', (e) => unhandled.push(e)); + +let passes = 0, fails = 0; +function ok(name, cond, detail) { + if (cond) { passes++; return; } + fails++; + console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); +} + +function mk(chatImpl) { + const printed = []; + const term = { + print: (msg, kind) => printed.push({ msg: String(msg), kind: kind || '' }), + moduleManager: { modules: { WebX11AIAssistantModule: chatImpl ? { chat: chatImpl } : null } }, + ollamaManager: { chat: () => Promise.resolve('ok') } + }; + const m = new Alias(); + m.terminal = term; + m.registerCommands(); + return { m, printed, term }; +} + +const tick = (ms) => new Promise(r => setTimeout(r, ms || 25)); + +(async () => { + // A1-A5: the busy rejection is printed, the returned promise settles, nothing escapes + const a = mk(() => Promise.reject(new Error(BUSY))); + const r = a.m.forwardToAI(['hi']); + ok('A1 forwardToAI returns a promise (callers can await it)', !!r && typeof r.then === 'function', typeof r); + let awaited; + try { awaited = await r; } catch (e) { awaited = 'REJECTED:' + e.message; } + ok('A2 the returned promise settles instead of rejecting', String(awaited).indexOf('REJECTED') !== 0, String(awaited)); + const errLine = a.printed.find(p => /^AI error: /.test(p.msg)); + ok('A3 the busy timeout is printed to the terminal', !!errLine && /still busy/.test(errLine.msg), + JSON.stringify(a.printed)); + ok('A4 it is printed as an error line', !!errLine && errLine.kind === 'error', errLine && errLine.kind); + await tick(); + ok('A5 no unhandledRejection escaped', unhandled.length === 0, + unhandled.map(e => (e && e.message) || String(e)).join('; ')); + + // A6-A7: the same through the command map the terminal dispatcher actually awaits + const b = mk(() => Promise.reject(new Error(BUSY))); + let cmdErr = null; + try { await b.m.commands['wx'](['second question']); } catch (e) { cmdErr = e; } + ok('A6 `wx` command does not reject', !cmdErr, cmdErr && cmdErr.message); + await tick(); + ok('A7 `wx` prints the busy error', b.printed.some(p => /AI error: .*still busy/.test(p.msg)), + JSON.stringify(b.printed)); + + // A8-A10: happy path — a resolving chat must print nothing at all + const c = mk(() => Promise.resolve({ role: 'assistant', content: 'pong' })); + let res; + try { res = await c.m.forwardToAI(['ping']); } catch (e) { res = 'REJECTED:' + e.message; } + await tick(); + ok('A8 a resolving chat prints nothing', c.printed.length === 0, JSON.stringify(c.printed)); + ok('A9 happy path does not throw', String(res).indexOf('REJECTED') !== 0, String(res)); + ok('A10 happy path leaks no unhandled rejection', unhandled.length === 0, String(unhandled.length)); + + // A11-A12: ollama fallback (no assistant module) is handled the same way + const d = mk(null); + d.term.ollamaManager.chat = () => Promise.reject(new Error('ollama offline')); + await d.m.forwardToAI(['hi']); + await tick(); + ok('A11 ollama fallback failure is printed', d.printed.some(p => /AI error: ollama offline/.test(p.msg)), + JSON.stringify(d.printed)); + ok('A12 ollama fallback leaks no unhandled rejection', unhandled.length === 0, String(unhandled.length)); + + // A13: usage is still printed when the question is missing + const e = mk(null); + e.m.forwardToAI([]); + ok('A13 usage printed when args are empty', e.printed.some(p => /Usage: webxai/.test(p.msg)), + JSON.stringify(e.printed)); + + console.log('test-aiasm-rejection: ' + passes + ' passed, ' + fails + ' failed'); + process.exit(fails ? 1 : 0); +})(); |