2026-02-10 20:10:36 +05:30
|
|
|
# ruff: noqa: S101
|
|
|
|
|
"""Unit tests for agent deletion behavior."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from uuid import UUID, uuid4
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-02-11 00:00:19 +05:30
|
|
|
import app.services.openclaw.provisioning_db as agent_service
|
2026-02-15 00:45:28 +05:30
|
|
|
from app.models.approvals import Approval
|
2026-02-10 20:10:36 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class _FakeSession:
|
|
|
|
|
committed: int = 0
|
|
|
|
|
deleted: list[object] = field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
def add(self, _value: object) -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def commit(self) -> None:
|
|
|
|
|
self.committed += 1
|
|
|
|
|
|
|
|
|
|
async def delete(self, value: object) -> None:
|
|
|
|
|
self.deleted.append(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class _AgentStub:
|
|
|
|
|
id: UUID
|
|
|
|
|
name: str
|
|
|
|
|
gateway_id: UUID
|
|
|
|
|
board_id: UUID | None = None
|
|
|
|
|
openclaw_session_id: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class _GatewayStub:
|
|
|
|
|
id: UUID
|
|
|
|
|
url: str
|
|
|
|
|
token: str | None
|
|
|
|
|
workspace_root: str
|
2026-02-22 20:24:41 +05:30
|
|
|
allow_insecure_tls: bool = False
|
2026-02-22 19:41:26 +05:30
|
|
|
disable_device_pairing: bool = False
|
2026-02-10 20:10:36 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-02-10 23:31:14 +05:30
|
|
|
async def test_delete_gateway_main_agent_does_not_require_board_id(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
2026-02-10 20:10:36 +05:30
|
|
|
session = _FakeSession()
|
|
|
|
|
service = agent_service.AgentLifecycleService(session) # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
|
gateway_id = uuid4()
|
|
|
|
|
agent = _AgentStub(
|
|
|
|
|
id=uuid4(),
|
|
|
|
|
name="Primary Gateway Agent",
|
|
|
|
|
gateway_id=gateway_id,
|
|
|
|
|
board_id=None,
|
|
|
|
|
openclaw_session_id="agent:gateway-x:main",
|
|
|
|
|
)
|
|
|
|
|
gateway = _GatewayStub(
|
|
|
|
|
id=gateway_id,
|
|
|
|
|
url="ws://gateway.example/ws",
|
|
|
|
|
token=None,
|
|
|
|
|
workspace_root="/tmp/openclaw",
|
|
|
|
|
)
|
2026-02-10 23:31:14 +05:30
|
|
|
ctx = SimpleNamespace(
|
|
|
|
|
organization=SimpleNamespace(id=uuid4()), member=SimpleNamespace(id=uuid4())
|
|
|
|
|
)
|
2026-02-10 20:10:36 +05:30
|
|
|
|
|
|
|
|
async def _fake_first_agent(_session: object) -> _AgentStub:
|
|
|
|
|
return agent
|
|
|
|
|
|
|
|
|
|
async def _fake_first_gateway(_session: object) -> _GatewayStub:
|
|
|
|
|
return gateway
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
agent_service.Agent,
|
|
|
|
|
"objects",
|
|
|
|
|
SimpleNamespace(by_id=lambda _id: SimpleNamespace(first=_fake_first_agent)),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
agent_service.Gateway,
|
|
|
|
|
"objects",
|
|
|
|
|
SimpleNamespace(by_id=lambda _id: SimpleNamespace(first=_fake_first_gateway)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def _no_access_check(*_args, **_kwargs) -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def _should_not_be_called(*_args, **_kwargs):
|
|
|
|
|
raise AssertionError("require_board/require_gateway should not be called for main agents")
|
|
|
|
|
|
2026-02-10 22:30:14 +05:30
|
|
|
called: dict[str, int] = {"delete_lifecycle": 0}
|
|
|
|
|
|
|
|
|
|
async def _fake_delete_agent_lifecycle(
|
|
|
|
|
_self,
|
|
|
|
|
*,
|
|
|
|
|
agent: object,
|
|
|
|
|
gateway: object,
|
|
|
|
|
delete_files: bool = True,
|
|
|
|
|
delete_session: bool = True,
|
|
|
|
|
) -> str | None:
|
|
|
|
|
_ = (_self, agent, gateway, delete_files, delete_session)
|
|
|
|
|
called["delete_lifecycle"] += 1
|
2026-02-10 20:10:36 +05:30
|
|
|
return "/tmp/openclaw/workspace-gateway-x"
|
|
|
|
|
|
2026-02-15 00:45:28 +05:30
|
|
|
updated_models: list[type[object]] = []
|
|
|
|
|
|
2026-02-10 20:10:36 +05:30
|
|
|
async def _fake_update_where(*_args, **_kwargs) -> None:
|
2026-02-15 00:45:28 +05:30
|
|
|
if len(_args) >= 2 and isinstance(_args[1], type):
|
|
|
|
|
updated_models.append(_args[1])
|
2026-02-10 20:10:36 +05:30
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(service, "require_agent_access", _no_access_check)
|
|
|
|
|
monkeypatch.setattr(service, "require_board", _should_not_be_called)
|
|
|
|
|
monkeypatch.setattr(service, "require_gateway", _should_not_be_called)
|
2026-02-10 22:30:14 +05:30
|
|
|
monkeypatch.setattr(
|
2026-02-10 23:31:14 +05:30
|
|
|
agent_service.OpenClawGatewayProvisioner,
|
2026-02-10 22:30:14 +05:30
|
|
|
"delete_agent_lifecycle",
|
|
|
|
|
_fake_delete_agent_lifecycle,
|
|
|
|
|
)
|
2026-02-10 20:10:36 +05:30
|
|
|
monkeypatch.setattr(agent_service.crud, "update_where", _fake_update_where)
|
|
|
|
|
monkeypatch.setattr(agent_service, "record_activity", lambda *_a, **_k: None)
|
|
|
|
|
|
|
|
|
|
result = await service.delete_agent(agent_id=str(agent.id), ctx=ctx) # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
|
assert result.ok is True
|
2026-02-10 22:30:14 +05:30
|
|
|
assert called["delete_lifecycle"] == 1
|
2026-02-15 00:45:28 +05:30
|
|
|
assert Approval in updated_models
|
2026-02-10 20:10:36 +05:30
|
|
|
assert session.deleted and session.deleted[0] == agent
|