rein-aharness/tests/test_adapter.py
custodian-sync f01b765668 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-09-04:
  - update .custodian-brief.md for rein-aharness

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06ba0-10aa-7ea0-b20a-4f3fac39efe9
2026-09-04 11:00:20 +02:00

168 lines
6.4 KiB
Python

import json
import subprocess
from unittest.mock import MagicMock, patch
import pytest
from llm_connect.exceptions import LLMSubprocessError, LLMTimeoutError
from llm_connect.models import RunConfig
from rein_aharness.adapter import AgenticClaudeCodeAdapter, _is_tool_event
def _fake_proc(lines: list[str], returncode: int = 0, stderr: str = "") -> MagicMock:
proc = MagicMock()
proc.stdin = MagicMock()
proc.stdout = iter(lines)
proc.stderr = MagicMock()
proc.stderr.read.return_value = stderr
proc.wait.return_value = returncode
return proc
def test_is_tool_event_true_for_tool_use() -> None:
event = {"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Read"}]}}
assert _is_tool_event(event) is True
def test_is_tool_event_true_for_tool_result() -> None:
event = {"type": "user", "message": {"content": [{"type": "tool_result", "content": "x"}]}}
assert _is_tool_event(event) is True
def test_is_tool_event_true_for_hook_lifecycle() -> None:
assert _is_tool_event({"type": "system", "subtype": "hook_started"}) is True
assert _is_tool_event({"type": "system", "subtype": "hook_response"}) is True
def test_is_tool_event_false_for_plain_text() -> None:
event = {"type": "assistant", "message": {"content": [{"type": "text", "text": "hi"}]}}
assert _is_tool_event(event) is False
def test_is_tool_event_false_for_init() -> None:
assert _is_tool_event({"type": "system", "subtype": "init"}) is False
def test_build_command_adds_stream_json_when_callback_set(tmp_path) -> None:
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, on_tool_event=lambda e: None)
cmd = adapter._build_command(RunConfig(timeout_seconds=60))
assert "--output-format" in cmd
assert "stream-json" in cmd
assert "--include-hook-events" in cmd
def test_build_command_omits_stream_json_without_callback(tmp_path) -> None:
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path)
cmd = adapter._build_command(RunConfig(timeout_seconds=60))
assert "--output-format" not in cmd
assert "--include-hook-events" not in cmd
def test_execute_streaming_invokes_callback_only_for_tool_events(tmp_path) -> None:
events: list[dict] = []
lines = [
json.dumps({"type": "system", "subtype": "init"}) + "\n",
json.dumps(
{"type": "assistant", "message": {"content": [{"type": "tool_use", "id": "t1", "name": "Read"}]}}
)
+ "\n",
json.dumps(
{"type": "user", "message": {"content": [{"type": "tool_result", "tool_use_id": "t1", "content": "hi"}]}}
)
+ "\n",
json.dumps({"type": "assistant", "message": {"content": [{"type": "text", "text": "Done."}]}}) + "\n",
json.dumps({"type": "result", "subtype": "success"}) + "\n",
]
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, on_tool_event=events.append)
config = RunConfig(timeout_seconds=30)
with patch("rein_aharness.adapter.subprocess.Popen", return_value=_fake_proc(lines)):
response = adapter.execute_prompt("do it", config)
assert response.content == "Done."
assert response.metadata["tool_event_count"] == 2
assert len(events) == 2
assert events[0]["message"]["content"][0]["type"] == "tool_use"
assert events[1]["message"]["content"][0]["type"] == "tool_result"
def test_execute_streaming_raises_on_nonzero_exit(tmp_path) -> None:
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, on_tool_event=lambda e: None)
config = RunConfig(timeout_seconds=30)
proc = _fake_proc([json.dumps({"type": "result", "subtype": "error"}) + "\n"], returncode=1, stderr="boom")
with patch("rein_aharness.adapter.subprocess.Popen", return_value=proc):
with pytest.raises(LLMSubprocessError, match="claude CLI exited"):
adapter.execute_prompt("do it", config)
def test_execute_streaming_raises_timeout(tmp_path) -> None:
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, on_tool_event=lambda e: None)
config = RunConfig(timeout_seconds=1)
proc = _fake_proc([])
proc.wait.side_effect = [subprocess.TimeoutExpired(cmd="claude", timeout=1), 0]
with patch("rein_aharness.adapter.subprocess.Popen", return_value=proc):
with pytest.raises(LLMTimeoutError):
adapter.execute_prompt("do it", config)
proc.kill.assert_called_once()
def test_execute_blocking_path_unchanged_without_callback(tmp_path) -> None:
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path)
config = RunConfig(timeout_seconds=30)
proc = MagicMock()
proc.communicate.return_value = ("plain output", "")
proc.returncode = 0
proc.poll.return_value = 0
with patch("rein_aharness.adapter.subprocess.Popen", return_value=proc) as popen:
response = adapter.execute_prompt("do it", config)
assert response.content == "plain output"
argv = popen.call_args.args[0]
assert "--output-format" not in argv
proc.communicate.assert_called_once()
def test_blocking_cancel_kills_process_and_raises(tmp_path) -> None:
from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled
cancel = ExecutionCancel()
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, cancel=cancel)
config = RunConfig(timeout_seconds=30)
proc = MagicMock()
proc.poll.return_value = None
def _communicate(input=None, timeout=None): # noqa: A002
cancel.cancel("lease-loss")
raise RuntimeError("subprocess interrupted")
proc.communicate.side_effect = _communicate
with patch("rein_aharness.adapter.subprocess.Popen", return_value=proc):
with pytest.raises(ExecutionCancelled, match="lease-loss"):
adapter.execute_prompt("do it", config)
proc.kill.assert_called_once()
def test_streaming_cancel_refuses_nonzero_exit(tmp_path) -> None:
from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled
cancel = ExecutionCancel()
adapter = AgenticClaudeCodeAdapter(
workdir=tmp_path, on_tool_event=lambda e: None, cancel=cancel
)
config = RunConfig(timeout_seconds=30)
proc = _fake_proc([])
proc.poll.return_value = None
def _wait(timeout=None):
cancel.cancel("signal")
return 1
proc.wait.side_effect = _wait
with patch("rein_aharness.adapter.subprocess.Popen", return_value=proc):
with pytest.raises(ExecutionCancelled, match="signal"):
adapter.execute_prompt("do it", config)