summaryrefslogtreecommitdiff
path: root/modules/test/test-wpm-index.js
diff options
context:
space:
mode:
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);
+});