summaryrefslogtreecommitdiff
path: root/modules/test/test-wpm-index.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-wpm-index.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-wpm-index.js')
-rw-r--r--modules/test/test-wpm-index.js164
1 files changed, 164 insertions, 0 deletions
diff --git a/modules/test/test-wpm-index.js b/modules/test/test-wpm-index.js
new file mode 100644
index 0000000..ae9cc35
--- /dev/null
+++ b/modules/test/test-wpm-index.js
@@ -0,0 +1,164 @@
+// node modules/test/test-wpm-index.js
+'use strict';
+// Regression test — RPT-20261109-123907-5a6273ab (source tool: wpm_search)
+//
+// packages.json `kanban` was authored with `description` while the other 31 entries
+// use `desc`, and modules/ext/wpm.js read `pkg.desc.toLowerCase()` inside search().
+// Result: EVERY `wpm search <kw>` threw
+// TypeError: Cannot read properties of undefined (reading 'toLowerCase')
+// for any keyword (pdf, git, zzz) because the bad entry sits mid-loop.
+//
+// This test loads the real served catalog + the real wpm.js (no network, no browser)
+// and asserts that search/list/info/remove/install cannot throw on it, and that an
+// entry with no desc/category at all is also survivable.
+//
+// Run: node modules/test/test-wpm-index.js
+
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..', '..');
+const WPM_SRC = path.join(ROOT, 'modules', 'ext', 'wpm.js');
+const CATALOG = path.join(ROOT, 'packages.json');
+
+let checks = 0;
+let failures = 0;
+function ok(cond, label) {
+ checks++;
+ if (cond) { console.log(' ok ' + label); return true; }
+ failures++;
+ console.error(' FAIL ' + label);
+ return false;
+}
+
+// ── Load wpm.js exactly as the browser would, minus the browser ──────────────
+// window is an empty object (a real browser has window.webx11 undefined until
+// module-loader.js runs), so the self-registration block at the bottom is skipped.
+const code = fs.readFileSync(WPM_SRC, 'utf8');
+const store = {};
+const fakeLocalStorage = {
+ getItem: (k) => (Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null),
+ setItem: (k, v) => { store[k] = String(v); },
+ removeItem: (k) => { delete store[k]; },
+};
+const noNetwork = () => Promise.reject(new Error('network disabled in test'));
+const WpmModule = new Function('window', 'localStorage', 'fetch', code + '\n;return WpmModule;')
+ ({}, fakeLocalStorage, noNetwork);
+
+function makeWpm(catalog) {
+ const logs = [];
+ const m = new WpmModule();
+ m.terminal = {
+ print: (...args) => { logs.push(args.map((a) => String(a)).join(' ')); },
+ moduleManager: {
+ loadModuleFromURL: async () => true,
+ unregisterModule: () => true,
+ },
+ };
+ m.installed = {};
+ m.saveInstalled = () => {};
+ // Pre-seed the index: loadIndex() returns this.index and never fetches.
+ m.index = catalog || JSON.parse(fs.readFileSync(CATALOG, 'utf8'));
+ return { m, logs };
+}
+
+async function main() {
+ const raw = JSON.parse(fs.readFileSync(CATALOG, 'utf8'));
+ const catalog = raw.packages;
+ const names = Object.keys(catalog);
+
+ console.log('\n[1] catalog shape + normalisation (' + names.length + ' packages)');
+ ok(names.length === 32, 'catalog still has 32 packages');
+ ok(!!catalog.kanban, 'kanban entry exists (the entry that broke search)');
+
+ const { m: probe } = makeWpm();
+ const map = probe._packageMap(probe.index);
+
+ const missingDesc = names.filter((n) => !map[n] || typeof map[n].desc !== 'string' || map[n].desc.length === 0);
+ const missingCat = names.filter((n) => !map[n] || typeof map[n].category !== 'string' || map[n].category.length === 0);
+ ok(missingDesc.length === 0, 'every entry normalises to a non-empty string desc' + (missingDesc.length ? ' — bad: ' + missingDesc.join(',') : ''));
+ ok(missingCat.length === 0, 'every entry normalises to a non-empty string category' + (missingCat.length ? ' — bad: ' + missingCat.join(',') : ''));
+ names.forEach((n) => {
+ ok(typeof catalog[n].url === 'string' && catalog[n].url.length > 0, n + ': raw url present');
+ });
+ ok(typeof catalog.kanban.desc === 'string' && catalog.kanban.desc.length > 0,
+ 'kanban now carries the canonical `desc` field');
+ ok(catalog.kanban.description === undefined || catalog.kanban.description === catalog.kanban.desc,
+ 'kanban legacy `description` (kept for other readers) agrees with desc');
+
+ console.log('\n[2] search over the real catalog for the keywords that used to throw');
+ for (const kw of ['pdf', 'git', 'zzz']) {
+ const { m, logs } = makeWpm();
+ let err = null;
+ try { await m.search(kw); } catch (e) { err = e; }
+ ok(!err, 'search("' + kw + '") did not throw' + (err ? ' — ' + err.name + ': ' + err.message : ''));
+ ok(logs.length > 0, 'search("' + kw + '") printed something (' + logs.length + ' lines)');
+ ok(!logs.some((l) => l.includes('undefined')), 'search("' + kw + '") printed no "undefined"');
+ if (kw === 'pdf') {
+ // search() prints the entry's display `name` ("File Manager"), not the map key.
+ ok(logs.some((l) => /pdf/i.test(l) && l.includes('File Manager')), 'search("pdf") finds the PDF-previewing file manager');
+ } else {
+ ok(logs.some((l) => l.includes('No packages matching')), 'search("' + kw + '") reports no match cleanly');
+ }
+ }
+
+ console.log('\n[3] the other catalog readers (list / info / remove / install) survive kanban');
+ {
+ const { m, logs } = makeWpm();
+ let err = null;
+ try {
+ await m.list();
+ await m.info('kanban');
+ } catch (e) { err = e; }
+ ok(!err, 'list()+info("kanban") did not throw' + (err ? ' — ' + err.message : ''));
+ ok(!logs.some((l) => l.includes('undefined')), 'list()+info("kanban") printed no "undefined"');
+ ok(logs.some((l) => l.includes('kanban - ')), 'list() renders the kanban line');
+
+ m.installed.kanban = { version: '1.0.0', installed: '2026-11-09T00:00:00Z' };
+ let rmErr = null;
+ try { await m.remove('kanban'); } catch (e) { rmErr = e; }
+ ok(!rmErr, 'remove("kanban") did not throw (name falls back to the map key)' + (rmErr ? ' — ' + rmErr.message : ''));
+
+ const inst = makeWpm();
+ let inErr = null;
+ try { await inst.m.install('kanban'); } catch (e) { inErr = e; }
+ ok(!inErr, 'install("kanban") did not throw (catalog fetch is stubbed to fail)');
+ ok(!inst.logs.some((l) => l.includes('undefined')), 'install("kanban") printed no "undefined"');
+ ok(inst.logs.some((l) => l.includes('Installing Kanban Board v1.0.0')), 'install("kanban") prints the normalised name/version');
+ }
+
+ console.log('\n[4] an entry with no desc/category/name/version at all cannot throw');
+ {
+ const { m, logs } = makeWpm();
+ m.index.packages.brokenpkg = { url: 'modules/ext/brokenpkg.js' };
+ let err = null;
+ try {
+ await m.search('zzz');
+ await m.search('broken');
+ await m.list();
+ await m.info('brokenpkg');
+ } catch (e) { err = e; }
+ ok(!err, 'field-less entry: search/list/info did not throw' + (err ? ' — ' + err.name + ': ' + err.message : ''));
+ ok(logs.length > 0, 'field-less entry: something was printed');
+ ok(!logs.some((l) => l.includes('undefined')), 'field-less entry: no "undefined" leaked to the terminal');
+ ok(logs.some((l) => l.includes('brokenpkg [')), 'field-less entry: search hit renders empty category, not a crash');
+ }
+
+ // also: an index whose `packages` key is missing entirely
+ console.log('\n[5] malformed index (no `packages` object) cannot throw');
+ {
+ const { m, logs } = makeWpm({ name: 'broken-index' });
+ let err = null;
+ try { await m.search('pdf'); await m.list(); } catch (e) { err = e; }
+ ok(!err, 'index without packages: search/list did not throw' + (err ? ' — ' + err.message : ''));
+ ok(logs.some((l) => l.includes('No packages matching')), 'index without packages: clean "no match" output');
+ }
+
+ console.log('\n' + (failures ? 'FAIL' : 'PASS') + ' — ' + (checks - failures) + '/' + checks + ' checks');
+ process.exit(failures ? 1 : 0);
+}
+
+main().catch((e) => {
+ console.error('FAIL — unexpected error: ' + (e && e.stack || e));
+ process.exit(1);
+});