diff --git a/clients/terminal/api.py b/clients/terminal/api.py index b735939..311bd5c 100644 --- a/clients/terminal/api.py +++ b/clients/terminal/api.py @@ -1,4 +1,12 @@ -"""REST API helpers for the terminal client.""" +"""REST API helpers for the terminal client. + +All helpers are ``async`` and share one ``httpx.AsyncClient`` (connection +pooling) so callers running inside the Textual event loop never block the UI on +a network round-trip — the await yields the loop while the request is in flight. +The shared client is created lazily on first use (inside a running loop) and +lives for the process; tests monkeypatch these functions wholesale, so the +client is never constructed in unit tests. +""" from __future__ import annotations @@ -6,34 +14,41 @@ from clients.terminal.config import settings - -def _client() -> httpx.Client: - return httpx.Client(base_url=settings.base_url, timeout=30.0) +# Lazily created on first use (in an async context). Reused across calls so +# HTTP connections are pooled rather than re-established per request. +_async_client: httpx.AsyncClient | None = None -def get_profiles() -> list[dict]: - with _client() as client: - resp = client.get("/agents/profiles") - resp.raise_for_status() - return resp.json() +def _client() -> httpx.AsyncClient: + global _async_client + if _async_client is None: + _async_client = httpx.AsyncClient(base_url=settings.base_url, timeout=30.0) + return _async_client -def get_profile(profile_id: str) -> dict | None: +async def get_profiles() -> list[dict]: + client = _client() + resp = await client.get("/agents/profiles") + resp.raise_for_status() + return resp.json() + + +async def get_profile(profile_id: str) -> dict | None: """Return the profile dict for ``profile_id`` from /agents/profiles, or None.""" - for p in get_profiles(): + for p in await get_profiles(): if p.get("id") == profile_id: return p return None -def get_profile_model(profile_id: str) -> str | None: +async def get_profile_model(profile_id: str) -> str | None: """Configured model for a profile (first item of its priority list), or None. Used to show a model in the status panel before the first request resolves the actually-served model. ``profile.model`` is a priority list; the first entry is the intended model. """ - profile = get_profile(profile_id) + profile = await get_profile(profile_id) if not profile: return None model = profile.get("model") @@ -42,45 +57,45 @@ return model or None -def list_sessions() -> list[dict]: - with _client() as client: - resp = client.get("/sessions") - resp.raise_for_status() - return resp.json() +async def list_sessions() -> list[dict]: + client = _client() + resp = await client.get("/sessions") + resp.raise_for_status() + return resp.json() -def create_session(profile_id: str | None = None) -> dict: +async def create_session(profile_id: str | None = None) -> dict: body: dict = {} if profile_id: body["profile_id"] = profile_id - with _client() as client: - resp = client.post("/sessions", json=body) - resp.raise_for_status() - return resp.json() + client = _client() + resp = await client.post("/sessions", json=body) + resp.raise_for_status() + return resp.json() -def get_session(session_id: str) -> dict: - with _client() as client: - resp = client.get(f"/sessions/{session_id}") - resp.raise_for_status() - return resp.json() +async def get_session(session_id: str) -> dict: + client = _client() + resp = await client.get(f"/sessions/{session_id}") + resp.raise_for_status() + return resp.json() -def get_todos(session_id: str) -> dict: +async def get_todos(session_id: str) -> dict: """Fetch the session's current todo list (GET /sessions/{id}/todos). Returns ``{"session_id": ..., "tasks": [{"index", "text", "status", "validation"}, ...]}``. Used to seed the side-panel todo on attach/switch before the first ``todo_updated`` WS event arrives. """ - with _client() as client: - resp = client.get(f"/sessions/{session_id}/todos") - resp.raise_for_status() - return resp.json() + client = _client() + resp = await client.get(f"/sessions/{session_id}/todos") + resp.raise_for_status() + return resp.json() -def stop_session(session_id: str) -> dict: - with _client() as client: - resp = client.post(f"/sessions/{session_id}/stop") - resp.raise_for_status() - return resp.json() +async def stop_session(session_id: str) -> dict: + client = _client() + resp = await client.post(f"/sessions/{session_id}/stop") + resp.raise_for_status() + return resp.json() \ No newline at end of file diff --git a/clients/terminal/cli.py b/clients/terminal/cli.py index 91bf257..25b613d 100644 --- a/clients/terminal/cli.py +++ b/clients/terminal/cli.py @@ -87,8 +87,20 @@ cwd: Path, resume_session_id: str | None, ) -> None: + asyncio.run( + _run_raw_async(prompt, new_session, profile_id, cwd, resume_session_id) + ) + + +async def _run_raw_async( + prompt: str | None, + new_session: bool, + profile_id: str | None, + cwd: Path, + resume_session_id: str | None, +) -> None: state = StateManager() - session_id = _resolve_session_id(state, new_session, profile_id, resume_session_id) + session_id = await _resolve_session_id(state, new_session, profile_id, resume_session_id) if not session_id: raise click.ClickException("Failed to create or resume a session.") @@ -96,10 +108,10 @@ client = NaviWebSocketClient(session_id, renderer=renderer, cwd=cwd) if prompt: - asyncio.run(_run_one_shot(client, prompt)) + await client.run_one_shot(prompt) return - asyncio.run(_run_interactive(client, state)) + await _run_interactive(client, state) def _run_tui( @@ -143,7 +155,7 @@ click.secho(f"Resume with: {_resume_hint(session_id)}", fg="cyan") -def _resolve_session_id( +async def _resolve_session_id( state: StateManager, force_new: bool, profile_id: str | None, @@ -151,7 +163,7 @@ ) -> str | None: if resume_session_id: try: - session = api.get_session(resume_session_id) + session = await api.get_session(resume_session_id) except Exception as exc: raise click.ClickException(f"Failed to resume session {resume_session_id[:8]}: {exc}") state.set_session_id(session["session_id"]) @@ -165,7 +177,7 @@ saved = state.get_session_id() if saved: try: - session = api.get_session(saved) + session = await api.get_session(saved) click.secho( f"Resumed session {session['session_id'][:8]} (profile {session['profile_id']})", fg="bright_black", @@ -176,7 +188,7 @@ profile = profile_id or settings.default_profile_id try: - session = api.create_session(profile) + session = await api.create_session(profile) except Exception as exc: click.secho(f"Failed to create session: {exc}", fg="red", err=True) return None @@ -189,10 +201,6 @@ return session["session_id"] -async def _run_one_shot(client: NaviWebSocketClient, prompt: str) -> None: - await client.run_one_shot(prompt) - - async def _run_interactive(client: NaviWebSocketClient, state: StateManager) -> None: click.secho("Navi Code interactive mode. Type /quit to exit, /help for commands.", fg="cyan") @@ -248,7 +256,7 @@ if head == "/new": try: - session = api.create_session(settings.default_profile_id) + session = await api.create_session(settings.default_profile_id) except Exception as exc: click.secho(f"Failed to create session: {exc}", fg="red", err=True) return True @@ -261,7 +269,7 @@ if head == "/sessions": try: - sessions = api.list_sessions() + sessions = await api.list_sessions() except Exception as exc: click.secho(f"Failed to list sessions: {exc}", fg="red", err=True) return True @@ -273,11 +281,11 @@ if head == "/switch" and len(parts) == 2: target = parts[1] try: - session = api.get_session(target) + session = await api.get_session(target) except Exception as exc: # Try to find by prefix try: - sessions = api.list_sessions() + sessions = await api.list_sessions() matches = [s for s in sessions if s["session_id"].startswith(target)] if len(matches) == 1: session = matches[0] @@ -295,7 +303,7 @@ if head == "/profile": try: - current = api.get_session(state.get_session_id() or "") + current = await api.get_session(state.get_session_id() or "") click.echo(f"Profile: {current.get('profile_id', 'unknown')}") click.echo(f"Session: {current.get('session_id', 'unknown')}") except Exception as exc: diff --git a/clients/terminal/tui/commands/builtin.py b/clients/terminal/tui/commands/builtin.py index 388cc9d..b15ab39 100644 --- a/clients/terminal/tui/commands/builtin.py +++ b/clients/terminal/tui/commands/builtin.py @@ -116,7 +116,7 @@ app._open_sessions_picker() return - session = self._resolve_session(target) + session = await self._resolve_session(target) if session is None: app._open_sessions_picker(initial_query=target) return @@ -124,7 +124,7 @@ app.run_worker(app._switch_session_worker(session["session_id"])) @staticmethod - def _resolve_session(target: str) -> dict | None: + async def _resolve_session(target: str) -> dict | None: """Resolve ``target`` to a single navi_code session, or None. Tries an exact id first (GET /sessions/{id}); if that fails or lands on a @@ -133,7 +133,7 @@ """ # Exact id hit. try: - session = api.get_session(target) + session = await api.get_session(target) except Exception: session = None if session is not None and session.get("profile_id") == settings.default_profile_id: @@ -141,7 +141,7 @@ # Prefix match among navi_code sessions. try: - sessions = api.list_sessions() + sessions = await api.list_sessions() except Exception: return None matches = [ @@ -168,7 +168,7 @@ ctx.chat_panel.handle_ws_event({"type": "error", "message": "No active session"}) return try: - session = api.get_session(ctx.session_id) + session = await api.get_session(ctx.session_id) except Exception as exc: ctx.chat_panel.handle_ws_event( {"type": "error", "message": f"Failed to get session: {exc}"} @@ -303,7 +303,7 @@ ) return try: - session = api.get_session(ctx.session_id) + session = await api.get_session(ctx.session_id) except Exception as exc: ctx.chat_panel.handle_ws_event( {"type": "error", "message": f"Failed to get session: {exc}"} diff --git a/clients/terminal/tui/screens/sessions_picker.py b/clients/terminal/tui/screens/sessions_picker.py index bf9f511..f229a31 100644 --- a/clients/terminal/tui/screens/sessions_picker.py +++ b/clients/terminal/tui/screens/sessions_picker.py @@ -103,8 +103,14 @@ yield ListView(id="sessions-list") def on_mount(self) -> None: + # Load sessions off the event loop (api.list_sessions is async now) so a + # slow/unreachable backend does not freeze the modal — the input is + # focusable immediately and the list populates when the request returns. + self.run_worker(self._load_sessions()) + + async def _load_sessions(self) -> None: try: - raw = api.list_sessions() + raw = await api.list_sessions() except Exception as exc: self.app.query_one("ChatPanel").handle_ws_event( {"type": "error", "message": f"Failed to list sessions: {exc}"} diff --git a/clients/terminal/tui/tui_app.py b/clients/terminal/tui/tui_app.py index 1f8860c..1d7f4b9 100644 --- a/clients/terminal/tui/tui_app.py +++ b/clients/terminal/tui/tui_app.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from pathlib import Path from textual.app import App, ComposeResult @@ -168,7 +167,7 @@ # error and let the user correct the id. Saved-state resume below # is the lenient path. try: - session = api.get_session(session_id) + session = await api.get_session(session_id) return session["session_id"] except Exception as exc: self._chat_panel.handle_ws_event( @@ -180,14 +179,14 @@ saved = self._state.get_session_id() if saved: try: - session = api.get_session(saved) + session = await api.get_session(saved) return session["session_id"] except Exception: self._state.clear_session_id() profile = profile_id or settings.default_profile_id try: - session = api.create_session(profile) + session = await api.create_session(profile) except Exception as exc: self._chat_panel.handle_ws_event( {"type": "error", "message": f"Failed to create session: {exc}"} @@ -201,7 +200,7 @@ history: list[dict] = [] session: dict = {} try: - session = api.get_session(session_id) + session = await api.get_session(session_id) self._ctx.profile_id = session.get("profile_id") or settings.default_profile_id history = session.get("messages") or [] except Exception: @@ -214,7 +213,7 @@ # model via a model_info event. Falls back to the global default, then "-". configured = None try: - configured = api.get_profile_model(self._ctx.profile_id) + configured = await api.get_profile_model(self._ctx.profile_id) except Exception: configured = None self._status_panel.set_model( @@ -227,7 +226,7 @@ # Seed the side-panel todo from the server so a resumed/switched session # shows its existing plan before the first todo_updated WS event arrives. try: - todos = api.get_todos(session_id) + todos = await api.get_todos(session_id) self._todo_panel.set_tasks(todos.get("tasks") or []) except Exception: self._todo_panel.clear() @@ -312,7 +311,7 @@ async def _create_new_session_worker(self) -> None: """Create a fresh navi_code session and switch to it.""" try: - session = api.create_session(settings.default_profile_id) + session = await api.create_session(settings.default_profile_id) except Exception as exc: self._chat_panel.handle_ws_event( {"type": "error", "message": f"Failed to create session: {exc}"} @@ -477,9 +476,7 @@ async def _stop_stream_worker(self, session_id: str) -> None: try: - result = api.stop_session(session_id) - if asyncio.iscoroutine(result): - await result + await api.stop_session(session_id) except Exception as exc: self._chat_panel.handle_ws_event( {"type": "error", "message": f"Failed to stop generation: {exc}"} diff --git a/tests/clients/test_chat_panel.py b/tests/clients/test_chat_panel.py index 2592380..9be018f 100644 --- a/tests/clients/test_chat_panel.py +++ b/tests/clients/test_chat_panel.py @@ -33,10 +33,22 @@ def mock_tui_api(monkeypatch: pytest.MonkeyPatch) -> None: import clients.terminal.api as api_module - monkeypatch.setattr(api_module, "create_session", lambda *a, **k: {"session_id": "s", "profile_id": "navi_code"}) - monkeypatch.setattr(api_module, "get_session", lambda sid: {"session_id": sid, "profile_id": "navi_code"}) - monkeypatch.setattr(api_module, "list_sessions", lambda: []) - monkeypatch.setattr(api_module, "get_profile_model", lambda pid: "configured-model") + async def fake_create_session(*a, **k): + return {"session_id": "s", "profile_id": "navi_code"} + + async def fake_get_session(sid): + return {"session_id": sid, "profile_id": "navi_code"} + + async def fake_list_sessions(): + return [] + + async def fake_get_profile_model(pid): + return "configured-model" + + monkeypatch.setattr(api_module, "create_session", fake_create_session) + monkeypatch.setattr(api_module, "get_session", fake_get_session) + monkeypatch.setattr(api_module, "list_sessions", fake_list_sessions) + monkeypatch.setattr(api_module, "get_profile_model", fake_get_profile_model) def _wrap_render(chat) -> list: diff --git a/tests/clients/test_input_box.py b/tests/clients/test_input_box.py index 0cb97e0..28ae9eb 100644 --- a/tests/clients/test_input_box.py +++ b/tests/clients/test_input_box.py @@ -28,16 +28,22 @@ def mock_tui_api(monkeypatch: pytest.MonkeyPatch) -> None: import clients.terminal.api as api_module - monkeypatch.setattr( - api_module, - "create_session", - lambda profile_id=None: {"session_id": "test-session", "profile_id": "navi_code"}, - ) - monkeypatch.setattr( - api_module, "get_session", lambda sid: {"session_id": sid, "profile_id": "navi_code"} - ) - monkeypatch.setattr(api_module, "list_sessions", lambda: []) - monkeypatch.setattr(api_module, "get_profile_model", lambda pid: "configured-model") + async def fake_create_session(profile_id=None): + return {"session_id": "test-session", "profile_id": "navi_code"} + + async def fake_get_session(sid): + return {"session_id": sid, "profile_id": "navi_code"} + + async def fake_list_sessions(): + return [] + + async def fake_get_profile_model(pid): + return "configured-model" + + monkeypatch.setattr(api_module, "create_session", fake_create_session) + monkeypatch.setattr(api_module, "get_session", fake_get_session) + monkeypatch.setattr(api_module, "list_sessions", fake_list_sessions) + monkeypatch.setattr(api_module, "get_profile_model", fake_get_profile_model) async def _set_text(pilot, text: str) -> None: diff --git a/tests/clients/test_sessions_picker.py b/tests/clients/test_sessions_picker.py index 5a3f111..4deb759 100644 --- a/tests/clients/test_sessions_picker.py +++ b/tests/clients/test_sessions_picker.py @@ -29,18 +29,21 @@ def mock_tui_api(monkeypatch: pytest.MonkeyPatch) -> None: import clients.terminal.api as api_module - monkeypatch.setattr( - api_module, - "create_session", - lambda profile_id=None: {"session_id": "new-sess", "profile_id": "navi_code"}, - ) - monkeypatch.setattr( - api_module, "get_session", lambda sid: {"session_id": sid, "profile_id": "navi_code", "messages": []} - ) - monkeypatch.setattr(api_module, "get_profile_model", lambda pid: "configured-model") + async def fake_create_session(profile_id=None): + return {"session_id": "new-sess", "profile_id": "navi_code"} + + async def fake_get_session(sid): + return {"session_id": sid, "profile_id": "navi_code", "messages": []} + + async def fake_get_profile_model(pid): + return "configured-model" + + monkeypatch.setattr(api_module, "create_session", fake_create_session) + monkeypatch.setattr(api_module, "get_session", fake_get_session) + monkeypatch.setattr(api_module, "get_profile_model", fake_get_profile_model) -def _sessions() -> list[dict]: +async def _sessions() -> list[dict]: return [ {"session_id": "navi-aaaa1111", "profile_id": "navi_code", "name": "alpha", "created_at": "2026-01-01"}, {"session_id": "dev-bbbb2222", "profile_id": "developer", "name": "other-profile", "created_at": "2026-01-02"}, @@ -48,6 +51,11 @@ ] +async def _raise_not_found(_sid: str) -> dict: + """Stand-in for an exact-id get_session miss (server 404).""" + raise RuntimeError("404") + + def _fake_bridge(monkeypatch: pytest.MonkeyPatch) -> None: import clients.terminal.tui.tui_app as tui_app_module @@ -163,7 +171,7 @@ @pytest.mark.anyio async def test_picker_api_error_dismisses_none(monkeypatch: pytest.MonkeyPatch) -> None: - def boom() -> list[dict]: + async def boom() -> list[dict]: raise RuntimeError("server down") monkeypatch.setattr("clients.terminal.api.list_sessions", boom) @@ -211,10 +219,12 @@ _fake_bridge(monkeypatch) created: list = [] - monkeypatch.setattr( - "clients.terminal.api.create_session", - lambda profile_id=None: created.append(profile_id) or {"session_id": "fresh-sess", "profile_id": "navi_code"}, - ) + + async def fake_create_session(profile_id=None): + created.append(profile_id) + return {"session_id": "fresh-sess", "profile_id": "navi_code"} + + monkeypatch.setattr("clients.terminal.api.create_session", fake_create_session) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() @@ -235,10 +245,12 @@ _fake_bridge(monkeypatch) created: list = [] - monkeypatch.setattr( - "clients.terminal.api.create_session", - lambda profile_id=None: created.append(profile_id) or {"session_id": "via-new", "profile_id": "navi_code"}, - ) + + async def fake_create_session(profile_id=None): + created.append(profile_id) + return {"session_id": "via-new", "profile_id": "navi_code"} + + monkeypatch.setattr("clients.terminal.api.create_session", fake_create_session) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() @@ -255,7 +267,7 @@ monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions) _fake_bridge(monkeypatch) # No exact-id match for the prefix; get_session raises so it falls to prefix match. - monkeypatch.setattr("clients.terminal.api.get_session", lambda sid: (_ for _ in ()).throw(RuntimeError("404"))) + monkeypatch.setattr("clients.terminal.api.get_session", _raise_not_found) attached: list[str] = [] orig_attach = NaviCodeTui.attach_session @@ -280,7 +292,7 @@ """/switch with an ambiguous prefix opens the picker pre-filtered to it.""" monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions) _fake_bridge(monkeypatch) - monkeypatch.setattr("clients.terminal.api.get_session", lambda sid: (_ for _ in ()).throw(RuntimeError("404"))) + monkeypatch.setattr("clients.terminal.api.get_session", _raise_not_found) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() @@ -314,7 +326,7 @@ """/switch opens the picker pre-filtered (so the user sees nothing matched).""" monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions) _fake_bridge(monkeypatch) - monkeypatch.setattr("clients.terminal.api.get_session", lambda sid: (_ for _ in ()).throw(RuntimeError("404"))) + monkeypatch.setattr("clients.terminal.api.get_session", _raise_not_found) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() diff --git a/tests/clients/test_terminal_client.py b/tests/clients/test_terminal_client.py index 558fe84..9fc6951 100644 --- a/tests/clients/test_terminal_client.py +++ b/tests/clients/test_terminal_client.py @@ -119,13 +119,13 @@ original_state_dir = config.settings.state_dir config.settings.state_dir = tmp_path - def fake_create_session(profile_id: str | None = None) -> dict: + async def fake_create_session(profile_id: str | None = None) -> dict: return { "session_id": "sess-raw-1234", "profile_id": profile_id or "navi_code", } - def fake_get_session(session_id: str) -> dict: + async def fake_get_session(session_id: str) -> dict: return { "session_id": session_id, "profile_id": "navi_code", @@ -155,10 +155,10 @@ (the backend never sends ``id``). Regression for a KeyError crash.""" from clients.terminal.cli import _handle_command - monkeypatch.setattr( - "clients.terminal.api.get_session", - lambda sid: {"session_id": "sess-abc-123", "profile_id": "navi_code"}, - ) + async def fake_get_session(sid): + return {"session_id": "sess-abc-123", "profile_id": "navi_code"} + + monkeypatch.setattr("clients.terminal.api.get_session", fake_get_session) state = StateManager(tmp_path) state.set_session_id("sess-abc-123") import asyncio @@ -172,13 +172,18 @@ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """--resume resolves that exact session and persists it to state.""" + import asyncio + import clients.terminal.api as api_module - monkeypatch.setattr(api_module, "get_session", lambda sid: { - "session_id": sid, - "profile_id": "navi_code", - "name": "Resumed", - }) + async def fake_get_session(sid): + return { + "session_id": sid, + "profile_id": "navi_code", + "name": "Resumed", + } + + monkeypatch.setattr(api_module, "get_session", fake_get_session) from clients.terminal import config @@ -186,7 +191,9 @@ config.settings.state_dir = tmp_path try: state = StateManager(tmp_path) - session_id = _resolve_session_id(state, force_new=False, profile_id=None, resume_session_id="sess-resume-xyz") + session_id = asyncio.run( + _resolve_session_id(state, force_new=False, profile_id=None, resume_session_id="sess-resume-xyz") + ) assert session_id == "sess-resume-xyz" assert state.get_session_id() == "sess-resume-xyz" finally: @@ -196,18 +203,25 @@ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """A --resume id that the server can't find is a hard error, not a silent new session.""" + import asyncio + import clients.terminal.api as api_module - def boom(_sid: str) -> dict: + async def boom(_sid: str) -> dict: raise RuntimeError("session not found") + async def fake_create_session(*_a, **_k): + return {"session_id": "NEW", "profile_id": "navi_code"} + monkeypatch.setattr(api_module, "get_session", boom) - monkeypatch.setattr(api_module, "create_session", lambda *_a, **_k: {"session_id": "NEW", "profile_id": "navi_code"}) + monkeypatch.setattr(api_module, "create_session", fake_create_session) import click state = StateManager(tmp_path) with pytest.raises(click.ClickException, match="Failed to resume"): - _resolve_session_id(state, force_new=False, profile_id=None, resume_session_id="sess-bad") + asyncio.run( + _resolve_session_id(state, force_new=False, profile_id=None, resume_session_id="sess-bad") + ) # Must NOT have created/persisted a new session behind the user's back. assert state.get_session_id() is None diff --git a/tests/clients/test_tui_app.py b/tests/clients/test_tui_app.py index 813b032..148c98e 100644 --- a/tests/clients/test_tui_app.py +++ b/tests/clients/test_tui_app.py @@ -36,16 +36,16 @@ """ import clients.terminal.api as api_module - def fake_create_session(profile_id: str | None = None) -> dict: + async def fake_create_session(profile_id: str | None = None) -> dict: return {"session_id": "test-session", "profile_id": profile_id or "navi_code"} - def fake_get_session(session_id: str) -> dict: + async def fake_get_session(session_id: str) -> dict: return {"session_id": session_id, "profile_id": "navi_code"} - def fake_list_sessions() -> list[dict]: + async def fake_list_sessions() -> list[dict]: return [] - def fake_get_profile_model(profile_id: str) -> str | None: + async def fake_get_profile_model(profile_id: str) -> str | None: return "configured-model" monkeypatch.setattr(api_module, "create_session", fake_create_session) @@ -409,7 +409,7 @@ fetched: list[str] = [] - def fake_get_todos(session_id: str) -> dict: + async def fake_get_todos(session_id: str) -> dict: fetched.append(session_id) return { "session_id": session_id, @@ -521,10 +521,12 @@ import clients.terminal.api as api_module seen: list[str] = [] - monkeypatch.setattr(api_module, "get_session", lambda sid: seen.append(sid) or { - "session_id": sid, - "profile_id": "navi_code", - }) + + async def fake_get_session(sid): + seen.append(sid) + return {"session_id": sid, "profile_id": "navi_code"} + + monkeypatch.setattr(api_module, "get_session", fake_get_session) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() @@ -541,15 +543,17 @@ """A bad --resume id surfaces an error and does NOT silently create a new session.""" import clients.terminal.api as api_module - def boom(_sid: str) -> dict: + async def boom(_sid: str) -> dict: raise RuntimeError("session not found") created: list[str] = [] + + async def fake_create_session(*_a, **_k): + created.append("x") + return {"session_id": "NEW", "profile_id": "navi_code"} + monkeypatch.setattr(api_module, "get_session", boom) - monkeypatch.setattr(api_module, "create_session", lambda *_a, **_k: created.append("x") or { - "session_id": "NEW", - "profile_id": "navi_code", - }) + monkeypatch.setattr(api_module, "create_session", fake_create_session) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() @@ -655,11 +659,14 @@ {"role": "assistant", "content": "earlier answer", "is_display": True}, {"role": "user", "content": "context-only", "is_display": False}, ] - monkeypatch.setattr(api_module, "get_session", lambda sid: { - "session_id": sid, - "profile_id": "navi_code", - "messages": messages, - }) + async def fake_get_session(sid): + return { + "session_id": sid, + "profile_id": "navi_code", + "messages": messages, + } + + monkeypatch.setattr(api_module, "get_session", fake_get_session) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() @@ -801,13 +808,17 @@ pass monkeypatch.setattr(tui_app_module, "WsBridge", FakeBridge) - monkeypatch.setattr(api_module, "get_session", lambda sid: { - "session_id": sid, - "profile_id": "navi_code", - "messages": [], - "context_token_count": 9000, - "max_context_tokens": 32000, - }) + + async def fake_get_session(sid): + return { + "session_id": sid, + "profile_id": "navi_code", + "messages": [], + "context_token_count": 9000, + "max_context_tokens": 32000, + } + + monkeypatch.setattr(api_module, "get_session", fake_get_session) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() diff --git a/tests/clients/test_tui_export.py b/tests/clients/test_tui_export.py index d97ce0e..63a7285 100644 --- a/tests/clients/test_tui_export.py +++ b/tests/clients/test_tui_export.py @@ -29,7 +29,7 @@ """Return a fake session payload used to test export.""" session_id = "sess-export-1234" - def fake_get_session(sid: str) -> dict: + async def fake_get_session(sid: str) -> dict: if sid != session_id: raise Exception("not found") return { @@ -42,20 +42,28 @@ ], } - def fake_create_session(profile_id: str | None = None) -> dict: + async def fake_create_session(profile_id: str | None = None) -> dict: return { "session_id": session_id, "profile_id": profile_id or "navi_code", } - def fake_list_sessions() -> list[dict]: - return [fake_get_session(session_id)] + async def fake_list_sessions() -> list[dict]: + return [await fake_get_session(session_id)] monkeypatch.setattr("clients.terminal.api.get_session", fake_get_session) monkeypatch.setattr("clients.terminal.api.create_session", fake_create_session) monkeypatch.setattr("clients.terminal.api.list_sessions", fake_list_sessions) - return fake_get_session(session_id) + return { + "session_id": session_id, + "profile_id": "navi_code", + "created_at": "2026-06-23T12:00:00", + "messages": [ + {"role": "user", "content": "Hello Navi"}, + {"role": "assistant", "content": "Hello!"}, + ], + } @pytest.mark.anyio @@ -97,10 +105,10 @@ ) -> None: """Running /export when there is no active session shows an error.""" - def fake_create_session(profile_id: str | None = None) -> dict: + async def fake_create_session(profile_id: str | None = None) -> dict: return {"session_id": "", "profile_id": profile_id or "navi_code"} - def fake_get_session(sid: str) -> dict: + async def fake_get_session(sid: str) -> dict: raise Exception("not found") monkeypatch.setattr("clients.terminal.api.create_session", fake_create_session)