Initial commit: hqt — HQ Terminal TUI
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>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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
|
||||
Reference in New Issue
Block a user