feat: factory-based DB wiring and ServiceError notifications in TUI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 19:27:08 -04:00
parent 208a9028e7
commit 98ea8a1a5c
3 changed files with 155 additions and 90 deletions
+2 -3
View File
@@ -131,8 +131,7 @@ def list_cmd():
settings = get_settings() settings = get_settings()
ensure_db(settings) ensure_db(settings)
engine = get_engine(settings) engine = get_engine(settings)
Session = get_session_factory(engine) factory = get_session_factory(engine)
with Session() as db: svc = ProjectService(factory)
svc = ProjectService(db)
for p in svc.list_all(): for p in svc.list_all():
click.echo(f" {p.name} ({p.path})") click.echo(f" {p.name} ({p.path})")
+43 -13
View File
@@ -1,5 +1,8 @@
import logging import logging
from collections.abc import Coroutine
from typing import Any
from sqlalchemy.orm import sessionmaker
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.binding import Binding from textual.binding import Binding
from textual.screen import ModalScreen from textual.screen import ModalScreen
@@ -8,6 +11,7 @@ from textual.widgets import Footer
from hqt.config import get_settings from hqt.config import get_settings
from hqt.db.engine import ensure_db, get_engine, get_session_factory from hqt.db.engine import ensure_db, get_engine, get_session_factory
from hqt.errors import ServiceError
from hqt.harnesses.registry import discover_harnesses, ensure_harnesses_in_db from hqt.harnesses.registry import discover_harnesses, ensure_harnesses_in_db
from hqt.projects.service import ProjectService from hqt.projects.service import ProjectService
from hqt.sessions.service import SessionService from hqt.sessions.service import SessionService
@@ -78,7 +82,9 @@ class HqtApp(App):
# project_id -> last selected session_id, so switching projects restores # project_id -> last selected session_id, so switching projects restores
# each project's previously highlighted session. # each project's previously highlighted session.
self._selected_session_by_project: dict[int, int] = {} self._selected_session_by_project: dict[int, int] = {}
self._db_session = None # Set in on_mount; declared non-optional so call sites (incl. tests
# that seed via app._db_factory()) don't need None-narrowing.
self._db_factory: sessionmaker = None # ty: ignore[invalid-assignment]
self._session_service: SessionService | None = None self._session_service: SessionService | None = None
self._project_service: ProjectService | None = None self._project_service: ProjectService | None = None
@@ -90,6 +96,22 @@ class HqtApp(App):
assert self._session_service is not None assert self._session_service is not None
return self._session_service return self._session_service
def _run_service_worker(self, coro: Coroutine[Any, Any, None]) -> None:
"""run_worker, but ServiceError surfaces as a notification.
Services raise ServiceError for expected failures (rows deleted
underneath an action, harness uninstalled); an uncaught exception
would kill the worker and crash the app.
"""
async def _wrapped() -> None:
try:
await coro
except ServiceError as err:
self.notify(str(err), severity="error")
self.run_worker(_wrapped())
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
# No Header: the project and session panels share the top row directly # No Header: the project and session panels share the top row directly
# (ProjectList docks left, SessionList fills the rest), with the keybind # (ProjectList docks left, SessionList fills the rest), with the keybind
@@ -103,9 +125,9 @@ class HqtApp(App):
settings = get_settings() settings = get_settings()
ensure_db(settings) ensure_db(settings)
engine = get_engine(settings) engine = get_engine(settings)
factory = get_session_factory(engine) self._db_factory = get_session_factory(engine)
self._db_session = factory() with self._db_factory() as db:
ensure_harnesses_in_db(self._db_session) ensure_harnesses_in_db(db)
runner = TmuxRunner(settings.tmux_path, settings.tui_session_name) runner = TmuxRunner(settings.tmux_path, settings.tui_session_name)
tmux = TmuxManager(runner) tmux = TmuxManager(runner)
# Theme the tmux status bar to match the Catppuccin Frappé TUI. The TUI # Theme the tmux status bar to match the Catppuccin Frappé TUI. The TUI
@@ -115,8 +137,8 @@ class HqtApp(App):
self.run_worker(runner.apply_theme(home_window=settings.tui_window_name)) self.run_worker(runner.apply_theme(home_window=settings.tui_window_name))
harnesses = discover_harnesses() harnesses = discover_harnesses()
log.info("Discovered harnesses: %s", list(harnesses.keys())) log.info("Discovered harnesses: %s", list(harnesses.keys()))
self._project_service = ProjectService(self._db_session) self._project_service = ProjectService(self._db_factory)
self._session_service = SessionService(self._db_session, tmux, harnesses) self._session_service = SessionService(self._db_factory, tmux, harnesses)
self._load_projects() self._load_projects()
self.set_interval(3, self._poll_sessions) self.set_interval(3, self._poll_sessions)
log.info("TUI mounted successfully") log.info("TUI mounted successfully")
@@ -132,7 +154,11 @@ class HqtApp(App):
async def _poll_sessions(self) -> None: async def _poll_sessions(self) -> None:
# Sync every session's tmux window label (session-wide), then update the # Sync every session's tmux window label (session-wide), then update the
# UI for the selected project from the same computed status — no recompute. # UI for the selected project from the same computed status — no recompute.
try:
infos = await self._sessions().sync_window_labels() infos = await self._sessions().sync_window_labels()
except ServiceError as err:
log.warning("Session poll failed: %s", err)
return
if self._selected_project_id is not None: if self._selected_project_id is not None:
subset = [ subset = [
i for i in infos if i.session.project_id == self._selected_project_id i for i in infos if i.session.project_id == self._selected_project_id
@@ -157,7 +183,7 @@ class HqtApp(App):
def on_project_selected(self, message: ProjectSelected) -> None: def on_project_selected(self, message: ProjectSelected) -> None:
self._selected_project_id = message.project_id self._selected_project_id = message.project_id
self.run_worker(self._refresh_sessions(restore_selection=True)) self._run_service_worker(self._refresh_sessions(restore_selection=True))
def on_session_selected(self, message: SessionSelected) -> None: def on_session_selected(self, message: SessionSelected) -> None:
if self._selected_project_id is not None: if self._selected_project_id is not None:
@@ -172,7 +198,11 @@ class HqtApp(App):
def on_dismiss(result: tuple[str, str] | None) -> None: def on_dismiss(result: tuple[str, str] | None) -> None:
if result: if result:
name, path = result name, path = result
try:
self._projects().create(name, path) self._projects().create(name, path)
except ServiceError as err:
self.notify(str(err), severity="error")
return
self._load_projects() self._load_projects()
self.push_screen(ProjectFormScreen(), on_dismiss) self.push_screen(ProjectFormScreen(), on_dismiss)
@@ -192,7 +222,7 @@ class HqtApp(App):
name, path = result name, path = result
try: try:
self._projects().update(project_id, name, path) self._projects().update(project_id, name, path)
except ValueError as err: except ServiceError as err:
self.notify(str(err), severity="error") self.notify(str(err), severity="error")
return return
self._load_projects() self._load_projects()
@@ -239,7 +269,7 @@ class HqtApp(App):
) )
await self._refresh_sessions() await self._refresh_sessions()
self.run_worker(_do()) self._run_service_worker(_do())
self.push_screen(NewSessionScreen(harness_names), on_dismiss) self.push_screen(NewSessionScreen(harness_names), on_dismiss)
@@ -263,7 +293,7 @@ class HqtApp(App):
if not ok: if not ok:
self.notify("Failed to attach session", severity="error") self.notify("Failed to attach session", severity="error")
self.run_worker(_do()) self._run_service_worker(_do())
def action_delete_session(self) -> None: def action_delete_session(self) -> None:
sid = self.query_one(SessionList).get_selected_session_id() sid = self.query_one(SessionList).get_selected_session_id()
@@ -273,7 +303,7 @@ class HqtApp(App):
await self._sessions().delete_session(sid) await self._sessions().delete_session(sid)
await self._refresh_sessions() await self._refresh_sessions()
self.run_worker(_do()) self._run_service_worker(_do())
def action_rename_session(self) -> None: def action_rename_session(self) -> None:
sid = self.query_one(SessionList).get_selected_session_id() sid = self.query_one(SessionList).get_selected_session_id()
@@ -288,7 +318,7 @@ class HqtApp(App):
def on_dismiss(result: str | None) -> None: def on_dismiss(result: str | None) -> None:
if result is not None: if result is not None:
self._sessions().rename_session(sid, result) self._sessions().rename_session(sid, result)
self.run_worker(self._refresh_sessions()) self._run_service_worker(self._refresh_sessions())
self.push_screen( self.push_screen(
RenameSessionScreen(initial_nickname=sess.nickname or ""), RenameSessionScreen(initial_nickname=sess.nickname or ""),
@@ -303,4 +333,4 @@ class HqtApp(App):
await self._sessions().stop_session(sid) await self._sessions().stop_session(sid)
await self._refresh_sessions() await self._refresh_sessions()
self.run_worker(_do()) self._run_service_worker(_do())
+106 -70
View File
@@ -49,20 +49,23 @@ async def test_session_refresh_no_duplicate_ids():
async with app.run_test(size=(120, 40)): async with app.run_test(size=(120, 40)):
from hqt.db.models import Project, Harness, Session from hqt.db.models import Project, Harness, Session
seed_db = app._db_factory()
proj = Project(name="test", path="/tmp/test") proj = Project(name="test", path="/tmp/test")
app._db_session.add(proj) seed_db.add(proj)
app._db_session.flush() seed_db.flush()
harness = app._db_session.query(Harness).first() harness = seed_db.query(Harness).first()
sess = Session( sess = Session(
project_id=proj.id, project_id=proj.id,
harness_id=harness.id, harness_id=harness.id,
tmux_session_name="hqt-test-dup", tmux_session_name="hqt-test-dup",
archived=False, archived=False,
) )
app._db_session.add(sess) seed_db.add(sess)
app._db_session.commit() seed_db.commit()
proj_id = proj.id
seed_db.close()
app._selected_project_id = proj.id app._selected_project_id = proj_id
# Call refresh multiple times rapidly (simulates poll timer overlap) # Call refresh multiple times rapidly (simulates poll timer overlap)
await app._refresh_sessions() await app._refresh_sessions()
await app._refresh_sessions() await app._refresh_sessions()
@@ -81,20 +84,24 @@ async def test_session_selection_preserved_across_refresh():
from hqt.db.models import Project, Harness, Session from hqt.db.models import Project, Harness, Session
from hqt.tui.widgets.session_list import SessionList from hqt.tui.widgets.session_list import SessionList
seed_db = app._db_factory()
proj = Project(name="test", path="/tmp/test-sel") proj = Project(name="test", path="/tmp/test-sel")
app._db_session.add(proj) seed_db.add(proj)
app._db_session.flush() seed_db.flush()
harness = app._db_session.query(Harness).first() harness = seed_db.query(Harness).first()
sess = Session( sess = Session(
project_id=proj.id, project_id=proj.id,
harness_id=harness.id, harness_id=harness.id,
tmux_session_name="hqt-test-sel", tmux_session_name="hqt-test-sel",
archived=False, archived=False,
) )
app._db_session.add(sess) seed_db.add(sess)
app._db_session.commit() seed_db.commit()
proj_id = proj.id
sess_id = sess.id
seed_db.close()
app._selected_project_id = proj.id app._selected_project_id = proj_id
await app._refresh_sessions() await app._refresh_sessions()
await pilot.pause() await pilot.pause()
@@ -104,12 +111,12 @@ async def test_session_selection_preserved_across_refresh():
await pilot.press("down") await pilot.press("down")
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == sess.id assert sl.get_selected_session_id() == sess_id
# Refresh should preserve selection # Refresh should preserve selection
await app._refresh_sessions() await app._refresh_sessions()
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == sess.id assert sl.get_selected_session_id() == sess_id
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -205,20 +212,24 @@ async def test_stop_session_action_calls_service_and_refreshes():
from hqt.db.models import Project, Harness, Session from hqt.db.models import Project, Harness, Session
from hqt.tui.widgets.session_list import SessionList from hqt.tui.widgets.session_list import SessionList
seed_db = app._db_factory()
proj = Project(name="stop-proj", path="/tmp/stop-proj") proj = Project(name="stop-proj", path="/tmp/stop-proj")
app._db_session.add(proj) seed_db.add(proj)
app._db_session.flush() seed_db.flush()
harness = app._db_session.query(Harness).first() harness = seed_db.query(Harness).first()
sess = Session( sess = Session(
project_id=proj.id, project_id=proj.id,
harness_id=harness.id, harness_id=harness.id,
tmux_session_name="hqt-stop-test", tmux_session_name="hqt-stop-test",
archived=False, archived=False,
) )
app._db_session.add(sess) seed_db.add(sess)
app._db_session.commit() seed_db.commit()
proj_id = proj.id
sess_id = sess.id
seed_db.close()
app._selected_project_id = proj.id app._selected_project_id = proj_id
await app._refresh_sessions() await app._refresh_sessions()
await pilot.pause() await pilot.pause()
@@ -228,7 +239,7 @@ async def test_stop_session_action_calls_service_and_refreshes():
lv.focus() lv.focus()
await pilot.press("down") await pilot.press("down")
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == sess.id assert sl.get_selected_session_id() == sess_id
# Replace service with a mock that records calls # Replace service with a mock that records calls
mock_service = MagicMock() mock_service = MagicMock()
@@ -241,7 +252,7 @@ async def test_stop_session_action_calls_service_and_refreshes():
await app.run_action("stop_session") await app.run_action("stop_session")
await pilot.pause() await pilot.pause()
mock_service.stop_session.assert_awaited_once_with(sess.id) mock_service.stop_session.assert_awaited_once_with(sess_id)
mock_service.list_sessions.assert_awaited() mock_service.list_sessions.assert_awaited()
@@ -269,11 +280,14 @@ async def test_new_session_on_dismiss_sequential_create_then_refresh():
async with app.run_test(size=(120, 40)) as pilot: async with app.run_test(size=(120, 40)) as pilot:
from hqt.db.models import Project from hqt.db.models import Project
seed_db = app._db_factory()
proj = Project(name="seq-proj", path="/tmp/seq-proj") proj = Project(name="seq-proj", path="/tmp/seq-proj")
app._db_session.add(proj) seed_db.add(proj)
app._db_session.flush() seed_db.flush()
app._db_session.commit() seed_db.commit()
app._selected_project_id = proj.id proj_id = proj.id
seed_db.close()
app._selected_project_id = proj_id
call_order: list[str] = [] call_order: list[str] = []
@@ -335,22 +349,26 @@ async def test_new_session_on_dismiss_notifies_on_spawn_failure():
async with app.run_test(size=(120, 40)) as pilot: async with app.run_test(size=(120, 40)) as pilot:
from hqt.db.models import Project from hqt.db.models import Project
seed_db = app._db_factory()
proj = Project(name="fail-proj", path="/tmp/fail-proj") proj = Project(name="fail-proj", path="/tmp/fail-proj")
app._db_session.add(proj) seed_db.add(proj)
app._db_session.flush() seed_db.flush()
app._db_session.commit() seed_db.commit()
app._selected_project_id = proj.id proj_id = proj.id
app._selected_project_id = proj_id
# Build a minimal fake Session object # Build a minimal fake Session object
harness_row = app._db_session.query(Harness).first() harness_row = seed_db.query(Harness).first()
fake_sess = DBSession( fake_sess = DBSession(
project_id=proj.id, project_id=proj_id,
harness_id=harness_row.id, harness_id=harness_row.id,
tmux_session_name="hqt-fail", tmux_session_name="hqt-fail",
archived=False, archived=False,
) )
app._db_session.add(fake_sess) seed_db.add(fake_sess)
app._db_session.flush() seed_db.flush()
seed_db.expunge(fake_sess)
seed_db.close()
fail_result = CreateSessionResult( fail_result = CreateSessionResult(
session=fake_sess, session=fake_sess,
@@ -404,21 +422,25 @@ async def test_new_session_on_dismiss_no_notify_on_success():
async with app.run_test(size=(120, 40)) as pilot: async with app.run_test(size=(120, 40)) as pilot:
from hqt.db.models import Project from hqt.db.models import Project
seed_db = app._db_factory()
proj = Project(name="ok-proj", path="/tmp/ok-proj") proj = Project(name="ok-proj", path="/tmp/ok-proj")
app._db_session.add(proj) seed_db.add(proj)
app._db_session.flush() seed_db.flush()
app._db_session.commit() seed_db.commit()
app._selected_project_id = proj.id proj_id = proj.id
app._selected_project_id = proj_id
harness_row = app._db_session.query(Harness).first() harness_row = seed_db.query(Harness).first()
fake_sess = DBSession( fake_sess = DBSession(
project_id=proj.id, project_id=proj_id,
harness_id=harness_row.id, harness_id=harness_row.id,
tmux_session_name="hqt-ok", tmux_session_name="hqt-ok",
archived=False, archived=False,
) )
app._db_session.add(fake_sess) seed_db.add(fake_sess)
app._db_session.flush() seed_db.flush()
seed_db.expunge(fake_sess)
seed_db.close()
ok_result = CreateSessionResult( ok_result = CreateSessionResult(
session=fake_sess, spawn_ok=True, spawn_error="" session=fake_sess, spawn_ok=True, spawn_error=""
@@ -890,11 +912,12 @@ async def test_first_session_selected_and_remembered_per_project():
from hqt.tui.widgets.project_list import ProjectSelected from hqt.tui.widgets.project_list import ProjectSelected
from hqt.tui.widgets.session_list import SessionList from hqt.tui.widgets.session_list import SessionList
h = app._db_session.query(Harness).first() seed_db = app._db_factory()
h = seed_db.query(Harness).first()
pa = Project(name="A", path="/tmp/proj-A") pa = Project(name="A", path="/tmp/proj-A")
pb = Project(name="B", path="/tmp/proj-B") pb = Project(name="B", path="/tmp/proj-B")
app._db_session.add_all([pa, pb]) seed_db.add_all([pa, pb])
app._db_session.flush() seed_db.flush()
a1 = Session( a1 = Session(
project_id=pa.id, project_id=pa.id,
harness_id=h.id, harness_id=h.id,
@@ -913,35 +936,38 @@ async def test_first_session_selected_and_remembered_per_project():
tmux_session_name="hqt-b1", tmux_session_name="hqt-b1",
archived=False, archived=False,
) )
app._db_session.add_all([a1, a2, b1]) seed_db.add_all([a1, a2, b1])
app._db_session.commit() seed_db.commit()
pa_id, pb_id = pa.id, pb.id
a1_id, a2_id, b1_id = a1.id, a2.id, b1.id
seed_db.close()
sl = app.query_one(SessionList) sl = app.query_one(SessionList)
# Enter project A: first session auto-selected. # Enter project A: first session auto-selected.
app.on_project_selected(ProjectSelected(pa.id)) app.on_project_selected(ProjectSelected(pa_id))
await app.workers.wait_for_complete() await app.workers.wait_for_complete()
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == a1.id assert sl.get_selected_session_id() == a1_id
# Move selection to the second session in A. # Move selection to the second session in A.
lv = sl.query_one("#session-list") lv = sl.query_one("#session-list")
lv.focus() lv.focus()
await pilot.press("down") await pilot.press("down")
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == a2.id assert sl.get_selected_session_id() == a2_id
# Switch to project B: its first session is auto-selected. # Switch to project B: its first session is auto-selected.
app.on_project_selected(ProjectSelected(pb.id)) app.on_project_selected(ProjectSelected(pb_id))
await app.workers.wait_for_complete() await app.workers.wait_for_complete()
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == b1.id assert sl.get_selected_session_id() == b1_id
# Switch back to A: the previously selected session (a2) is restored. # Switch back to A: the previously selected session (a2) is restored.
app.on_project_selected(ProjectSelected(pa.id)) app.on_project_selected(ProjectSelected(pa_id))
await app.workers.wait_for_complete() await app.workers.wait_for_complete()
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == a2.id assert sl.get_selected_session_id() == a2_id
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1076,10 +1102,11 @@ async def test_rename_session_opens_prefilled_and_updates():
from hqt.tui.screens.rename_session import RenameSessionScreen from hqt.tui.screens.rename_session import RenameSessionScreen
from textual.widgets import Input from textual.widgets import Input
seed_db = app._db_factory()
proj = Project(name="ren-proj", path="/tmp/ren-proj") proj = Project(name="ren-proj", path="/tmp/ren-proj")
app._db_session.add(proj) seed_db.add(proj)
app._db_session.flush() seed_db.flush()
harness = app._db_session.query(Harness).first() harness = seed_db.query(Harness).first()
sess = Session( sess = Session(
project_id=proj.id, project_id=proj.id,
harness_id=harness.id, harness_id=harness.id,
@@ -1087,10 +1114,13 @@ async def test_rename_session_opens_prefilled_and_updates():
tmux_session_name="hqt-ren", tmux_session_name="hqt-ren",
archived=False, archived=False,
) )
app._db_session.add(sess) seed_db.add(sess)
app._db_session.commit() seed_db.commit()
proj_id = proj.id
sess_id = sess.id
seed_db.close()
app._selected_project_id = proj.id app._selected_project_id = proj_id
await app._refresh_sessions() await app._refresh_sessions()
await pilot.pause() await pilot.pause()
@@ -1098,7 +1128,7 @@ async def test_rename_session_opens_prefilled_and_updates():
sl.query_one("#session-list").focus() sl.query_one("#session-list").focus()
await pilot.press("down") await pilot.press("down")
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == sess.id assert sl.get_selected_session_id() == sess_id
await app.run_action("rename_session") await app.run_action("rename_session")
await pilot.pause() await pilot.pause()
@@ -1111,7 +1141,9 @@ async def test_rename_session_opens_prefilled_and_updates():
await app.workers.wait_for_complete() await app.workers.wait_for_complete()
await pilot.pause() await pilot.pause()
assert app._db_session.get(Session, sess.id).nickname == "after" verify_db = app._db_factory()
assert verify_db.get(Session, sess_id).nickname == "after"
verify_db.close()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1153,10 +1185,11 @@ async def test_session_list_jk_navigation():
app = HqtApp() app = HqtApp()
async with app.run_test(size=(120, 40)) as pilot: async with app.run_test(size=(120, 40)) as pilot:
seed_db = app._db_factory()
proj = Project(name="jk-proj", path="/tmp/jk-proj") proj = Project(name="jk-proj", path="/tmp/jk-proj")
app._db_session.add(proj) seed_db.add(proj)
app._db_session.flush() seed_db.flush()
harness = app._db_session.query(Harness).first() harness = seed_db.query(Harness).first()
sessions = [] sessions = []
for n in range(2): for n in range(2):
s = Session( s = Session(
@@ -1166,11 +1199,14 @@ async def test_session_list_jk_navigation():
tmux_session_name=f"hqt-jk-{n}", tmux_session_name=f"hqt-jk-{n}",
archived=False, archived=False,
) )
app._db_session.add(s) seed_db.add(s)
sessions.append(s) sessions.append(s)
app._db_session.commit() seed_db.commit()
proj_id = proj.id
session_ids = [s.id for s in sessions]
seed_db.close()
app._selected_project_id = proj.id app._selected_project_id = proj_id
# restore_selection mirrors a real project switch, which highlights the # restore_selection mirrors a real project switch, which highlights the
# first session — giving j/k a defined starting point. # first session — giving j/k a defined starting point.
await app._refresh_sessions(restore_selection=True) await app._refresh_sessions(restore_selection=True)
@@ -1181,7 +1217,7 @@ async def test_session_list_jk_navigation():
await pilot.pause() await pilot.pause()
await pilot.press("j") await pilot.press("j")
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == sessions[1].id assert sl.get_selected_session_id() == session_ids[1]
await pilot.press("k") await pilot.press("k")
await pilot.pause() await pilot.pause()
assert sl.get_selected_session_id() == sessions[0].id assert sl.get_selected_session_id() == session_ids[0]