from unittest.mock import patch from click.testing import CliRunner from hqt import cli, sandbox from hqt.config import Settings def _which(found: set[str]): return lambda name: f"/usr/bin/{name}" if name in found else None def test_doctor_reports_bwrap_present(): """Test that doctor reports bubblewrap when present.""" # tmux is discovered via cli.shutil.which; bwrap availability comes from the # shared sandbox.is_available() helper (the single source of truth). with ( patch.object(cli.shutil, "which", side_effect=_which({"tmux", "bwrap"})), patch.object(sandbox, "is_available", return_value=True), ): result = CliRunner().invoke(cli.main, ["doctor"]) assert result.exit_code == 0 assert "bubblewrap: ✓" in result.output def test_doctor_reports_bwrap_missing(): """Test that doctor reports bubblewrap as missing when not present.""" with ( patch.object(cli.shutil, "which", side_effect=_which({"tmux"})), patch.object(sandbox, "is_available", return_value=False), ): result = CliRunner().invoke(cli.main, ["doctor"]) assert result.exit_code == 0 assert "bubblewrap: ✗" in result.output def test_tool_cmd_opens_tool_window(monkeypatch, tmp_path): monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) calls = {} async def fake_for_window(self, window, tool): calls["tool"] = (window, tool) return "@9" monkeypatch.setattr( "hqt.sessions.service.SessionService.open_tool_window_for_window", fake_for_window, ) result = CliRunner().invoke(cli.main, ["tool", "lazygit", "hqt-5"]) assert result.exit_code == 0, result.output assert calls["tool"] == ("hqt-5", "lazygit") def test_tool_cmd_clone_dispatches_to_clone(monkeypatch, tmp_path): monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) calls = {} async def fake_clone(self, window): calls["clone"] = window return "RESULT" monkeypatch.setattr( "hqt.sessions.service.SessionService.clone_session_for_window", fake_clone ) result = CliRunner().invoke(cli.main, ["tool", "clone", "hqt-5"]) assert result.exit_code == 0, result.output assert calls["clone"] == "hqt-5" def test_tool_cmd_stop_dispatches_to_stop(monkeypatch, tmp_path): monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) calls = {} async def fake_stop(self, window): calls["stop"] = window monkeypatch.setattr( "hqt.sessions.service.SessionService.stop_session_for_window", fake_stop ) result = CliRunner().invoke(cli.main, ["tool", "stop", "hqt-5"]) assert result.exit_code == 0, result.output assert calls["stop"] == "hqt-5" def test_tool_cmd_rename_applies_new_nickname(monkeypatch, tmp_path): from types import SimpleNamespace monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) monkeypatch.setattr( "hqt.sessions.service.SessionService.session_id_for_window", lambda self, window: 5, ) monkeypatch.setattr( "hqt.sessions.service.SessionService.get_session", lambda self, sid: SimpleNamespace(nickname="old"), ) # Prefill the prompt with the current nickname; user edits to "new". seen = {} def fake_read(prompt, prefill=""): seen["prefill"] = prefill return "new" monkeypatch.setattr(cli, "_read_line_from_tty", fake_read) calls = {} monkeypatch.setattr( "hqt.sessions.service.SessionService.rename_session", lambda self, sid, nickname: calls.setdefault("args", (sid, nickname)), ) result = CliRunner().invoke(cli.main, ["tool", "rename", "hqt-5"]) assert result.exit_code == 0, result.output assert seen["prefill"] == "old" assert calls["args"] == (5, "new") def test_tool_cmd_rename_aborted_leaves_name(monkeypatch, tmp_path): from types import SimpleNamespace monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) monkeypatch.setattr( "hqt.sessions.service.SessionService.session_id_for_window", lambda self, window: 5, ) monkeypatch.setattr( "hqt.sessions.service.SessionService.get_session", lambda self, sid: SimpleNamespace(nickname="old"), ) monkeypatch.setattr(cli, "_read_line_from_tty", lambda prompt, prefill="": None) calls = {} monkeypatch.setattr( "hqt.sessions.service.SessionService.rename_session", lambda self, sid, nickname: calls.setdefault("args", (sid, nickname)), ) result = CliRunner().invoke(cli.main, ["tool", "rename", "hqt-5"]) assert result.exit_code == 0, result.output assert "args" not in calls # None return = aborted, no DB write def test_tool_cmd_rename_unknown_window_raises(monkeypatch, tmp_path): monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) monkeypatch.setattr( "hqt.sessions.service.SessionService.session_id_for_window", lambda self, window: None, ) result = CliRunner().invoke(cli.main, ["tool", "rename", "hqt-5"]) assert result.exit_code == 1 assert "not an hqt session window" in result.output def test_tool_cmd_reset_windows_dispatches_to_manager(monkeypatch, tmp_path): monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) calls = {} async def fake_reset(self, home_window): calls["home"] = home_window return True monkeypatch.setattr("hqt.tmux.manager.TmuxManager.reset_window_indexes", fake_reset) result = CliRunner().invoke(cli.main, ["tool", "reset-windows", "hqt-5"]) assert result.exit_code == 0, result.output # Compacts against the configured home/TUI window, not the trigger window. assert calls["home"] == Settings(db_path=tmp_path / "t.db").tui_window_name def test_tool_cmd_reset_windows_failure_reports_error(monkeypatch, tmp_path): monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) async def fake_reset(self, home_window): return False monkeypatch.setattr("hqt.tmux.manager.TmuxManager.reset_window_indexes", fake_reset) result = CliRunner().invoke(cli.main, ["tool", "reset-windows", "hqt-5"]) assert result.exit_code == 1 assert "reset window indexes" in result.output def test_tool_cmd_reports_service_error(monkeypatch, tmp_path): from hqt.errors import ServiceError monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) async def boom(self, window, tool): raise ServiceError("'hqt-5' is not an hqt session window") monkeypatch.setattr( "hqt.sessions.service.SessionService.open_tool_window_for_window", boom ) result = CliRunner().invoke(cli.main, ["tool", "nvim", "hqt-5"]) assert result.exit_code == 1 assert "not an hqt session window" in result.output def test_palette_cmd_shows_popup_for_session_window(monkeypatch, tmp_path): monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) monkeypatch.setattr( "hqt.sessions.service.SessionService.session_id_for_window", lambda self, window: 5, ) runs = [] monkeypatch.setattr(cli.subprocess, "run", lambda argv, **kw: runs.append(argv)) result = CliRunner().invoke(cli.main, ["palette", "hqt-5"]) assert result.exit_code == 0, result.output assert len(runs) == 1 argv = runs[0] assert "display-popup" in argv popup_cmd = argv[-1] assert "fzf" in popup_cmd assert "nvim" in popup_cmd and "clone" in popup_cmd assert "stop" in popup_cmd assert "rename" in popup_cmd assert "reset-windows" in popup_cmd # the window is baked into the command for `hqt tool {} ` assert "hqt-5" in popup_cmd def test_palette_cmd_hints_for_non_session_window(monkeypatch, tmp_path): monkeypatch.setattr( "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") ) monkeypatch.setattr( "hqt.sessions.service.SessionService.session_id_for_window", lambda self, window: None, ) runs = [] monkeypatch.setattr(cli.subprocess, "run", lambda argv, **kw: runs.append(argv)) result = CliRunner().invoke(cli.main, ["palette", "nvim"]) assert result.exit_code == 0, result.output assert len(runs) == 1 argv = runs[0] assert "display-message" in argv assert "display-popup" not in argv