"""Guest-agent tools with a scripted fake QGA.""" import base64 import pytest from fastmcp import Client from fastmcp.exceptions import ToolError from conftest import FakeQMPClient, result_data, write_registry from mcqemu.server import mcp from test_lifecycle import seeded_record def b64(s: str) -> str: return base64.b64encode(s.encode()).decode() def arm_sync(): FakeQMPClient.responses["guest-sync"] = lambda args: args["id"] async def test_guest_ping(dirs, fake_qmp): arm_sync() FakeQMPClient.responses["guest-ping"] = {} write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data(await client.call_tool("guest_ping", {"name": "vm1"})) assert data["guest_agent"] == "responding" async def test_guest_agent_absent_gives_actionable_error(dirs, fake_qmp): FakeQMPClient.connect_error = ConnectionRefusedError("nobody listening") write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: with pytest.raises(ToolError, match="qemu-guest-agent"): await client.call_tool("guest_ping", {"name": "vm1"}) async def test_guest_exec_collects_output(dirs, fake_qmp): arm_sync() FakeQMPClient.responses["guest-exec"] = {"pid": 77} FakeQMPClient.responses["guest-exec-status"] = { "exited": True, "exitcode": 0, "out-data": b64("Linux vm1 6.1.0\n"), } write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data( await client.call_tool( "guest_exec", {"name": "vm1", "command": "uname", "args": ["-a"]} ) ) assert data["exitcode"] == 0 assert "Linux vm1" in data["stdout"] exec_call = next(a for c, a in FakeQMPClient.calls if c == "guest-exec") assert exec_call == {"path": "uname", "capture-output": True, "arg": ["-a"]} async def test_guest_exec_timeout(dirs, fake_qmp): arm_sync() FakeQMPClient.responses["guest-exec"] = {"pid": 77} FakeQMPClient.responses["guest-exec-status"] = {"exited": False} write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: with pytest.raises(ToolError, match="still running"): await client.call_tool( "guest_exec", {"name": "vm1", "command": "sleep", "args": ["999"], "timeout": 1} ) async def test_guest_file_round_trip(dirs, fake_qmp): arm_sync() FakeQMPClient.responses["guest-file-open"] = 5 FakeQMPClient.responses["guest-file-write"] = {"count": 12} FakeQMPClient.responses["guest-file-read"] = { "count": 12, "buf-b64": b64("hello guest\n"), "eof": True, } FakeQMPClient.responses["guest-file-close"] = {} write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data( await client.call_tool( "guest_file_write", {"name": "vm1", "path": "/tmp/x", "content": "hello guest\n"}, ) ) assert data["bytes_written"] == 12 data = result_data( await client.call_tool("guest_file_read", {"name": "vm1", "path": "/tmp/x"}) ) assert data["content"] == "hello guest\n" assert data["truncated"] is False open_calls = [a for c, a in FakeQMPClient.calls if c == "guest-file-open"] assert {"path": "/tmp/x", "mode": "w"} in open_calls assert {"path": "/tmp/x", "mode": "r"} in open_calls assert len([c for c, _ in FakeQMPClient.calls if c == "guest-file-close"]) == 2 async def test_guest_session_disables_negotiation(dirs, fake_qmp): arm_sync() FakeQMPClient.responses["guest-ping"] = {} seen = {} original_connect = FakeQMPClient.connect async def spy_connect(self, address): seen["negotiate"] = self.negotiate seen["await_greeting"] = self.await_greeting await original_connect(self, address) FakeQMPClient.connect = spy_connect try: write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: await client.call_tool("guest_ping", {"name": "vm1"}) finally: FakeQMPClient.connect = original_connect assert seen == {"negotiate": False, "await_greeting": False}