232 lines
7.2 KiB
Python
232 lines
7.2 KiB
Python
"""Tests for `--resume latest` and `--in DIR` launch sugar.
|
|
|
|
`hermes --tui --resume latest --in ./dir` (and the classic-CLI equivalents)
|
|
resolve "latest" through the same workspace-scoped MRU lookup as `-c`, with
|
|
`--in` re-homing the process before any session resolution happens.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from argparse import Namespace
|
|
|
|
import pytest
|
|
|
|
|
|
def _args(**overrides):
|
|
base = {
|
|
"cli": False,
|
|
"continue_last": None,
|
|
"in_dir": None,
|
|
"model": None,
|
|
"no_restore_cwd": False,
|
|
"provider": None,
|
|
"query": None,
|
|
"resume": None,
|
|
"safe_mode": False,
|
|
"toolsets": None,
|
|
"tui": True,
|
|
"tui_dev": False,
|
|
"worktree": False,
|
|
}
|
|
base.update(overrides)
|
|
return Namespace(**base)
|
|
|
|
|
|
@pytest.fixture
|
|
def main_mod(monkeypatch):
|
|
import hermes_cli.main as mod
|
|
|
|
monkeypatch.setattr(mod, "_has_any_provider_configured", lambda: True)
|
|
monkeypatch.setattr(mod, "_sync_bundled_skills_for_startup", lambda: False)
|
|
monkeypatch.setattr(mod, "_pin_kanban_board_env", lambda: None)
|
|
return mod
|
|
|
|
|
|
@pytest.fixture
|
|
def launched(main_mod, monkeypatch):
|
|
"""Capture the _launch_tui call instead of exec'ing Node."""
|
|
captured = {}
|
|
|
|
def fake_launch(resume_session_id=None, **kwargs):
|
|
captured["resume"] = resume_session_id
|
|
captured.update(kwargs)
|
|
raise SystemExit(0)
|
|
|
|
monkeypatch.setattr(main_mod, "_launch_tui", fake_launch)
|
|
return captured
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# argparse surface
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_top_level_parser_accepts_in_and_resume_latest():
|
|
from hermes_cli._parser import build_top_level_parser
|
|
|
|
parser, _subparsers, _chat = build_top_level_parser()
|
|
args = parser.parse_args(["--tui", "--resume", "latest", "--in", "./dir"])
|
|
assert args.tui is True
|
|
assert args.resume == "latest"
|
|
assert args.in_dir == "./dir"
|
|
|
|
|
|
def test_chat_subparser_accepts_in_flag():
|
|
from hermes_cli._parser import build_top_level_parser
|
|
|
|
parser, _subparsers, _chat = build_top_level_parser()
|
|
args = parser.parse_args(["chat", "--in", "/tmp", "--resume", "latest"])
|
|
assert args.in_dir == "/tmp"
|
|
assert args.resume == "latest"
|
|
|
|
|
|
def test_top_level_in_value_not_mistaken_for_subcommand(monkeypatch):
|
|
# `hermes --in chat` — "chat" is the flag's value, not the subcommand.
|
|
import sys
|
|
|
|
import hermes_cli.main as mod
|
|
|
|
monkeypatch.setattr(sys, "argv", ["hermes", "--in", "chat", "--resume", "latest"])
|
|
assert mod._first_positional_argv() is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# --resume latest resolution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_resume_latest_resolves_to_mru_session(main_mod, launched, monkeypatch):
|
|
monkeypatch.setattr(
|
|
main_mod, "_resolve_last_session", lambda source="cli": "20260807_120000_abc123"
|
|
)
|
|
# Keyword must NOT fall through to title resolution.
|
|
monkeypatch.setattr(
|
|
main_mod,
|
|
"_resolve_session_by_name_or_id",
|
|
lambda val: val if val != "latest" else pytest.fail("'latest' hit title resolution"),
|
|
)
|
|
|
|
with pytest.raises(SystemExit) as exc:
|
|
main_mod.cmd_chat(_args(resume="latest"))
|
|
assert exc.value.code == 0
|
|
assert launched["resume"] == "20260807_120000_abc123"
|
|
|
|
|
|
def test_resume_latest_tui_falls_back_to_cli_source(main_mod, launched, monkeypatch):
|
|
calls = []
|
|
|
|
def fake_resolve(source="cli"):
|
|
calls.append(source)
|
|
return "cli_session_1" if source == "cli" else None
|
|
|
|
monkeypatch.setattr(main_mod, "_resolve_last_session", fake_resolve)
|
|
monkeypatch.setattr(main_mod, "_resolve_session_by_name_or_id", lambda v: v)
|
|
|
|
with pytest.raises(SystemExit) as exc:
|
|
main_mod.cmd_chat(_args(resume="latest"))
|
|
assert exc.value.code == 0
|
|
assert calls == ["tui", "cli"]
|
|
assert launched["resume"] == "cli_session_1"
|
|
|
|
|
|
def test_resume_latest_is_case_insensitive(main_mod, launched, monkeypatch):
|
|
monkeypatch.setattr(main_mod, "_resolve_last_session", lambda source="cli": "sess_1")
|
|
monkeypatch.setattr(main_mod, "_resolve_session_by_name_or_id", lambda v: v)
|
|
|
|
with pytest.raises(SystemExit):
|
|
main_mod.cmd_chat(_args(resume="Latest"))
|
|
assert launched["resume"] == "sess_1"
|
|
|
|
|
|
def test_resume_latest_no_sessions_exits_with_error(main_mod, monkeypatch, capsys):
|
|
monkeypatch.setattr(main_mod, "_resolve_last_session", lambda source="cli": None)
|
|
|
|
with pytest.raises(SystemExit) as exc:
|
|
main_mod.cmd_chat(_args(resume="latest"))
|
|
assert exc.value.code == 1
|
|
out = capsys.readouterr().out
|
|
assert "No previous TUI session found" in out
|
|
|
|
|
|
def test_resume_real_id_untouched_by_latest_keyword(main_mod, launched, monkeypatch):
|
|
monkeypatch.setattr(
|
|
main_mod,
|
|
"_resolve_last_session",
|
|
lambda source="cli": pytest.fail("MRU lookup must not run for explicit IDs"),
|
|
)
|
|
monkeypatch.setattr(main_mod, "_resolve_session_by_name_or_id", lambda v: v)
|
|
|
|
with pytest.raises(SystemExit):
|
|
main_mod.cmd_chat(_args(resume="20260807_120000_abc123"))
|
|
assert launched["resume"] == "20260807_120000_abc123"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# --in DIR
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_in_dir_chdirs_before_session_resolution(main_mod, launched, monkeypatch, tmp_path):
|
|
import os
|
|
|
|
target = tmp_path / "projdir"
|
|
target.mkdir()
|
|
start = os.getcwd()
|
|
seen_cwd = {}
|
|
|
|
def fake_resolve(source="cli"):
|
|
seen_cwd["at_resolve"] = os.getcwd()
|
|
return "sess_scoped"
|
|
|
|
monkeypatch.setattr(main_mod, "_resolve_last_session", fake_resolve)
|
|
monkeypatch.setattr(main_mod, "_resolve_session_by_name_or_id", lambda v: v)
|
|
|
|
try:
|
|
with pytest.raises(SystemExit):
|
|
main_mod.cmd_chat(_args(resume="latest", in_dir=str(target)))
|
|
finally:
|
|
os.chdir(start)
|
|
|
|
assert seen_cwd["at_resolve"] == str(target.resolve())
|
|
assert launched["resume"] == "sess_scoped"
|
|
|
|
|
|
def test_in_dir_sets_no_restore_cwd(main_mod, launched, monkeypatch, tmp_path):
|
|
import os
|
|
|
|
target = tmp_path / "pin-here"
|
|
target.mkdir()
|
|
start = os.getcwd()
|
|
|
|
args = _args(resume=None, in_dir=str(target))
|
|
try:
|
|
with pytest.raises(SystemExit):
|
|
main_mod.cmd_chat(args)
|
|
finally:
|
|
os.chdir(start)
|
|
|
|
assert args.no_restore_cwd is True
|
|
|
|
|
|
def test_in_dir_missing_directory_exits(main_mod, monkeypatch, tmp_path, capsys):
|
|
with pytest.raises(SystemExit) as exc:
|
|
main_mod.cmd_chat(_args(in_dir=str(tmp_path / "nope")))
|
|
assert exc.value.code == 1
|
|
assert "--in directory not found" in capsys.readouterr().out
|
|
|
|
|
|
def test_in_dir_expands_user_home(main_mod, launched, monkeypatch, tmp_path):
|
|
import os
|
|
|
|
home = tmp_path / "home"
|
|
(home / "proj").mkdir(parents=True)
|
|
monkeypatch.setenv("HOME", str(home))
|
|
start = os.getcwd()
|
|
|
|
try:
|
|
with pytest.raises(SystemExit):
|
|
main_mod.cmd_chat(_args(in_dir="~/proj"))
|
|
assert os.getcwd() == str((home / "proj").resolve())
|
|
finally:
|
|
os.chdir(start)
|