From caf9304064d31a8dc13ba2929b72211cb51f07f6 Mon Sep 17 00:00:00 2001 From: zl-q Date: Thu, 19 Mar 2026 00:52:12 +0800 Subject: [PATCH] =?UTF-8?q?test(backend):=20=E6=9B=B4=E6=96=B0=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/core/agentscope/events/test_store.py | 6 +- .../core/agentscope/runtime/test_runner.py | 295 +----------------- .../unit/core/agentscope/test_agent_prompt.py | 104 ++---- .../core/agentscope/test_runtime_prompt.py | 73 ----- .../unit/core/agentscope/test_toolkit.py | 2 +- backend/tests/unit/v1/agent/test_service.py | 2 +- backend/tests/unit/v1/agent/test_utils.py | 4 +- 7 files changed, 48 insertions(+), 438 deletions(-) delete mode 100644 backend/tests/unit/core/agentscope/test_runtime_prompt.py diff --git a/backend/tests/unit/core/agentscope/events/test_store.py b/backend/tests/unit/core/agentscope/events/test_store.py index e39b913..5c572ae 100644 --- a/backend/tests/unit/core/agentscope/events/test_store.py +++ b/backend/tests/unit/core/agentscope/events/test_store.py @@ -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 diff --git a/backend/tests/unit/core/agentscope/runtime/test_runner.py b/backend/tests/unit/core/agentscope/runtime/test_runner.py index 8ba65ba..1782096 100644 --- a/backend/tests/unit/core/agentscope/runtime/test_runner.py +++ b/backend/tests/unit/core/agentscope/runtime/test_runner.py @@ -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" diff --git a/backend/tests/unit/core/agentscope/test_agent_prompt.py b/backend/tests/unit/core/agentscope/test_agent_prompt.py index c84c331..4291066 100644 --- a/backend/tests/unit/core/agentscope/test_agent_prompt.py +++ b/backend/tests/unit/core/agentscope/test_agent_prompt.py @@ -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 "" 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 diff --git a/backend/tests/unit/core/agentscope/test_runtime_prompt.py b/backend/tests/unit/core/agentscope/test_runtime_prompt.py deleted file mode 100644 index a4059f9..0000000 --- a/backend/tests/unit/core/agentscope/test_runtime_prompt.py +++ /dev/null @@ -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 diff --git a/backend/tests/unit/core/agentscope/test_toolkit.py b/backend/tests/unit/core/agentscope/test_toolkit.py index 20f1a7e..1c83427 100644 --- a/backend/tests/unit/core/agentscope/test_toolkit.py +++ b/backend/tests/unit/core/agentscope/test_toolkit.py @@ -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"}, diff --git a/backend/tests/unit/v1/agent/test_service.py b/backend/tests/unit/v1/agent/test_service.py index 843d6d2..3d6f266 100644 --- a/backend/tests/unit/v1/agent/test_service.py +++ b/backend/tests/unit/v1/agent/test_service.py @@ -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": [], diff --git a/backend/tests/unit/v1/agent/test_utils.py b/backend/tests/unit/v1/agent/test_utils.py index 70e7ff9..22a9487 100644 --- a/backend/tests/unit/v1/agent/test_utils.py +++ b/backend/tests/unit/v1/agent/test_utils.py @@ -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"}}} }, )