2026-03-05 15:34:37 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-03-08 17:34:28 +08:00
|
|
|
from io import BytesIO
|
2026-03-05 15:34:37 +08:00
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
|
2026-03-07 17:30:20 +08:00
|
|
|
from ag_ui.core import RunAgentInput
|
2026-03-05 15:34:37 +08:00
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
|
from app import app
|
|
|
|
|
from core.auth.models import CurrentUser
|
2026-03-07 17:30:20 +08:00
|
|
|
from v1.agent import router as agent_router
|
2026-03-05 15:34:37 +08:00
|
|
|
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
|
|
|
|
|
|
2026-03-12 09:29:57 +08:00
|
|
|
async def enqueue_run(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
run_input: RunAgentInput,
|
|
|
|
|
current_user: CurrentUser,
|
|
|
|
|
):
|
2026-03-13 17:27:18 +08:00
|
|
|
del current_user
|
2026-03-05 15:34:37 +08:00
|
|
|
return SimpleNamespace(
|
|
|
|
|
task_id="task-run-1",
|
2026-03-07 17:30:20 +08:00
|
|
|
thread_id=run_input.thread_id,
|
|
|
|
|
run_id=run_input.run_id,
|
|
|
|
|
created=False,
|
2026-03-05 15:34:37 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def stream_events(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
2026-03-07 17:30:20 +08:00
|
|
|
thread_id: str,
|
2026-03-05 15:34:37 +08:00
|
|
|
last_event_id: str | None,
|
|
|
|
|
current_user: CurrentUser,
|
|
|
|
|
) -> list[dict[str, object]]:
|
2026-03-07 17:30:20 +08:00
|
|
|
del thread_id, current_user
|
2026-03-05 15:34:37 +08:00
|
|
|
if self._stream_called:
|
|
|
|
|
return []
|
|
|
|
|
self._stream_called = True
|
|
|
|
|
return [
|
2026-03-07 17:30:20 +08:00
|
|
|
{
|
|
|
|
|
"id": "2-0",
|
|
|
|
|
"event": {
|
|
|
|
|
"type": "RUN_STARTED",
|
|
|
|
|
"threadId": "00000000-0000-0000-0000-000000000001",
|
|
|
|
|
"runId": "run-1",
|
|
|
|
|
},
|
|
|
|
|
"cursor": last_event_id,
|
|
|
|
|
}
|
2026-03-05 15:34:37 +08:00
|
|
|
]
|
|
|
|
|
|
2026-03-07 17:30:20 +08:00
|
|
|
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",
|
2026-03-16 16:11:06 +08:00
|
|
|
"scope": "history_day",
|
|
|
|
|
"day": "2026-03-07",
|
|
|
|
|
"hasMore": False,
|
|
|
|
|
"messages": [],
|
2026-03-07 17:30:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 09:29:57 +08:00
|
|
|
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",
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 17:27:18 +08:00
|
|
|
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",
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:34:37 +08:00
|
|
|
|
2026-03-11 21:33:25 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 18:42:59 +08:00
|
|
|
class _TerminalStreamAgentService(_FakeAgentService):
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.stream_calls = 0
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
self.stream_calls += 1
|
|
|
|
|
if self.stream_calls == 1:
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
"id": "9-0",
|
|
|
|
|
"event": {
|
|
|
|
|
"type": "RUN_FINISHED",
|
|
|
|
|
"threadId": "00000000-0000-0000-0000-000000000001",
|
|
|
|
|
"runId": "run-1",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
2026-03-05 15:34:37 +08:00
|
|
|
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",
|
2026-03-07 17:30:20 +08:00
|
|
|
json={
|
|
|
|
|
"threadId": "00000000-0000-0000-0000-000000000001",
|
|
|
|
|
"runId": "run-1",
|
|
|
|
|
"state": {},
|
|
|
|
|
"messages": [{"id": "u1", "role": "user", "content": "hello"}],
|
|
|
|
|
"tools": [],
|
|
|
|
|
"context": [],
|
2026-03-19 18:42:59 +08:00
|
|
|
"forwardedProps": {"agent_type": "worker"},
|
2026-03-07 17:30:20 +08:00
|
|
|
},
|
2026-03-05 15:34:37 +08:00
|
|
|
)
|
|
|
|
|
assert unauthorized.status_code == 401
|
|
|
|
|
|
|
|
|
|
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-05 15:34:37 +08:00
|
|
|
)
|
|
|
|
|
authorized = client.post(
|
|
|
|
|
"/api/v1/agent/runs",
|
2026-03-07 17:30:20 +08:00
|
|
|
json={
|
|
|
|
|
"threadId": "00000000-0000-0000-0000-000000000001",
|
|
|
|
|
"runId": "run-1",
|
|
|
|
|
"state": {},
|
|
|
|
|
"messages": [{"id": "u1", "role": "user", "content": "hello"}],
|
|
|
|
|
"tools": [],
|
|
|
|
|
"context": [],
|
2026-03-19 18:42:59 +08:00
|
|
|
"forwardedProps": {"agent_type": "worker"},
|
2026-03-07 17:30:20 +08:00
|
|
|
},
|
2026-03-05 15:34:37 +08:00
|
|
|
)
|
|
|
|
|
assert authorized.status_code == 202
|
2026-03-07 17:30:20 +08:00
|
|
|
assert authorized.json()["taskId"] == "task-run-1"
|
|
|
|
|
assert authorized.json()["threadId"] == "00000000-0000-0000-0000-000000000001"
|
|
|
|
|
assert authorized.json()["runId"] == "run-1"
|
2026-03-05 15:34:37 +08:00
|
|
|
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(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-05 15:34:37 +08:00
|
|
|
)
|
|
|
|
|
client = TestClient(app)
|
2026-03-07 17:30:20 +08:00
|
|
|
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]
|
2026-03-05 15:34:37 +08:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
response = client.get(
|
2026-03-07 17:30:20 +08:00
|
|
|
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/events?idle_limit=1",
|
2026-03-05 15:34:37 +08:00
|
|
|
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
|
2026-03-07 17:30:20 +08:00
|
|
|
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 = {}
|
|
|
|
|
|
|
|
|
|
|
2026-03-11 21:33:25 +08:00
|
|
|
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(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-11 21:33:25 +08:00
|
|
|
)
|
|
|
|
|
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 = {}
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 18:42:59 +08:00
|
|
|
def test_stream_stops_after_terminal_run_event() -> None:
|
|
|
|
|
service = _TerminalStreamAgentService()
|
|
|
|
|
app.dependency_overrides[get_agent_service] = lambda: service
|
|
|
|
|
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
|
|
|
|
|
id=uuid4(), phone="+8613812345678"
|
|
|
|
|
)
|
|
|
|
|
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=3"
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.headers["content-type"].startswith("text/event-stream")
|
|
|
|
|
assert "event: RUN_FINISHED" in response.text
|
|
|
|
|
assert service.stream_calls == 1
|
|
|
|
|
finally:
|
|
|
|
|
agent_router._acquire_sse_slot = original_acquire # type: ignore[assignment]
|
|
|
|
|
agent_router._release_sse_slot = original_release # type: ignore[assignment]
|
|
|
|
|
app.dependency_overrides = {}
|
|
|
|
|
|
|
|
|
|
|
2026-03-07 17:30:20 +08:00
|
|
|
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(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-07 17:30:20 +08:00
|
|
|
)
|
|
|
|
|
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(
|
2026-03-13 17:27:18 +08:00
|
|
|
"/api/v1/agent/history",
|
|
|
|
|
params={"threadId": "00000000-0000-0000-0000-000000000001"},
|
2026-03-07 17:30:20 +08:00
|
|
|
)
|
|
|
|
|
assert unauthorized.status_code == 401
|
|
|
|
|
|
|
|
|
|
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-07 17:30:20 +08:00
|
|
|
)
|
|
|
|
|
authorized = client.get(
|
2026-03-13 17:27:18 +08:00
|
|
|
"/api/v1/agent/history",
|
|
|
|
|
params={
|
|
|
|
|
"threadId": "00000000-0000-0000-0000-000000000001",
|
|
|
|
|
"before": "2026-03-07",
|
|
|
|
|
},
|
2026-03-07 17:30:20 +08:00
|
|
|
)
|
|
|
|
|
assert authorized.status_code == 200
|
|
|
|
|
payload = authorized.json()
|
2026-03-16 16:11:06 +08:00
|
|
|
assert payload["scope"] == "history_day"
|
2026-03-07 17:30:20 +08:00
|
|
|
assert payload["threadId"] == "00000000-0000-0000-0000-000000000001"
|
2026-03-16 16:11:06 +08:00
|
|
|
assert payload["day"] == "2026-03-07"
|
2026-03-07 17:30:20 +08:00
|
|
|
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(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-07 17:30:20 +08:00
|
|
|
)
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
try:
|
|
|
|
|
response = client.get("/api/v1/agent/history")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
body = response.json()
|
2026-03-16 16:11:06 +08:00
|
|
|
assert body["scope"] == "history_day"
|
2026-03-07 17:30:20 +08:00
|
|
|
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(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-07 17:30:20 +08:00
|
|
|
)
|
|
|
|
|
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": [],
|
2026-03-19 18:42:59 +08:00
|
|
|
"forwardedProps": {"agent_type": "worker"},
|
2026-03-07 17:30:20 +08:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 422
|
2026-03-05 15:34:37 +08:00
|
|
|
finally:
|
|
|
|
|
app.dependency_overrides = {}
|
2026-03-08 16:01:16 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-08 16:01:16 +08:00
|
|
|
)
|
|
|
|
|
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": [],
|
2026-03-19 18:42:59 +08:00
|
|
|
"forwardedProps": {"agent_type": "worker"},
|
2026-03-08 16:01:16 +08:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
finally:
|
|
|
|
|
app.dependency_overrides = {}
|
|
|
|
|
|
|
|
|
|
|
2026-03-12 09:29:57 +08:00
|
|
|
def test_upload_attachment_returns_reference() -> None:
|
|
|
|
|
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
|
|
|
|
|
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-12 09:29:57 +08:00
|
|
|
)
|
|
|
|
|
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 = {}
|
|
|
|
|
|
|
|
|
|
|
2026-03-13 17:27:18 +08:00
|
|
|
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(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-13 17:27:18 +08:00
|
|
|
)
|
|
|
|
|
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 = {}
|
|
|
|
|
|
|
|
|
|
|
2026-03-08 17:34:28 +08:00
|
|
|
def test_asr_transcribe_returns_sync_transcript(monkeypatch) -> None:
|
|
|
|
|
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-08 17:34:28 +08:00
|
|
|
)
|
|
|
|
|
|
2026-03-10 17:44:29 +08:00
|
|
|
async def mock_transcribe_file(file_path: str, filename: str) -> str:
|
|
|
|
|
assert file_path.endswith(".wav")
|
|
|
|
|
assert filename == "test.wav"
|
2026-03-08 17:34:28 +08:00
|
|
|
return "这是测试转写结果"
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
2026-03-10 17:44:29 +08:00
|
|
|
"v1.agent.service.asr_service.transcribe_file",
|
|
|
|
|
mock_transcribe_file,
|
2026-03-08 17:34:28 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
2026-03-10 17:44:29 +08:00
|
|
|
wav_content = b"RIFF\x24\x80\x00\x00WAVEfmt "
|
2026-03-08 17:34:28 +08:00
|
|
|
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 = {}
|
2026-03-10 17:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_asr_transcribe_rejects_oversized_audio(monkeypatch) -> None:
|
|
|
|
|
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-10 17:44:29 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 = {}
|
|
|
|
|
|
|
|
|
|
|
2026-03-11 20:51:56 +08:00
|
|
|
def test_asr_transcribe_rejects_non_wav_audio(monkeypatch) -> None:
|
2026-03-10 17:44:29 +08:00
|
|
|
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-10 17:44:29 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 = {}
|
|
|
|
|
|
|
|
|
|
|
2026-03-11 20:51:56 +08:00
|
|
|
def test_asr_transcribe_rejects_invalid_wav_payload(monkeypatch) -> None:
|
2026-03-10 17:44:29 +08:00
|
|
|
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
|
2026-03-19 18:42:59 +08:00
|
|
|
id=uuid4(), phone="+8613812345678"
|
2026-03-10 17:44:29 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 = {}
|