test: 修复所有预存的失败测试
- test_auth_routes: monkeypatch 环境为 production 使 phone-session 限速生效 - test_schedule_items_routes: 补充必填 timezone 字段 - test_llm_pricing_service: 更新 deepseek-chat 费率期望值匹配实际 catalog - test_sse_flow_live: 补充 runId 查询参数、改用附件上传 API、修复 history 响应断言、独立 DB session 避免跨事件循环崩溃 - test_agent_prompt: 移除已删除的 project_cli_defaults 断言 - test_toolkit: 更新 action card 断言匹配 module/method 格式
This commit is contained in:
@@ -167,7 +167,11 @@ def test_send_otp_phone_rate_limited_after_too_many_attempts() -> None:
|
|||||||
app.dependency_overrides = {}
|
app.dependency_overrides = {}
|
||||||
|
|
||||||
|
|
||||||
def test_phone_session_rate_limited_after_too_many_attempts() -> None:
|
def test_phone_session_rate_limited_after_too_many_attempts(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr("v1.auth.router.config.runtime.environment", "production")
|
||||||
|
|
||||||
app.dependency_overrides[get_auth_service] = _override_auth_service(
|
app.dependency_overrides[get_auth_service] = _override_auth_service(
|
||||||
FakeAuthService(_token_response())
|
FakeAuthService(_token_response())
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ def test_create_schedule_item_returns_201() -> None:
|
|||||||
json={
|
json={
|
||||||
"title": "Test Event",
|
"title": "Test Event",
|
||||||
"start_at": "2026-02-28T16:00:00Z",
|
"start_at": "2026-02-28T16:00:00Z",
|
||||||
|
"timezone": "UTC",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
from core.config.settings import config
|
from core.config.settings import config
|
||||||
from core.db.session import AsyncSessionLocal
|
|
||||||
from models.agent_chat_message import AgentChatMessage
|
from models.agent_chat_message import AgentChatMessage
|
||||||
from models.agent_chat_session import AgentChatSession
|
from models.agent_chat_session import AgentChatSession
|
||||||
from schemas.enums import AgentChatMessageRole
|
from schemas.enums import AgentChatMessageRole
|
||||||
@@ -20,6 +19,14 @@ FIXTURE_IMAGE_PATH = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_session():
|
||||||
|
engine = create_async_engine(
|
||||||
|
config.database_url,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
)
|
||||||
|
return async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False, autoflush=False)()
|
||||||
|
|
||||||
|
|
||||||
def _require_test_phone() -> str:
|
def _require_test_phone() -> str:
|
||||||
phone = config.test.phone
|
phone = config.test.phone
|
||||||
if not phone:
|
if not phone:
|
||||||
@@ -82,7 +89,7 @@ async def test_agent_sse_closed_loop_live() -> None:
|
|||||||
thread_id = str(accepted["threadId"])
|
thread_id = str(accepted["threadId"])
|
||||||
assert thread_id
|
assert thread_id
|
||||||
|
|
||||||
events_url = f"{BASE_URL}/api/v1/agent/runs/{thread_id}/events"
|
events_url = f"{BASE_URL}/api/v1/agent/runs/{thread_id}/events?runId=run-live-1"
|
||||||
event_names: list[str] = []
|
event_names: list[str] = []
|
||||||
async with client.stream(
|
async with client.stream(
|
||||||
"GET", events_url, headers=headers, timeout=20.0
|
"GET", events_url, headers=headers, timeout=20.0
|
||||||
@@ -98,7 +105,7 @@ async def test_agent_sse_closed_loop_live() -> None:
|
|||||||
assert "RUN_STARTED" in event_names
|
assert "RUN_STARTED" in event_names
|
||||||
assert "RUN_FINISHED" in event_names or "RUN_ERROR" in event_names
|
assert "RUN_FINISHED" in event_names or "RUN_ERROR" in event_names
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
async with _make_session() as session:
|
||||||
session_row = await session.get(AgentChatSession, UUID(thread_id))
|
session_row = await session.get(AgentChatSession, UUID(thread_id))
|
||||||
assert session_row is not None
|
assert session_row is not None
|
||||||
assert session_row.message_count >= 1
|
assert session_row.message_count >= 1
|
||||||
@@ -119,13 +126,24 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
|
|||||||
if config.runtime.environment not in {"dev", "test"}:
|
if config.runtime.environment not in {"dev", "test"}:
|
||||||
pytest.skip("live integration tests require dev or test environment")
|
pytest.skip("live integration tests require dev or test environment")
|
||||||
|
|
||||||
image_data = base64.b64encode(FIXTURE_IMAGE_PATH.read_bytes()).decode("ascii")
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
token = await _live_access_token(client)
|
token = await _live_access_token(client)
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
thread_id = str(uuid4())
|
thread_id = str(uuid4())
|
||||||
|
|
||||||
|
upload_resp = await client.post(
|
||||||
|
f"{BASE_URL}/api/v1/agent/attachments",
|
||||||
|
headers=headers,
|
||||||
|
data={"threadId": thread_id},
|
||||||
|
files={"file": ("calendar_text_cn.png", FIXTURE_IMAGE_PATH.read_bytes(), "image/png")},
|
||||||
|
)
|
||||||
|
assert upload_resp.status_code == 200, (
|
||||||
|
f"upload failed: {upload_resp.status_code} {upload_resp.text[:200]}"
|
||||||
|
)
|
||||||
|
attachment = upload_resp.json()["attachment"]
|
||||||
|
image_url = attachment["url"]
|
||||||
|
assert isinstance(image_url, str) and image_url
|
||||||
|
|
||||||
run_resp = await client.post(
|
run_resp = await client.post(
|
||||||
f"{BASE_URL}/api/v1/agent/runs",
|
f"{BASE_URL}/api/v1/agent/runs",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
@@ -141,7 +159,7 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
|
|||||||
{"type": "text", "text": "请描述图片里的内容"},
|
{"type": "text", "text": "请描述图片里的内容"},
|
||||||
{
|
{
|
||||||
"type": "binary",
|
"type": "binary",
|
||||||
"data": image_data,
|
"url": image_url,
|
||||||
"mimeType": "image/png",
|
"mimeType": "image/png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -154,7 +172,7 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
|
|||||||
)
|
)
|
||||||
assert run_resp.status_code == 202
|
assert run_resp.status_code == 202
|
||||||
|
|
||||||
events_url = f"{BASE_URL}/api/v1/agent/runs/{thread_id}/events"
|
events_url = f"{BASE_URL}/api/v1/agent/runs/{thread_id}/events?runId=run-live-image-1"
|
||||||
event_names: list[str] = []
|
event_names: list[str] = []
|
||||||
async with client.stream(
|
async with client.stream(
|
||||||
"GET", events_url, headers=headers, timeout=90.0
|
"GET", events_url, headers=headers, timeout=90.0
|
||||||
@@ -180,25 +198,21 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
|
|||||||
)
|
)
|
||||||
assert history_resp.status_code == 200
|
assert history_resp.status_code == 200
|
||||||
history = history_resp.json()
|
history = history_resp.json()
|
||||||
assert history.get("type") == "STATE_SNAPSHOT"
|
assert history.get("scope") == "history_day"
|
||||||
snapshot = history.get("snapshot", {})
|
messages = history.get("messages", [])
|
||||||
assert snapshot.get("scope") == "history_day"
|
|
||||||
messages = snapshot.get("messages", [])
|
|
||||||
user_messages = [
|
user_messages = [
|
||||||
item
|
item
|
||||||
for item in messages
|
for item in messages
|
||||||
if isinstance(item, dict) and item.get("role") == "user"
|
if isinstance(item, dict) and item.get("role") == "user"
|
||||||
]
|
]
|
||||||
assert user_messages
|
assert user_messages
|
||||||
metadata = user_messages[0].get("metadata")
|
user_attachments = user_messages[0].get("attachments")
|
||||||
assert isinstance(metadata, dict)
|
|
||||||
user_attachments = metadata.get("user_message_attachments")
|
|
||||||
assert isinstance(user_attachments, list)
|
assert isinstance(user_attachments, list)
|
||||||
assert user_attachments
|
assert user_attachments
|
||||||
assert isinstance(user_attachments[0], dict)
|
assert isinstance(user_attachments[0], dict)
|
||||||
assert isinstance(user_attachments[0].get("path"), str)
|
assert isinstance(user_attachments[0].get("url"), str)
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
async with _make_session() as session:
|
||||||
session_row = await session.get(AgentChatSession, UUID(thread_id))
|
session_row = await session.get(AgentChatSession, UUID(thread_id))
|
||||||
assert session_row is not None
|
assert session_row is not None
|
||||||
assert session_row.message_count >= 1
|
assert session_row.message_count >= 1
|
||||||
@@ -288,7 +302,7 @@ async def test_agent_tool_call_result_persisted_live() -> None:
|
|||||||
f"no terminal event, got: {event_names}"
|
f"no terminal event, got: {event_names}"
|
||||||
)
|
)
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
async with _make_session() as session:
|
||||||
rows = await session.execute(
|
rows = await session.execute(
|
||||||
select(AgentChatMessage).where(
|
select(AgentChatMessage).where(
|
||||||
AgentChatMessage.session_id == UUID(thread_id),
|
AgentChatMessage.session_id == UUID(thread_id),
|
||||||
|
|||||||
@@ -42,14 +42,12 @@ def test_build_agent_prompt_for_router_contains_identity_and_config() -> None:
|
|||||||
|
|
||||||
assert "- type: router" in prompt
|
assert "- type: router" in prompt
|
||||||
assert "[Router Agent]" in prompt
|
assert "[Router Agent]" in prompt
|
||||||
assert "When the task will require project_cli, include canonical tool input defaults in context_summary using the exact shape `project_cli_defaults={\"module\":...,\"method\":...,\"input\":{...}}` whenever they can be determined safely." in prompt
|
assert "- Set context_summary to a brief but execution-useful summary of the relevant context, including known IDs, dates, time ranges, and prior tool outcomes when they matter." in prompt
|
||||||
assert "Standardize every time value mentioned in context_summary to the exact project_cli input format that would be required downstream: dates as `YYYY-MM-DD`, local datetimes as RFC3339 with timezone offset, and event ids as raw UUID strings." in prompt
|
|
||||||
assert "For relative time requests like today, tomorrow, or next Monday, resolve them using system_time_local and place the resolved standardized value into project_cli_defaults.input instead of leaving natural-language time phrases." in prompt
|
|
||||||
assert "context_messages.mode=day" in prompt
|
assert "context_messages.mode=day" in prompt
|
||||||
assert "context_messages.count=2" in prompt
|
assert "context_messages.count=2" in prompt
|
||||||
|
|
||||||
|
|
||||||
def test_build_worker_contract_prompt_prefers_resolved_dates_from_context_summary() -> None:
|
def test_build_worker_contract_prompt_contains_objective_and_context() -> None:
|
||||||
prompt = build_worker_contract_prompt(
|
prompt = build_worker_contract_prompt(
|
||||||
router_output=RouterAgentOutput(
|
router_output=RouterAgentOutput(
|
||||||
objective="查询今天日程",
|
objective="查询今天日程",
|
||||||
@@ -58,4 +56,7 @@ def test_build_worker_contract_prompt_prefers_resolved_dates_from_context_summar
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "If context_summary contains project_cli_defaults, prefer using those exact module/method/input values directly." in prompt
|
assert "Keep routed objective unchanged." in prompt
|
||||||
|
assert "Use context_summary to understand conversational background and reuse concrete facts already known from earlier context." in prompt
|
||||||
|
assert "requires_tool_evidence" in prompt
|
||||||
|
assert "2026-04-24" in prompt
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ def test_view_skill_file_reads_calendar_action_card() -> None:
|
|||||||
block = response.content[0]
|
block = response.content[0]
|
||||||
text = block["text"] if isinstance(block, dict) else block.text
|
text = block["text"] if isinstance(block, dict) else block.text
|
||||||
assert "get_event" in text
|
assert "get_event" in text
|
||||||
assert '"action": "get_event"' in text
|
assert '"method": "read"' in text
|
||||||
|
assert '"mode": "event"' in text
|
||||||
assert skill_session.has_read(skill_name="calendar") is True
|
assert skill_session.has_read(skill_name="calendar") is True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ def test_build_usage_metadata_falls_back_when_provider_cost_incomplete() -> None
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert metadata["cost"] == pytest.approx(0.0023)
|
assert metadata["cost"] == pytest.approx(0.0012)
|
||||||
assert metadata["costSource"] == "catalog_fallback_incomplete_provider_cost"
|
assert metadata["costSource"] == "catalog_fallback_incomplete_provider_cost"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user