diff --git a/src/praisonai-desktop/engine/server.py b/src/praisonai-desktop/engine/server.py index b410c5f5e..598ec02ef 100644 --- a/src/praisonai-desktop/engine/server.py +++ b/src/praisonai-desktop/engine/server.py @@ -1674,11 +1674,16 @@ def do_POST(self): except (ValueError, TypeError): self.send_error(400) return - saved = save_settings(patch) if "launch_at_login" in patch: - saved = dict(saved) - saved["launch_at_login_result"] = set_launch_at_login( - bool(patch["launch_at_login"])) + # Persist what actually happened, not what was asked. Writing + # the request first made the toggle report a login item that + # was never registered -- and survive restarts saying so. + result = set_launch_at_login(bool(patch["launch_at_login"])) + patch = {**patch, "launch_at_login": bool(result.get("enabled"))} + saved = dict(save_settings(patch)) + saved["launch_at_login_result"] = result + else: + saved = save_settings(patch) self._json(saved) return diff --git a/src/praisonai-desktop/engine/test_portability.py b/src/praisonai-desktop/engine/test_portability.py index 211b4a0b9..219364784 100644 --- a/src/praisonai-desktop/engine/test_portability.py +++ b/src/praisonai-desktop/engine/test_portability.py @@ -689,5 +689,87 @@ def test_the_vocabulary_is_the_eleven_events_expected(self): self.assertEqual(self._emitted_events(), self.EXPECTED) +class LaunchAtLogin(unittest.TestCase): + """The toggle must persist what actually happened, not what was asked. + + Registering a login item only works in the installed macOS .app bundle: + `set_launch_at_login` returns {"enabled": False} everywhere else -- every + Windows and Linux user, and any macOS user running from a checkout. The + handler used to save the *request* and merely attach the honest result to + the response, which nothing read. So the toggle rendered on, persisted, and + survived restarts while no login item existed anywhere. + """ + + def setUp(self): + import io + + self.home = pathlib.Path(tempfile.mkdtemp(prefix="praison-launch-")) + self._data_dir = server.DATA_DIR + self._settings_path = server.SETTINGS_PATH + self._set = server.set_launch_at_login + server.DATA_DIR = self.home + server.SETTINGS_PATH = self.home / "settings.json" + self._io = io + + def tearDown(self): + server.DATA_DIR = self._data_dir + server.SETTINGS_PATH = self._settings_path + server.set_launch_at_login = self._set + shutil.rmtree(self.home, ignore_errors=True) + + def _post_settings(self, patch): + """Drive the real /settings POST handler and return its JSON reply.""" + body = json.dumps(patch).encode() + + class FakeHandler(server.Handler): + def __init__(self): + self.path = "/settings" + self.headers = {"Content-Length": str(len(body))} + self.rfile = self._io_module.BytesIO(body) + self.wfile = self._io_module.BytesIO() + + def send_response(self, *_a, **_k): + pass + + def send_header(self, *_a, **_k): + pass + + def end_headers(self): + pass + + FakeHandler._io_module = self._io + handler = FakeHandler() + handler.do_POST() + raw = handler.wfile.getvalue() + return json.loads(raw) if raw else {} + + def test_a_request_that_could_not_register_is_not_persisted_as_on(self): + # Stub the platform action to the answer every non-bundle host gives. + server.set_launch_at_login = lambda on: { + "ok": False, "enabled": False, + "message": "Only available in the installed app."} + + reply = self._post_settings({"launch_at_login": True}) + + self.assertFalse(reply.get("launch_at_login"), + "the toggle reported on though nothing was registered") + self.assertEqual( + reply.get("launch_at_login_result", {}).get("message"), + "Only available in the installed app.", + "the response dropped the explanation for why it did not stick") + self.assertFalse( + server.load_settings().get("launch_at_login"), + "the un-registered login item survived to the next launch") + + def test_a_request_that_registered_is_persisted_as_on(self): + server.set_launch_at_login = lambda on: {"ok": True, "enabled": bool(on)} + + reply = self._post_settings({"launch_at_login": True}) + + self.assertTrue(reply.get("launch_at_login")) + self.assertTrue(server.load_settings().get("launch_at_login"), + "a real registration did not persist") + + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/src/praisonai-desktop/ui/index.html b/src/praisonai-desktop/ui/index.html index 9e67f1c62..fb73636f7 100644 --- a/src/praisonai-desktop/ui/index.html +++ b/src/praisonai-desktop/ui/index.html @@ -1832,8 +1832,19 @@

Fine-tune a model

async function saveCfg(patch){ CFG={...CFG,...patch}; applyPrefs(CFG); cfg = {...cfg, ...patch}; - await fetch('http://127.0.0.1:'+PORT+'/settings',{method:'POST', - headers:{'content-type':'application/json'},body:JSON.stringify(patch)}); + let saved=null; + try{ + saved=await (await fetch('http://127.0.0.1:'+PORT+'/settings',{method:'POST', + headers:{'content-type':'application/json'},body:JSON.stringify(patch)})).json(); + }catch{} + // The server persists what actually happened, not what we asked. Reconcile + // any key it wrote back so the current session never shows a value that was + // never saved -- e.g. "Open at login" the OS refused to register. + if(saved && typeof saved==='object'){ + for(const k of Object.keys(patch)) if(k in saved){ CFG[k]=saved[k]; cfg[k]=saved[k]; } + applyPrefs(CFG); + } + return saved; } function control(def, onChange){ @@ -1985,10 +1996,18 @@

Fine-tune a model

err.style.display='none'; if(def.confirm && def.confirm.when(cfgGet(def.key),nv) && !await askConfirm(def.confirm.message,{ok:'Continue'})) { renderSettings(); return; } - await saveCfg({[def.key]:nv}); + const saved=await saveCfg({[def.key]:nv}); if(def.key==='theme') applyTheme(nv); if(def.key==='model') modelName.textContent=nv; renderSettings(); + // If the OS declined to register the login item, the toggle has already + // snapped back to the persisted value above; tell the user why. + if(def.key==='launch_at_login' && saved && saved.launch_at_login_result + && nv && !saved.launch_at_login_result.enabled){ + const r=document.getElementById('setting-launch_at_login'); + const w=r&&r.querySelector('.warn'); + if(w){ w.textContent=saved.launch_at_login_result.message||'Not available.'; w.style.display=''; } + } }; const multiline = def.control.kind==='text' && def.control.multiline;