121 lines
4.2 KiB
Python
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}
|