返回 CodeWhale
win32-input.test.mjs
根目录 / crates / tui / plugins / computer-use / tests / win32-input.test.mjs
1 // Windows backend input truthfulness tests. No Windows box (or GUI) is
2 // involved: a fake powershell.exe is placed on PATH, captures the decoded
3 // -EncodedCommand payload of every spawn, and exits with a code the test
4 // controls — mirroring how the failure-truthfulness was verified when the
5 // fix was developed on the codewhale side before this port.
6 import { test as nodeTest } from "node:test";
7 // This transport fixture is a POSIX shell executable, not a Windows PE file.
8 // Windows executes the generated commands against managed stubs in
9 // win32-native-contract.test.mjs; portable runner tests cover every host.
10 const test = (name, fn) => nodeTest(name, { skip: process.platform === "win32" ? "POSIX fake-executable fixture; use the Windows managed-stub suite" : false }, fn);
11 import assert from "node:assert/strict";
12 import fs from "node:fs";
13 import os from "node:os";
14 import path from "node:path";
15 import win32 from "../src/backends/win32.mjs";
16 import { ExecError } from "../src/exec.mjs";
17
18 // Fake powershell.exe logic. It lives in a .mjs file because Node's loader
19 // refuses to execute a script whose name ends in .exe; powershell.exe itself
20 // is a #!/bin/sh launcher that execs node on this file (extension is
21 // irrelevant to the kernel, only to Node's module loader).
22 const FAKE_PS_MJS = `
23 import fs from "node:fs";
24 const args = process.argv.slice(2);
25 const i = args.indexOf("-EncodedCommand");
26 const script = i >= 0 ? Buffer.from(args[i + 1], "base64").toString("utf16le") : "";
27 if (process.env.CU_FAKE_PS_CAPTURE) {
28 fs.appendFileSync(process.env.CU_FAKE_PS_CAPTURE, JSON.stringify({ args, script }) + "\\n");
29 }
30 if (process.env.CU_FAKE_PS_STDOUT) process.stdout.write(process.env.CU_FAKE_PS_STDOUT);
31 if (process.env.CU_FAKE_PS_STDERR) process.stderr.write(process.env.CU_FAKE_PS_STDERR);
32 process.exit(Number(process.env.CU_FAKE_PS_EXIT || 0));
33 `;
34
35 /**
36 * Install a controllable fake powershell.exe on PATH (or, with onPath:false,
37 * guarantee no powershell.exe at all) and restore every mutation in t.after.
38 * Returns calls(): the decoded {args, script} of each spawn so far.
39 */
40 function fakePowershell(t, { exit = 0, stdout = "", stderr = "", onPath = true } = {}) {
41 const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cu-win32-test-"));
42 const capture = path.join(dir, "captured.jsonl");
43 if (onPath) {
44 const mjs = path.join(dir, "ps-fake.mjs");
45 fs.writeFileSync(mjs, FAKE_PS_MJS);
46 const bin = path.join(dir, "powershell.exe");
47 fs.writeFileSync(bin, `#!/bin/sh\nexec node ${JSON.stringify(mjs)} "$@"\n`);
48 fs.chmodSync(bin, 0o755);
49 }
50 const saved = {
51 PATH: process.env.PATH,
52 CU_FAKE_PS_EXIT: process.env.CU_FAKE_PS_EXIT,
53 CU_FAKE_PS_STDOUT: process.env.CU_FAKE_PS_STDOUT,
54 CU_FAKE_PS_STDERR: process.env.CU_FAKE_PS_STDERR,
55 CU_FAKE_PS_CAPTURE: process.env.CU_FAKE_PS_CAPTURE,
56 };
57 process.env.PATH = onPath ? `${dir}${path.delimiter}${process.env.PATH}` : dir;
58 process.env.CU_FAKE_PS_EXIT = String(exit);
59 process.env.CU_FAKE_PS_STDOUT = stdout;
60 process.env.CU_FAKE_PS_STDERR = stderr;
61 process.env.CU_FAKE_PS_CAPTURE = capture;
62 t.after(() => {
63 process.env.PATH = saved.PATH;
64 for (const [k, v] of Object.entries({
65 CU_FAKE_PS_EXIT: saved.CU_FAKE_PS_EXIT,
66 CU_FAKE_PS_STDOUT: saved.CU_FAKE_PS_STDOUT,
67 CU_FAKE_PS_STDERR: saved.CU_FAKE_PS_STDERR,
68 CU_FAKE_PS_CAPTURE: saved.CU_FAKE_PS_CAPTURE,
69 })) {
70 if (v === undefined) delete process.env[k];
71 else process.env[k] = v;
72 }
73 fs.rmSync(dir, { recursive: true, force: true });
74 });
75 const calls = () =>
76 fs.existsSync(capture)
77 ? fs.readFileSync(capture, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l))
78 : [];
79 return { calls };
80 }
81
82 test("win32: targeted left_mouse_down both moves and presses in one self-contained command", async (t) => {
83 const fake = fakePowershell(t);
84 const backend = win32.create({ exec: { persistentInputOwner: true } });
85 const r = await backend.left_mouse_down({ target: { x: 12, y: 34 } });
86 assert.deepEqual(r, { action_sent: true });
87 const calls = fake.calls();
88 assert.equal(calls.length, 1, "exactly one PowerShell process — no bootstrap process is relied on");
89 assert.ok(calls[0].args.includes("-EncodedCommand"), "script travels as an encoded command");
90 const script = calls[0].script;
91 assert.match(script, /Add-Type -TypeDefinition/, "the action command carries the type definition itself");
92 assert.match(script, /public static class User32/);
93 assert.match(script, /\[User32\]::SetCursorPos\(12, 34\)/, "targeted press moves the cursor");
94 assert.match(script, /\[User32\]::mouse_event\(\[User32\]::LEFTDOWN/, "targeted press also presses");
95 });
96
97 test("win32: PowerShell exit != 0 becomes an error, never action_sent:true", async (t) => {
98 fakePowershell(t, { exit: 1, stderr: "unable to find type [User32]" });
99 const backend = win32.create({ exec: { persistentInputOwner: true } });
100 const cases = [
101 ["left_mouse_down", () => backend.left_mouse_down({ target: { x: 1, y: 2 } })],
102 ["mouse_move", () => backend.mouse_move({ target: { x: 1, y: 2 } })],
103 ["key", () => backend.key({ text: "enter" })],
104 ];
105 for (const [name, fn] of cases) {
106 await assert.rejects(fn(), (e) => {
107 assert.ok(e instanceof ExecError, `${name} rejects with ExecError`);
108 assert.match(e.message, /exited 1/, `${name} reports the nonzero exit code`);
109 assert.match(e.message, /unable to find type/, `${name} surfaces PowerShell stderr`);
110 return true;
111 }, `${name} must not report success when PowerShell exits nonzero`);
112 }
113 });
114
115 test("win32: spawn failure (powershell.exe missing) becomes an error, never action_sent:true", async (t) => {
116 fakePowershell(t, { onPath: false });
117 const backend = win32.create({ exec: { persistentInputOwner: true } });
118 await assert.rejects(backend.left_mouse_down({ target: { x: 1, y: 2 } }), (e) => {
119 assert.ok(e instanceof ExecError);
120 assert.match(e.message, /exited -1|ENOENT/);
121 return true;
122 });
123 });
124
125 test("win32: successful input still reports success", async (t) => {
126 const fake = fakePowershell(t);
127 const backend = win32.create({ exec: { persistentInputOwner: true } });
128 assert.deepEqual(await backend.mouse_move({ target: { x: 5, y: 6 } }), { action_sent: true, at: { x: 5, y: 6 } });
129 const click = await backend.left_click({ target: { x: 9, y: 8 } });
130 assert.equal(click.action_sent, true);
131 const clickScript = fake.calls()[1].script;
132 assert.match(clickScript, /\[User32\]::SetCursorPos\(9, 8\)/);
133 assert.match(clickScript, /\[User32\]::LEFTDOWN/);
134 assert.match(clickScript, /\[User32\]::LEFTUP/);
135 });
136
137 test("win32: every User32-backed action carries the Add-Type definition in its own process", async (t) => {
138 const fake = fakePowershell(t);
139 const backend = win32.create({ exec: { persistentInputOwner: true } });
140 const at = { x: 3, y: 4 };
141 const actions = [
142 ["mouse_move", () => backend.mouse_move({ target: at })],
143 ["left_mouse_down", () => backend.left_mouse_down({ target: at })],
144 ["left_mouse_up", () => backend.left_mouse_up()],
145 ["left_click", () => backend.left_click({ target: at })],
146 ["double_click", () => backend.double_click({ target: at })],
147 ["right_click", () => backend.right_click({ target: at })],
148 ["middle_click", () => backend.middle_click({ target: at })],
149 ["left_click_drag", () => backend.left_click_drag({ from_target: at, to: { x: 10, y: 11 } })],
150 ["scroll", () => backend.scroll({ target: at, direction: "down", amount: 2 })],
151 ["key", () => backend.key({ text: "ctrl+a" })],
152 ["hold_key", () => backend.hold_key({ text: "a", duration: 0.05 })],
153 ];
154 for (const [, fn] of actions) await fn();
155 const calls = fake.calls();
156 assert.equal(calls.length, actions.length, "exactly one self-contained PowerShell process per action");
157 for (const [i, [name]] of actions.entries()) {
158 assert.match(calls[i].script, /Add-Type -TypeDefinition/, `${name} is self-contained`);
159 assert.match(calls[i].script, /public static class User32/, `${name} defines User32 inline`);
160 }
161 });
162
163 test("win32: cursor_position is self-contained and parses JSON output", async (t) => {
164 const fake = fakePowershell(t, { stdout: '{"x": 11, "y": 22}' });
165 const backend = win32.create({ exec: { persistentInputOwner: true } });
166 assert.deepEqual(await backend.cursor_position(), { x: 11, y: 22 });
167 const script = fake.calls()[0].script;
168 assert.match(script, /Add-Type -TypeDefinition/, "cursor_position carries the type definition");
169 assert.match(script, /GetCursorPos/);
170 });
171
171 lines Plain Text