"""See & drive tools with fake QMP; screendump writes a synthetic PNG.""" import struct 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 fake_png(width: int, height: int) -> bytes: """Just enough PNG for signature + IHDR dimension parsing.""" return ( b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\x0dIHDR" + struct.pack(">II", width, height) + b"\x08\x06\x00\x00\x00" + b"\x00" * 16 ) def arm_screendump(width=640, height=480): def handler(args): with open(args["filename"], "wb") as f: f.write(fake_png(width, height)) return {} FakeQMPClient.responses["screendump"] = handler async def test_screenshot_returns_png_image(dirs, fake_qmp): arm_screendump() write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: result = await client.call_tool("vm_screenshot", {"name": "vm1"}) block = result.content[0] assert block.type == "image" assert block.mimeType == "image/png" call = next(a for c, a in FakeQMPClient.calls if c == "screendump") assert call["format"] == "png" async def test_send_keys_chords(dirs, fake_qmp): FakeQMPClient.responses["send-key"] = {} write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data( await client.call_tool( "vm_send_keys", {"name": "vm1", "keys": ["ctrl-alt-f2", "enter"], "delay_ms": 0} ) ) assert data["keys_sent"] == 2 sent = [a["keys"] for c, a in FakeQMPClient.calls if c == "send-key"] assert sent[0] == [ {"type": "qcode", "data": "ctrl"}, {"type": "qcode", "data": "alt"}, {"type": "qcode", "data": "f2"}, ] assert sent[1] == [{"type": "qcode", "data": "ret"}] async def test_send_keys_unknown_key(dirs, fake_qmp): write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: with pytest.raises(ToolError, match="Unknown key"): await client.call_tool("vm_send_keys", {"name": "vm1", "keys": ["warpdrive"]}) async def test_type_text_with_shift_and_enter(dirs, fake_qmp): FakeQMPClient.responses["send-key"] = {} write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data( await client.call_tool( "vm_type_text", {"name": "vm1", "text": "Hi!", "enter": True, "delay_ms": 0} ) ) assert data["characters_typed"] == 4 # H, i, !, Enter sent = [a["keys"] for c, a in FakeQMPClient.calls if c == "send-key"] assert sent[0] == [{"type": "qcode", "data": "shift"}, {"type": "qcode", "data": "h"}] assert sent[1] == [{"type": "qcode", "data": "i"}] assert sent[2] == [{"type": "qcode", "data": "shift"}, {"type": "qcode", "data": "1"}] assert sent[3] == [{"type": "qcode", "data": "ret"}] async def test_click_scales_to_abs_space(dirs, fake_qmp): arm_screendump(width=640, height=480) FakeQMPClient.responses["input-send-event"] = {} write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data(await client.call_tool("vm_click", {"name": "vm1", "x": 320, "y": 240})) assert "left at (320, 240)" in data["clicked"] events = [a["events"] for c, a in FakeQMPClient.calls if c == "input-send-event"] move = events[0] assert move[0]["data"] == {"axis": "x", "value": int(320 * 32767 / 639)} assert move[1]["data"] == {"axis": "y", "value": int(240 * 32767 / 479)} assert events[1][0]["data"] == {"down": True, "button": "left"} assert events[2][0]["data"] == {"down": False, "button": "left"} async def test_click_outside_display_rejected(dirs, fake_qmp): arm_screendump(width=640, height=480) write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: with pytest.raises(ToolError, match="outside"): await client.call_tool("vm_click", {"name": "vm1", "x": 9999, "y": 10}) async def test_serial_read_tail(dirs, fake_qmp): record = seeded_record(dirs, "vm1", serial_log=str(dirs.state / "serial.log")) (dirs.state / "serial.log").write_text("\n".join(f"line{i}" for i in range(100))) write_registry(dirs, record) async with Client(mcp) as client: data = result_data( await client.call_tool("vm_serial_read", {"name": "vm1", "tail_lines": 3}) ) assert data["log_bytes"] > 0 assert data["tail"] == "line97\nline98\nline99" def hmp_calls(): return [a["command-line"] for c, a in FakeQMPClient.calls if c == "human-monitor-command"] async def test_mouse_move_chunks_deltas(dirs, fake_qmp): FakeQMPClient.responses["human-monitor-command"] = "" write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data( await client.call_tool( "vm_mouse_move", {"name": "vm1", "dx": 70, "dy": -75, "step": 32} ) ) assert data["moved"] == [70, -75] assert hmp_calls() == [ "mouse_move 32 0", "mouse_move 32 0", "mouse_move 6 0", "mouse_move 0 -32", "mouse_move 0 -32", "mouse_move 0 -11", ] async def test_mouse_move_home_pins_to_corner(dirs, fake_qmp): arm_screendump(width=320, height=200) FakeQMPClient.responses["human-monitor-command"] = "" write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data( await client.call_tool( "vm_mouse_move", {"name": "vm1", "home": "bottom-right", "step": 100} ) ) assert data["homed"] == "bottom-right" moves = hmp_calls() # ceil(320/100) + 2 = 6 homing steps, all toward +x/+y assert moves == ["mouse_move 100 100"] * 6 async def test_mouse_move_home_top_left_direction(dirs, fake_qmp): arm_screendump(width=100, height=100) FakeQMPClient.responses["human-monitor-command"] = "" write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: await client.call_tool("vm_mouse_move", {"name": "vm1", "home": "top-left", "step": 50}) assert set(hmp_calls()) == {"mouse_move -50 -50"} async def test_mouse_move_click_sequence(dirs, fake_qmp): FakeQMPClient.responses["human-monitor-command"] = "" write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: data = result_data( await client.call_tool( "vm_mouse_move", {"name": "vm1", "click": "left", "double": True} ) ) assert data["clicked"] == "left double" assert hmp_calls() == ["mouse_button 1", "mouse_button 0"] * 2 async def test_mouse_move_right_click(dirs, fake_qmp): FakeQMPClient.responses["human-monitor-command"] = "" write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: await client.call_tool("vm_mouse_move", {"name": "vm1", "click": "right"}) assert hmp_calls() == ["mouse_button 2", "mouse_button 0"] async def test_mouse_move_rejects_bad_step(dirs, fake_qmp): write_registry(dirs, seeded_record(dirs, "vm1")) async with Client(mcp) as client: with pytest.raises(ToolError, match="step"): await client.call_tool("vm_mouse_move", {"name": "vm1", "dx": 10, "step": 500})