939 lines
31 KiB
JavaScript
939 lines
31 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import http from "node:http";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
async function writeOpenMarkerScript(markerFile) {
|
|
const scriptDir = await fs.mkdtemp(path.join(os.tmpdir(), "boss-open-marker-script-"));
|
|
const scriptPath = path.join(scriptDir, "open-marker.mjs");
|
|
await fs.writeFile(
|
|
scriptPath,
|
|
`import fs from "node:fs";\nfs.writeFileSync(${JSON.stringify(markerFile)}, process.argv[2] || "", "utf8");\n`,
|
|
"utf8",
|
|
);
|
|
await fs.chmod(scriptPath, 0o755);
|
|
return { scriptDir, scriptPath };
|
|
}
|
|
|
|
async function writeArgumentMarkerCommand(markerFile, commandName = "open") {
|
|
const scriptDir = await fs.mkdtemp(path.join(os.tmpdir(), `boss-${commandName}-marker-script-`));
|
|
const scriptPath = path.join(scriptDir, commandName);
|
|
await fs.writeFile(
|
|
scriptPath,
|
|
[
|
|
"#!/usr/bin/env node",
|
|
'import fs from "node:fs";',
|
|
`fs.writeFileSync(${JSON.stringify(markerFile)}, JSON.stringify(process.argv.slice(2)), "utf8");`,
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
await fs.chmod(scriptPath, 0o755);
|
|
return { scriptDir, scriptPath };
|
|
}
|
|
|
|
async function writeBrowserAutomationScript(logFile) {
|
|
const scriptDir = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-automation-script-"));
|
|
const scriptPath = path.join(scriptDir, "browser-automation.mjs");
|
|
await fs.writeFile(
|
|
scriptPath,
|
|
[
|
|
'#!/usr/bin/env node',
|
|
'import fs from "node:fs";',
|
|
`fs.appendFileSync(${JSON.stringify(logFile)}, JSON.stringify(process.argv.slice(2)) + "\\n", "utf8");`,
|
|
'if (process.argv.includes("eval")) {',
|
|
' process.stdout.write("Boss Automated Title\\n");',
|
|
'}',
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
await fs.chmod(scriptPath, 0o755);
|
|
return { scriptDir, scriptPath };
|
|
}
|
|
|
|
async function writeCodexHomePlaywrightWrapper(codexHome, logFile) {
|
|
const wrapperDir = path.join(codexHome, "skills", "playwright", "scripts");
|
|
await fs.mkdir(wrapperDir, { recursive: true });
|
|
const wrapperPath = path.join(wrapperDir, "playwright_cli.sh");
|
|
await fs.writeFile(
|
|
wrapperPath,
|
|
[
|
|
"#!/bin/zsh",
|
|
`printf '%s\\n' \"$(python3 -c 'import json,sys; args=sys.argv[1:]; print(json.dumps(args[1:] if args[:1] == [\"--\"] else args))' -- \"$@\")\" >> ${JSON.stringify(logFile)}`,
|
|
'if [[ " $* " == *" eval "* ]]; then',
|
|
' printf "Boss Auto Wrapper Title\\n"',
|
|
"fi",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
await fs.chmod(wrapperPath, 0o755);
|
|
return wrapperPath;
|
|
}
|
|
|
|
async function runRuntimeWithServer(scriptPath, payload, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [scriptPath], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
...(options.env || {}),
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(JSON.stringify(payload));
|
|
child.stdin.end();
|
|
});
|
|
}
|
|
|
|
async function runRuntime(scriptPath, payload, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [scriptPath], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_BROWSER_AUTOMATION_MODE: "off",
|
|
...(options.env || {}),
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(JSON.stringify(payload));
|
|
child.stdin.end();
|
|
});
|
|
}
|
|
|
|
test("browser smoke runtime returns normalized completed payload", async () => {
|
|
const result = await runRuntime(path.join(repoRoot, "scripts", "browser-control-smoke.mjs"), {
|
|
requestKind: "browser_control",
|
|
requestId: "browser-smoke-1",
|
|
objective: "打开 boss 控制台首页",
|
|
context: {
|
|
riskLevel: "medium",
|
|
},
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.requestId, "browser-smoke-1");
|
|
assert.match(result.replyBody, /浏览器控制已完成/);
|
|
assert.match(result.replyBody, /打开 boss 控制台首页/);
|
|
});
|
|
|
|
test("browser smoke runtime emits target url when objective contains a website", async () => {
|
|
const result = await runRuntime(path.join(repoRoot, "scripts", "browser-control-smoke.mjs"), {
|
|
requestKind: "browser_control",
|
|
requestId: "browser-smoke-url",
|
|
objective: "打开 https://example.com 看一下首页",
|
|
context: {
|
|
riskLevel: "medium",
|
|
dryRun: true,
|
|
},
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.targetUrl, "https://example.com");
|
|
assert.match(result.executionSummary, /open_url/);
|
|
});
|
|
|
|
test("browser smoke runtime can invoke configured browser automation command", async () => {
|
|
const markerDir = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-automation-marker-"));
|
|
const markerFile = path.join(markerDir, "automation.log");
|
|
let automationScript;
|
|
try {
|
|
automationScript = await writeBrowserAutomationScript(markerFile);
|
|
const result = await runRuntime(
|
|
path.join(repoRoot, "scripts", "browser-control-smoke.mjs"),
|
|
{
|
|
requestKind: "browser_control",
|
|
requestId: "browser-automation-1",
|
|
objective: "打开 https://example.com 看一下首页",
|
|
context: {
|
|
riskLevel: "medium",
|
|
dryRun: false,
|
|
},
|
|
},
|
|
{
|
|
env: {
|
|
BOSS_BROWSER_AUTOMATION_MODE: "playwright",
|
|
BOSS_BROWSER_AUTOMATION_COMMAND: process.execPath,
|
|
BOSS_BROWSER_AUTOMATION_ARGS_JSON: JSON.stringify([automationScript.scriptPath]),
|
|
BOSS_BROWSER_AUTOMATION_SESSION: "boss-browser-test",
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.match(result.replyBody, /Boss Automated Title/);
|
|
const lines = (await fs.readFile(markerFile, "utf8")).trim().split(/\r?\n/).map((line) => JSON.parse(line));
|
|
assert.deepEqual(lines[0], ["--session", "boss-browser-test", "open", "https://example.com"]);
|
|
assert.deepEqual(lines[1], ["--session", "boss-browser-test", "eval", "document.title"]);
|
|
} finally {
|
|
if (automationScript?.scriptDir) {
|
|
await fs.rm(automationScript.scriptDir, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(markerDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("browser smoke runtime auto-detects bundled playwright wrapper and uses request id as session", async () => {
|
|
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-autodetect-"));
|
|
const logFile = path.join(tmpRoot, "wrapper.log");
|
|
try {
|
|
const codexHome = path.join(tmpRoot, ".codex");
|
|
await writeCodexHomePlaywrightWrapper(codexHome, logFile);
|
|
const result = await runRuntimeWithServer(
|
|
path.join(repoRoot, "scripts", "browser-control-smoke.mjs"),
|
|
{
|
|
requestKind: "browser_control",
|
|
requestId: "browser-auto-session",
|
|
objective: "打开 https://example.com 看一下首页",
|
|
context: {
|
|
riskLevel: "medium",
|
|
dryRun: false,
|
|
},
|
|
},
|
|
{
|
|
env: {
|
|
CODEX_HOME: codexHome,
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.match(result.replyBody, /Boss Auto Wrapper Title/);
|
|
const lines = (await fs.readFile(logFile, "utf8")).trim().split(/\r?\n/).map((line) => JSON.parse(line));
|
|
assert.deepEqual(lines[0], ["--session", "browser-auto-session", "open", "https://example.com"]);
|
|
assert.deepEqual(lines[1], ["--session", "browser-auto-session", "eval", "document.title"]);
|
|
} finally {
|
|
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("browser smoke runtime fetches page title for a reachable target url", async () => {
|
|
const server = http.createServer((_request, response) => {
|
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
response.end("<html><head><title>Boss Browser Runtime Test</title></head><body>ok</body></html>");
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const address = server.address();
|
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
try {
|
|
const result = await runRuntimeWithServer(path.join(repoRoot, "scripts", "browser-control-smoke.mjs"), {
|
|
requestKind: "browser_control",
|
|
requestId: "browser-smoke-title",
|
|
objective: `打开 http://127.0.0.1:${port}/ 看一下首页`,
|
|
context: {
|
|
riskLevel: "medium",
|
|
dryRun: false,
|
|
},
|
|
}, {
|
|
env: {
|
|
BOSS_BROWSER_AUTOMATION_MODE: "fetch",
|
|
},
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.match(result.replyBody, /Boss Browser Runtime Test/);
|
|
assert.match(result.executionSummary, /title=Boss Browser Runtime Test/);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
}
|
|
});
|
|
|
|
test("computer use smoke runtime returns normalized completed payload", async () => {
|
|
const result = await runRuntime(path.join(repoRoot, "scripts", "computer-use-smoke.mjs"), {
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-smoke-1",
|
|
objective: "打开系统设置",
|
|
context: {
|
|
riskLevel: "high",
|
|
dryRun: true,
|
|
},
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.requestId, "computer-smoke-1");
|
|
assert.match(result.replyBody, /桌面控制已完成/);
|
|
assert.match(result.replyBody, /打开系统设置/);
|
|
});
|
|
|
|
test("computer use smoke runtime emits target app when objective contains an app name", async () => {
|
|
const result = await runRuntime(path.join(repoRoot, "scripts", "computer-use-smoke.mjs"), {
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-smoke-app",
|
|
objective: "打开微信并准备切到聊天窗口",
|
|
context: {
|
|
riskLevel: "medium",
|
|
dryRun: true,
|
|
},
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.targetApp, "微信");
|
|
assert.match(result.executionSummary, /open_wechat|open_app/);
|
|
});
|
|
|
|
test("computer use smoke runtime auto-handles safe cross-platform dialog snapshots", async () => {
|
|
const result = await runRuntime(
|
|
path.join(repoRoot, "scripts", "computer-use-smoke.mjs"),
|
|
{
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-dialog-safe",
|
|
objective: "打开 QQ",
|
|
context: {
|
|
riskLevel: "medium",
|
|
dryRun: true,
|
|
},
|
|
},
|
|
{
|
|
env: {
|
|
BOSS_DIALOG_GUARD_ENABLED: "true",
|
|
BOSS_DIALOG_GUARD_SNAPSHOT_JSON: JSON.stringify({
|
|
platform: "win32",
|
|
deviceId: "win-node",
|
|
appName: "QQ",
|
|
title: "Welcome",
|
|
text: "Welcome. Not now",
|
|
buttons: ["Get started", "Not now"],
|
|
}),
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.dialogGuard?.disposition, "auto_action");
|
|
assert.equal(result.dialogGuard?.button, "Not now");
|
|
assert.match(result.executionSummary, /dialogGuard=auto_action/);
|
|
});
|
|
|
|
test("computer use smoke runtime invokes configured platform dialog action command for safe dialogs", async () => {
|
|
const markerDir = await fs.mkdtemp(path.join(os.tmpdir(), "boss-dialog-action-marker-"));
|
|
const markerFile = path.join(markerDir, "action.json");
|
|
let actionCommand;
|
|
try {
|
|
actionCommand = await writeArgumentMarkerCommand(markerFile, "dialog-action");
|
|
const result = await runRuntime(
|
|
path.join(repoRoot, "scripts", "computer-use-smoke.mjs"),
|
|
{
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-dialog-action",
|
|
objective: "打开 QQ",
|
|
context: {
|
|
riskLevel: "medium",
|
|
dryRun: true,
|
|
},
|
|
},
|
|
{
|
|
env: {
|
|
BOSS_DIALOG_GUARD_ENABLED: "true",
|
|
BOSS_WINDOWS_DIALOG_GUARD_ACTION_COMMAND: actionCommand.scriptPath,
|
|
BOSS_DIALOG_GUARD_SNAPSHOT_JSON: JSON.stringify({
|
|
platform: "win32",
|
|
deviceId: "win-node",
|
|
appName: "QQ",
|
|
title: "Welcome",
|
|
text: "Welcome. Not now",
|
|
buttons: ["Get started", "Not now"],
|
|
}),
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.dialogGuard?.actionApplied, true);
|
|
const args = JSON.parse(await fs.readFile(markerFile, "utf8"));
|
|
assert.ok(args.includes("--platform"));
|
|
assert.ok(args.includes("win32"));
|
|
assert.ok(args.includes("--button"));
|
|
assert.ok(args.includes("Not now"));
|
|
} finally {
|
|
if (actionCommand?.scriptDir) {
|
|
await fs.rm(actionCommand.scriptDir, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(markerDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computer use smoke runtime pauses before action for blocked system permission dialogs", async () => {
|
|
const result = await runRuntime(
|
|
path.join(repoRoot, "scripts", "computer-use-smoke.mjs"),
|
|
{
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-dialog-blocked",
|
|
objective: "打开系统设置",
|
|
context: {
|
|
riskLevel: "high",
|
|
dryRun: false,
|
|
},
|
|
},
|
|
{
|
|
env: {
|
|
BOSS_DIALOG_GUARD_ENABLED: "true",
|
|
BOSS_DIALOG_GUARD_SNAPSHOT_JSON: JSON.stringify({
|
|
platform: "darwin",
|
|
deviceId: "mac-node",
|
|
appName: "System Settings",
|
|
title: "Screen Recording",
|
|
text: "BossComputerUseHelper would like to record this computer's screen",
|
|
buttons: ["Allow", "Don't Allow"],
|
|
}),
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(result.status, "needs_user_action");
|
|
assert.equal(result.kind, "dialog_intervention_required");
|
|
assert.equal(result.risk, "high");
|
|
assert.deepEqual(result.availableActions, ["handled_on_device", "cancel_task"]);
|
|
});
|
|
|
|
test("browser smoke runtime writes action artifact when BOSS_CONTROL_ARTIFACT_DIR is set", async () => {
|
|
const artifactDir = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-artifacts-"));
|
|
try {
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [path.join(repoRoot, "scripts", "browser-control-smoke.mjs")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_CONTROL_ARTIFACT_DIR: artifactDir,
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(
|
|
JSON.stringify({
|
|
requestKind: "browser_control",
|
|
requestId: "browser-artifact-1",
|
|
objective: "打开 https://example.com 看一下首页",
|
|
context: { dryRun: true },
|
|
}),
|
|
);
|
|
child.stdin.end();
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.ok(Array.isArray(result.artifacts));
|
|
assert.ok(result.artifacts[0]?.path);
|
|
const artifactText = await fs.readFile(result.artifacts[0].path, "utf8");
|
|
assert.match(artifactText, /https:\/\/example\.com/);
|
|
} finally {
|
|
await fs.rm(artifactDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("browser smoke runtime can execute an injected url opener when not dry-run", async () => {
|
|
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-open-"));
|
|
let openerScript;
|
|
const markerFile = path.join(marker, "opened.txt");
|
|
try {
|
|
openerScript = await writeOpenMarkerScript(markerFile);
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [path.join(repoRoot, "scripts", "browser-control-smoke.mjs")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_BROWSER_AUTOMATION_MODE: "off",
|
|
BOSS_BROWSER_OPEN_COMMAND: process.execPath,
|
|
BOSS_BROWSER_OPEN_ARGS_JSON: JSON.stringify([openerScript.scriptPath]),
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(
|
|
JSON.stringify({
|
|
requestKind: "browser_control",
|
|
requestId: "browser-open-1",
|
|
objective: "打开 https://example.com 看一下首页",
|
|
context: { dryRun: false },
|
|
}),
|
|
);
|
|
child.stdin.end();
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.targetUrl, "https://example.com");
|
|
assert.match(result.executionSummary, /executed/);
|
|
const openedUrl = await fs.readFile(markerFile, "utf8");
|
|
assert.equal(openedUrl, "https://example.com");
|
|
} finally {
|
|
if (openerScript?.scriptDir) {
|
|
await fs.rm(openerScript.scriptDir, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(marker, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computer use smoke runtime can execute an injected app opener when not dry-run", async () => {
|
|
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-computer-open-"));
|
|
let openerScript;
|
|
const markerFile = path.join(marker, "opened.txt");
|
|
try {
|
|
openerScript = await writeOpenMarkerScript(markerFile);
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [path.join(repoRoot, "scripts", "computer-use-smoke.mjs")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_COMPUTER_USE_MODE: "open",
|
|
BOSS_COMPUTER_USE_OPEN_APP_COMMAND: process.execPath,
|
|
BOSS_COMPUTER_USE_OPEN_APP_ARGS_JSON: JSON.stringify([openerScript.scriptPath]),
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(
|
|
JSON.stringify({
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-open-1",
|
|
objective: "打开微信并准备切到聊天窗口",
|
|
context: { dryRun: false },
|
|
}),
|
|
);
|
|
child.stdin.end();
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.targetApp, "微信");
|
|
assert.match(result.executionSummary, /executed/);
|
|
const openedApp = await fs.readFile(markerFile, "utf8");
|
|
assert.equal(openedApp, "微信");
|
|
} finally {
|
|
if (openerScript?.scriptDir) {
|
|
await fs.rm(openerScript.scriptDir, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(marker, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computer use smoke runtime defaults to open -a style args for macOS opener", async () => {
|
|
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-computer-open-default-"));
|
|
let openerCommand;
|
|
const markerFile = path.join(marker, "argv.json");
|
|
try {
|
|
openerCommand = await writeArgumentMarkerCommand(markerFile, "open");
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [path.join(repoRoot, "scripts", "computer-use-smoke.mjs")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_COMPUTER_USE_MODE: "open",
|
|
BOSS_COMPUTER_USE_OPEN_APP_COMMAND: openerCommand.scriptPath,
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(
|
|
JSON.stringify({
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-open-default",
|
|
objective: "打开微信并准备切到聊天窗口",
|
|
context: { dryRun: false },
|
|
}),
|
|
);
|
|
child.stdin.end();
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
const argv = JSON.parse(await fs.readFile(markerFile, "utf8"));
|
|
assert.deepEqual(argv, ["-a", "微信"]);
|
|
} finally {
|
|
if (openerCommand?.scriptDir) {
|
|
await fs.rm(openerCommand.scriptDir, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(marker, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computer use smoke runtime can prepend configured open -a style args", async () => {
|
|
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-computer-open-default-"));
|
|
let openerCommand;
|
|
const markerFile = path.join(marker, "argv.json");
|
|
try {
|
|
openerCommand = await writeArgumentMarkerCommand(markerFile, "open");
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [path.join(repoRoot, "scripts", "computer-use-smoke.mjs")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_COMPUTER_USE_MODE: "open",
|
|
BOSS_COMPUTER_USE_OPEN_APP_COMMAND: openerCommand.scriptPath,
|
|
BOSS_COMPUTER_USE_OPEN_APP_ARGS_JSON: JSON.stringify(["-a"]),
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(
|
|
JSON.stringify({
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-open-default",
|
|
objective: "打开微信并准备切到聊天窗口",
|
|
context: { dryRun: false },
|
|
}),
|
|
);
|
|
child.stdin.end();
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
const argv = JSON.parse(await fs.readFile(markerFile, "utf8"));
|
|
assert.deepEqual(argv, ["-a", "微信"]);
|
|
} finally {
|
|
if (openerCommand?.scriptDir) {
|
|
await fs.rm(openerCommand.scriptDir, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(marker, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computer use smoke runtime supports osascript mode via injected command", async () => {
|
|
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-computer-osascript-"));
|
|
let openerScript;
|
|
const markerFile = path.join(marker, "argv.json");
|
|
try {
|
|
openerScript = await writeArgumentMarkerCommand(markerFile, "osascript");
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [path.join(repoRoot, "scripts", "computer-use-smoke.mjs")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_COMPUTER_USE_MODE: "osascript",
|
|
PATH: `${openerScript.scriptDir}:${process.env.PATH || ""}`,
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(
|
|
JSON.stringify({
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-osascript",
|
|
objective: "打开微信并准备切到聊天窗口",
|
|
context: { dryRun: false },
|
|
}),
|
|
);
|
|
child.stdin.end();
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.match(result.executionSummary, /mode=osascript/);
|
|
const argv = JSON.parse(await fs.readFile(markerFile, "utf8"));
|
|
assert.equal(argv[0], "-e");
|
|
assert.match(argv[1], /tell application "微信"/);
|
|
} finally {
|
|
if (openerScript?.scriptDir) {
|
|
await fs.rm(openerScript.scriptDir, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(marker, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computer use smoke runtime types quoted text in osascript mode and can submit", async () => {
|
|
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-computer-osascript-type-"));
|
|
let openerScript;
|
|
const markerFile = path.join(marker, "argv.json");
|
|
try {
|
|
openerScript = await writeArgumentMarkerCommand(markerFile, "osascript");
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [path.join(repoRoot, "scripts", "computer-use-smoke.mjs")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_COMPUTER_USE_MODE: "osascript",
|
|
PATH: `${openerScript.scriptDir}:${process.env.PATH || ""}`,
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(
|
|
JSON.stringify({
|
|
requestKind: "desktop_control",
|
|
requestId: "computer-osascript-type",
|
|
objective: "打开微信并输入“Boss 已接管当前线程”后发送",
|
|
context: { dryRun: false },
|
|
}),
|
|
);
|
|
child.stdin.end();
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.equal(result.typedText, "Boss 已接管当前线程");
|
|
const argv = JSON.parse(await fs.readFile(markerFile, "utf8"));
|
|
assert.equal(argv[0], "-e");
|
|
assert.match(argv[1], /keystroke "Boss 已接管当前线程"/);
|
|
assert.match(argv[1], /key code 36/);
|
|
} finally {
|
|
if (openerScript?.scriptDir) {
|
|
await fs.rm(openerScript.scriptDir, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(marker, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computer use smoke runtime writes action artifact when BOSS_CONTROL_ARTIFACT_DIR is set", async () => {
|
|
const artifactDir = await fs.mkdtemp(path.join(os.tmpdir(), "boss-desktop-artifacts-"));
|
|
try {
|
|
const result = await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [path.join(repoRoot, "scripts", "computer-use-smoke.mjs")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
BOSS_CONTROL_ARTIFACT_DIR: artifactDir,
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1) || ""));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(
|
|
JSON.stringify({
|
|
requestKind: "desktop_control",
|
|
requestId: "desktop-artifact-1",
|
|
objective: "打开系统设置",
|
|
context: { dryRun: true },
|
|
}),
|
|
);
|
|
child.stdin.end();
|
|
});
|
|
|
|
assert.equal(result.status, "completed");
|
|
assert.ok(Array.isArray(result.artifacts));
|
|
assert.ok(result.artifacts[0]?.path);
|
|
const artifactText = await fs.readFile(result.artifacts[0].path, "utf8");
|
|
assert.match(artifactText, /系统设置/);
|
|
assert.match(artifactText, /mode/);
|
|
} finally {
|
|
await fs.rm(artifactDir, { recursive: true, force: true });
|
|
}
|
|
});
|