From abe6cd64666e1fa61cc79ea4392cd78339e581d8 Mon Sep 17 00:00:00 2001 From: Agando Date: Tue, 27 Oct 2026 14:13:35 +0100 Subject: fix(newos4): connect to Ollama on first use, honest write_file, list_files sees /sys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- modules/test/test-ollama-autoconnect.js | 98 +++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 modules/test/test-ollama-autoconnect.js (limited to 'modules/test/test-ollama-autoconnect.js') diff --git a/modules/test/test-ollama-autoconnect.js b/modules/test/test-ollama-autoconnect.js new file mode 100644 index 0000000..b9db4ff --- /dev/null +++ b/modules/test/test-ollama-autoconnect.js @@ -0,0 +1,98 @@ +// modules/test/test-ollama-autoconnect.js — run: node +// Proves the first message lazily connects instead of dying with "Use ollamaconnect first", +// and that a genuinely unreachable endpoint still produces the error (after one attempt). +const fsMod = require('fs'), path = require('path'); +const ROOT = path.join(__dirname, '..', 'core'); +const store = {}; +global.localStorage = { + getItem: k => (k in store ? store[k] : null), + setItem: (k, v) => { store[k] = String(v); }, + removeItem: k => { delete store[k]; }, +}; +global.window = { addEventListener: () => {}, location: { origin: 'https://webx11.duckdns.org' } }; +global.document = { addEventListener: () => {} }; + +const OllamaManager = new Function( + fsMod.readFileSync(path.join(ROOT, 'ollama-manager.js'), 'utf8') + '\nreturn OllamaManager;')(); + +let fails = 0, passes = 0; +function ok(name, cond, detail) { + if (cond) { passes++; return; } + fails++; + console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); +} + +function mk(behaviour) { + const out = []; + const term = { + print: (m, t) => out.push({ text: String(m), type: t || '' }), + log: () => {}, + // chat() prints the model answer through this, even in non-streaming mode + printStreamingResponse: (t) => out.push({ text: String(t), type: 'stream' }), + }; + const om = Object.create(OllamaManager.prototype); + Object.assign(om, { + terminal: term, connected: false, models: [], currentModel: 'm1', + endpoint: 'https://webx11.duckdns.org/ollama', fallbackEndpoints: [], + providers: [{ name: 'Ollama', type: 'ollama', endpoint: 'https://webx11.duckdns.org/ollama', model: 'm1', priority: 1, enabled: true }], + currentProvider: null, fallbackCount: 0, responseHooks: [], _usageLog: [], _usageBuffer: [], + _abortController: null, _connecting: null, + saveSettings: () => {}, _saveProviders: () => {}, _logUsage: () => {}, _flushUsageLog: () => {}, + }); + om.currentProvider = om.providers[0]; + global.fetch = behaviour; + return { om, out }; +} + +const json = (body, okFlag = true) => Promise.resolve({ ok: okFlag, status: okFlag ? 200 : 500, json: async () => body }); + +(async () => { + // A. healthy endpoint: the first chat() call connects, then answers + { + const seen = []; + const { om, out } = mk((url) => { + seen.push(url); + if (url.includes('/api/tags')) return json({ models: [{ name: 'm1' }] }); + // chat() goes through /api/generate for ollama providers + return json({ response: 'pong', message: { content: 'pong' }, eval_count: 1, prompt_eval_count: 1 }); + }); + const res = await om.chat('ping'); + const text = out.map(o => o.text).join(' | '); + ok('A1 connect is attempted lazily', seen.some(u => u.includes('/api/tags')), seen.join(', ')); + ok('A2 the user sees a connecting hint', /Connecting to Ollama/.test(text), text.slice(0, 160)); + ok('A3 manager is connected after the attempt', om.connected === true); + ok('A4 the message is answered', String(res).includes('pong'), String(res).slice(0, 120)); + ok('A5 no "ollamaconnect first" error', !/ollamaconnect first/.test(text), text.slice(0, 200)); + } + + // B. dead endpoint: one attempt, then an actionable error + { + const seen = []; + const { om, out } = mk((url) => { seen.push(url); return Promise.reject(new TypeError('Failed to fetch')); }); + const res = await om.chat('ping'); + const text = out.map(o => o.text).join(' | '); + ok('B1 still attempts the connection', seen.some(u => u.includes('/api/tags')), seen.join(', ')); + ok('B2 returns null', res === null, String(res)); + ok('B3 error names the real cause', /endpoint is unreachable/.test(text), text.slice(0, 240)); + ok('B4 auto-connect failure is reported once', /Auto-connect failed/.test(text), text.slice(0, 240)); + ok('B5 manager stays disconnected', om.connected === false); + } + + // C. concurrent callers share one attempt + { + let tagCalls = 0; + const { om } = mk((url) => { + if (url.includes('/api/tags')) { tagCalls++; return json({ models: [{ name: 'm1' }] }); } + return json({ response: 'ok', message: { content: 'ok' }, eval_count: 1, prompt_eval_count: 1 }); + }); + await Promise.all([om.chat('a'), om.chat('b'), om.chat('c')]); + ok('C1 three concurrent messages probe /api/tags once', tagCalls === 1, 'tagCalls=' + tagCalls); + } + + if (fails === 0) { + console.log('test-ollama-autoconnect: ' + passes + ' passed, 0 failed'); + } else { + console.log('test-ollama-autoconnect: ' + passes + ' passed, ' + fails + ' FAILED'); + process.exit(1); + } +})(); -- cgit v1.2.3