Files
social-app/backend/tests/integration/v1/agent/test_routes.py
T
qzl 8539f05a66 feat: 增强 HomeScreen 录音交互与 ChatBloc 状态管理
- 新增录音启动延迟处理,解决权限未就绪时的竞态问题
- 实现历史分页滚动位置保持,提升加载体验
- 添加文本输入框点击键盘显示与焦点管理
- 优化 ChatBloc provider 到 MultiBlocProvider 支持
- 修复 ApiException 429 错误详情解析(支持 JSON 字符串 body)
- 改进 LocalNotificationService 精确闹钟权限请求
- 优化 UiSchemaRenderer GridView children 生成
- 支持导航 action 的 replace 参数
- 移除 Agent router 速率限制逻辑(_allow_run_request, _allow_transcribe_request)
- 补充相关单元测试与集成测试
2026-03-18 17:03:22 +08:00

498 lines
16 KiB
Python

from __future__ import annotations
from io import BytesIO
from types import SimpleNamespace
from uuid import uuid4
from ag_ui.core import RunAgentInput
from fastapi.testclient import TestClient
from app import app
from core.auth.models import CurrentUser
from v1.agent import router as agent_router
from v1.agent.dependencies import get_agent_service
from v1.users.dependencies import get_current_user
class _FakeAgentService:
def __init__(self) -> None:
self._stream_called = False
async def enqueue_run(
self,
*,
run_input: RunAgentInput,
current_user: CurrentUser,
):
del current_user
return SimpleNamespace(
task_id="task-run-1",
thread_id=run_input.thread_id,
run_id=run_input.run_id,
created=False,
)
async def stream_events(
self,
*,
thread_id: str,
last_event_id: str | None,
current_user: CurrentUser,
) -> list[dict[str, object]]:
del thread_id, current_user
if self._stream_called:
return []
self._stream_called = True
return [
{
"id": "2-0",
"event": {
"type": "RUN_STARTED",
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-1",
},
"cursor": last_event_id,
}
]
async def get_user_history_snapshot(
self,
*,
current_user: CurrentUser,
thread_id: str | None,
before: str | None,
) -> dict[str, object]:
del current_user, before
return {
"threadId": thread_id or "00000000-0000-0000-0000-000000000001",
"scope": "history_day",
"day": "2026-03-07",
"hasMore": False,
"messages": [],
}
async def upload_attachment(
self,
*,
thread_id: str,
filename: str | None,
content_type: str | None,
payload: bytes,
current_user: CurrentUser,
) -> dict[str, str]:
del filename, content_type, payload, current_user
return {
"bucket": "bucket-test",
"path": f"agent-inputs/user/{thread_id}/upload.png",
"mimeType": "image/png",
"url": "https://signed.example/upload.png",
}
async def create_attachment_signed_url(
self,
*,
bucket: str,
path: str,
current_user: CurrentUser,
) -> dict[str, str]:
del current_user
return {
"bucket": bucket,
"path": path,
"url": "https://signed.example/temp-url.png",
}
class _FailingStreamAgentService(_FakeAgentService):
async def stream_events(
self,
*,
thread_id: str,
last_event_id: str | None,
current_user: CurrentUser,
) -> list[dict[str, object]]:
del thread_id, last_event_id, current_user
raise RuntimeError("redis timeout")
def test_run_requires_auth_and_returns_202_task_id() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
client = TestClient(app)
try:
unauthorized = client.post(
"/api/v1/agent/runs",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-1",
"state": {},
"messages": [{"id": "u1", "role": "user", "content": "hello"}],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert unauthorized.status_code == 401
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
authorized = client.post(
"/api/v1/agent/runs",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-1",
"state": {},
"messages": [{"id": "u1", "role": "user", "content": "hello"}],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert authorized.status_code == 202
assert authorized.json()["taskId"] == "task-run-1"
assert authorized.json()["threadId"] == "00000000-0000-0000-0000-000000000001"
assert authorized.json()["runId"] == "run-1"
assert authorized.json()["created"] is False
finally:
app.dependency_overrides = {}
def test_stream_reads_from_last_event_id() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
original_acquire = agent_router._acquire_sse_slot
original_release = agent_router._release_sse_slot
async def _allow_slot(*, user_id: str) -> bool:
del user_id
return True
async def _noop_release(*, user_id: str) -> None:
del user_id
return None
agent_router._acquire_sse_slot = _allow_slot # type: ignore[assignment]
agent_router._release_sse_slot = _noop_release # type: ignore[assignment]
try:
response = client.get(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/events?idle_limit=1",
headers={"Last-Event-ID": "1-0"},
)
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/event-stream")
assert "id: 2-0" in response.text
assert "event: RUN_STARTED" in response.text
assert '"threadId":"00000000-0000-0000-0000-000000000001"' in response.text
finally:
agent_router._acquire_sse_slot = original_acquire # type: ignore[assignment]
agent_router._release_sse_slot = original_release # type: ignore[assignment]
app.dependency_overrides = {}
def test_stream_handles_stream_backend_errors_without_connection_crash() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FailingStreamAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
original_acquire = agent_router._acquire_sse_slot
original_release = agent_router._release_sse_slot
async def _allow_slot(*, user_id: str) -> bool:
del user_id
return True
async def _noop_release(*, user_id: str) -> None:
del user_id
return None
agent_router._acquire_sse_slot = _allow_slot # type: ignore[assignment]
agent_router._release_sse_slot = _noop_release # type: ignore[assignment]
try:
response = client.get(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/events?idle_limit=1"
)
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/event-stream")
finally:
agent_router._acquire_sse_slot = original_acquire # type: ignore[assignment]
agent_router._release_sse_slot = original_release # type: ignore[assignment]
app.dependency_overrides = {}
def test_stream_rejects_invalid_last_event_id() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
try:
response = client.get(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/events",
headers={"Last-Event-ID": "bad-id"},
)
assert response.status_code == 422
finally:
app.dependency_overrides = {}
def test_history_returns_state_snapshot() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
client = TestClient(app)
try:
unauthorized = client.get(
"/api/v1/agent/history",
params={"threadId": "00000000-0000-0000-0000-000000000001"},
)
assert unauthorized.status_code == 401
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
authorized = client.get(
"/api/v1/agent/history",
params={
"threadId": "00000000-0000-0000-0000-000000000001",
"before": "2026-03-07",
},
)
assert authorized.status_code == 200
payload = authorized.json()
assert payload["scope"] == "history_day"
assert payload["threadId"] == "00000000-0000-0000-0000-000000000001"
assert payload["day"] == "2026-03-07"
finally:
app.dependency_overrides = {}
def test_user_history_returns_latest_snapshot() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
try:
response = client.get("/api/v1/agent/history")
assert response.status_code == 200
body = response.json()
assert body["scope"] == "history_day"
assert body["threadId"] == "00000000-0000-0000-0000-000000000001"
finally:
app.dependency_overrides = {}
def test_run_rejects_oversized_user_text_payload() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/agent/runs",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-oversize",
"state": {},
"messages": [
{
"id": "u1",
"role": "user",
"content": "x" * 11000,
}
],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert response.status_code == 422
finally:
app.dependency_overrides = {}
def test_run_rejects_client_supplied_history_messages() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/agent/runs",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-history",
"state": {},
"messages": [
{"id": "a1", "role": "assistant", "content": "old"},
{"id": "u1", "role": "user", "content": "new"},
],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert response.status_code == 422
finally:
app.dependency_overrides = {}
def test_upload_attachment_returns_reference() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
file_payload = BytesIO(b"png")
file_payload.name = "demo.png"
try:
response = client.post(
"/api/v1/agent/attachments",
data={"threadId": "00000000-0000-0000-0000-000000000001"},
files={"file": ("demo.png", file_payload, "image/png")},
)
assert response.status_code == 200
body = response.json()
attachment = body["attachment"]
assert attachment["mimeType"] == "image/png"
assert "00000000-0000-0000-0000-000000000001" in attachment["path"]
finally:
app.dependency_overrides = {}
def test_create_attachment_signed_url_returns_url() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
try:
response = client.get(
"/api/v1/agent/attachments/signed-url",
params={
"bucket": "bucket-test",
"path": "agent-inputs/user/thread/upload.png",
},
)
assert response.status_code == 200
body = response.json()
assert body["bucket"] == "bucket-test"
assert body["path"] == "agent-inputs/user/thread/upload.png"
assert body["url"].startswith("https://signed.example/")
finally:
app.dependency_overrides = {}
def test_asr_transcribe_returns_sync_transcript(monkeypatch) -> None:
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
async def mock_transcribe_file(file_path: str, filename: str) -> str:
assert file_path.endswith(".wav")
assert filename == "test.wav"
return "这是测试转写结果"
monkeypatch.setattr(
"v1.agent.service.asr_service.transcribe_file",
mock_transcribe_file,
)
client = TestClient(app)
wav_content = b"RIFF\x24\x80\x00\x00WAVEfmt "
wav_file = BytesIO(wav_content)
wav_file.name = "test.wav"
try:
response = client.post(
"/api/v1/agent/transcribe",
files={"audio": ("test.wav", wav_file, "audio/wav")},
)
assert response.status_code == 200
data = response.json()
assert "transcript" in data
assert data["transcript"] == "这是测试转写结果"
finally:
app.dependency_overrides = {}
def test_asr_transcribe_rejects_oversized_audio(monkeypatch) -> None:
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
monkeypatch.setattr(agent_router, "_MAX_TRANSCRIBE_AUDIO_BYTES", 4)
client = TestClient(app)
oversized = BytesIO(b"12345")
oversized.name = "test.wav"
try:
response = client.post(
"/api/v1/agent/transcribe",
files={"audio": ("test.wav", oversized, "audio/wav")},
)
assert response.status_code == 400
assert response.json()["detail"] == "Audio file too large"
finally:
app.dependency_overrides = {}
def test_asr_transcribe_rejects_non_wav_audio(monkeypatch) -> None:
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
fake_mp3 = BytesIO(b"fake-mp3")
fake_mp3.name = "test.mp3"
try:
response = client.post(
"/api/v1/agent/transcribe",
files={"audio": ("test.mp3", fake_mp3, "audio/mpeg")},
)
assert response.status_code == 400
assert response.json()["detail"] == "Unsupported audio format"
finally:
app.dependency_overrides = {}
def test_asr_transcribe_rejects_invalid_wav_payload(monkeypatch) -> None:
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
fake_payload = BytesIO(b"not-a-wav")
fake_payload.name = "test.wav"
try:
response = client.post(
"/api/v1/agent/transcribe",
files={"audio": ("test.wav", fake_payload, "audio/wav")},
)
assert response.status_code == 400
assert response.json()["detail"] == "Unsupported audio format"
finally:
app.dependency_overrides = {}