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>
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
from hqt.db.models import Base
|
|
from hqt.mcp.service import McpService
|
|
from hqt.projects.service import ProjectService
|
|
|
|
|
|
@pytest.fixture
|
|
def db():
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
with Session(engine) as session:
|
|
yield session
|
|
|
|
|
|
class TestProjectService:
|
|
def test_create_and_get(self, db):
|
|
svc = ProjectService(db)
|
|
p = svc.create("test", "/tmp/test")
|
|
assert p.id is not None
|
|
assert svc.get(p.id).name == "test"
|
|
|
|
def test_list_excludes_archived(self, db):
|
|
svc = ProjectService(db)
|
|
svc.create("a", "/a")
|
|
p2 = svc.create("b", "/b")
|
|
svc.archive(p2.id)
|
|
assert len(svc.list_all()) == 1
|
|
assert len(svc.list_all(include_archived=True)) == 2
|
|
|
|
def test_update_name_and_path(self, db):
|
|
svc = ProjectService(db)
|
|
p = svc.create("old", "/old/path")
|
|
updated = svc.update(p.id, "new", "/new/path")
|
|
assert updated.name == "new"
|
|
assert updated.path == "/new/path"
|
|
assert svc.get(p.id).path == "/new/path"
|
|
|
|
def test_update_duplicate_path_raises(self, db):
|
|
svc = ProjectService(db)
|
|
svc.create("a", "/a")
|
|
p2 = svc.create("b", "/b")
|
|
with pytest.raises(ValueError, match="already uses path"):
|
|
svc.update(p2.id, "b", "/a")
|
|
# DB session must remain usable after rollback, values unchanged
|
|
assert svc.get(p2.id).path == "/b"
|
|
assert svc.get(p2.id).name == "b"
|
|
|
|
def test_update_unknown_id_raises(self, db):
|
|
svc = ProjectService(db)
|
|
with pytest.raises(ValueError, match="not found"):
|
|
svc.update(9999, "x", "/x")
|
|
|
|
|
|
class TestMcpService:
|
|
def test_crud(self, db):
|
|
svc = McpService(db)
|
|
server = svc.create("test-mcp", "stdio", command="node", args=["server.js"])
|
|
assert server.id is not None
|
|
assert svc.get("test-mcp") is not None
|
|
assert len(svc.list_all()) == 1
|
|
svc.delete("test-mcp")
|
|
assert svc.get("test-mcp") is None
|
|
|
|
def test_bind_unbind(self, db):
|
|
psvc = ProjectService(db)
|
|
msvc = McpService(db)
|
|
project = psvc.create("proj", "/proj")
|
|
server = msvc.create("s1", "stdio", command="cmd")
|
|
msvc.bind_to_project(project.id, server.id)
|
|
assert len(msvc.get_project_mcps(project.id)) == 1
|
|
msvc.unbind_from_project(project.id, server.id)
|
|
assert len(msvc.get_project_mcps(project.id)) == 0
|