feat: factory-based DB wiring and ServiceError notifications in TUI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+2
-3
@@ -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
@@ -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
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user