Satisfy project quality gates
This commit is contained in:
+99
-44
@@ -17,7 +17,7 @@ def mock_services(tmp_path, monkeypatch):
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_mounts():
|
||||
app = HqtApp()
|
||||
async with app.run_test() as pilot:
|
||||
async with app.run_test():
|
||||
assert app.title == "HQ Terminal"
|
||||
|
||||
|
||||
@@ -27,16 +27,17 @@ async def test_no_header_bar():
|
||||
from textual.widgets import Header
|
||||
|
||||
app = HqtApp()
|
||||
async with app.run_test() as pilot:
|
||||
async with app.run_test():
|
||||
assert len(app.query(Header)) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_has_project_and_session_panels():
|
||||
app = HqtApp()
|
||||
async with app.run_test() as pilot:
|
||||
async with app.run_test():
|
||||
from hqt.tui.widgets.project_list import ProjectList
|
||||
from hqt.tui.widgets.session_list import SessionList
|
||||
|
||||
assert app.query_one(ProjectList) is not None
|
||||
assert app.query_one(SessionList) is not None
|
||||
|
||||
@@ -45,7 +46,7 @@ async def test_app_has_project_and_session_panels():
|
||||
async def test_session_refresh_no_duplicate_ids():
|
||||
"""Regression test: rapid session refreshes must not crash with DuplicateIds."""
|
||||
app = HqtApp()
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
async with app.run_test(size=(120, 40)):
|
||||
from hqt.db.models import Project, Harness, Session
|
||||
|
||||
proj = Project(name="test", path="/tmp/test")
|
||||
@@ -116,9 +117,12 @@ async def test_session_selection_preserved_across_refresh():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_session_info(session_id: int, name: str, harness_name: str, status: str, alive: bool):
|
||||
def _make_session_info(
|
||||
session_id: int, name: str, harness_name: str, status: str, alive: bool
|
||||
):
|
||||
"""Build a minimal SessionInfo-like object for widget testing."""
|
||||
from hqt.sessions.service import SessionInfo
|
||||
|
||||
harness = MagicMock()
|
||||
harness.name = harness_name
|
||||
session = MagicMock()
|
||||
@@ -150,13 +154,23 @@ async def test_session_list_label_includes_status():
|
||||
label_texts = [str(child.query_one(Label).render()) for child in lv.children]
|
||||
|
||||
# Every label must contain the status word
|
||||
assert any("working" in t for t in label_texts), f"Expected 'working' in labels: {label_texts}"
|
||||
assert any("idle" in t for t in label_texts), f"Expected 'idle' in labels: {label_texts}"
|
||||
assert any("dead" in t for t in label_texts), f"Expected 'dead' in labels: {label_texts}"
|
||||
assert any("working" in t for t in label_texts), (
|
||||
f"Expected 'working' in labels: {label_texts}"
|
||||
)
|
||||
assert any("idle" in t for t in label_texts), (
|
||||
f"Expected 'idle' in labels: {label_texts}"
|
||||
)
|
||||
assert any("dead" in t for t in label_texts), (
|
||||
f"Expected 'dead' in labels: {label_texts}"
|
||||
)
|
||||
|
||||
# Symbols: working → ◐, idle → ●, dead → ○
|
||||
assert any("◐" in t for t in label_texts), f"Expected '◐' in labels: {label_texts}"
|
||||
assert any("○" in t for t in label_texts), f"Expected '○' in labels: {label_texts}"
|
||||
assert any("◐" in t for t in label_texts), (
|
||||
f"Expected '◐' in labels: {label_texts}"
|
||||
)
|
||||
assert any("○" in t for t in label_texts), (
|
||||
f"Expected '○' in labels: {label_texts}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -270,7 +284,10 @@ async def test_new_session_on_dismiss_sequential_create_then_refresh():
|
||||
await asyncio.sleep(0.05)
|
||||
call_order.append("create")
|
||||
from hqt.sessions.service import CreateSessionResult
|
||||
return CreateSessionResult(session=MagicMock(), spawn_ok=True, spawn_error="")
|
||||
|
||||
return CreateSessionResult(
|
||||
session=MagicMock(), spawn_ok=True, spawn_error=""
|
||||
)
|
||||
|
||||
async def fake_list(project_id):
|
||||
call_order.append("list")
|
||||
@@ -284,6 +301,7 @@ async def test_new_session_on_dismiss_sequential_create_then_refresh():
|
||||
|
||||
# Patch discover_harnesses so the early-return guard doesn't fire.
|
||||
from unittest.mock import patch
|
||||
|
||||
with patch("hqt.tui.app.discover_harnesses", return_value={"claude": object()}):
|
||||
app.action_new_session()
|
||||
await pilot.pause()
|
||||
@@ -296,7 +314,9 @@ async def test_new_session_on_dismiss_sequential_create_then_refresh():
|
||||
await app.workers.wait_for_complete()
|
||||
await pilot.pause()
|
||||
|
||||
assert call_order == ["create", "list"], f"Expected create before list, got: {call_order}"
|
||||
assert call_order == ["create", "list"], (
|
||||
f"Expected create before list, got: {call_order}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -360,11 +380,17 @@ async def test_new_session_on_dismiss_notifies_on_spawn_failure():
|
||||
await pilot.pause()
|
||||
|
||||
# Should have notified with severity="error"
|
||||
error_notifs = [(m, kw) for m, kw in notify_calls if kw.get("severity") == "error"]
|
||||
assert len(error_notifs) >= 1, f"Expected error notification, got: {notify_calls}"
|
||||
error_notifs = [
|
||||
(m, kw) for m, kw in notify_calls if kw.get("severity") == "error"
|
||||
]
|
||||
assert len(error_notifs) >= 1, (
|
||||
f"Expected error notification, got: {notify_calls}"
|
||||
)
|
||||
# Message should contain the first line of the error
|
||||
msg_text = error_notifs[0][0]
|
||||
assert "command not found" in msg_text or "claude" in msg_text, f"Unexpected message: {msg_text}"
|
||||
assert "command not found" in msg_text or "claude" in msg_text, (
|
||||
f"Unexpected message: {msg_text}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -394,7 +420,9 @@ async def test_new_session_on_dismiss_no_notify_on_success():
|
||||
app._db_session.add(fake_sess)
|
||||
app._db_session.flush()
|
||||
|
||||
ok_result = CreateSessionResult(session=fake_sess, spawn_ok=True, spawn_error="")
|
||||
ok_result = CreateSessionResult(
|
||||
session=fake_sess, spawn_ok=True, spawn_error=""
|
||||
)
|
||||
|
||||
mock_service = MagicMock()
|
||||
mock_service.create_session = AsyncMock(return_value=ok_result)
|
||||
@@ -417,8 +445,12 @@ async def test_new_session_on_dismiss_no_notify_on_success():
|
||||
await app.workers.wait_for_complete()
|
||||
await pilot.pause()
|
||||
|
||||
error_notifs = [(m, kw) for m, kw in notify_calls if kw.get("severity") == "error"]
|
||||
assert len(error_notifs) == 0, f"Expected no error notification, got: {error_notifs}"
|
||||
error_notifs = [
|
||||
(m, kw) for m, kw in notify_calls if kw.get("severity") == "error"
|
||||
]
|
||||
assert len(error_notifs) == 0, (
|
||||
f"Expected no error notification, got: {error_notifs}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -469,7 +501,7 @@ async def test_new_session_enter_selects_highlighted_harness():
|
||||
|
||||
await pilot.press("enter") # open the dropdown
|
||||
await pilot.pause()
|
||||
await pilot.press("down") # move to the second harness
|
||||
await pilot.press("down") # move to the second harness
|
||||
await pilot.pause()
|
||||
await pilot.press("enter") # confirm the highlighted harness
|
||||
await pilot.pause()
|
||||
@@ -609,9 +641,9 @@ async def test_edit_project_no_selection_warns():
|
||||
await app.run_action("edit_project")
|
||||
await pilot.pause()
|
||||
|
||||
assert any(
|
||||
kw.get("severity") == "warning" for _, kw in notify_calls
|
||||
), f"Expected warning notification, got: {notify_calls}"
|
||||
assert any(kw.get("severity") == "warning" for _, kw in notify_calls), (
|
||||
f"Expected warning notification, got: {notify_calls}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -694,14 +726,14 @@ async def test_edit_project_duplicate_path_notifies_error():
|
||||
@pytest.mark.asyncio
|
||||
async def test_frappe_theme_fully_applied():
|
||||
app = HqtApp()
|
||||
async with app.run_test() as pilot:
|
||||
async with app.run_test():
|
||||
assert app.current_theme.name == "catppuccin-frappe"
|
||||
variables = app.get_css_variables()
|
||||
# Spot-check: explicit values, not Textual auto-derivations
|
||||
assert variables["primary"] == "#8CAAEE" # Blue
|
||||
assert variables["background"] == "#292C3C" # Mantle
|
||||
assert variables["surface"] == "#414559" # Surface0
|
||||
assert variables["border"] == "#babbf1" # Lavender
|
||||
assert variables["primary"] == "#8CAAEE" # Blue
|
||||
assert variables["background"] == "#292C3C" # Mantle
|
||||
assert variables["surface"] == "#414559" # Surface0
|
||||
assert variables["border"] == "#babbf1" # Lavender
|
||||
assert variables["footer-background"] == "#51576d" # Surface1
|
||||
assert variables["input-cursor-background"] == "#f2d5cf" # Rosewater
|
||||
|
||||
@@ -809,10 +841,14 @@ async def test_dialog_button_focus_is_visibly_highlighted():
|
||||
# $accent (Frappé Peach #ef9f76) fill — peach is red-dominant, so the
|
||||
# focused button is unmistakably distinct from the blue/ghost defaults.
|
||||
# (Textual adds a small focus background-tint, so allow ±2 per channel.)
|
||||
assert abs(bg.r - 0xEF) <= 2 and abs(bg.g - 0x9F) <= 2 and abs(bg.b - 0x76) <= 2, (
|
||||
f"{btn_id} focus background {bg.hex}, expected ~Peach #EF9F76"
|
||||
assert (
|
||||
abs(bg.r - 0xEF) <= 2
|
||||
and abs(bg.g - 0x9F) <= 2
|
||||
and abs(bg.b - 0x76) <= 2
|
||||
), f"{btn_id} focus background {bg.hex}, expected ~Peach #EF9F76"
|
||||
assert bg.r > bg.b, (
|
||||
f"{btn_id} focus highlight should be peach, not blue ({bg.hex})"
|
||||
)
|
||||
assert bg.r > bg.b, f"{btn_id} focus highlight should be peach, not blue ({bg.hex})"
|
||||
assert btn.styles.color.hex == "#292C3C", (
|
||||
f"{btn_id} focus text color {btn.styles.color.hex}, expected dark"
|
||||
)
|
||||
@@ -837,7 +873,9 @@ async def test_first_project_selected_on_load():
|
||||
await pilot.pause()
|
||||
|
||||
pl = app.query_one(ProjectList)
|
||||
first_id = pl.query_one("#project-list").children[0].data
|
||||
first_item_id = pl.query_one("#project-list").children[0].id
|
||||
assert first_item_id is not None
|
||||
first_id = int(first_item_id.removeprefix("proj-"))
|
||||
assert pl.get_selected_project_id() == first_id
|
||||
assert app._selected_project_id == first_id
|
||||
|
||||
@@ -857,9 +895,24 @@ async def test_first_session_selected_and_remembered_per_project():
|
||||
pb = Project(name="B", path="/tmp/proj-B")
|
||||
app._db_session.add_all([pa, pb])
|
||||
app._db_session.flush()
|
||||
a1 = Session(project_id=pa.id, harness_id=h.id, tmux_session_name="hqt-a1", archived=False)
|
||||
a2 = Session(project_id=pa.id, harness_id=h.id, tmux_session_name="hqt-a2", archived=False)
|
||||
b1 = Session(project_id=pb.id, harness_id=h.id, tmux_session_name="hqt-b1", archived=False)
|
||||
a1 = Session(
|
||||
project_id=pa.id,
|
||||
harness_id=h.id,
|
||||
tmux_session_name="hqt-a1",
|
||||
archived=False,
|
||||
)
|
||||
a2 = Session(
|
||||
project_id=pa.id,
|
||||
harness_id=h.id,
|
||||
tmux_session_name="hqt-a2",
|
||||
archived=False,
|
||||
)
|
||||
b1 = Session(
|
||||
project_id=pb.id,
|
||||
harness_id=h.id,
|
||||
tmux_session_name="hqt-b1",
|
||||
archived=False,
|
||||
)
|
||||
app._db_session.add_all([a1, a2, b1])
|
||||
app._db_session.commit()
|
||||
|
||||
@@ -901,17 +954,21 @@ def test_format_session_text_colors_and_escapes():
|
||||
|
||||
text = format_session_text("mywork", "claude", "working", "◐")
|
||||
assert text.startswith("[#a6d189]◐[/]") # Green symbol
|
||||
assert "\\[claude]" in text # escaped, so brackets render
|
||||
assert "\\[claude]" in text # escaped, so brackets render
|
||||
assert "working" in text
|
||||
|
||||
|
||||
def test_format_session_text_status_colors():
|
||||
from hqt.tui.widgets.session_list import format_session_text
|
||||
|
||||
assert format_session_text("x", "h", "waiting", "◉").startswith("[#e5c890]") # Yellow
|
||||
assert format_session_text("x", "h", "idle", "●").startswith("[#81c8be]") # Teal
|
||||
assert format_session_text("x", "h", "active", "●").startswith("[#81c8be]") # Teal
|
||||
assert format_session_text("x", "h", "dead", "○").startswith("[#737994]") # Overlay0
|
||||
assert format_session_text("x", "h", "waiting", "◉").startswith(
|
||||
"[#e5c890]"
|
||||
) # Yellow
|
||||
assert format_session_text("x", "h", "idle", "●").startswith("[#81c8be]") # Teal
|
||||
assert format_session_text("x", "h", "active", "●").startswith("[#81c8be]") # Teal
|
||||
assert format_session_text("x", "h", "dead", "○").startswith(
|
||||
"[#737994]"
|
||||
) # Overlay0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -1005,9 +1062,9 @@ async def test_rename_session_no_selection_warns():
|
||||
await app.run_action("rename_session")
|
||||
await pilot.pause()
|
||||
|
||||
assert any(
|
||||
kw.get("severity") == "warning" for _, kw in notify_calls
|
||||
), f"Expected warning notification, got: {notify_calls}"
|
||||
assert any(kw.get("severity") == "warning" for _, kw in notify_calls), (
|
||||
f"Expected warning notification, got: {notify_calls}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -1128,5 +1185,3 @@ async def test_session_list_jk_navigation():
|
||||
await pilot.press("k")
|
||||
await pilot.pause()
|
||||
assert sl.get_selected_session_id() == sessions[0].id
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user