| """Tests for the standalone Thread inspector seam.""" |
|
|
| from dataclasses import dataclass, field |
|
|
| import pytest |
|
|
| from arena.thread_inspector import ( |
| AgentRunFailed, |
| FileAccessFailed, |
| ThreadInspector, |
| ThreadNotFound, |
| ) |
|
|
|
|
| class FakeResult: |
| def __init__(self, *, error: str | None = None, entries=None, file_data=None) -> None: |
| self.error = error |
| self.entries = entries |
| self.file_data = file_data |
|
|
|
|
| class FakeBackend: |
| def __init__(self) -> None: |
| self.listed: list[str] = [] |
| self.reads: list[str] = [] |
|
|
| def ls(self, path: str) -> FakeResult: |
| self.listed.append(path) |
| return FakeResult(entries=[{"path": f"{path}/hi.txt", "is_dir": False}]) |
|
|
| def read(self, path: str) -> FakeResult: |
| self.reads.append(path) |
| return FakeResult(file_data={"content": f"contents of {path}"}) |
|
|
|
|
| class FakeAgent: |
| def __init__(self) -> None: |
| self.invocations = 0 |
|
|
| def invoke(self, payload, config): |
| self.invocations += 1 |
| return {"messages": [{"role": "assistant", "content": f"reply #{self.invocations}"}]} |
|
|
|
|
| @dataclass |
| class FakeSession: |
| thread_id: str |
| provider: str |
| backend: FakeBackend |
| agent: FakeAgent |
| messages: list[dict[str, str]] = field(default_factory=list) |
| closed: bool = False |
|
|
| def close(self) -> None: |
| self.closed = True |
|
|
|
|
| def make_inspector(): |
| backend = FakeBackend() |
| agent = FakeAgent() |
|
|
| def factory(thread_id: str | None = None): |
| return FakeSession( |
| thread_id=thread_id or "thread-1", |
| provider="local", |
| backend=backend, |
| agent=agent, |
| ) |
|
|
| return ThreadInspector(session_factory=factory), backend, agent |
|
|
|
|
| def test_inspector_create_thread_records_session() -> None: |
| inspector, _, _ = make_inspector() |
| response = inspector.create_thread() |
|
|
| assert response.thread_id == "thread-1" |
| assert response.sandbox_provider == "local" |
| assert "thread-1" in inspector.sessions |
|
|
|
|
| def test_inspector_send_message_appends_user_and_assistant_turns() -> None: |
| inspector, _, _ = make_inspector() |
| inspector.create_thread() |
|
|
| response = inspector.send_message("thread-1", "hello") |
|
|
| session = inspector.sessions["thread-1"] |
| assert session.messages[0]["role"] == "user" |
| assert session.messages[0]["content"] == "hello" |
| assert session.messages[1]["role"] == "assistant" |
| assert response.content == "reply #1" |
|
|
|
|
| def test_inspector_send_message_wraps_agent_failures() -> None: |
| inspector, _, _ = make_inspector() |
| inspector.create_thread() |
|
|
| original_invoke = inspector.sessions["thread-1"].agent.invoke |
|
|
| def boom(payload, config): |
| original_invoke(payload, config) |
| raise RuntimeError("kaboom") |
|
|
| inspector.sessions["thread-1"].agent.invoke = boom |
|
|
| with pytest.raises(AgentRunFailed): |
| inspector.send_message("thread-1", "hello") |
|
|
|
|
| def test_inspector_list_files_uses_backend_path() -> None: |
| inspector, backend, _ = make_inspector() |
| inspector.create_thread() |
|
|
| entries = inspector.list_files("thread-1", path="/work") |
|
|
| assert [entry.path for entry in entries] == ["/work/hi.txt"] |
| assert backend.listed == ["/work"] |
|
|
|
|
| def test_inspector_read_file_returns_decoded_content() -> None: |
| inspector, backend, _ = make_inspector() |
| inspector.create_thread() |
|
|
| payload = inspector.read_file("thread-1", "/work/hi.txt") |
|
|
| assert payload == {"path": "/work/hi.txt", "content": "contents of /work/hi.txt"} |
| assert backend.reads == ["/work/hi.txt"] |
|
|
|
|
| def test_inspector_list_files_wraps_backend_error() -> None: |
| inspector, _, _ = make_inspector() |
| inspector.create_thread() |
|
|
| inspector.sessions["thread-1"].backend.ls = lambda path: FakeResult(error="nope") |
|
|
| with pytest.raises(FileAccessFailed): |
| inspector.list_files("thread-1", path="/work") |
|
|
|
|
| def test_inspector_delete_thread_drops_session_and_closes_backend() -> None: |
| inspector, _, _ = make_inspector() |
| inspector.create_thread() |
| session = inspector.sessions["thread-1"] |
|
|
| result = inspector.delete_thread("thread-1") |
|
|
| assert result == {"deleted": True} |
| assert "thread-1" not in inspector.sessions |
| assert session.closed is True |
|
|
|
|
| def test_inspector_unknown_thread_raises_thread_not_found() -> None: |
| inspector, _, _ = make_inspector() |
|
|
| with pytest.raises(ThreadNotFound): |
| inspector.send_message("missing", "hello") |
| with pytest.raises(ThreadNotFound): |
| inspector.list_files("missing", path="/") |
| with pytest.raises(ThreadNotFound): |
| inspector.read_file("missing", "/x") |
| with pytest.raises(ThreadNotFound): |
| inspector.delete_thread("missing") |
|
|