aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-ai-fs-tools.js
blob: 3968abed111c49ed8b2931342e563724bd3932d1 (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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// modules/test/test-ai-fs-tools.js — run: node
// Proves MAINT-011 (list_files must see the virtual /sys mount) and MAINT-012
// (write_file must not report success when the write was refused).
const fsMod = require('fs'), path = require('path');
const ROOT = path.join(__dirname, '..', 'core');
const ls = {};
global.localStorage = { getItem: k => (k in ls ? ls[k] : null), setItem: (k, v) => { ls[k] = String(v); }, removeItem: k => { delete ls[k]; } };
global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } };
global.window = global;

const load = (file, cls) => new Function(fsMod.readFileSync(path.join(ROOT, file), 'utf8') + '\nreturn ' + cls + ';')();
const VFS = load('virtual-fs.js', 'VirtualFileSystem');
const AI = load('module-aiassist.js', 'WebX11AIAssistantModule');

function mk() {
    const out = [];
    const vfs = new VFS();
    vfs.saveToStorage = () => true;
    // Bypass the constructor (it touches the DOM) but keep the real prototype methods.
    const ai = Object.create(AI.prototype);
    ai.terminal = { print: (m, t) => out.push({ text: String(m), type: t || '' }), log: () => {}, fs: vfs };
    ai.checkGuardrail = () => null;
    return { ai, vfs, out };
}

let fails = 0, passes = 0;
function ok(name, cond, detail) {
    if (cond) { passes++; return; }
    fails++;
    console.log('FAIL ' + name + (detail ? ' — ' + detail : ''));
}
const run = async (ai, name, args) => JSON.parse(await ai.runTool(name, args));

(async () => {
    // A. list_files on the virtual /sys mount
    {
        const { ai } = mk();
        const res = await run(ai, 'list_files', { path: '/sys' });
        ok('A1 /sys lists entries instead of an error', Array.isArray(res), JSON.stringify(res).slice(0, 120));
        ok('A2 /sys contains the modules mount', Array.isArray(res) && res.some(e => e.name === 'modules'),
            JSON.stringify(res).slice(0, 160));
    }

    // B. a genuinely missing directory still reports the error
    {
        const { ai } = mk();
        const res = await run(ai, 'list_files', { path: '/no-such-dir' });
        ok('B1 missing path reports Directory not found', !!res.error && /Directory not found/.test(res.error), JSON.stringify(res));
    }

    // C. write_file into a missing parent must not claim success
    {
        const { ai, vfs } = mk();
        const res = await ai.runTool('write_file', { path: '/missing/dir/f.txt', content: 'x' });
        ok('C1 refused write reports failure', typeof res === 'string' && /Write FAILED/.test(res), String(res));
        ok('C2 nothing was created', vfs.readFile('/missing/dir/f.txt') === null);
    }

    // D. the same write succeeds once the parent exists
    {
        const { ai, vfs } = mk();
        vfs.mkdirRecursive('/missing/dir');
        const res = await ai.runTool('write_file', { path: '/missing/dir/f.txt', content: 'x' });
        ok('D1 successful write still reports success', /File written/.test(String(res)), String(res));
        ok('D2 content landed', vfs.readFile('/missing/dir/f.txt') === 'x');
    }

    // E. write onto a directory node is refused, and the directory survives
    {
        const { ai, vfs } = mk();
        vfs.mkdirRecursive('/adir');
        vfs.writeFile('/adir/keep.txt', 'k');
        const res = await ai.runTool('write_file', { path: '/adir', content: 'x' });
        ok('E1 write onto a directory is refused', /Write FAILED/.test(String(res)), String(res));
        ok('E2 directory contents intact', vfs.readFile('/adir/keep.txt') === 'k');
    }

    // F. a read-only virtual path names that as the reason
    {
        const { ai } = mk();
        const res = await ai.runTool('write_file', { path: '/sys/info', content: 'x' });
        ok('F1 virtual path reports read-only', /Write FAILED: \/sys\/info — read-only virtual path/.test(String(res)), String(res));
    }

    // G. list_files on a FILE is an error, on '/' is the root listing
    {
        const { ai, vfs } = mk();
        vfs.writeFile('/file.txt', 'x');
        const onFile = await run(ai, 'list_files', { path: '/file.txt' });
        ok('G1 listing a file reports Directory not found', !!onFile.error && /\/file\.txt/.test(onFile.error), JSON.stringify(onFile));
        const onRoot = await run(ai, 'list_files', { path: '/' });
        ok('G2 root lists the virtual sys mount', Array.isArray(onRoot) && onRoot.some(e => e.name === 'sys'), JSON.stringify(onRoot).slice(0, 120));
    }

    // H. no argument means the CURRENT directory, not the root
    {
        const { ai, vfs } = mk();
        vfs.mkdirRecursive('/work');
        vfs.writeFile('/work/inside.txt', 'x');
        vfs.writeFile('/root-only.txt', 'y');
        vfs.currentPath = ['work'];
        const cwd = await run(ai, 'list_files', {});
        const names = (cwd.error ? [] : cwd.map(e => e.name));
        ok('H1 no-arg lists the cwd', names.includes('inside.txt') && !names.includes('root-only.txt'), JSON.stringify(cwd).slice(0, 140));
        ok('H2 cwd is restored after the call', JSON.stringify(vfs.currentPath) === '["work"]', JSON.stringify(vfs.currentPath));
    }

    // I. a relative path resolves from the cwd
    {
        const { ai, vfs } = mk();
        vfs.mkdirRecursive('/docs');
        vfs.writeFile('/docs/sub.txt', 'x');
        vfs.currentPath = ['docs'];
        const res = await run(ai, 'list_files', { path: 'sub.txt' });
        ok('I1 a relative file path is resolved, not guessed', !!res.error, JSON.stringify(res));
        const dirRes = await run(ai, 'list_files', { path: '.' });
        ok('I2 "." lists the cwd', Array.isArray(dirRes) && dirRes.some(e => e.name === 'sub.txt'), JSON.stringify(dirRes).slice(0, 120));
    }

    if (fails === 0) {
        console.log('test-ai-fs-tools: ' + passes + ' passed, 0 failed');
    } else {
        console.log('test-ai-fs-tools: ' + passes + ' passed, ' + fails + ' FAILED');
        process.exit(1);
    }
})();