Files
2026-07-13 13:12:33 +08:00

348 lines
10 KiB
Python

from __future__ import annotations
import pytest
from opensquilla.agents.registry import AgentRegistry
from opensquilla.gateway.config import GatewayConfig
from opensquilla.gateway.rpc import RpcContext, get_dispatcher
class _FailingModelSelector:
async def list_models_detailed(self):
raise RuntimeError("provider unavailable")
def _ctx(config: GatewayConfig, registry: AgentRegistry) -> RpcContext:
return RpcContext(conn_id="test", config=config, agent_registry=registry)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("params", "accepted_params"),
[
(
{"agentId": "ops", "sessionKey": "agent:ops:main", "timeoutMs": 100},
{"agentId": "ops", "sessionKey": "agent:ops:main", "timeoutMs": 100},
),
(
{"agent_id": "ops", "session_key": "agent:ops:main", "timeout_ms": 100},
{"agentId": "ops", "sessionKey": "agent:ops:main", "timeoutMs": 100},
),
],
)
async def test_agent_wait_reports_runtime_bridge_unavailable_with_compat_params(
params: dict[str, object],
accepted_params: dict[str, object],
) -> None:
result = await get_dispatcher().dispatch(
"r1",
"agent.wait",
params,
RpcContext(conn_id="test", config=GatewayConfig()),
)
assert result.error is not None
assert result.error.code == "agent.unavailable"
assert result.error.details["reason"] == "runtime_bridge_unavailable"
assert result.error.details["acceptedParams"] == accepted_params
assert result.error.details["supportedParams"] == [
"agentId",
"agent_id",
"sessionKey",
"session_key",
"timeoutMs",
"timeout_ms",
]
assert "agents.list" in result.error.details["availableRpcMethods"]
@pytest.mark.asyncio
@pytest.mark.parametrize("params", [{"agentId": 123}, {"sessionKey": " "}])
async def test_agent_wait_rejects_non_string_identifiers(params: dict[str, object]) -> None:
result = await get_dispatcher().dispatch(
"r1",
"agent.wait",
params,
RpcContext(conn_id="test", config=GatewayConfig()),
)
assert result.error is not None
assert result.error.code == "INVALID_REQUEST"
@pytest.mark.asyncio
async def test_agents_rpc_list_uses_config_backed_registry() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
await registry.create_agent(agent_id="ops", model="openai/test")
result = await get_dispatcher().dispatch("r1", "agents.list", {}, _ctx(cfg, registry))
assert result.error is None, result.error
assert [agent["id"] for agent in result.payload["agents"]] == ["main", "ops"]
assert result.payload["agents"][1]["model"] == "openai/test"
@pytest.mark.asyncio
async def test_agents_rpc_list_without_registry_returns_empty() -> None:
result = await get_dispatcher().dispatch(
"r1",
"agents.list",
{},
RpcContext(conn_id="test", config=GatewayConfig()),
)
assert result.error is None, result.error
assert result.payload == {"agents": []}
@pytest.mark.asyncio
async def test_models_rpc_list_without_provider_selector_returns_empty() -> None:
result = await get_dispatcher().dispatch(
"r1",
"models.list",
{},
RpcContext(conn_id="test"),
)
assert result.error is None, result.error
assert result.payload == {"models": [], "errors": []}
@pytest.mark.asyncio
async def test_models_rpc_list_selector_crash_returns_empty() -> None:
# A selector whose list_models_detailed itself raises (as opposed to
# per-provider failures, which it reports in ``errors``) still yields an
# empty envelope rather than an RPC error.
result = await get_dispatcher().dispatch(
"r1",
"models.list",
{},
RpcContext(conn_id="test", provider_selector=_FailingModelSelector()),
)
assert result.error is None, result.error
assert result.payload == {"models": [], "errors": []}
class _DetailedModelSelector:
async def list_models_detailed(self):
from opensquilla.provider.selector import ModelListResult, ProviderListError
from opensquilla.provider.types import ModelInfo
return ModelListResult(
models=[
ModelInfo(provider="ollama", model_id="test-model-good").model_dump()
],
errors=[
ProviderListError(
provider="openrouter",
model_hint="openrouter/test-model-locked",
kind="auth_invalid",
detail="401 invalid api key ***",
)
],
)
@pytest.mark.asyncio
async def test_models_rpc_list_surfaces_classified_provider_errors() -> None:
result = await get_dispatcher().dispatch(
"r1",
"models.list",
{},
RpcContext(conn_id="test", provider_selector=_DetailedModelSelector()),
)
assert result.error is None, result.error
assert [m["id"] for m in result.payload["models"]] == ["test-model-good"]
assert result.payload["errors"] == [
{"provider": "openrouter", "kind": "auth_invalid", "detail": "401 invalid api key ***"}
]
@pytest.mark.asyncio
async def test_models_rpc_list_filters_rows_but_keeps_errors() -> None:
# Filters narrow the rows only; a provider whose listing failed must stay
# visible even when its rows are filtered away.
result = await get_dispatcher().dispatch(
"r1",
"models.list",
{"provider": "openrouter"},
RpcContext(conn_id="test", provider_selector=_DetailedModelSelector()),
)
assert result.error is None, result.error
assert result.payload["models"] == []
assert [e["provider"] for e in result.payload["errors"]] == ["openrouter"]
@pytest.mark.asyncio
async def test_agents_rpc_create_accepts_explicit_id() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
result = await get_dispatcher().dispatch(
"r1",
"agents.create",
{"id": "ops", "name": "Operations", "model": "openai/test"},
_ctx(cfg, registry),
)
assert result.error is None, result.error
assert result.payload["id"] == "ops"
assert result.payload["name"] == "Operations"
assert cfg.agents[0].model == "openai/test"
@pytest.mark.asyncio
async def test_agents_rpc_delete_removes_config_entry() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
await registry.create_agent(agent_id="ops")
result = await get_dispatcher().dispatch(
"r1",
"agents.delete",
{"id": "ops"},
_ctx(cfg, registry),
)
assert result.error is None, result.error
assert result.payload is None
assert cfg.agents == []
@pytest.mark.asyncio
async def test_agents_rpc_create_duplicate_returns_agent_exists_code() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
await registry.create_agent(agent_id="ops")
result = await get_dispatcher().dispatch(
"r1",
"agents.create",
{"id": "ops"},
_ctx(cfg, registry),
)
assert result.error is not None
assert result.error.code == "agent.exists"
assert result.error.details == {"agentId": "ops"}
@pytest.mark.asyncio
async def test_agents_rpc_delete_main_returns_builtin_immutable() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
result = await get_dispatcher().dispatch(
"r1",
"agents.delete",
{"id": "main"},
_ctx(cfg, registry),
)
assert result.error is not None
assert result.error.code == "agent.builtin_immutable"
@pytest.mark.asyncio
async def test_agents_rpc_update_main_returns_builtin_immutable() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
result = await get_dispatcher().dispatch(
"r1",
"agents.update",
{"id": "main", "name": "renamed"},
_ctx(cfg, registry),
)
assert result.error is not None
assert result.error.code == "agent.builtin_immutable"
@pytest.mark.asyncio
async def test_agents_rpc_update_missing_returns_agent_not_found() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
result = await get_dispatcher().dispatch(
"r1",
"agents.update",
{"id": "ghost", "model": "openai/test"},
_ctx(cfg, registry),
)
assert result.error is not None
assert result.error.code == "agent.not_found"
assert result.error.details == {"agentId": "ghost"}
@pytest.mark.asyncio
async def test_agents_rpc_delete_missing_returns_agent_not_found() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
result = await get_dispatcher().dispatch(
"r1",
"agents.delete",
{"id": "ghost"},
_ctx(cfg, registry),
)
assert result.error is not None
assert result.error.code == "agent.not_found"
@pytest.mark.asyncio
async def test_agents_rpc_update_workspace_field_persists() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
await registry.create_agent(agent_id="ops")
result = await get_dispatcher().dispatch(
"r1",
"agents.update",
{"id": "ops", "workspace": "/tmp/ops"},
_ctx(cfg, registry),
)
assert result.error is None, result.error
assert cfg.agents[0].workspace == "/tmp/ops"
@pytest.mark.asyncio
async def test_agents_rpc_update_enabled_toggle_persists() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
await registry.create_agent(agent_id="ops")
result = await get_dispatcher().dispatch(
"r1",
"agents.update",
{"id": "ops", "enabled": False},
_ctx(cfg, registry),
)
assert result.error is None, result.error
assert cfg.agents[0].enabled is False
@pytest.mark.asyncio
async def test_agents_rpc_update_agent_dir_camelcase_persists() -> None:
cfg = GatewayConfig()
registry = AgentRegistry(cfg, persist_changes=False)
await registry.create_agent(agent_id="ops")
result = await get_dispatcher().dispatch(
"r1",
"agents.update",
{"id": "ops", "agentDir": ".opensquilla/ops-dir"},
_ctx(cfg, registry),
)
assert result.error is None, result.error
assert cfg.agents[0].agent_dir == ".opensquilla/ops-dir"