"""Agent 循环工具确认 approve/deny 端点测试。 覆盖: - approve / deny 成功 resolve 记录(200, status=resolved) - 重复 resolve 返回 404(记录已消费) - 记录不存在的场景 → 404 - registry 未挂载 → 503 """ from __future__ import annotations from collections.abc import AsyncGenerator import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy import select from app.application.agent.confirmation import ConfirmationRegistry from app.db.session import AsyncSessionLocal from app.main import app from app.models.safety import PendingConfirmationRecord _RUN_ID = "run_test_confirmation_0001" _CONFIRM = "confirm_test_confirmation_0001" @pytest.fixture async def client(auth_headers: dict[str, str]) -> AsyncGenerator[AsyncClient, None]: """挂载 ConfirmationRegistry 到 app.state。""" registry = ConfirmationRegistry() app.state.confirmation_registry = registry transport = ASGITransport(app=app) try: async with AsyncClient( transport=transport, base_url="http://test", headers=auth_headers ) as http_client: yield http_client finally: app.state.confirmation_registry = ConfirmationRegistry() async def _seed_pending_record() -> None: """在 DB 中插入一条待确认记录。""" async with AsyncSessionLocal() as session: record = PendingConfirmationRecord( id=_CONFIRM, tool="shell", input_json='{"cmd": "echo hi"}', risk_level="L3", reason="高风险命令需确认", run_id=_RUN_ID, character_id="character_test", status="pending", ) session.add(record) await session.commit() @pytest.mark.asyncio async def test_approve_resolves_future(client: AsyncClient) -> None: await _seed_pending_record() resp = await client.post(f"/api/v1/runs/{_RUN_ID}/confirmations/{_CONFIRM}/approve") assert resp.status_code == 200 assert resp.json() == {"approved": True, "status": "resolved"} # 验证 DB 状态已更新 async with AsyncSessionLocal() as session: record = ( await session.execute( select(PendingConfirmationRecord).where(PendingConfirmationRecord.id == _CONFIRM) ) ).scalar_one_or_none() assert record is not None assert record.status == "approved" assert record.resolved_at is not None @pytest.mark.asyncio async def test_deny_resolves_future(client: AsyncClient) -> None: await _seed_pending_record() resp = await client.post(f"/api/v1/runs/{_RUN_ID}/confirmations/{_CONFIRM}/deny") assert resp.status_code == 200 assert resp.json() == {"approved": False, "status": "resolved"} async with AsyncSessionLocal() as session: record = ( await session.execute( select(PendingConfirmationRecord).where(PendingConfirmationRecord.id == _CONFIRM) ) ).scalar_one_or_none() assert record is not None assert record.status == "denied" @pytest.mark.asyncio async def test_resolve_idempotent_second_call_returns_404(client: AsyncClient) -> None: """重复 approve:第一次成功,第二次返回 404(记录已 resolve)。""" await _seed_pending_record() first = await client.post(f"/api/v1/runs/{_RUN_ID}/confirmations/{_CONFIRM}/approve") assert first.status_code == 200 second = await client.post(f"/api/v1/runs/{_RUN_ID}/confirmations/{_CONFIRM}/approve") assert second.status_code == 404 assert second.json()["error"]["code"] == "confirmation_not_found" @pytest.mark.asyncio async def test_missing_confirmation_returns_404(client: AsyncClient) -> None: """确认项不存在 → 404。""" resp = await client.post(f"/api/v1/runs/{_RUN_ID}/confirmations/{_CONFIRM}/deny") assert resp.status_code == 404 assert resp.json()["error"]["code"] == "confirmation_not_found" @pytest.mark.asyncio async def test_registry_not_configured_returns_503() -> None: app.state.confirmation_registry = None try: transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as http_client: resp = await http_client.post( f"/api/v1/runs/{_RUN_ID}/confirmations/{_CONFIRM}/approve" ) assert resp.status_code == 503 assert resp.json()["error"]["code"] == "service_unavailable" finally: app.state.confirmation_registry = ConfirmationRegistry() if __name__ == "__main__": pytest.main([__file__, "-v"])