Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/praisonai-desktop/engine/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment on lines +1681 to +1683

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Registration can outlive persistence

If login-item registration succeeds and save_settings then raises, the new ordering leaves the login item registered without updating durable settings, causing the application to launch at login after restart while the saved option remains off or stale.

Knowledge Base Used: Persistence, security, and configuration

saved["launch_at_login_result"] = result
Comment thread
greptile-apps[bot] marked this conversation as resolved.
else:
saved = save_settings(patch)
self._json(saved)
return

Expand Down
82 changes: 82 additions & 0 deletions src/praisonai-desktop/engine/test_portability.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
25 changes: 22 additions & 3 deletions src/praisonai-desktop/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1832,8 +1832,19 @@ <h2>Fine-tune a model</h2>
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]; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale responses overwrite settings

If the same setting is changed again before its first request completes, each response unconditionally overwrites CFG and cfg. An older response arriving last therefore restores the earlier value and rerenders the UI against the user's latest choice.

applyPrefs(CFG);
}
return saved;
}

function control(def, onChange){
Expand Down Expand Up @@ -1985,10 +1996,18 @@ <h2>Fine-tune a model</h2>
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;
Expand Down
Loading