summaryrefslogtreecommitdiff
path: root/modules/test/test-ai-test-module-commands.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/test/test-ai-test-module-commands.js')
-rw-r--r--modules/test/test-ai-test-module-commands.js105
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);
+})();