test(backend): 更新后端测试文件

This commit is contained in:
zl-q
2026-03-19 00:52:12 +08:00
parent 522b53c993
commit caf9304064
7 changed files with 48 additions and 438 deletions
@@ -99,9 +99,9 @@ async def test_store_persists_worker_output_with_answer_as_content(
assert append_kwargs["seq"] == 7
assert append_kwargs["content"] == "worker-answer"
metadata = cast(dict[str, Any], append_kwargs["metadata"])
assert sorted(metadata.keys()) == ["agent_type", "run_id", "worker_agent_output"]
assert metadata["worker_agent_output"]["answer"] == "worker-answer"
assert metadata["worker_agent_output"]["ui_hints"]["intent"] == "message"
assert sorted(metadata.keys()) == ["agent_output", "agent_type", "run_id"]
assert metadata["agent_output"]["answer"] == "worker-answer"
assert metadata["agent_output"]["ui_hints"]["intent"] == "message"
assert append_kwargs["cost"] == Decimal("0.123")
assert captured["message_delta"] == 1
assert captured["token_delta"] == 8
@@ -1,53 +1,10 @@
from __future__ import annotations
from unittest.mock import AsyncMock
import pytest
from ag_ui.core import RunAgentInput
from agentscope.message import Msg
from core.agentscope.runtime.runner import (
AgentScopeRunner,
StageExecutionResult,
SystemAgentRuntimeConfig,
)
from core.agentscope.utils import safe_json_loads_with_repair
from schemas.agent.runtime_models import (
RouterAgentOutput,
UiMode,
WorkerAgentOutputRich,
)
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
from schemas.user.context import UserContext, parse_profile_settings
class _FakePipeline:
def __init__(self) -> None:
self.events: list[dict[str, object]] = []
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
self.events.append({"session_id": session_id, "event": event})
return "1-0"
class _FakeSessionCtx:
def __init__(self, session: object) -> None:
self._session = session
async def __aenter__(self) -> object:
return self._session
async def __aexit__(self, exc_type, exc, tb) -> None:
del exc_type, exc, tb
def _user_context() -> UserContext:
return UserContext(
id="00000000-0000-0000-0000-000000000001",
username="alice",
email="alice@example.com",
settings=parse_profile_settings(None),
)
from core.agentscope.runtime.runner import AgentScopeRunner
from schemas.agent.system_agent import AgentType
def _run_input() -> RunAgentInput:
@@ -64,215 +21,19 @@ def _run_input() -> RunAgentInput:
)
def _router_output(*, ui_mode: UiMode) -> RouterAgentOutput:
return RouterAgentOutput.model_validate(
{
"normalized_task_input": {
"user_text": "hello",
"multimodal_summary": [],
},
"key_entities": [],
"constraints": [],
"task_typing": {"primary": "knowledge", "secondary": []},
"execution_mode": "onestep",
"result_typing": {"primary": "direct_answer", "secondary": []},
"ui": {
"ui_mode": ui_mode.value,
"ui_decision_reason": "need structure"
if ui_mode == UiMode.RICH
else "plain text",
},
}
)
def test_resolve_stage_agent_type_defaults_to_worker() -> None:
assert AgentScopeRunner._resolve_stage_agent_type("") == AgentType.WORKER
assert AgentScopeRunner._resolve_stage_agent_type("worker") == AgentType.WORKER
assert AgentScopeRunner._resolve_stage_agent_type("unknown") == AgentType.WORKER
def test_build_worker_input_messages_includes_field_guide() -> None:
runner = AgentScopeRunner()
messages = runner._build_worker_input_messages(
router_output=_router_output(ui_mode=UiMode.NONE)
)
assert len(messages) == 1
content = str(messages[0].content)
assert "[Worker Contract]" in content
assert "Use normalized_task_input as objective text." in content
assert "multimodal_summary/key_entities/constraints" in content
assert "key_entities" in content
assert "constraints" in content
assert "Infer deterministic missing required tool args" in content
def test_safe_json_loads_with_repair_parses_valid_json() -> None:
parsed = safe_json_loads_with_repair('{"operation":"create","title":"test"}')
assert parsed["operation"] == "create"
assert parsed["title"] == "test"
def test_resolve_stage_agent_type_supports_memory() -> None:
assert AgentScopeRunner._resolve_stage_agent_type("memory") == AgentType.MEMORY
@pytest.mark.asyncio
async def test_execute_uses_router_ui_mode_to_select_worker_output_model(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def test_resolve_runtime_client_time_from_forwarded_props() -> None:
runner = AgentScopeRunner()
pipeline = _FakePipeline()
worker_model_holder: dict[str, type[object]] = {}
class _CommitSession:
async def commit(self) -> None:
return None
monkeypatch.setattr(
"core.agentscope.runtime.runner.AsyncSessionLocal",
lambda: _FakeSessionCtx(_CommitSession()),
)
async def _load_system_agent_config(**kwargs):
return SystemAgentRuntimeConfig(
agent_type=kwargs["agent_type"],
model_code="qwen3.5-flash"
if kwargs["agent_type"] == AgentType.ROUTER
else "deepseek-chat",
api_base_url="https://example.com/v1",
api_key="sk-test",
llm_config=SystemAgentLLMConfig(
temperature=0.1, max_tokens=256, timeout_seconds=30
),
)
monkeypatch.setattr(runner, "_load_system_agent_config", _load_system_agent_config)
async def _run_router_stage(**kwargs):
return StageExecutionResult(
message=Msg(name="router", content="", role="assistant"),
payload=_router_output(ui_mode=UiMode.RICH).model_dump(mode="json"),
response_metadata={
"model": "qwen3.5-flash",
"inputTokens": 12,
"outputTokens": 6,
"cost": 0.001,
"latencyMs": 50,
},
)
monkeypatch.setattr(runner, "_run_router_stage", _run_router_stage)
async def _persist_router_message(**kwargs) -> None:
assert kwargs["model_code"] == "qwen3.5-flash"
monkeypatch.setattr(
"core.agentscope.runtime.runner.persist_router_message",
_persist_router_message,
)
async def _run_worker_stage(**kwargs):
worker_model_holder["model"] = kwargs["worker_output_model"]
return StageExecutionResult(
message=Msg(name="worker", content="done", role="assistant"),
payload=WorkerAgentOutputRich.model_validate(
{
"status": "success",
"answer": "done",
"key_points": [],
"result_type": "direct_answer",
"suggested_actions": [],
"error": None,
"ui_hints": None,
}
).model_dump(mode="json", exclude_none=True),
response_metadata={
"model": "deepseek-chat",
"inputTokens": 8,
"outputTokens": 4,
"cost": 0.002,
"latencyMs": 40,
},
)
monkeypatch.setattr(runner, "_run_worker_stage", _run_worker_stage)
result = await runner.execute(
user_context=_user_context(),
context_messages=[],
pipeline=pipeline,
run_input=_run_input(),
)
assert worker_model_holder["model"].__name__ == "WorkerAgentOutputRich"
event_types = []
for item in pipeline.events:
event = item.get("event")
if isinstance(event, dict):
event_types.append(event.get("type"))
assert event_types == [
"STEP_STARTED",
"STEP_FINISHED",
"STEP_STARTED",
"STEP_FINISHED",
]
assert result["router"]["ui"]["ui_mode"] == "rich"
assert result["worker"]["answer"] == "done"
@pytest.mark.asyncio
async def test_execute_passes_runtime_client_time_to_router_and_worker(
monkeypatch: pytest.MonkeyPatch,
) -> None:
runner = AgentScopeRunner()
pipeline = _FakePipeline()
captured: dict[str, object] = {}
class _CommitSession:
async def commit(self) -> None:
return None
monkeypatch.setattr(
"core.agentscope.runtime.runner.AsyncSessionLocal",
lambda: _FakeSessionCtx(_CommitSession()),
)
async def _load_system_agent_config(**kwargs):
return SystemAgentRuntimeConfig(
agent_type=kwargs["agent_type"],
model_code="model-a",
api_base_url="https://example.com/v1",
api_key="sk-test",
llm_config=SystemAgentLLMConfig(
temperature=0.1, max_tokens=256, timeout_seconds=30
),
)
monkeypatch.setattr(runner, "_load_system_agent_config", _load_system_agent_config)
async def _run_router_stage(**kwargs):
captured["router_timezone"] = kwargs["runtime_client_time"].device_timezone
return StageExecutionResult(
message=Msg(name="router", content="", role="assistant"),
payload=_router_output(ui_mode=UiMode.NONE).model_dump(mode="json"),
response_metadata={},
)
async def _run_worker_stage(**kwargs):
captured["worker_timezone"] = kwargs["runtime_client_time"].device_timezone
return StageExecutionResult(
message=Msg(name="worker", content="ok", role="assistant"),
payload={
"status": "success",
"answer": "ok",
"key_points": [],
"result_type": "direct_answer",
"suggested_actions": [],
"error": None,
},
response_metadata={},
)
monkeypatch.setattr(runner, "_run_router_stage", _run_router_stage)
monkeypatch.setattr(runner, "_run_worker_stage", _run_worker_stage)
monkeypatch.setattr(
"core.agentscope.runtime.runner.persist_router_message", AsyncMock()
)
run_input = RunAgentInput.model_validate(
{
"threadId": "00000000-0000-0000-0000-000000000010",
@@ -291,38 +52,6 @@ async def test_execute_passes_runtime_client_time_to_router_and_worker(
}
)
await runner.execute(
user_context=_user_context(),
context_messages=[],
pipeline=pipeline,
run_input=run_input,
)
assert captured["router_timezone"] == "America/Los_Angeles"
assert captured["worker_timezone"] == "America/Los_Angeles"
def test_resolve_provider_api_key_maps_volcengine_to_ark(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"core.agentscope.runtime.runner.config.llm.provider_keys",
{"ARK": "ark-key", "DASHSCOPE": "dash-key"},
)
assert (
AgentScopeRunner._resolve_provider_api_key(factory_name="volcengine")
== "ark-key"
)
def test_resolve_provider_api_key_raises_when_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"core.agentscope.runtime.runner.config.llm.provider_keys",
{"DASHSCOPE": "dash-key"},
)
with pytest.raises(RuntimeError, match="provider api key missing"):
AgentScopeRunner._resolve_provider_api_key(factory_name="deepseek")
resolved = runner._resolve_runtime_client_time(run_input=run_input)
assert resolved is not None
assert resolved.device_timezone == "America/Los_Angeles"
@@ -1,85 +1,41 @@
from __future__ import annotations
from core.agentscope.prompts.agent_prompt import (
ROUTER_AGENT_INSTRUCTION,
WORKER_AGENT_INSTRUCTION,
build_agent_prompt,
build_intent_user_prompt,
build_worker_contract_prompt,
)
from schemas.agent.runtime_models import RouterAgentOutput
from schemas.agent.system_agent import AgentType
from core.agentscope.prompts.agent_prompt import build_agent_prompt
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
def test_build_intent_user_prompt_embeds_router_schema_for_text_input() -> None:
prompt = build_intent_user_prompt(user_input="请总结这张截图")
assert isinstance(prompt, str)
assert ROUTER_AGENT_INSTRUCTION in prompt
assert "[Output Schema]" in prompt
assert '"normalized_task_input"' in prompt
assert "[User Input]" in prompt
def test_build_agent_prompt_for_router_focuses_on_routing_contract() -> None:
prompt = build_agent_prompt(agent_type=AgentType.ROUTER)
def test_build_agent_prompt_for_worker_contains_runtime_config() -> None:
prompt = build_agent_prompt(
agent_type=AgentType.WORKER,
llm_config=SystemAgentLLMConfig.model_validate(
{
"temperature": 0.2,
"context_messages": {"mode": "number", "count": 20},
"enabled_tool_groups": ["read", "write"],
}
),
)
assert "<!-- AGENT_START -->" in prompt
assert "[Agent Identity]" in prompt
assert "- type: router" in prompt
assert ROUTER_AGENT_INSTRUCTION in prompt
assert "extract intent and route strategy" in prompt
assert "never answer user directly" in prompt
assert "multimodal_summary" in prompt
assert "Set execution_mode by complexity" in prompt
assert "result_typing.primary" in prompt
def test_build_agent_prompt_for_worker_relies_on_injected_schema() -> None:
prompt = build_agent_prompt(agent_type=AgentType.WORKER)
assert "- type: worker" in prompt
assert WORKER_AGENT_INSTRUCTION in prompt
assert "execute routed objective" in prompt
assert "objective/constraints contract" in prompt
assert "Infer deterministic required tool arguments" in prompt
assert "Ask minimal clarification" in prompt
assert "never fabricate execution state" in prompt
assert (
"The worker output schema is injected at runtime; follow it exactly." in prompt
)
assert "Do not add fields that are not present in the injected schema." in prompt
assert "ui_mode=rich" not in prompt
assert "ui_mode=none" not in prompt
assert "context_messages.mode=number" in prompt
assert "context_messages.count=20" in prompt
assert "enabled_tool_groups=read,write" in prompt
def test_build_worker_contract_prompt_is_concise_and_actionable() -> None:
router_output = RouterAgentOutput.model_validate(
{
"normalized_task_input": {
"user_text": "创建明天 9 点会议",
"multimodal_summary": ["图片里有会议时间"],
},
"key_entities": [
{
"name": "start",
"type": "datetime",
"value": "2026-03-17T09:00:00+08:00",
}
],
"constraints": [
{"key": "timezone", "value": "Asia/Shanghai", "required": True}
],
"task_typing": {"primary": "scheduling", "secondary": []},
"execution_mode": "tool_assisted",
"result_typing": {"primary": "execution_report", "secondary": []},
"ui": {"ui_mode": "none", "ui_decision_reason": "plain"},
}
def test_build_agent_prompt_for_memory_uses_memory_rules() -> None:
prompt = build_agent_prompt(
agent_type=AgentType.MEMORY,
llm_config=SystemAgentLLMConfig.model_validate(
{
"context_messages": {"mode": "day", "count": 2},
"enabled_tool_groups": ["read"],
}
),
)
prompt = build_worker_contract_prompt(router_output=router_output)
assert "[Worker Contract]" in prompt
assert "Infer deterministic missing required tool args" in prompt
assert "[RouterAgentOutput]" in prompt
assert '"execution_mode":"tool_assisted"' in prompt
assert "- type: memory" in prompt
assert "[Memory Agent]" in prompt
assert "context_messages.mode=day" in prompt
assert "context_messages.count=2" in prompt
assert "enabled_tool_groups=read" in prompt
@@ -1,73 +0,0 @@
from __future__ import annotations
from core.agentscope.prompts.runtime_prompt import build_intent_user_prompt
def test_build_intent_user_prompt_keeps_multimodal_blocks() -> None:
prompt = build_intent_user_prompt(
user_input=[
{
"id": "u1",
"role": "user",
"content": [
{"type": "text", "text": "请识别图片内容"},
{
"type": "binary",
"mimeType": "image/png",
"data": "aGVsbG8=",
},
],
}
]
)
assert isinstance(prompt, list)
assert prompt
assert prompt[0]["type"] == "text"
assert "[Output Schema]" in prompt[0]["text"]
image_blocks = [item for item in prompt if item.get("type") == "image"]
assert len(image_blocks) == 1
source = image_blocks[0]["source"]
assert source["type"] == "base64"
assert source["media_type"] == "image/png"
assert source["data"] == "aGVsbG8="
def test_build_intent_user_prompt_filters_non_image_binary_block() -> None:
prompt = build_intent_user_prompt(
user_input=[
{
"id": "u1",
"role": "user",
"content": [
{"type": "text", "text": "请处理这个输入"},
{
"type": "binary",
"mimeType": "application/pdf",
"data": "aGVsbG8=",
},
],
}
]
)
assert isinstance(prompt, list)
image_blocks = [item for item in prompt if item.get("type") == "image"]
assert image_blocks == []
def test_build_intent_user_prompt_includes_previous_context_messages() -> None:
prompt = build_intent_user_prompt(
user_input=[
{"id": "u1", "role": "user", "content": "我的口令是蓝鲸42"},
{"id": "a1", "role": "assistant", "content": "已记住"},
{"id": "u2", "role": "user", "content": "请重复口令"},
]
)
assert isinstance(prompt, list)
assert prompt
instruction = prompt[0].get("text", "")
assert isinstance(instruction, str)
assert "[Conversation Context]" in instruction
assert "\\u84dd\\u9cb842" in instruction
@@ -19,7 +19,7 @@ def test_build_stage_toolkit_filters_requested_tools_by_agent_type(monkeypatch)
)
build_stage_toolkit(
agent_type=AgentType.ROUTER,
agent_type=AgentType.WORKER,
session=cast(Any, object()),
owner_id=uuid4(),
enabled_tool_names={"calendar_read", "calendar_write", "user_lookup"},
+1 -1
View File
@@ -355,7 +355,7 @@ async def test_get_history_snapshot_filters_out_tool_messages() -> None:
"content": "今天共有 3 条日程。",
"metadata": {
"run_id": "run-1",
"worker_agent_output": {
"agent_output": {
"status": "success",
"answer": "今天共有 3 条日程。",
"key_points": [],
+1 -3
View File
@@ -34,9 +34,7 @@ def test_convert_message_to_history_uses_ui_schema_key_for_assistant_message() -
message = _FakeMessage(
role="assistant",
metadata={
"worker_agent_output": {
"ui_schema": {"version": "2.0", "root": {"type": "stack"}}
}
"agent_output": {"ui_schema": {"version": "2.0", "root": {"type": "stack"}}}
},
)