Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/services/sprite_agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import logging
import re
import secrets
import shutil
from collections.abc import AsyncIterator
from uuid import UUID

Expand Down Expand Up @@ -594,6 +595,13 @@ async def run_chat(
raise NeedsAuth
except agent_auth.ProviderNotConfigured:
raise RuntimeError("cloud agent is not configured")
# Fail loud before spawning: a missing CLI produces a cryptic FileNotFoundError
# deep in asyncio that doesn't tell the operator what to install.
if not shutil.which(auth.harness.bin):
raise RuntimeError(
f"agent CLI '{auth.harness.bin}' not found on PATH — "
f"install it on the worker host so the agent can run turns"
)
async with _TurnLock(session_id):
history = await _load_history(owner_user_id, session_id, user_id)
await memory_service.push_event(
Expand Down
6 changes: 6 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ async def fake_write_file(sprite, abs_path, contents):
monkeypatch.setattr(sprite_service, "acquire", fake_acquire)
monkeypatch.setattr(sprite_service, "exec_stream", fake_exec_stream)
monkeypatch.setattr(sprite_service, "write_file", fake_write_file)
# The CLI-missing guard in run_chat checks shutil.which; CI runners don't
# have the harness CLI installed, so make it look present by default.
# Tests that need the missing-CLI path override this on their own monkeypatch.
import shutil as _shutil

monkeypatch.setattr(_shutil, "which", lambda bin: f"/usr/local/bin/{bin}")
fake_redis = FakeRedis()
monkeypatch.setattr(sprite_agent_service, "_get_redis", lambda: fake_redis)
monkeypatch.setattr(settings, "ANTHROPIC_API_KEY", "sk-ant-test-key")
Expand Down
24 changes: 24 additions & 0 deletions backend/tests/test_curator.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,30 @@ async def boom(user_id, curated_through, now):
assert row["last_run_outcome"] == "failed"


@pytest.mark.asyncio
async def test_missing_cli_raises_clear_error(client: AsyncClient, _db_pool, monkeypatch):
"""When the harness CLI isn't installed on the worker, fail loud with a
message that names the binary and says to install it — not a cryptic
FileNotFoundError from deep in asyncio. Reproduces issue #780."""
import shutil

from backend.tasks.agent_schedules import _run_curator_now

key, uid = await _register(client)
curator = await agent_service.get_or_create_curator(uid)

monkeypatch.setattr(shutil, "which", lambda bin: None)

with pytest.raises(RuntimeError, match="claude"):
await _run_curator_now(UUID(curator["id"]))

row = await _db_pool.fetchrow(
"SELECT last_run_error FROM agents WHERE id = $1", UUID(curator["id"])
)
assert "claude" in row["last_run_error"]
assert "PATH" in row["last_run_error"]


@pytest.mark.asyncio
async def test_recompute_409_when_nothing_changed(client: AsyncClient, _db_pool):
key, uid = await _register(client)
Expand Down
Loading