feat: factory-based DB wiring and ServiceError notifications in TUI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+106
-70
@@ -49,20 +49,23 @@ async def test_session_refresh_no_duplicate_ids():
|
||||
async with app.run_test(size=(120, 40)):
|
||||
from hqt.db.models import Project, Harness, Session
|
||||
|
||||
seed_db = app._db_factory()
|
||||
proj = Project(name="test", path="/tmp/test")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
harness = app._db_session.query(Harness).first()
|
||||
seed_db.add(proj)
|
||||
seed_db.flush()
|
||||
harness = seed_db.query(Harness).first()
|
||||
sess = Session(
|
||||
project_id=proj.id,
|
||||
harness_id=harness.id,
|
||||
tmux_session_name="hqt-test-dup",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add(sess)
|
||||
app._db_session.commit()
|
||||
seed_db.add(sess)
|
||||
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)
|
||||
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.tui.widgets.session_list import SessionList
|
||||
|
||||
seed_db = app._db_factory()
|
||||
proj = Project(name="test", path="/tmp/test-sel")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
harness = app._db_session.query(Harness).first()
|
||||
seed_db.add(proj)
|
||||
seed_db.flush()
|
||||
harness = seed_db.query(Harness).first()
|
||||
sess = Session(
|
||||
project_id=proj.id,
|
||||
harness_id=harness.id,
|
||||
tmux_session_name="hqt-test-sel",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add(sess)
|
||||
app._db_session.commit()
|
||||
seed_db.add(sess)
|
||||
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 pilot.pause()
|
||||
|
||||
@@ -104,12 +111,12 @@ async def test_session_selection_preserved_across_refresh():
|
||||
await pilot.press("down")
|
||||
await pilot.pause()
|
||||
|
||||
assert sl.get_selected_session_id() == sess.id
|
||||
assert sl.get_selected_session_id() == sess_id
|
||||
|
||||
# Refresh should preserve selection
|
||||
await app._refresh_sessions()
|
||||
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.tui.widgets.session_list import SessionList
|
||||
|
||||
seed_db = app._db_factory()
|
||||
proj = Project(name="stop-proj", path="/tmp/stop-proj")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
harness = app._db_session.query(Harness).first()
|
||||
seed_db.add(proj)
|
||||
seed_db.flush()
|
||||
harness = seed_db.query(Harness).first()
|
||||
sess = Session(
|
||||
project_id=proj.id,
|
||||
harness_id=harness.id,
|
||||
tmux_session_name="hqt-stop-test",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add(sess)
|
||||
app._db_session.commit()
|
||||
seed_db.add(sess)
|
||||
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 pilot.pause()
|
||||
|
||||
@@ -228,7 +239,7 @@ async def test_stop_session_action_calls_service_and_refreshes():
|
||||
lv.focus()
|
||||
await pilot.press("down")
|
||||
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
|
||||
mock_service = MagicMock()
|
||||
@@ -241,7 +252,7 @@ async def test_stop_session_action_calls_service_and_refreshes():
|
||||
await app.run_action("stop_session")
|
||||
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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
from hqt.db.models import Project
|
||||
|
||||
seed_db = app._db_factory()
|
||||
proj = Project(name="seq-proj", path="/tmp/seq-proj")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
app._db_session.commit()
|
||||
app._selected_project_id = proj.id
|
||||
seed_db.add(proj)
|
||||
seed_db.flush()
|
||||
seed_db.commit()
|
||||
proj_id = proj.id
|
||||
seed_db.close()
|
||||
app._selected_project_id = proj_id
|
||||
|
||||
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:
|
||||
from hqt.db.models import Project
|
||||
|
||||
seed_db = app._db_factory()
|
||||
proj = Project(name="fail-proj", path="/tmp/fail-proj")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
app._db_session.commit()
|
||||
app._selected_project_id = proj.id
|
||||
seed_db.add(proj)
|
||||
seed_db.flush()
|
||||
seed_db.commit()
|
||||
proj_id = proj.id
|
||||
app._selected_project_id = proj_id
|
||||
|
||||
# Build a minimal fake Session object
|
||||
harness_row = app._db_session.query(Harness).first()
|
||||
harness_row = seed_db.query(Harness).first()
|
||||
fake_sess = DBSession(
|
||||
project_id=proj.id,
|
||||
project_id=proj_id,
|
||||
harness_id=harness_row.id,
|
||||
tmux_session_name="hqt-fail",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add(fake_sess)
|
||||
app._db_session.flush()
|
||||
seed_db.add(fake_sess)
|
||||
seed_db.flush()
|
||||
seed_db.expunge(fake_sess)
|
||||
seed_db.close()
|
||||
|
||||
fail_result = CreateSessionResult(
|
||||
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:
|
||||
from hqt.db.models import Project
|
||||
|
||||
seed_db = app._db_factory()
|
||||
proj = Project(name="ok-proj", path="/tmp/ok-proj")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
app._db_session.commit()
|
||||
app._selected_project_id = proj.id
|
||||
seed_db.add(proj)
|
||||
seed_db.flush()
|
||||
seed_db.commit()
|
||||
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(
|
||||
project_id=proj.id,
|
||||
project_id=proj_id,
|
||||
harness_id=harness_row.id,
|
||||
tmux_session_name="hqt-ok",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add(fake_sess)
|
||||
app._db_session.flush()
|
||||
seed_db.add(fake_sess)
|
||||
seed_db.flush()
|
||||
seed_db.expunge(fake_sess)
|
||||
seed_db.close()
|
||||
|
||||
ok_result = CreateSessionResult(
|
||||
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.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")
|
||||
pb = Project(name="B", path="/tmp/proj-B")
|
||||
app._db_session.add_all([pa, pb])
|
||||
app._db_session.flush()
|
||||
seed_db.add_all([pa, pb])
|
||||
seed_db.flush()
|
||||
a1 = Session(
|
||||
project_id=pa.id,
|
||||
harness_id=h.id,
|
||||
@@ -913,35 +936,38 @@ async def test_first_session_selected_and_remembered_per_project():
|
||||
tmux_session_name="hqt-b1",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add_all([a1, a2, b1])
|
||||
app._db_session.commit()
|
||||
seed_db.add_all([a1, a2, b1])
|
||||
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)
|
||||
|
||||
# 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 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.
|
||||
lv = sl.query_one("#session-list")
|
||||
lv.focus()
|
||||
await pilot.press("down")
|
||||
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.
|
||||
app.on_project_selected(ProjectSelected(pb.id))
|
||||
app.on_project_selected(ProjectSelected(pb_id))
|
||||
await app.workers.wait_for_complete()
|
||||
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.
|
||||
app.on_project_selected(ProjectSelected(pa.id))
|
||||
app.on_project_selected(ProjectSelected(pa_id))
|
||||
await app.workers.wait_for_complete()
|
||||
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 textual.widgets import Input
|
||||
|
||||
seed_db = app._db_factory()
|
||||
proj = Project(name="ren-proj", path="/tmp/ren-proj")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
harness = app._db_session.query(Harness).first()
|
||||
seed_db.add(proj)
|
||||
seed_db.flush()
|
||||
harness = seed_db.query(Harness).first()
|
||||
sess = Session(
|
||||
project_id=proj.id,
|
||||
harness_id=harness.id,
|
||||
@@ -1087,10 +1114,13 @@ async def test_rename_session_opens_prefilled_and_updates():
|
||||
tmux_session_name="hqt-ren",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add(sess)
|
||||
app._db_session.commit()
|
||||
seed_db.add(sess)
|
||||
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 pilot.pause()
|
||||
|
||||
@@ -1098,7 +1128,7 @@ async def test_rename_session_opens_prefilled_and_updates():
|
||||
sl.query_one("#session-list").focus()
|
||||
await pilot.press("down")
|
||||
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 pilot.pause()
|
||||
@@ -1111,7 +1141,9 @@ async def test_rename_session_opens_prefilled_and_updates():
|
||||
await app.workers.wait_for_complete()
|
||||
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()
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
seed_db = app._db_factory()
|
||||
proj = Project(name="jk-proj", path="/tmp/jk-proj")
|
||||
app._db_session.add(proj)
|
||||
app._db_session.flush()
|
||||
harness = app._db_session.query(Harness).first()
|
||||
seed_db.add(proj)
|
||||
seed_db.flush()
|
||||
harness = seed_db.query(Harness).first()
|
||||
sessions = []
|
||||
for n in range(2):
|
||||
s = Session(
|
||||
@@ -1166,11 +1199,14 @@ async def test_session_list_jk_navigation():
|
||||
tmux_session_name=f"hqt-jk-{n}",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add(s)
|
||||
seed_db.add(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
|
||||
# first session — giving j/k a defined starting point.
|
||||
await app._refresh_sessions(restore_selection=True)
|
||||
@@ -1181,7 +1217,7 @@ async def test_session_list_jk_navigation():
|
||||
await pilot.pause()
|
||||
await pilot.press("j")
|
||||
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.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