135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
"""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"])
|