feat: add archive_session and unarchive_session to SessionService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -826,6 +826,37 @@ class SessionService:
|
|||||||
db.delete(sess)
|
db.delete(sess)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
async def archive_session(self, session_id: int) -> None:
|
||||||
|
"""Archive a session: kill its tmux window but keep the DB row.
|
||||||
|
|
||||||
|
A recoverable "remove from view" — the window/harness process is torn
|
||||||
|
down (freeing resources) but the row, ``harness_session_id``, and any
|
||||||
|
worktree are preserved so the session can be restored or referenced
|
||||||
|
later. No-op if the session does not exist.
|
||||||
|
"""
|
||||||
|
with self.factory() as db:
|
||||||
|
sess = db.get(Session, session_id)
|
||||||
|
if sess is None:
|
||||||
|
return
|
||||||
|
if await self.tmux.window_exists(sess.tmux_session_name):
|
||||||
|
await self.tmux.kill(sess.tmux_session_name)
|
||||||
|
sess.archived = True
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
async def unarchive_session(self, session_id: int) -> None:
|
||||||
|
"""Restore an archived session by clearing its archived flag.
|
||||||
|
|
||||||
|
The tmux window stays gone (the session reads as dead until resumed);
|
||||||
|
this only makes the row visible in the active list again. No-op if the
|
||||||
|
session does not exist.
|
||||||
|
"""
|
||||||
|
with self.factory() as db:
|
||||||
|
sess = db.get(Session, session_id)
|
||||||
|
if sess is None:
|
||||||
|
return
|
||||||
|
sess.archived = False
|
||||||
|
db.commit()
|
||||||
|
|
||||||
async def worktree_state_for(self, session_id: int) -> WorktreeState | None:
|
async def worktree_state_for(self, session_id: int) -> WorktreeState | None:
|
||||||
"""Return the WorktreeState for a session's worktree, or None.
|
"""Return the WorktreeState for a session's worktree, or None.
|
||||||
|
|
||||||
|
|||||||
@@ -2170,3 +2170,43 @@ async def test_list_sessions_archived_filter(service, db, tmux):
|
|||||||
|
|
||||||
archived = await service.list_sessions(project_id=1, archived=True)
|
archived = await service.list_sessions(project_id=1, archived=True)
|
||||||
assert [i.session.id for i in archived] == [arch_id]
|
assert [i.session.id for i in archived] == [arch_id]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_archive_session_kills_window_and_keeps_row(service, db, tmux):
|
||||||
|
await service.create_session(project_id=1, harness_name="claude-code")
|
||||||
|
await service.archive_session(1)
|
||||||
|
|
||||||
|
tmux.kill.assert_called_once_with("hqt-1")
|
||||||
|
db.expire_all()
|
||||||
|
row = db.get(Session, 1)
|
||||||
|
assert row is not None
|
||||||
|
assert row.archived is True
|
||||||
|
# Excluded from the default (active) listing.
|
||||||
|
assert await service.list_sessions(project_id=1) == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_archive_session_no_kill_when_window_missing(service, db, tmux):
|
||||||
|
await service.create_session(project_id=1, harness_name="claude-code")
|
||||||
|
tmux.window_exists = AsyncMock(return_value=False)
|
||||||
|
tmux.kill.reset_mock()
|
||||||
|
|
||||||
|
await service.archive_session(1)
|
||||||
|
|
||||||
|
tmux.kill.assert_not_called()
|
||||||
|
db.expire_all()
|
||||||
|
assert db.get(Session, 1).archived is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unarchive_session_restores_to_active_list(service, db, tmux):
|
||||||
|
await service.create_session(project_id=1, harness_name="claude-code")
|
||||||
|
await service.archive_session(1)
|
||||||
|
|
||||||
|
await service.unarchive_session(1)
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
|
assert db.get(Session, 1).archived is False
|
||||||
|
active = await service.list_sessions(project_id=1)
|
||||||
|
assert [i.session.id for i in active] == [1]
|
||||||
|
|||||||
Reference in New Issue
Block a user