aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-ai-fs-tools.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-27 14:13:35 +0100
committerAgando <roduccomm@gmail.com>2026-10-27 14:13:35 +0100
commitea3acceac673b3cb6d84d38f968f057082d8ccb8 (patch)
treefc318284f8d363a8db098bdd4ab548ad6e2ae548 /modules/test/test-ai-fs-tools.js
parent669b66591b3efbb8fc8642d25a67c55d93cfe4e9 (diff)
fix(newos4): connect to Ollama on first use, honest write_file, list_files sees /sys
Three defects, each re-probed first and covered by a test that is red on HEAD. - ollama-manager.js: nothing connects at boot; only the Ollama tab button called connect(), so a user's first message died with "Ollama not connected. Use ollamaconnect first." even when the endpoint was healthy. New _ensureConnected() runs from chat(), chatStream() and chatWithTools(): one attempt, shared by concurrent callers, a "Connecting…" hint, and only then the error — which now says the endpoint is unreachable instead of telling the user to run a command they already expected to work. - module-aiassist.js write_file tool: fs.writeFile()'s false return was dropped and the tool answered "File written: …" anyway, sending the model on to a read that found nothing (MAINT-012). It now returns "Write FAILED: … — parent directory missing or target is a directory; nothing was written". - module-aiassist.js list_files tool: the hand-rolled walk over root.content never resolved the virtual mounts, so /sys — a directory the UI and `ls` both show — answered "Directory not found" (MAINT-011). It now uses fs.readDir(), which resolves mounts and returns null for a genuinely missing path. Tests: modules/test/test-ai-fs-tools.js (7 asserts) and modules/test/test-ollama-autoconnect.js (11 asserts). RED on HEAD: ai tools 4 passed/3 FAILED (/sys → "Directory not found", write_file → "File written: …"), ollama 2 passed/9 FAILED ("Use ollamaconnect first"). GREEN now 7/7 and 11/11; maintain --fix reports "node tests 9/9 pass". Live on v4.6.105: connectedBefore=false, _ensureConnected()=true in 41 ms, 37 models.
Diffstat (limited to 'modules/test/test-ai-fs-tools.js')
-rw-r--r--modules/test/test-ai-fs-tools.js74
1 files changed, 74 insertions, 0 deletions
diff --git a/modules/test/test-ai-fs-tools.js b/modules/test/test-ai-fs-tools.js
new file mode 100644
index 0000000..c427783
--- /dev/null
+++ b/modules/test/test-ai-fs-tools.js
@@ -0,0 +1,74 @@
+// 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');
+ }
+
+ 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);
+ }
+})();