aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-ai-test-module-commands.js
blob: 96830114450764778d249c49493122ba8098acae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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);
})();