初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+206
View File
@@ -0,0 +1,206 @@
"""send_message 接入 run_agent_turn 的验证测试(阶段2b 步骤B)。
mock 边界:
- ``app.application.chat.send_message.run_agent_turn`` —— 不真实跑 Agent 循环,
只校验 send_message 是否正确接入(传 tools/default_tools、on_chunk、on_tool_event)。
- ``app.application.chat.send_message.default_tools`` —— 校验被调用。
"""
from __future__ import annotations
from collections.abc import AsyncGenerator
from typing import Any
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from app.ai_provider.types.chat import ChatChunk
from app.application.agent.runtime import AgentTurnResult
from app.db.session import AsyncSessionLocal
from app.main import app
from app.models.runtime import Event
@pytest.fixture
async def client(
monkeypatch: pytest.MonkeyPatch, auth_headers: dict[str, str]
) -> AsyncGenerator[AsyncClient, None]:
# 清理前序(test_tools 等)残留的 app.state 避免污染
for _attr in ("tool_runtime", "confirmation_registry", "confirmation_store", "event_store"):
app.state.__dict__.pop(_attr, None)
agent_content = "agent 最终回复正文。"
async def fake_run_agent_turn(
*,
on_chunk: Any,
on_tool_event: Any,
**_kwargs: object,
) -> AgentTurnResult:
# 模拟 Agent 内部流:先一段 reasoning(若 thinking 启用)、再 content chunk
await on_chunk(ChatChunk(delta_reasoning="思考摘要"))
await on_chunk(ChatChunk(delta_content=agent_content))
# 模拟一次工具调用生命周期
await on_tool_event("tool.started", {"tool_name": "echo", "tool_call_id": "c1"})
await on_tool_event(
"tool.completed", {"tool_name": "echo", "tool_call_id": "c1", "status": "success"}
)
return AgentTurnResult(content=agent_content, reasoning="思考摘要")
monkeypatch.setattr("app.application.chat.send_message.run_agent_turn", fake_run_agent_turn)
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport, base_url="http://test", headers=auth_headers
) as http_client:
yield http_client
async def _create_session(client: AsyncClient) -> str:
resp = await client.post("/api/v1/chat/sessions", json={"title": "agent 接入"})
assert resp.status_code == 200
return str(resp.json()["session_id"])
@pytest.mark.asyncio
async def test_send_message_invokes_run_agent_turn_with_tools_and_callbacks(
client: AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
"""send_message 调用 run_agent_turn 时传了 tools=default_tools()。"""
app.dependency_overrides.clear()
captured: dict[str, Any] = {}
captured_count: int = 0
real_default_tools = __import__(
"app.application.chat.send_message", fromlist=["default_tools"]
).default_tools
def spy_default_tools(*args: Any, **kwargs: Any) -> list[dict[str, Any]]:
nonlocal captured_count
captured_count += 1
captured["kwargs"] = kwargs
captured["tools"] = real_default_tools(*args, **kwargs)
return captured["tools"]
monkeypatch.setattr("app.application.chat.send_message.default_tools", spy_default_tools)
from app.application.chat.send_message import drain_pending_streams
session_id = await _create_session(client)
sent = await client.post(
f"/api/v1/chat/sessions/{session_id}/messages",
json={"content": "你好", "modality": "text"},
)
assert sent.status_code == 200
await drain_pending_streams()
# default_tools 被调用,至少包含 5 个核心工具
assert captured_count > 0, f"spy_default_tools never called, captured={captured}"
assert "tools" in captured
tool_names = {t["function"]["name"] for t in captured["tools"]}
assert tool_names >= {"datetime", "read_file", "list_dir", "write_file", "shell"}
assert captured["tools"][0]["type"] == "function"
names = {t["function"]["name"] for t in captured["tools"]}
assert {"datetime", "shell"} <= names
@pytest.mark.asyncio
async def test_send_message_persists_agent_result_content_and_tool_events(
client: AsyncClient,
) -> None:
"""message.completed 的 content == agent_result.content;tool.* 事件落库 source=agent。"""
from app.application.chat.send_message import drain_pending_streams
session_id = await _create_session(client)
sent = await client.post(
f"/api/v1/chat/sessions/{session_id}/messages",
json={"content": "你好", "modality": "text", "thinking": True},
)
assert sent.status_code == 200
run_id = str(sent.json()["run_id"])
await drain_pending_streams()
async with AsyncSessionLocal() as db:
events = (await db.execute(select(Event).where(Event.run_id == run_id))).scalars().all()
by_type: dict[str, list[Event]] = {}
for ev in events:
by_type.setdefault(ev.type, []).append(ev)
# 关键事件齐全
assert "run.started" in by_type
assert "message.delta" in by_type # on_chunk 触发了 content flush
assert "message.completed" in by_type
assert "run.completed" in by_type
# tool 事件存在且 source=agent
assert "tool.started" in by_type
assert "tool.completed" in by_type
assert all(ev.source == "agent" for ev in by_type["tool.started"])
assert all(ev.source == "agent" for ev in by_type["tool.completed"])
# message.completed 的 content == agent_result.content(权威值)
completed = by_type["message.completed"][-1]
import json
payload = json.loads(completed.payload_json)
assert payload["content"] == "agent 最终回复正文。"
@pytest.mark.asyncio
async def test_send_message_writes_extracted_session_goal_to_run(
client: AsyncClient,
) -> None:
"""太一最小闭环:会话目标从用户消息提取写入 run.goal,取代默认值。"""
from app.application.chat.send_message import drain_pending_streams
from app.models.runtime import Run
session_id = await _create_session(client)
sent = await client.post(
f"/api/v1/chat/sessions/{session_id}/messages",
json={"content": "帮我整理今天的笔记", "modality": "text"},
)
assert sent.status_code == 200
run_id = str(sent.json()["run_id"])
await drain_pending_streams()
async with AsyncSessionLocal() as db:
run = (await db.execute(select(Run).where(Run.id == run_id))).scalar_one()
assert run.goal == "帮我整理今天的笔记"
@pytest.mark.asyncio
async def test_send_message_injects_session_goal_into_system_messages(
client: AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
"""洛书最小闭环:会话目标作为 system instruction 注入 ReAct。"""
from app.application.chat.send_message import drain_pending_streams
captured: dict[str, Any] = {}
async def capture_agent_turn(*, messages: Any, **_kwargs: object) -> AgentTurnResult:
captured["messages"] = messages
return AgentTurnResult(content="好的。", reasoning="")
monkeypatch.setattr("app.application.chat.send_message.run_agent_turn", capture_agent_turn)
session_id = await _create_session(client)
await client.post(
f"/api/v1/chat/sessions/{session_id}/messages",
json={"content": "帮我整理笔记", "modality": "text"},
)
await drain_pending_streams()
# to_langchain_messages 把每个 system message 作为独立 SystemMessage,
# 目标片段出现在某个 system message 中(非 messages[0])
system_text = "\n".join(
str(m.content) for m in captured["messages"] if getattr(m, "role", "") == "system"
)
assert "当前会话目标:帮我整理笔记" in system_text