mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
399 lines
12 KiB
Python
399 lines
12 KiB
Python
"""roboco.exceptions coverage — every concrete exception's __init__ path.
|
|
|
|
Exercises every `__init__` branch (with and without optional `details`)
|
|
and verifies `to_dict` shape on the base class. Pure construction tests —
|
|
no DB, no fixtures.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import uuid4
|
|
|
|
from roboco.exceptions import (
|
|
AgentBusyError,
|
|
AgentError,
|
|
AgentNotAvailableError,
|
|
AlreadyExistsError,
|
|
AuthenticationError,
|
|
ChannelAccessDeniedError,
|
|
ChannelError,
|
|
DatabaseError,
|
|
GitCommandError,
|
|
GitError,
|
|
GitTimeoutError,
|
|
InvalidStateError,
|
|
LLMError,
|
|
NotFoundError,
|
|
NotificationError,
|
|
NotificationPermissionError,
|
|
PermissionDeniedError,
|
|
RAGError,
|
|
RobocoError,
|
|
ServiceError,
|
|
SessionClosedError,
|
|
TaskBlockedError,
|
|
TaskClaimError,
|
|
TaskError,
|
|
TaskLifecycleError,
|
|
ValidationError,
|
|
)
|
|
|
|
_TIMEOUT_SECONDS = 30
|
|
|
|
|
|
def test_roboco_error_to_dict_shape() -> None:
|
|
err = RobocoError("boom", code="X", details={"a": 1})
|
|
out = err.to_dict()
|
|
assert out["error"]["code"] == "X"
|
|
assert out["error"]["message"] == "boom"
|
|
assert out["error"]["details"] == {"a": 1}
|
|
|
|
|
|
def test_roboco_error_default_details_is_empty_dict() -> None:
|
|
err = RobocoError("no details")
|
|
assert err.details == {}
|
|
|
|
|
|
def test_not_found_error_with_details() -> None:
|
|
rid = uuid4()
|
|
err = NotFoundError("Task", rid, details={"hint": "check spelling"})
|
|
assert err.code == "NOT_FOUND"
|
|
assert err.details["hint"] == "check spelling"
|
|
assert err.details["resource_id"] == str(rid)
|
|
|
|
|
|
def test_already_exists_error_with_details() -> None:
|
|
err = AlreadyExistsError("Project", "myproj", details={"existing_id": "abc"})
|
|
assert err.code == "ALREADY_EXISTS"
|
|
assert err.details["existing_id"] == "abc"
|
|
|
|
|
|
def test_validation_error_with_field_and_value() -> None:
|
|
err = ValidationError("Invalid", field="email", value="not-an-email")
|
|
assert err.code == "VALIDATION_ERROR"
|
|
assert err.details["field"] == "email"
|
|
assert err.details["value"] == "not-an-email"
|
|
|
|
|
|
def test_validation_error_with_none_value() -> None:
|
|
err = ValidationError("Required", field="name")
|
|
assert err.details["value"] is None
|
|
|
|
|
|
def test_validation_error_with_extra_details() -> None:
|
|
err = ValidationError("Bad", field="x", value=1, details={"extra": "yes"})
|
|
assert err.details["extra"] == "yes"
|
|
|
|
|
|
def test_invalid_state_error_with_allowed_states() -> None:
|
|
err = InvalidStateError(
|
|
current_state="closed", operation="reopen", allowed_states=["open"]
|
|
)
|
|
assert err.code == "INVALID_STATE"
|
|
assert "allowed from: open" in err.message
|
|
|
|
|
|
def test_invalid_state_error_no_allowed_states() -> None:
|
|
err = InvalidStateError("done", "modify")
|
|
assert err.code == "INVALID_STATE"
|
|
assert "allowed from" not in err.message
|
|
|
|
|
|
def test_invalid_state_error_with_extra_details() -> None:
|
|
err = InvalidStateError("a", "b", details={"x": 1})
|
|
assert err.details["x"] == 1
|
|
|
|
|
|
def test_permission_denied_error_with_resource() -> None:
|
|
err = PermissionDeniedError(
|
|
action="delete", resource="task/123", agent_id="be-dev-1"
|
|
)
|
|
assert "on task/123" in err.message
|
|
assert err.details["agent_id"] == "be-dev-1"
|
|
|
|
|
|
def test_permission_denied_error_no_resource() -> None:
|
|
err = PermissionDeniedError(action="cancel")
|
|
assert "Permission denied: cancel" in err.message
|
|
assert err.details["agent_id"] is None
|
|
|
|
|
|
def test_permission_denied_error_with_uuid_agent() -> None:
|
|
aid = uuid4()
|
|
err = PermissionDeniedError(action="x", agent_id=aid)
|
|
assert err.details["agent_id"] == str(aid)
|
|
|
|
|
|
def test_permission_denied_error_with_extra_details() -> None:
|
|
err = PermissionDeniedError(action="x", details={"extra": "yes"})
|
|
assert err.details["extra"] == "yes"
|
|
|
|
|
|
def test_authentication_error_default_message() -> None:
|
|
err = AuthenticationError()
|
|
assert err.code == "AUTHENTICATION_REQUIRED"
|
|
assert err.message == "Authentication required"
|
|
|
|
|
|
def test_authentication_error_with_custom_message() -> None:
|
|
err = AuthenticationError("Token expired")
|
|
assert err.message == "Token expired"
|
|
|
|
|
|
def test_task_error_with_uuid_task_id() -> None:
|
|
tid = uuid4()
|
|
err = TaskError("oops", task_id=tid)
|
|
assert err.details["task_id"] == str(tid)
|
|
|
|
|
|
def test_task_error_no_task_id() -> None:
|
|
err = TaskError("oops")
|
|
assert err.details["task_id"] is None
|
|
|
|
|
|
def test_task_error_with_extra_details() -> None:
|
|
err = TaskError("x", task_id="t1", details={"extra": "y"})
|
|
assert err.details["extra"] == "y"
|
|
|
|
|
|
def test_task_lifecycle_error_default_message() -> None:
|
|
err = TaskLifecycleError("pending", "claimed")
|
|
assert err.code == "TASK_LIFECYCLE_ERROR"
|
|
assert "pending" in err.message
|
|
assert "claimed" in err.message
|
|
|
|
|
|
def test_task_lifecycle_error_with_valid_transitions() -> None:
|
|
err = TaskLifecycleError(
|
|
"pending", "claimed", valid_transitions=["pending->in_progress"]
|
|
)
|
|
assert "pending->in_progress" in err.message
|
|
|
|
|
|
def test_task_lifecycle_error_uses_hint_when_known() -> None:
|
|
"""The (claimed, awaiting_documentation) pair has a transition hint."""
|
|
err = TaskLifecycleError("claimed", "awaiting_documentation")
|
|
assert "i_will_work_on" in err.message
|
|
|
|
|
|
def test_task_lifecycle_error_with_explicit_message() -> None:
|
|
err = TaskLifecycleError("a", "b", message="Custom")
|
|
assert err.message == "Custom"
|
|
|
|
|
|
def test_task_lifecycle_error_attaches_state_attrs() -> None:
|
|
err = TaskLifecycleError("pending", "claimed")
|
|
assert err.current_status == "pending"
|
|
assert err.target_status == "claimed"
|
|
|
|
|
|
def test_task_lifecycle_error_extra_kwargs() -> None:
|
|
"""Extra kwargs end up in details."""
|
|
err = TaskLifecycleError("a", "b", extra="present")
|
|
assert err.details["extra"] == "present"
|
|
|
|
|
|
def test_task_blocked_error() -> None:
|
|
tid = uuid4()
|
|
blockers = [uuid4(), uuid4()]
|
|
err = TaskBlockedError(task_id=tid, blocking_task_ids=blockers)
|
|
assert err.code == "TASK_BLOCKED"
|
|
assert len(err.details["blocking_task_ids"]) == len(blockers)
|
|
|
|
|
|
def test_task_blocked_error_with_extra_details() -> None:
|
|
err = TaskBlockedError(
|
|
task_id="t1", blocking_task_ids=["a"], details={"why": "tests"}
|
|
)
|
|
assert err.details["why"] == "tests"
|
|
|
|
|
|
def test_task_claim_error() -> None:
|
|
err = TaskClaimError(task_id="t1", reason="role mismatch")
|
|
assert err.code == "TASK_CLAIM_ERROR"
|
|
assert err.details["reason"] == "role mismatch"
|
|
|
|
|
|
def test_task_claim_error_with_extra_details() -> None:
|
|
err = TaskClaimError(task_id="t1", reason="x", details={"hint": "y"})
|
|
assert err.details["hint"] == "y"
|
|
|
|
|
|
def test_agent_error_with_uuid() -> None:
|
|
aid = uuid4()
|
|
err = AgentError("oops", agent_id=aid)
|
|
assert err.details["agent_id"] == str(aid)
|
|
|
|
|
|
def test_agent_error_no_agent_id() -> None:
|
|
err = AgentError("oops")
|
|
assert err.details["agent_id"] is None
|
|
|
|
|
|
def test_agent_error_with_extra_details() -> None:
|
|
err = AgentError("x", agent_id="a1", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_agent_not_available_error() -> None:
|
|
err = AgentNotAvailableError(agent_id="a1", status="offline")
|
|
assert err.code == "AGENT_NOT_AVAILABLE"
|
|
assert err.details["status"] == "offline"
|
|
|
|
|
|
def test_agent_not_available_error_with_details() -> None:
|
|
err = AgentNotAvailableError(
|
|
agent_id="a1", status="offline", details={"why": "down"}
|
|
)
|
|
assert err.details["why"] == "down"
|
|
|
|
|
|
def test_agent_busy_error() -> None:
|
|
err = AgentBusyError(agent_id="a1", current_task_id="t1")
|
|
assert err.code == "AGENT_BUSY"
|
|
assert err.details["current_task_id"] == "t1"
|
|
|
|
|
|
def test_agent_busy_error_with_details() -> None:
|
|
err = AgentBusyError(agent_id="a1", current_task_id="t1", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_channel_error_with_uuid() -> None:
|
|
cid = uuid4()
|
|
err = ChannelError("nope", channel_id=cid)
|
|
assert err.details["channel_id"] == str(cid)
|
|
|
|
|
|
def test_channel_error_no_channel_id() -> None:
|
|
err = ChannelError("nope")
|
|
assert err.details["channel_id"] is None
|
|
|
|
|
|
def test_channel_error_with_extra_details() -> None:
|
|
err = ChannelError("x", channel_id="c1", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_channel_access_denied_error() -> None:
|
|
err = ChannelAccessDeniedError(channel_id="c1", agent_id="a1", access_type="write")
|
|
assert err.code == "CHANNEL_ACCESS_DENIED"
|
|
assert err.details["access_type"] == "write"
|
|
|
|
|
|
def test_channel_access_denied_error_default_access_type() -> None:
|
|
err = ChannelAccessDeniedError(channel_id="c1", agent_id="a1")
|
|
assert err.details["access_type"] == "read"
|
|
|
|
|
|
def test_channel_access_denied_error_with_details() -> None:
|
|
err = ChannelAccessDeniedError(channel_id="c1", agent_id="a1", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_session_closed_error_default_reason() -> None:
|
|
sid = uuid4()
|
|
err = SessionClosedError(session_id=sid)
|
|
assert err.code == "SESSION_CLOSED"
|
|
assert err.details["session_id"] == str(sid)
|
|
|
|
|
|
def test_session_closed_error_custom_reason() -> None:
|
|
err = SessionClosedError(session_id="s1", reason="Timeout")
|
|
assert err.message == "Timeout"
|
|
|
|
|
|
def test_session_closed_error_with_details() -> None:
|
|
err = SessionClosedError(session_id="s1", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_notification_error_basic() -> None:
|
|
err = NotificationError("nope")
|
|
assert err.code == "NOTIFICATION_ERROR"
|
|
assert err.message == "nope"
|
|
|
|
|
|
def test_notification_error_with_custom_code() -> None:
|
|
err = NotificationError("nope", code="CUSTOM")
|
|
assert err.code == "CUSTOM"
|
|
|
|
|
|
def test_notification_permission_error() -> None:
|
|
err = NotificationPermissionError(agent_id="a1", agent_role="developer")
|
|
assert err.code == "NOTIFICATION_PERMISSION_DENIED"
|
|
assert err.details["agent_role"] == "developer"
|
|
|
|
|
|
def test_notification_permission_error_with_details() -> None:
|
|
err = NotificationPermissionError(
|
|
agent_id="a1", agent_role="developer", details={"k": "v"}
|
|
)
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_service_error_basic() -> None:
|
|
err = ServiceError(service="redis", message="connection refused")
|
|
assert err.code == "SERVICE_ERROR"
|
|
assert "redis" in err.message
|
|
|
|
|
|
def test_service_error_with_extra_details() -> None:
|
|
err = ServiceError(service="r", message="x", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_database_error_basic() -> None:
|
|
err = DatabaseError("query failed", operation="select")
|
|
assert err.code == "SERVICE_ERROR"
|
|
assert err.details["operation"] == "select"
|
|
|
|
|
|
def test_database_error_with_details() -> None:
|
|
err = DatabaseError("x", operation="select", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_llm_error_basic() -> None:
|
|
err = LLMError("rate limited", model="claude-haiku-4-5")
|
|
assert err.details["model"] == "claude-haiku-4-5"
|
|
|
|
|
|
def test_llm_error_with_details() -> None:
|
|
err = LLMError("x", model="m", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_rag_error_basic() -> None:
|
|
err = RAGError("indexing failed", operation="index")
|
|
assert err.details["operation"] == "index"
|
|
|
|
|
|
def test_rag_error_with_details() -> None:
|
|
err = RAGError("x", operation="search", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_git_error_basic() -> None:
|
|
err = GitError("conflict")
|
|
assert err.code == "SERVICE_ERROR"
|
|
|
|
|
|
def test_git_error_with_details() -> None:
|
|
err = GitError("x", details={"k": "v"})
|
|
assert err.details["k"] == "v"
|
|
|
|
|
|
def test_git_command_error() -> None:
|
|
err = GitCommandError(command="git push", stderr="rejected")
|
|
assert err.command == "git push"
|
|
assert err.stderr == "rejected"
|
|
assert err.details["command"] == "git push"
|
|
|
|
|
|
def test_git_timeout_error() -> None:
|
|
err = GitTimeoutError(command="git fetch", timeout=_TIMEOUT_SECONDS)
|
|
assert err.command == "git fetch"
|
|
assert err.timeout == _TIMEOUT_SECONDS
|
|
assert err.details["timeout"] == _TIMEOUT_SECONDS
|