diff options
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); +})(); |