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.
175 lines
6.8 KiB
Python
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")
|