scripts/smoke-cua-win-embedded.mjs
Rama main · réplica de lectura del repositorio original.
// Windows CUA smoke: the staged win32 SDK bundle loads through the packaged
// app's env-var redirect, and one embedded driver host starts and stops. Run it
// after `pnpm build:cua:win`.
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";
import { execFile, spawn } from "node:child_process";
import { createInterface } from "node:readline";
import { promisify } from "node:util";
import { createBackgroundExecutable } from "./cua-windows-background.mjs";
if (process.platform !== "win32") {
console.error("smoke-cua-win-embedded is Windows-only");
process.exit(1);
}
const stage = join(import.meta.dirname, "..", "dist-native", "cua-win32-x64");
const binary = join(stage, "cua-driver.exe");
const background = join(stage, "cua-driver-background.exe");
const bundle = join(stage, "cua-sdk", "cua-sdk.mjs");
const dll = join(stage, "cua-sdk", "native", "cua_driver_sdk.dll");
for (const [label, file] of [["cua-driver.exe", binary], ["background daemon", background], ["cua-sdk bundle", bundle], ["sdk dll", dll]]) {
if (!existsSync(file)) {
console.error(`staged ${label} is missing at ${file} — run pnpm build:cua:win first`);
process.exit(1);
}
}
if (!createBackgroundExecutable(readFileSync(binary)).equals(readFileSync(background))) {
throw new Error("background daemon does not match the CLI's executable sections and derived PE headers");
}
// The bundled resolver patch (prepare-cua-win.mjs) redirects the SDK's native
// library lookup to exactly this path; verify the redirect is present, or the
// packaged app would fall back to node_modules paths that do not exist.
const bundledSource = readFileSync(bundle, "utf8");
if (!bundledSource.includes("OPENMAUSBOT_CUA_SDK_LIBRARY")) {
console.error("staged cua-sdk.mjs lacks the OPENMAUSBOT_CUA_SDK_LIBRARY resolver patch — re-run pnpm build:cua:win");
process.exit(1);
}
process.env.OPENMAUSBOT_CUA_SDK_LIBRARY = dll;
process.env.CUA_DRIVER_RS_TELEMETRY_ENABLED = "0";
const watchdog = setTimeout(() => {
console.error("smoke:cua-win timed out");
process.exit(1); // Closing the host also closes the daemon's parent-liveness pipe.
}, 90_000);
async function checkNoConsole(pid) {
// Probe only this smoke's daemon from a separate, hidden process. An existing
// process with no console makes AttachConsole fail with ERROR_INVALID_HANDLE.
// Cold PowerShell/Add-Type compilation can exceed 10s on shared CI runners.
await promisify(execFile)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `
Add-Type -TypeDefinition @'
using System.Runtime.InteropServices;
public static class ConsoleProbe {
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool AttachConsole(uint pid);
}
'@
[ConsoleProbe]::FreeConsole() | Out-Null
$attached = [ConsoleProbe]::AttachConsole([uint32]$env:OMB_SMOKE_DAEMON_PID)
$failure = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
if ($attached) { [ConsoleProbe]::FreeConsole() | Out-Null; throw 'Background daemon has a console' }
if ($failure -ne 6) { throw "Console probe failed with unexpected Windows error $failure" }
`], { windowsHide: true, timeout: 30_000, env: { ...process.env, OMB_SMOKE_DAEMON_PID: String(pid) } });
console.log("Windows confirms the background daemon has no console");
}
const sdk = await import(pathToFileURL(bundle).href);
if (typeof sdk.EmbeddedCuaDriverHost !== "function") {
console.error("staged bundle does not export EmbeddedCuaDriverHost");
process.exit(1);
}
// No screen capture or input: initialize the retained CLI's MCP connection to
// the GUI-subsystem daemon and enumerate its tool definitions, then stop both.
async function checkProxy(socketPath) {
const child = spawn(binary, ["mcp", "--embedded", "--socket", socketPath], {
windowsHide: true, stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, CUA_DRIVER_EMBEDDED: "1", CUA_DRIVER_HOST_BUNDLE_ID: "com.openmausbot.app" },
});
const lines = createInterface({ input: child.stdout });
let stderr = "";
child.stderr.on("data", bytes => { stderr = (stderr + bytes).slice(-4096); });
const pending = new Map();
const rejectAll = error => { for (const item of pending.values()) item.reject(error); pending.clear(); };
child.on("error", rejectAll);
child.on("exit", () => rejectAll(new Error(`MCP proxy exited: ${stderr}`)));
lines.on("line", line => {
let message;
try { message = JSON.parse(line); } catch { return; }
const item = pending.get(message.id);
if (!item) return;
pending.delete(message.id);
if (message.error) item.reject(new Error(JSON.stringify(message.error)));
else item.resolve(message.result);
});
const send = message => child.stdin.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\n");
const request = (id, method, params) => new Promise((resolve, reject) => {
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`MCP ${method} timed out`)); }, 5000);
pending.set(id, {
resolve: result => { clearTimeout(timer); resolve(result); },
reject: error => { clearTimeout(timer); reject(error); },
});
send({ id, method, params });
});
try {
const initialized = await request(1, "initialize", {
protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "openmausbot-smoke", version: "1" },
});
if (!initialized?.serverInfo) throw new Error("MCP initialization missing serverInfo");
send({ method: "notifications/initialized" });
const list = await request(2, "tools/list", {});
if (!Array.isArray(list?.tools) || !list.tools.length) throw new Error("MCP proxy returned no tools");
console.log(`CLI proxy connected to background daemon: ${list.tools.length} tools`);
} finally {
lines.close();
child.stdin.end();
if (child.exitCode === null && child.signalCode === null) child.kill();
rejectAll(new Error("MCP smoke ended"));
}
}
const host = new sdk.EmbeddedCuaDriverHost(background, "com.openmausbot.app");
try {
const conn = await host.start({ signal: AbortSignal.timeout(15_000) });
if (!conn?.socketPath) throw new Error("embedded host reported no socketPath");
console.log("embedded host started:", {
pid: conn.pid,
driverVersion: conn.driverVersion,
contractVersion: conn.contractVersion,
socketPath: conn.socketPath,
});
if (!Number.isInteger(conn.pid) || conn.pid <= 0) throw new Error("missing daemon PID");
await checkNoConsole(conn.pid);
await checkProxy(conn.socketPath);
await host.stop();
host.uniffiDestroy?.();
if (!Number.isInteger(conn.pid) || conn.pid <= 0) throw new Error("missing daemon PID");
const running = () => {
try { process.kill(conn.pid, 0); return true; }
catch (error) { if (error.code === "ESRCH") return false; throw error; }
};
for (let attempt = 0; attempt < 50 && running(); attempt++) await delay(100);
if (running()) throw new Error("owned daemon survived host.stop()");
clearTimeout(watchdog);
console.log("smoke:cua-win OK — staged bundle drives a real embedded host");
} catch (err) {
try {
await host.stop();
} catch {
}
host.uniffiDestroy?.();
console.error("smoke:cua-win FAILED:", err);
process.exit(1);
}