90944948f5
TUI for orchestrating AI coding harness sessions (Claude Code, Codex, Kiro, etc.) via tmux. Click CLI bootstraps a Textual TUI over ProjectService/SessionService backed by SQLite, spawning harness sessions as tmux windows through TmuxManager. Includes recent fixes: - Visible Tab focus highlight on dialog OK/Cancel buttons - Auto-select first project on launch - Auto-select first session + per-project session-selection memory - tmux new-window targets an explicit free index, fixing "index N in use" failures (broken spawn/attach in attached sessions) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
import pytest
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from hqt.db.models import Base, Harness
|
|
from hqt.projects.service import ProjectService
|
|
from hqt.sessions.service import SessionService
|
|
from hqt.tmux.manager import TmuxManager, SpawnResult
|
|
from hqt.harnesses.configurators.claude import ClaudeConfigurator
|
|
|
|
|
|
@pytest.fixture
|
|
def setup():
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
factory = sessionmaker(bind=engine)
|
|
db = factory()
|
|
db.add(Harness(name="claude-code", display_name="Claude Code"))
|
|
db.commit()
|
|
|
|
tmux = MagicMock(spec=TmuxManager)
|
|
tmux.spawn = AsyncMock(return_value=SpawnResult(ok=True, window_id="@1", error=""))
|
|
tmux.kill = AsyncMock()
|
|
tmux.is_alive = AsyncMock(return_value=True)
|
|
tmux.window_exists = AsyncMock(return_value=True)
|
|
tmux.poll_info = AsyncMock(return_value={})
|
|
tmux.capture_pane = AsyncMock(return_value="")
|
|
tmux.attach = AsyncMock(return_value=True)
|
|
|
|
configurator = ClaudeConfigurator()
|
|
harnesses = {"claude-code": configurator}
|
|
|
|
proj_svc = ProjectService(db)
|
|
sess_svc = SessionService(db=db, tmux=tmux, harnesses=harnesses)
|
|
return proj_svc, sess_svc, tmux
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_lifecycle(setup):
|
|
proj_svc, sess_svc, tmux = setup
|
|
|
|
# Create project
|
|
project = proj_svc.create(name="test-proj", path="/tmp/test-proj")
|
|
assert project.id == 1
|
|
|
|
# Create session
|
|
create_result = await sess_svc.create_session(project_id=project.id, harness_name="claude-code", nickname="e2e")
|
|
session = create_result.session
|
|
assert session.id is not None
|
|
tmux.spawn.assert_called_once()
|
|
|
|
# List sessions - alive
|
|
from hqt.tmux.runner import WindowInfo
|
|
tmux.poll_info.return_value = {session.tmux_session_name: WindowInfo(alive=True, last_activity=0)}
|
|
infos = await sess_svc.list_sessions(project_id=project.id)
|
|
assert len(infos) == 1
|
|
assert infos[0].alive is True
|
|
|
|
# Stop session
|
|
await sess_svc.stop_session(session_id=session.id)
|
|
tmux.kill.assert_called_once_with(session.tmux_session_name)
|
|
|
|
# List sessions - dead
|
|
tmux.poll_info.return_value = {session.tmux_session_name: WindowInfo(alive=False, last_activity=0)}
|
|
infos = await sess_svc.list_sessions(project_id=project.id)
|
|
assert len(infos) == 1
|
|
assert infos[0].alive is False
|
|
|
|
# Delete session
|
|
tmux.window_exists.return_value = False
|
|
await sess_svc.delete_session(session_id=session.id)
|
|
|
|
# List sessions - empty
|
|
infos = await sess_svc.list_sessions(project_id=project.id)
|
|
assert len(infos) == 0
|