Files
Artemis-Eapil/tests/unit/mcp/test_automation_tools.py
T

157 lines
6.2 KiB
Python

"""MCP transport contracts: no real device actions, files and HTTP are isolated."""
from unittest.mock import AsyncMock
import httpx
import pytest
from mcp_server.tools import automation as tools
@pytest.mark.asyncio
async def test_favorites_share_backend_preserve_request_id_and_refuse_secrets(monkeypatch):
request = AsyncMock(return_value={"id": "favorite"})
monkeypatch.setattr(tools, "_request", request)
await tools.mobile_favorites("create", source_kind="batch", source_id="source")
request.assert_awaited_once_with(
"POST",
"/api/automation/favorites",
json={"source_kind": "batch", "source_id": "source", "version": None},
)
request.reset_mock()
with pytest.raises(ValueError):
await tools.mobile_favorites("run", favorite_id="favorite")
with pytest.raises(ValueError, match="密钥"):
await tools.mobile_favorites(
"update",
favorite_id="favorite",
changes={"case_configs": {"0": {"api_key": "never-send"}}},
)
request.assert_not_awaited()
await tools.mobile_favorites(
"run",
favorite_id="favorite",
request_id="stable",
changes={"task_config": {"model": "original"}},
)
request.assert_awaited_once_with(
"POST",
"/api/automation/favorites/favorite/run",
json={"task_config": {"model": "original"}, "request_id": "stable"},
)
@pytest.mark.asyncio
async def test_config_requires_explicit_persistence_and_excludes_secrets(monkeypatch):
request = AsyncMock(
return_value={
"model": "old",
"base_url": "https://example.test/v1",
"api_key_configured": True,
"env_path": "/private/.env",
"reasoning_effort": "high",
}
)
monkeypatch.setattr(tools, "_request", request)
with pytest.raises(ValueError, match="persist_defaults"):
await tools.mobile_config(changes={"model": "new"})
with pytest.raises(ValueError, match="密钥"):
await tools.mobile_config(changes={"api_key": "never-sent"}, persist_defaults=True)
request.assert_not_awaited()
await tools.mobile_config(changes={"model": "new"}, persist_defaults=True)
payload = request.call_args.kwargs["json"]
assert payload == {
"model": "new",
"base_url": "https://example.test/v1",
"reasoning_effort": "high",
}
await tools.mobile_config(
changes={"rate_limit_retry_seconds": 90, "tool_fallback_model": "gpt-6-luna"},
persist_defaults=True,
)
payload = request.call_args.kwargs["json"]
assert payload["rate_limit_retry_seconds"] == 90
assert payload["tool_fallback_model"] == "gpt-6-luna"
@pytest.mark.asyncio
async def test_preview_is_read_only_and_create_is_draft(monkeypatch, tmp_path):
source = tmp_path / "cases.xlsx"
source.write_bytes(b"test workbook")
request = AsyncMock(return_value={"valid": True, "tasks": [], "errors": []})
monkeypatch.setattr(tools, "_request", request)
await tools.mobile_task_file("preview", str(source))
assert request.call_args.args == ("POST", "/api/automation/import/preview")
request.reset_mock()
with pytest.raises(ValueError):
await tools.mobile_batch("create", batch={"name": "x"})
request.assert_not_awaited()
selected = [{"name": "选中用例", "goal": "观察设置"}]
await tools.mobile_batch(
"create", request_id="stable-id", batch={"name": "x", "cases": selected, "repeat_count": 3}
)
assert request.await_count == 1
assert request.call_args.kwargs["json"]["request_id"] == "stable-id"
assert request.call_args.kwargs["json"]["cases"] == selected
assert request.call_args.kwargs["json"]["repeat_count"] == 3
assert request.call_args.args[1] == "/api/automation/batches"
@pytest.mark.asyncio
async def test_running_batch_start_is_idempotent(monkeypatch):
request = AsyncMock(return_value={"id": "batch", "status": "running"})
monkeypatch.setattr(tools, "_request", request)
await tools.mobile_batch("start", batch_id="batch")
assert request.await_count == 1
assert request.call_args.args[0] == "GET"
@pytest.mark.asyncio
async def test_delete_batch_uses_the_same_backend_as_the_web_ui(monkeypatch):
request = AsyncMock(return_value={"batch_id": "batch", "status": "deleted"})
monkeypatch.setattr(tools, "_request", request)
assert (await tools.mobile_batch("delete", batch_id="batch"))["status"] == "deleted"
request.assert_awaited_once_with("DELETE", "/api/automation/batches/batch")
@pytest.mark.asyncio
async def test_download_writes_actual_binary_atomically(monkeypatch, tmp_path):
monkeypatch.setattr(tools, "EXPORT_DIR", tmp_path)
monkeypatch.setattr(tools, "_base_url", AsyncMock(return_value="http://test"))
client_type = httpx.AsyncClient
transport = httpx.MockTransport(
lambda request: httpx.Response(200, content=b"PK\x03\x04report-bytes")
)
monkeypatch.setattr(
tools.httpx, "AsyncClient", lambda **kw: client_type(transport=transport, **kw)
)
result = await tools._download(
"/api/automation/reports/test/docx",
"test.docx",
owner_kind="sessions",
owner_id="test",
)
from pathlib import Path
assert Path(result["path"]).read_bytes() == b"PK\x03\x04report-bytes"
assert Path(result["path"]).is_absolute()
assert Path(result["path"]).parent == tmp_path / "sessions" / "test"
assert result["download_url"] == "http://test/api/automation/reports/test/docx"
assert not list(tmp_path.rglob("*.part"))
@pytest.mark.asyncio
async def test_export_uses_saved_formats_without_mutating_execution(monkeypatch):
request = AsyncMock(
side_effect=[{"formats": ["xlsx"]}, {"verdict": "未验证", "status": "completed"}]
)
download = AsyncMock(return_value={"path": "/exports/report.xlsx"})
monkeypatch.setattr(tools, "_request", request)
monkeypatch.setattr(tools, "_download", download)
result = await tools.mobile_export_report(session_id="session")
assert result["report"]["verdict"] == "未验证"
assert len(result["files"]) == 1
assert all(call.args[0] == "GET" for call in request.call_args_list)
assert download.call_args.args[0].endswith("/xlsx")