char-slop/ai-dots

ai dotfiles

git clone https://git.t4t.associates/char-slop/ai-dots

Charlotte Somcomputer use for windowsdc2c12a

main
7.0 KiB169 linesraw
1import assert from "node:assert/strict";
2import childProcess from "node:child_process";
3import { mkdtemp, readFile, rm } from "node:fs/promises";
4import { syncBuiltinESMExports } from "node:module";
5import { tmpdir } from "node:os";
6import { join } from "node:path";
7import { test, type TestContext } from "node:test";
8import type {
9  ContextEvent, ContextEventResult, ExtensionAPI, ExtensionToolContext,
10  SessionEntry, ToolDefinition,
11} from "@earendil-works/pi-coding-agent";
12import computerUse from "../computer-use.ts";
13import { parameters, type Action } from "../computer-use/backend.ts";
14import { createWindowsBackend } from "../computer-use/windows.ts";
15
16const png = Buffer.from(
17  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aV1kAAAAASUVORK5CYII=",
18  "base64",
19);
20
21async function fixture(t: TestContext, failStartup = false) {
22  const directory = await mkdtemp(join(tmpdir(), "pi-desktop-test-"));
23  const log = join(directory, "actions.jsonl");
24  const spawn = childProcess.spawn;
25  let processes = 0;
26  t.mock.method(childProcess, "spawn", () => {
27    processes++;
28    return spawn(process.execPath, ["-e", failStartup ? `
29      console.error("Desktop initialization failed");
30      process.exit(1);
31    ` : `
32      const fs = require("node:fs");
33      const rl = require("node:readline").createInterface({ input: process.stdin });
34      const record = (value) => fs.appendFileSync(${JSON.stringify(log)}, JSON.stringify(value) + "\\n");
35      rl.on("line", (line) => {
36        const action = JSON.parse(line);
37        record(action);
38        if (action.key === "Wait") return;
39        const response = action.key === "Unknown"
40          ? { error: "Unsupported key name: Unknown" }
41          : action.action === "screenshot" ? { png: ${JSON.stringify(png.toString("base64"))} } : {};
42        console.log(JSON.stringify(response));
43      });
44      rl.on("close", () => record("released"));
45    `], { stdio: ["pipe", "pipe", "pipe"] });
46  });
47  syncBuiltinESMExports();
48  t.after(async () => {
49    t.mock.restoreAll();
50    syncBuiltinESMExports();
51    await rm(directory, { recursive: true, force: true });
52  });
53
54  let tool: ToolDefinition<typeof parameters, { screenshotIds: string[] }> | undefined;
55  let context: ((event: ContextEvent) => ContextEventResult) | undefined;
56  let agentEnd: (() => void) | undefined;
57  const platform = Object.getOwnPropertyDescriptor(process, "platform")!;
58  try {
59    Object.defineProperty(process, "platform", { value: "win32" });
60    computerUse({
61      on(name: string, handler: unknown) {
62        if (name === "context") context = handler as typeof context;
63        if (name === "agent_end") agentEnd = handler as typeof agentEnd;
64      },
65      registerTool(definition: ToolDefinition<typeof parameters, { screenshotIds: string[] }>) { tool = definition; },
66    } as ExtensionAPI);
67  } finally {
68    Object.defineProperty(process, "platform", platform);
69  }
70  assert.ok(tool);
71  assert.ok(context);
72  assert.ok(agentEnd);
73  const branch: SessionEntry[] = [];
74  return {
75    tool,
76    context,
77    agentEnd,
78    branch,
79    processes: () => processes,
80    records: async () => (await readFile(log, "utf8")).trim().split("\n").map((line) => JSON.parse(line)),
81    execute(actions: Action[], signal?: AbortSignal) {
82      return tool!.execute("call-1", { actions }, signal, undefined, {
83        sessionManager: { getBranch: () => branch },
84      } as ExtensionToolContext);
85    },
86  };
87}
88
89test("Windows actions share one helper and preserve drag state and Unicode payloads", async (t) => {
90  const f = await fixture(t);
91  const actions: Action[] = [
92    { action: "pointer", x: 100, y: 200, buttons: ["left"] },
93    { action: "pointer", x: 700, y: 400, buttons: ["left"] },
94    { action: "scroll", x: 700, y: 400, direction: "right", ticks: 3, buttons: ["left"] },
95    { action: "pointer", x: 700, y: 400, buttons: [] },
96    { action: "type", text: 'é中😀\n\t"\\' },
97    { action: "key", key: "a", modifiers: ["ctrl"] },
98    { action: "new_browser_tab", url: "https://example.org/" },
99    { action: "screenshot" },
100  ];
101  const result = await f.execute(actions);
102  assert.equal(f.processes(), 1);
103  assert.deepEqual(await f.records(), [...actions, "released"]);
104  assert.deepEqual(result.content.filter((part) => part.type === "image"), [
105    { type: "image", data: png.toString("base64"), mimeType: "image/png" },
106  ]);
107});
108
109test("screenshot visibility and recall remain shared across backends", async (t) => {
110  const f = await fixture(t);
111  const result = await f.execute([{ action: "screenshot" }]);
112  const { screenshotIds: [id] } = result.details as { screenshotIds: string[] };
113  const message = {
114    role: "toolResult" as const, toolName: "computer_use", toolCallId: "call-1",
115    content: result.content, details: result.details, isError: false, timestamp: Date.now(),
116  };
117  f.branch.push({ type: "message", id: "entry-1", parentId: null, timestamp: new Date().toISOString(), message });
118  const event: ContextEvent = { type: "context", messages: [message] };
119  assert.deepEqual(f.context(event).messages, [message]);
120  f.agentEnd();
121  assert.deepEqual(f.context(event).messages, [{
122    ...message, content: message.content.filter((part) => part.type !== "image"),
123  }]);
124  const recalled = await f.execute([{ action: "recall", id }]);
125  assert.deepEqual(recalled.content.filter((part) => part.type === "image"), result.content.filter((part) => part.type === "image"));
126  assert.equal(f.processes(), 1);
127  assert.deepEqual(f.context(event).messages, [message]);
128});
129
130test("native errors still close the helper and release inputs", async (t) => {
131  const f = await fixture(t);
132  await assert.rejects(f.execute([{ action: "key", key: "Unknown" }]), /Unsupported key name/);
133  assert.equal((await f.records()).at(-1), "released");
134});
135
136test("cancellation closes stdin gracefully instead of leaving held inputs", async (t) => {
137  const f = await fixture(t);
138  const controller = new AbortController();
139  const result = f.execute([{ action: "key", key: "Wait" }], controller.signal);
140  controller.abort();
141  await assert.rejects(result, /Operation aborted/);
142  assert.equal((await f.records()).at(-1), "released");
143});
144
145test("helper startup diagnostics are surfaced", async (t) => {
146  const f = await fixture(t, true);
147  await assert.rejects(f.execute([{ action: "screenshot" }]), /Desktop initialization failed/);
148});
149
150test("unsafe browser URLs fail before starting a desktop helper", async (t) => {
151  const f = await fixture(t);
152  await assert.rejects(f.execute([{ action: "new_browser_tab", url: "file:///C:/Windows" }]), /Only HTTP and HTTPS/);
153  assert.equal(f.processes(), 0);
154});
155
156test("native Windows screen capture returns a 1920×1080 PNG", {
157  skip: process.platform !== "win32" || process.env.PI_TEST_WINDOWS_DESKTOP !== "1",
158}, async () => {
159  const desktop = createWindowsBackend();
160  try {
161    const image = await desktop.execute({ action: "screenshot" });
162    assert.ok(image);
163    assert.equal(image.subarray(1, 4).toString(), "PNG");
164    assert.equal(image.readUInt32BE(16), 1920);
165    assert.equal(image.readUInt32BE(20), 1080);
166  } finally {
167    await desktop.close();
168  }
169});