135 lines
5.4 KiB
Python
135 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from claire.rclaude import Rclaude, RclaudeError
|
|
|
|
|
|
def _fake_runner(stdout: str = "", stderr: str = "", returncode: int = 0):
|
|
def runner(*args, **kwargs):
|
|
return subprocess.CompletedProcess(
|
|
args=args[0] if args else [], returncode=returncode, stdout=stdout, stderr=stderr
|
|
)
|
|
|
|
return runner
|
|
|
|
|
|
def test_rclaude_raises_when_binary_missing(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr("shutil.which", lambda _: None)
|
|
rcl = Rclaude(binary="rclaude")
|
|
with pytest.raises(RclaudeError, match="not found on PATH"):
|
|
rcl.version()
|
|
|
|
|
|
def test_rclaude_raises_on_nonzero_exit(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/rclaude")
|
|
rcl = Rclaude(
|
|
binary="rclaude",
|
|
runner=_fake_runner(stderr="boom", returncode=1),
|
|
)
|
|
with pytest.raises(RclaudeError, match="exited 1"):
|
|
rcl.version()
|
|
|
|
|
|
def test_rclaude_list_tmux_parses_tsv(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/rclaude")
|
|
output = (
|
|
"local\ttmux\tclaude-natalie-foo-1715964800\t1 windows (created ...)\n"
|
|
"apricot\ttmux\tclaude-natalie-bar-1715964900\t2 windows (created ...)\n"
|
|
)
|
|
rcl = Rclaude(runner=_fake_runner(stdout=output))
|
|
rows = rcl.list_tmux()
|
|
assert len(rows) == 2
|
|
assert rows[0].host == "local"
|
|
assert rows[0].session_name == "claude-natalie-foo-1715964800"
|
|
assert rows[0].detail.startswith("1 windows")
|
|
assert rows[1].host == "apricot"
|
|
|
|
|
|
def test_rclaude_list_sessions_parses_tsv(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/rclaude")
|
|
# `rclaude list sessions --tsv` interleaves tmux rows with session rows;
|
|
# the wrapper should skip the tmux ones.
|
|
output = (
|
|
"local\ttmux\tclaude-foo\t1 windows\n"
|
|
"local\tsession\t94fc4f45-0160-4fb8-9c23-475a0c63c983\thello world\t/Users/x/Code\t1779098754\n"
|
|
"apricot\tsession\tbd306333-eb2d-4b65-8a50-2179b2697756\tworking on rclaude\t/home/u/proj\t1779098693\n"
|
|
)
|
|
rcl = Rclaude(runner=_fake_runner(stdout=output))
|
|
rows = rcl.list_sessions()
|
|
assert len(rows) == 2
|
|
assert rows[0].host == "local"
|
|
assert str(rows[0].uuid) == "94fc4f45-0160-4fb8-9c23-475a0c63c983"
|
|
assert rows[0].cwd == "/Users/x/Code"
|
|
assert rows[0].mtime_epoch == 1779098754
|
|
assert rows[0].snippet == "hello world"
|
|
assert rows[1].host == "apricot"
|
|
assert rows[1].cwd == "/home/u/proj"
|
|
|
|
|
|
def test_rclaude_triage_parses_tsv(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/rclaude")
|
|
output = (
|
|
"local\ttriage\t94fc4f45-0160-4fb8-9c23-475a0c63c983\t1\tin_progress"
|
|
"\tMulti-agent setup\tResume #31\t/Users/x/Code\t1779098754\n"
|
|
"apricot\ttriage\t41daf63a-245b-4194-b9ee-b704f833400f\t2\tblocked"
|
|
"\tWrong branch\tUpdate config\t/home/u/proj\t1779098796\n"
|
|
)
|
|
rcl = Rclaude(runner=_fake_runner(stdout=output))
|
|
rows = rcl.triage()
|
|
assert len(rows) == 2
|
|
assert rows[0].host == "local"
|
|
assert str(rows[0].uuid) == "94fc4f45-0160-4fb8-9c23-475a0c63c983"
|
|
assert rows[0].priority == 1
|
|
assert rows[0].status == "in_progress"
|
|
assert rows[0].summary == "Multi-agent setup"
|
|
assert rows[0].next_action == "Resume #31"
|
|
assert rows[0].cwd == "/Users/x/Code"
|
|
assert rows[0].mtime_epoch == 1779098754
|
|
assert rows[1].host == "apricot"
|
|
assert rows[1].priority == 2
|
|
|
|
|
|
def test_rclaude_triage_passes_flags(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/rclaude")
|
|
captured_args: list[list[str]] = []
|
|
|
|
def runner(args, **kwargs):
|
|
captured_args.append(list(args))
|
|
return subprocess.CompletedProcess(args=args, returncode=0, stdout="", stderr="")
|
|
|
|
rcl = Rclaude(runner=runner)
|
|
rcl.triage(refresh=True, limit=5)
|
|
assert captured_args == [["rclaude", "triage", "--tsv", "--refresh", "--limit", "5"]]
|
|
|
|
|
|
def test_spawn_strips_resume_env_vars(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""`spawn` must never inherit RCLAUDE_RESUME_ID from the environment.
|
|
|
|
Guards the 2026-05-20 regression: `claire web` launched from inside a
|
|
`rclaude resume`-spawned shell carried RCLAUDE_RESUME_ID in os.environ,
|
|
so rclaude resumed an unrelated stale session instead of spawning a
|
|
fresh orchestrator — and discovery then timed out.
|
|
"""
|
|
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/rclaude")
|
|
monkeypatch.setenv("RCLAUDE_RESUME_ID", "bd306333-eb2d-4b65-8a50-2179b2697756")
|
|
monkeypatch.setenv("RCLAUDE_RESUME", "1")
|
|
captured_env: list[dict[str, str] | None] = []
|
|
|
|
def runner(args, **kwargs):
|
|
captured_env.append(kwargs.get("env"))
|
|
return subprocess.CompletedProcess(
|
|
args=args, returncode=0, stdout="claude-natalie-proj-1779326655\n", stderr=""
|
|
)
|
|
|
|
rcl = Rclaude(runner=runner)
|
|
name = rcl.spawn(host="apricot", cwd="/var/home/lilith/Code/@projects/@claire")
|
|
assert name == "claude-natalie-proj-1779326655"
|
|
env = captured_env[0]
|
|
assert env is not None
|
|
# Resume directives stripped; spawn's own RCLAUDE_DETACHED kept.
|
|
assert "RCLAUDE_RESUME_ID" not in env
|
|
assert "RCLAUDE_RESUME" not in env
|
|
assert env["RCLAUDE_DETACHED"] == "1"
|