mcqemu/tests/test_guest.py

121 lines
4.2 KiB
Python

"""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}