"""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_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", } @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() await tools.mobile_batch("create", request_id="stable-id", batch={"name": "x"}) assert request.await_count == 1 assert request.call_args.kwargs["json"]["request_id"] == "stable-id" 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") from pathlib import Path assert Path(result["path"]).read_bytes() == b"PK\x03\x04report-bytes" assert Path(result["path"]).is_absolute() assert result["download_url"] == "http://test/api/automation/reports/test/docx" assert not list(tmp_path.glob("*.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")