// 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); })();