diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 47c476b01579..30a9a9a34163 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -940,7 +940,7 @@ def test_session_resume_uses_parent_lineage_for_display(monkeypatch): lambda agent, *a: {"model": "test", "tools": {}, "skills": {}}, ) monkeypatch.setattr( - server, "_init_session", lambda sid, key, agent, history, cols=80: None + server, "_init_session", lambda sid, key, agent, history, cols=80, **_kwargs: None ) resp = server.handle_request( @@ -983,7 +983,7 @@ def test_session_resume_passes_stored_runtime_to_agent(monkeypatch): monkeypatch.setattr(server, "_make_agent", fake_make_agent) monkeypatch.setattr(server, "_session_info", lambda agent, *a: {"model": agent.model, "provider": agent.provider}) - def fake_init_session(sid, key, agent, history, cols=80): + def fake_init_session(sid, key, agent, history, cols=80, **_kwargs): server._sessions[sid] = {"agent": agent, "session_key": key} monkeypatch.setattr(server, "_init_session", fake_init_session) @@ -1006,6 +1006,94 @@ def test_session_resume_passes_stored_runtime_to_agent(monkeypatch): assert server._sessions[runtime_sid]["model_override"] == captured["model_override"] +def test_session_resume_profile_uses_profile_db_cwd(monkeypatch, tmp_path): + target = "stored-profile-session" + launch_cwd = tmp_path / "launch" + profile_cwd = tmp_path / "worker" + profile_home = tmp_path / "profiles" / "worker" + launch_cwd.mkdir() + profile_cwd.mkdir() + profile_home.mkdir(parents=True) + captured = {} + + class ProfileDB: + def get_session(self, _target): + return {"id": target, "cwd": str(profile_cwd)} + + def get_session_by_title(self, _target): + return None + + def reopen_session(self, _target): + captured["reopened"] = _target + + def get_messages_as_conversation(self, _target, include_ancestors=False): + return [{"role": "user", "content": "hello"}] + + def update_session_cwd(self, *_args): + raise AssertionError("profile row already has cwd") + + class LaunchDB: + def get_session(self, _target): + return {"id": target, "cwd": str(launch_cwd)} + + def update_session_cwd(self, *_args): + captured["launch_update"] = True + + profile_db = ProfileDB() + launch_db = LaunchDB() + + class FakeWorker: + def __init__(self, *_args, **_kwargs): + pass + + def close(self): + pass + + def fake_make_agent(sid, key, session_id=None, session_db=None, **kwargs): + captured["agent_db"] = session_db + return types.SimpleNamespace(model="test/model") + + monkeypatch.setenv("TERMINAL_CWD", str(launch_cwd)) + monkeypatch.setattr(server, "_profile_home", lambda _profile: profile_home) + monkeypatch.setattr("hermes_state.SessionDB", lambda db_path=None: profile_db) + monkeypatch.setattr(server, "_get_db", lambda: launch_db) + monkeypatch.setattr(server, "_enable_gateway_prompts", lambda: None) + monkeypatch.setattr(server, "_set_session_context", lambda target: []) + monkeypatch.setattr(server, "_clear_session_context", lambda tokens: None) + monkeypatch.setattr(server, "_make_agent", fake_make_agent) + monkeypatch.setattr(server, "_SlashWorker", FakeWorker) + monkeypatch.setattr(server, "_wire_callbacks", lambda _sid: None) + monkeypatch.setattr(server, "_emit", lambda *args, **kwargs: None) + monkeypatch.setattr( + server, + "_session_info", + lambda _agent, session=None: {"cwd": session.get("cwd") if session else ""}, + ) + + import tools.approval as approval + + monkeypatch.setattr(approval, "register_gateway_notify", lambda key, cb: None) + monkeypatch.setattr(approval, "load_permanent_allowlist", lambda: None) + + try: + resp = server.handle_request( + { + "id": "1", + "method": "session.resume", + "params": {"session_id": target, "profile": "worker"}, + } + ) + + assert "error" not in resp + sid = resp["result"]["session_id"] + assert captured["agent_db"] is profile_db + assert server._sessions[sid]["cwd"] == str(profile_cwd) + assert resp["result"]["info"]["cwd"] == str(profile_cwd) + assert "launch_update" not in captured + finally: + server._sessions.clear() + + def test_stored_session_runtime_overrides_skips_bare_billing_provider(): """A bare billing bucket ("custom"/"auto"/"openrouter") must not be restored as the provider identity on resume. A custom endpoint that never used `/model` persists only diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 58cc406deb55..e0517253dba9 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3428,7 +3428,15 @@ def _make_agent( ) -def _init_session(sid: str, key: str, agent, history: list, cols: int = 80): +def _init_session( + sid: str, + key: str, + agent, + history: list, + cols: int = 80, + cwd: str | None = None, + session_db=None, +): now = time.time() with _sessions_lock: _sessions[sid] = { @@ -3443,7 +3451,7 @@ def _init_session(sid: str, key: str, agent, history: list, cols: int = 80): "running": False, "attached_images": [], "image_counter": 0, - "cwd": _completion_cwd(), + "cwd": cwd or _completion_cwd(), "cols": cols, "slash_worker": None, "show_reasoning": _load_show_reasoning(), @@ -3458,7 +3466,7 @@ def _init_session(sid: str, key: str, agent, history: list, cols: int = 80): # session (stdio for Ink, JSON-RPC WS for the dashboard sidebar). "transport": current_transport() or _stdio_transport, } - db = _get_db() + db = session_db if session_db is not None else _get_db() if db is not None: row = db.get_session(key) if row and row.get("cwd"): @@ -4071,6 +4079,10 @@ def _(rid, params: dict) -> dict: target = found["id"] else: return _err(rid, 4007, "session not found") + profile_resume_cwd = str(found.get("cwd") or "").strip() or _profile_configured_cwd( + profile_home + ) + def _reuse_live_payload(sid: str, session: dict) -> dict: payload = _live_session_payload( sid, @@ -4118,7 +4130,7 @@ def _(rid, params: dict) -> dict: lease.release() return _err(rid, 5000, f"resume failed: {e}") messages = _history_to_messages(history) - cwd = os.getenv("TERMINAL_CWD", os.getcwd()) + cwd = profile_resume_cwd or os.getenv("TERMINAL_CWD", os.getcwd()) now = time.time() # A delegated child mid-run emits no native session events of its own — # report its liveness from the relay registry so the window paints a @@ -4261,7 +4273,24 @@ def _(rid, params: dict) -> dict: payload["resumed"] = target return _ok(rid, payload) try: - _init_session(sid, target, agent, history, cols=cols) + init_home_token = ( + set_hermes_home_override(str(profile_home)) + if profile_home is not None + else None + ) + try: + _init_session( + sid, + target, + agent, + history, + cols=cols, + cwd=profile_resume_cwd, + session_db=db, + ) + finally: + if init_home_token is not None: + reset_hermes_home_override(init_home_token) if sid in _sessions: if stored_runtime_overrides.get("model_override") is not None: _sessions[sid]["model_override"] = stored_runtime_overrides[