# ruff: noqa: INP001 """Integration tests for gateway runtime agent import preview and import APIs.""" from __future__ import annotations from types import SimpleNamespace from uuid import uuid4 import pytest from fastapi import APIRouter, FastAPI from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine from sqlmodel import SQLModel, select from sqlmodel.ext.asyncio.session import AsyncSession from app import models as _models from app.api.deps import require_org_admin from app.api.gateways import router as gateways_router from app.db.session import get_session from app.models.agents import Agent from app.models.boards import Board from app.models.gateways import Gateway from app.models.organization_members import OrganizationMember from app.models.organizations import Organization from app.models.users import User import app.services.openclaw.admin_service as admin_service from app.core.agent_tokens import hash_agent_token from app.services.organizations import OrganizationContext async def _make_engine() -> AsyncEngine: engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) return engine def _build_test_app( session_maker: async_sessionmaker[AsyncSession], ctx: OrganizationContext, ) -> FastAPI: app = FastAPI() api_v1 = APIRouter(prefix="/api/v1") api_v1.include_router(gateways_router) app.include_router(api_v1) async def _override_get_session() -> AsyncSession: async with session_maker() as session: yield session async def _override_require_org_admin() -> OrganizationContext: return ctx app.dependency_overrides[get_session] = _override_get_session app.dependency_overrides[require_org_admin] = _override_require_org_admin return app @pytest.mark.asyncio async def test_gateway_agent_import_preview_reports_importable_and_tracked( monkeypatch: pytest.MonkeyPatch, ) -> None: engine = await _make_engine() session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) organization = Organization(id=uuid4(), name="Pipeline") user = User(id=uuid4(), clerk_user_id="user_123", email="user@example.com") member = OrganizationMember( id=uuid4(), organization_id=organization.id, user_id=user.id, role="owner", all_boards_read=True, all_boards_write=True, ) gateway = Gateway( id=uuid4(), organization_id=organization.id, name="Main Gateway", url="ws://gateway.example.local", workspace_root="/workspace", ) board = Board( id=uuid4(), organization_id=organization.id, name="Platform", slug="platform", gateway_id=gateway.id, ) tracked_agent = Agent( id=uuid4(), board_id=None, gateway_id=gateway.id, name="Already Tracked", openclaw_session_id="agent:existing-main:main", is_board_lead=False, ) lead_runtime_id = f"lead-{board.id}" async def _fake_openclaw_call( method: str, params: object | None = None, *, config: object, ) -> object: _ = params, config if method == "agents.list": return { "agents": [ {"id": "existing-main", "name": "Existing Main"}, {"id": "legacy-worker", "name": "Legacy Worker"}, {"id": lead_runtime_id, "name": "Legacy Lead"}, ] } if method == "sessions.list": return { "sessions": [ {"key": "agent:existing-main:main"}, {"key": "agent:legacy-worker:main"}, {"key": f"agent:{lead_runtime_id}:main"}, ] } raise AssertionError(f"Unexpected method {method}") monkeypatch.setattr(admin_service, "openclaw_call", _fake_openclaw_call) try: async with session_maker() as session: session.add(organization) session.add(user) session.add(member) session.add(gateway) session.add(board) session.add(tracked_agent) await session.commit() app = _build_test_app( session_maker, OrganizationContext(organization=organization, member=member), ) async with AsyncClient( transport=ASGITransport(app=app), base_url="http://testserver", ) as client: response = await client.get( f"/api/v1/gateways/{gateway.id}/agents/import-preview" ) assert response.status_code == 200 data = response.json() assert data["discovered_count"] == 3 assert data["importable_count"] == 2 assert data["already_tracked_count"] == 1 candidates = {item["gateway_agent_id"]: item for item in data["candidates"]} assert candidates["existing-main"]["importable"] is False assert candidates["existing-main"]["existing_agent_id"] == str(tracked_agent.id) assert candidates["legacy-worker"]["importable"] is True assert candidates[lead_runtime_id]["importable"] is True assert candidates[lead_runtime_id]["inferred_board_id"] == str(board.id) assert candidates[lead_runtime_id]["inferred_is_board_lead"] is True finally: await engine.dispose() @pytest.mark.asyncio async def test_gateway_agent_import_creates_selected_candidates( monkeypatch: pytest.MonkeyPatch, ) -> None: engine = await _make_engine() session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) organization = Organization(id=uuid4(), name="Pipeline") user = User(id=uuid4(), clerk_user_id="user_123", email="user@example.com") member = OrganizationMember( id=uuid4(), organization_id=organization.id, user_id=user.id, role="owner", all_boards_read=True, all_boards_write=True, ) gateway = Gateway( id=uuid4(), organization_id=organization.id, name="Main Gateway", url="ws://gateway.example.local", workspace_root="/workspace", ) board = Board( id=uuid4(), organization_id=organization.id, name="Platform", slug="platform", gateway_id=gateway.id, ) tracked_agent = Agent( id=uuid4(), board_id=None, gateway_id=gateway.id, name="Already Tracked", openclaw_session_id="agent:existing-main:main", is_board_lead=False, ) lead_runtime_id = f"lead-{board.id}" async def _fake_openclaw_call( method: str, params: object | None = None, *, config: object, ) -> object: _ = params, config if method == "agents.list": return { "agents": [ {"id": "existing-main", "name": "Existing Main"}, {"id": "legacy-worker", "name": "Legacy Worker"}, {"id": lead_runtime_id, "name": "Legacy Lead"}, ] } if method == "sessions.list": return { "sessions": [ {"key": "agent:existing-main:main"}, {"key": "agent:legacy-worker:main"}, {"key": f"agent:{lead_runtime_id}:main"}, ] } raise AssertionError(f"Unexpected method {method}") monkeypatch.setattr(admin_service, "openclaw_call", _fake_openclaw_call) try: async with session_maker() as session: session.add(organization) session.add(user) session.add(member) session.add(gateway) session.add(board) session.add(tracked_agent) await session.commit() app = _build_test_app( session_maker, OrganizationContext(organization=organization, member=member), ) async with AsyncClient( transport=ASGITransport(app=app), base_url="http://testserver", ) as client: response = await client.post( f"/api/v1/gateways/{gateway.id}/agents/import", json={ "gateway_agent_ids": [ "existing-main", "legacy-worker", lead_runtime_id, "unknown-agent", ] }, ) assert response.status_code == 200 data = response.json() assert data["imported_count"] == 2 assert data["skipped_count"] == 2 assert set(data["skipped_gateway_agent_ids"]) == {"existing-main", "unknown-agent"} assert len(data["imported_agent_ids"]) == 2 async with session_maker() as session: gateway_agents = ( await session.exec( select(Agent).where(Agent.gateway_id == gateway.id) ) ).all() # original tracked + imported worker + imported lead assert len(gateway_agents) == 3 by_name = {agent.name: agent for agent in gateway_agents} imported_worker = by_name["Legacy Worker"] imported_lead = by_name["Legacy Lead"] assert imported_worker.board_id is None assert imported_worker.is_board_lead is False assert imported_worker.openclaw_session_id == "agent:legacy-worker:main" assert imported_lead.board_id == board.id assert imported_lead.is_board_lead is True assert imported_lead.openclaw_session_id == f"agent:{lead_runtime_id}:main" finally: await engine.dispose() @pytest.mark.asyncio async def test_gateway_agent_import_reconcile_updates_supported_agents( monkeypatch: pytest.MonkeyPatch, ) -> None: engine = await _make_engine() session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) organization = Organization(id=uuid4(), name="Pipeline") user = User(id=uuid4(), clerk_user_id="user_123", email="user@example.com") member = OrganizationMember( id=uuid4(), organization_id=organization.id, user_id=user.id, role="owner", all_boards_read=True, all_boards_write=True, ) gateway = Gateway( id=uuid4(), organization_id=organization.id, name="Main Gateway", url="ws://gateway.example.local", workspace_root="/workspace", ) board = Board( id=uuid4(), organization_id=organization.id, name="Platform", slug="platform", gateway_id=gateway.id, ) lead_runtime_id = f"lead-{board.id}" async def _fake_openclaw_call( method: str, params: object | None = None, *, config: object, ) -> object: _ = params, config if method == "agents.list": return { "agents": [ {"id": "legacy-worker", "name": "Legacy Worker"}, {"id": lead_runtime_id, "name": "Legacy Lead"}, ] } if method == "sessions.list": return { "sessions": [ {"key": "agent:legacy-worker:main"}, {"key": f"agent:{lead_runtime_id}:main"}, ] } raise AssertionError(f"Unexpected method {method}") async def _fake_run_lifecycle( self: object, *, gateway: Gateway, agent_id: object, board: Board | None, user: object, action: str, auth_token: str | None, force_bootstrap: bool, reset_session: bool, wake: bool, deliver_wakeup: bool, wakeup_verb: str | None, clear_confirm_token: bool, raise_gateway_errors: bool, ) -> Agent: _ = ( user, action, auth_token, force_bootstrap, reset_session, wake, deliver_wakeup, wakeup_verb, clear_confirm_token, raise_gateway_errors, ) assert board is not None session = getattr(self, "session") agent = await session.get(Agent, agent_id) assert agent is not None assert agent.gateway_id == gateway.id agent.status = "online" agent.agent_token_hash = hash_agent_token("rotated-token") session.add(agent) await session.commit() await session.refresh(agent) return agent monkeypatch.setattr(admin_service, "openclaw_call", _fake_openclaw_call) monkeypatch.setattr( admin_service.AgentLifecycleOrchestrator, "run_lifecycle", _fake_run_lifecycle, ) try: async with session_maker() as session: session.add(organization) session.add(user) session.add(member) session.add(gateway) session.add(board) await session.commit() app = _build_test_app( session_maker, OrganizationContext(organization=organization, member=member), ) async with AsyncClient( transport=ASGITransport(app=app), base_url="http://testserver", ) as client: response = await client.post( f"/api/v1/gateways/{gateway.id}/agents/import", json={ "gateway_agent_ids": ["legacy-worker", lead_runtime_id], "reconcile_after_import": True, }, ) assert response.status_code == 200 data = response.json() assert data["imported_count"] == 2 assert data["reconcile"] is not None assert data["reconcile"]["attempted"] == 2 assert data["reconcile"]["updated"] == 1 assert data["reconcile"]["skipped"] == 1 assert len(data["reconcile"]["errors"]) == 1 async with session_maker() as session: agents = ( await session.exec( select(Agent).where(Agent.gateway_id == gateway.id) ) ).all() by_name = {agent.name: agent for agent in agents} assert by_name["Legacy Lead"].status == "online" assert by_name["Legacy Lead"].agent_token_hash assert by_name["Legacy Worker"].status == "offline" assert by_name["Legacy Worker"].agent_token_hash is None finally: await engine.dispose()