Files
Artemis-Eapil/tests/unit/mcp/test_automation_tools.py
T
chenmingxuan 83bcfd54d5 feat: release 2.1.0 with optional Flash final AI review
Default Flash final review to off and persist the setting across the UI,
MCP, SDK and queued tasks. Keep execution separate from test verdicts.

Bundle UI fonts and licenses to remove Google Fonts from startup builds.
Align backend/frontend versions and document installation and upgrades.
2026-10-23 12:36:56 +08:00

175 lines
6.8 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_config_can_save_flash_review_without_exposing_task_overrides(monkeypatch):
request = AsyncMock(
return_value={"flash_final_check_enabled": False, "recording_enabled": False}
)
monkeypatch.setattr(tools, "_request", request)
await tools.mobile_config(
section="capabilities",
changes={"flash_final_check_enabled": True},
persist_defaults=True,
)
assert request.call_args.args == ("POST", "/api/system/capability-settings")
assert request.call_args.kwargs["json"] == {
"flash_final_check_enabled": True,
"recording_enabled": False,
}
@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")