feat: add validation for gateway main agent requirement on board mutations
This commit is contained in:
@@ -58,6 +58,22 @@ INCLUDE_SELF_QUERY = Query(default=False)
|
||||
INCLUDE_DONE_QUERY = Query(default=False)
|
||||
PER_BOARD_TASK_LIMIT_QUERY = Query(default=5, ge=0, le=100)
|
||||
AGENT_BOARD_ROLE_TAGS = cast("list[str | Enum]", ["agent-lead", "agent-worker"])
|
||||
_ERR_GATEWAY_MAIN_AGENT_REQUIRED = (
|
||||
"gateway must have a gateway main agent before boards can be created or updated"
|
||||
)
|
||||
|
||||
|
||||
async def _require_gateway_main_agent(session: AsyncSession, gateway: Gateway) -> None:
|
||||
main_agent = (
|
||||
await Agent.objects.filter_by(gateway_id=gateway.id)
|
||||
.filter(col(Agent.board_id).is_(None))
|
||||
.first(session)
|
||||
)
|
||||
if main_agent is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=_ERR_GATEWAY_MAIN_AGENT_REQUIRED,
|
||||
)
|
||||
|
||||
|
||||
async def _require_gateway(
|
||||
@@ -77,6 +93,7 @@ async def _require_gateway(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail="gateway_id is invalid",
|
||||
)
|
||||
await _require_gateway_main_agent(session, gateway)
|
||||
return gateway
|
||||
|
||||
|
||||
@@ -161,6 +178,11 @@ async def _apply_board_update(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail="gateway_id is required",
|
||||
)
|
||||
await _require_gateway(
|
||||
session,
|
||||
board.gateway_id,
|
||||
organization_id=board.organization_id,
|
||||
)
|
||||
board.updated_at = utcnow()
|
||||
return await crud.save(session, board)
|
||||
|
||||
|
||||
144
backend/tests/test_boards_gateway_agent_validation.py
Normal file
144
backend/tests/test_boards_gateway_agent_validation.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# ruff: noqa: S101
|
||||
"""Validation tests for gateway-main-agent requirements on board mutations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.api import boards
|
||||
from app.models.boards import Board
|
||||
from app.models.gateways import Gateway
|
||||
from app.schemas.boards import BoardUpdate
|
||||
|
||||
|
||||
def _gateway(*, organization_id: UUID) -> Gateway:
|
||||
return Gateway(
|
||||
id=uuid4(),
|
||||
organization_id=organization_id,
|
||||
name="Main Gateway",
|
||||
url="ws://gateway.example/ws",
|
||||
workspace_root="/tmp/openclaw",
|
||||
)
|
||||
|
||||
|
||||
class _FakeAgentQuery:
|
||||
def __init__(self, main_agent: object | None) -> None:
|
||||
self._main_agent = main_agent
|
||||
|
||||
def filter(self, *_args: Any, **_kwargs: Any) -> _FakeAgentQuery:
|
||||
return self
|
||||
|
||||
async def first(self, _session: object) -> object | None:
|
||||
return self._main_agent
|
||||
|
||||
|
||||
@dataclass
|
||||
class _FakeAgentObjects:
|
||||
main_agent: object | None
|
||||
last_filter_by: dict[str, object] | None = None
|
||||
|
||||
def filter_by(self, **kwargs: object) -> _FakeAgentQuery:
|
||||
self.last_filter_by = kwargs
|
||||
return _FakeAgentQuery(self.main_agent)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_gateway_rejects_when_gateway_has_no_main_agent(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
organization_id = uuid4()
|
||||
gateway = _gateway(organization_id=organization_id)
|
||||
fake_objects = _FakeAgentObjects(main_agent=None)
|
||||
|
||||
async def _fake_get_by_id(_session: object, _model: object, _gateway_id: object) -> Gateway:
|
||||
return gateway
|
||||
|
||||
monkeypatch.setattr(boards.crud, "get_by_id", _fake_get_by_id)
|
||||
monkeypatch.setattr(boards.Agent, "objects", fake_objects)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await boards._require_gateway(
|
||||
session=object(), # type: ignore[arg-type]
|
||||
gateway_id=gateway.id,
|
||||
organization_id=organization_id,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 422
|
||||
assert "gateway main agent" in str(exc_info.value.detail).lower()
|
||||
assert fake_objects.last_filter_by == {"gateway_id": gateway.id}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_gateway_accepts_when_gateway_has_main_agent(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
organization_id = uuid4()
|
||||
gateway = _gateway(organization_id=organization_id)
|
||||
fake_objects = _FakeAgentObjects(main_agent=object())
|
||||
|
||||
async def _fake_get_by_id(_session: object, _model: object, _gateway_id: object) -> Gateway:
|
||||
return gateway
|
||||
|
||||
monkeypatch.setattr(boards.crud, "get_by_id", _fake_get_by_id)
|
||||
monkeypatch.setattr(boards.Agent, "objects", fake_objects)
|
||||
|
||||
resolved = await boards._require_gateway(
|
||||
session=object(), # type: ignore[arg-type]
|
||||
gateway_id=gateway.id,
|
||||
organization_id=organization_id,
|
||||
)
|
||||
|
||||
assert resolved.id == gateway.id
|
||||
assert fake_objects.last_filter_by == {"gateway_id": gateway.id}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_board_update_validates_current_gateway_main_agent(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
board = Board(
|
||||
id=uuid4(),
|
||||
organization_id=uuid4(),
|
||||
name="Platform",
|
||||
slug="platform",
|
||||
gateway_id=uuid4(),
|
||||
)
|
||||
payload = BoardUpdate(name="Platform X")
|
||||
calls: list[UUID] = []
|
||||
|
||||
async def _fake_require_gateway(
|
||||
_session: object,
|
||||
gateway_id: object,
|
||||
*,
|
||||
organization_id: UUID | None = None,
|
||||
) -> Gateway:
|
||||
_ = organization_id
|
||||
if not isinstance(gateway_id, UUID):
|
||||
raise AssertionError("expected UUID gateway id")
|
||||
calls.append(gateway_id)
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=boards._ERR_GATEWAY_MAIN_AGENT_REQUIRED,
|
||||
)
|
||||
|
||||
async def _fake_save(_session: object, _board: Board) -> Board:
|
||||
return _board
|
||||
|
||||
monkeypatch.setattr(boards, "_require_gateway", _fake_require_gateway)
|
||||
monkeypatch.setattr(boards.crud, "save", _fake_save)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await boards._apply_board_update(
|
||||
payload=payload,
|
||||
session=object(), # type: ignore[arg-type]
|
||||
board=board,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 422
|
||||
assert exc_info.value.detail == boards._ERR_GATEWAY_MAIN_AGENT_REQUIRED
|
||||
assert calls == [board.gateway_id]
|
||||
Reference in New Issue
Block a user