diff --git a/src/praisonai-desktop/frontend/tests/engine-down.test.mjs b/src/praisonai-desktop/frontend/tests/engine-down.test.mjs index f2c2f7b16f..f641b68af0 100644 --- a/src/praisonai-desktop/frontend/tests/engine-down.test.mjs +++ b/src/praisonai-desktop/frontend/tests/engine-down.test.mjs @@ -77,6 +77,37 @@ for (const [id, label] of [['settings', 'Settings'], ['logsBtn', 'Engine log'], }); } +test('Export does not fake a backup when the engine is unreachable', async () => { + const writes = []; + const dom = new JSDOM(HTML, { + runScripts: 'dangerously', resources: 'usable', url: ORIGIN + '/', + beforeParse(w) { + w.__TAURI__ = { core: { invoke: async () => ({ + state: 'failed', reason: 'Engine crashed', + detail: 'exited before it was ready', tail: 'traceback' }) }, + event: { listen: async () => () => {} } }; + w.fetch = async () => { throw new TypeError('fetch failed'); }; + w.requestAnimationFrame = (cb) => setTimeout(cb, 0); + w.navigator.clipboard = { writeText: async (t) => { writes.push(t); } }; + w.confirm = () => true; w.prompt = () => 'x'; w.alert = () => {}; w.scrollTo = () => {}; + w.matchMedia = () => ({ matches: false, addEventListener() {}, removeEventListener() {} }); + Object.defineProperty(w.HTMLElement.prototype, 'scrollIntoView', { value() {} }); + }, + }); + const { window } = dom; + await new Promise((r) => setTimeout(r, 400)); + + await window.runAction({ action: 'export' }); + await new Promise((r) => setTimeout(r, 20)); + + assert.deepEqual(writes, [], + 'the clipboard was written despite the export failing -- a stale paste would masquerade as a backup'); + assert.ok(window.document.getElementById('toast'), + 'the failed export said nothing'); + assert.match(window.document.getElementById('toast').textContent, /did not run|could not/i, + 'the failed export produced no error message'); +}); + test('no unhandled rejection escapes any of the three', async () => { const b = await bootFailed(); for (const id of ['settings', 'logsBtn', 'search']) { diff --git a/src/praisonai-desktop/ui/index.html b/src/praisonai-desktop/ui/index.html index 9e67f1c625..f5ae3c5028 100644 --- a/src/praisonai-desktop/ui/index.html +++ b/src/praisonai-desktop/ui/index.html @@ -1900,10 +1900,23 @@

Fine-tune a model

async function runAction(def){ if(def.confirm && !await askConfirm(def.confirm.message,{ok:'Continue'})) return; + // A failed export that says nothing is worse than no export: the user pastes + // a stale clipboard into a file and believes they have a backup. Every branch + // is a bare fetch that rejects when the engine is unreachable, so report it. + try{ await runActionInner(def); } + catch(e){ toast('That did not run: '+(e && e.message ? e.message : e)); } +} +async function runActionInner(def){ if(def.action==='export'){ - const {chats}=await (await fetch('http://127.0.0.1:'+PORT+'/chats')).json(); + const cr=await fetch('http://127.0.0.1:'+PORT+'/chats'); + if(!cr.ok) throw new Error('Could not read conversations ('+cr.status+')'); + const {chats}=await cr.json(); const all=[]; - for(const c of chats) all.push(await (await fetch('http://127.0.0.1:'+PORT+'/chats/'+c.id)).json()); + for(const c of chats){ + const dr=await fetch('http://127.0.0.1:'+PORT+'/chats/'+c.id); + if(!dr.ok) throw new Error('Could not read a conversation ('+dr.status+')'); + all.push(await dr.json()); + } // A sandboxed webview blocks a download the page starts itself, so the // export goes to the clipboard and says so rather than failing silently. await navigator.clipboard.writeText(JSON.stringify(all,null,2));