feat: add Windows computer-use plugin adapter and validation tools
This commit is contained in:
1 parent
6431b68d5b
commit
254ce4ffb3
11 files changed
+700
-2
No files matched your search
@@ -0,0 +1,142 @@
|
||||
"""Opt-in Windows smoke: opens two disposable fixture windows, never user apps.
|
||||
|
||||
Run with ARTEMIS's Python environment and the built Windows executable path.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import io
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
from PIL import Image
|
||||
|
||||
|
||||
async def main(executable: Path):
|
||||
processes = []
|
||||
with tempfile.TemporaryDirectory(prefix="artemis-capture-smoke-") as directory:
|
||||
folder = Path(directory)
|
||||
title = "ARTEMIS-Capture-" + uuid4().hex[:8]
|
||||
|
||||
async def launch(cover=False):
|
||||
ready = folder / ("cover.ready" if cover else "target.ready")
|
||||
command = [
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-File",
|
||||
str(Path(__file__).with_name("capture-fixture.ps1")),
|
||||
"-Title",
|
||||
title + ("-cover" if cover else ""),
|
||||
"-ReadyFile",
|
||||
str(ready),
|
||||
]
|
||||
if cover:
|
||||
command.append("-Cover")
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*command,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
processes.append(process)
|
||||
for _ in range(200):
|
||||
if ready.exists():
|
||||
await asyncio.sleep(0.5)
|
||||
return
|
||||
if process.returncode is not None:
|
||||
raise RuntimeError("Fixture failed to start")
|
||||
await asyncio.sleep(0.05)
|
||||
raise TimeoutError("Fixture was not ready")
|
||||
|
||||
try:
|
||||
await launch()
|
||||
async with (
|
||||
stdio_client(
|
||||
StdioServerParameters(command=str(executable), args=["mcp"])
|
||||
) as streams,
|
||||
ClientSession(*streams) as session,
|
||||
):
|
||||
await session.initialize()
|
||||
|
||||
async def observe():
|
||||
# PID is discovered from our owned fixture; never target another app.
|
||||
result = await session.call_tool(
|
||||
"get_app_state", {"app": str(processes[0].pid)}
|
||||
)
|
||||
assert not result.isError, result
|
||||
block = next((b for b in result.content if b.type == "image"), None)
|
||||
assert block is not None, (
|
||||
"Target window did not return a usable image"
|
||||
)
|
||||
with Image.open(
|
||||
io.BytesIO(base64.b64decode(block.data))
|
||||
) as picture:
|
||||
rgb = picture.convert("RGB")
|
||||
pixels = list(rgb.getdata())
|
||||
green = sum(g > 220 and r < 40 and b < 40 for r, g, b in pixels)
|
||||
magenta = sum(
|
||||
r > 220 and b > 220 and g < 40 for r, g, b in pixels
|
||||
)
|
||||
assert green / len(pixels) > 0.5, (
|
||||
"Capture lost the target surface"
|
||||
)
|
||||
assert magenta == 0, "Capture included the overlapping window"
|
||||
content = "\n".join(
|
||||
b.text for b in result.content if b.type == "text"
|
||||
)
|
||||
bounds = re.search(
|
||||
r"Capture coordinate target[^\n]*Frame:.*x: (\d+), y: (\d+), width: (\d+), height: (\d+)",
|
||||
content,
|
||||
)
|
||||
assert bounds, (
|
||||
"Fixture button is missing from the accessibility tree"
|
||||
)
|
||||
x, y, w, h = map(int, bounds.groups())
|
||||
r, g, b = rgb.getpixel((x + 8, y + h // 2))
|
||||
assert r > 180 and g < 80 and b < 80, (
|
||||
"Accessibility coordinates do not match screenshot pixels at this DPI"
|
||||
)
|
||||
return picture.size, (x + w // 2, y + h // 2)
|
||||
|
||||
before = await observe()
|
||||
await launch(cover=True)
|
||||
after = await observe()
|
||||
assert before == after
|
||||
clicked = await session.call_tool(
|
||||
"click",
|
||||
{
|
||||
"app": str(processes[0].pid),
|
||||
"click_method": "app_post",
|
||||
"x": after[1][0],
|
||||
"y": after[1][1],
|
||||
},
|
||||
)
|
||||
assert not clicked.isError, clicked
|
||||
for _ in range(40):
|
||||
if (folder / "target.ready.clicked").exists():
|
||||
break
|
||||
await asyncio.sleep(0.05)
|
||||
assert (folder / "target.ready.clicked").read_text() == "clicked", (
|
||||
"Click did not reach the target control"
|
||||
)
|
||||
print(
|
||||
f"PASS: target surface preserved behind overlapping fixture ({before[0][0]}x{before[0][1]}), coordinate alignment and child-control click verified"
|
||||
)
|
||||
finally:
|
||||
for process in reversed(processes):
|
||||
if process.returncode is None:
|
||||
process.terminate()
|
||||
await asyncio.wait_for(process.wait(), timeout=10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("executable", type=Path)
|
||||
asyncio.run(main(parser.parse_args().executable.resolve()))
|
||||
Reference in new issue
Block a user