1272 lines
43 KiB
JavaScript
1272 lines
43 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 normalizes bare domains from app control text", async () => {
|
||
const result = await runRuntime(path.join(repoRoot, "scripts", "browser-control-smoke.mjs"), {
|
||
requestKind: "browser_control",
|
||
requestId: "browser-smoke-bare-domain",
|
||
objective: "打开Chrome浏览器,访问 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 derives a YouTube search url from natural language playback requests", async () => {
|
||
const result = await runRuntime(path.join(repoRoot, "scripts", "browser-control-smoke.mjs"), {
|
||
requestKind: "browser_control",
|
||
requestId: "browser-youtube-search",
|
||
objective: "打开浏览器,用浏览器打开YouTube,找一个蔡徐坤的MV播放",
|
||
context: {
|
||
riskLevel: "medium",
|
||
dryRun: true,
|
||
},
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(
|
||
result.targetUrl,
|
||
"https://www.youtube.com/results?search_query=%E8%94%A1%E5%BE%90%E5%9D%A4%20MV",
|
||
);
|
||
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",
|
||
BOSS_BROWSER_VISIBLE_OPEN_AFTER_AUTOMATION: "off",
|
||
},
|
||
},
|
||
);
|
||
|
||
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 still opens a visible browser after automation succeeds", async () => {
|
||
const markerDir = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-visible-after-automation-"));
|
||
const automationLog = path.join(markerDir, "automation.log");
|
||
const openerMarker = path.join(markerDir, "visible-open.txt");
|
||
let automationScript;
|
||
let openerScript;
|
||
try {
|
||
automationScript = await writeBrowserAutomationScript(automationLog);
|
||
openerScript = await writeOpenMarkerScript(openerMarker);
|
||
const result = await runRuntimeWithServer(
|
||
path.join(repoRoot, "scripts", "browser-control-smoke.mjs"),
|
||
{
|
||
requestKind: "browser_control",
|
||
requestId: "browser-visible-after-automation",
|
||
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_OPEN_COMMAND: process.execPath,
|
||
BOSS_BROWSER_OPEN_ARGS_JSON: JSON.stringify([openerScript.scriptPath]),
|
||
},
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.match(result.replyBody, /Boss Automated Title/);
|
||
assert.match(result.executionSummary, /browser_automation_executed\+open_url_executed/);
|
||
assert.equal(await fs.readFile(openerMarker, "utf8"), "https://example.com");
|
||
} finally {
|
||
if (automationScript?.scriptDir) {
|
||
await fs.rm(automationScript.scriptDir, { recursive: true, force: true });
|
||
}
|
||
if (openerScript?.scriptDir) {
|
||
await fs.rm(openerScript.scriptDir, { recursive: true, force: true });
|
||
}
|
||
await fs.rm(markerDir, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("browser smoke runtime falls back to opener when browser automation fails", async () => {
|
||
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-automation-fallback-"));
|
||
const openerMarker = path.join(marker, "opened.txt");
|
||
let openerScript;
|
||
try {
|
||
const failingAutomationPath = path.join(marker, "failing-automation.mjs");
|
||
await fs.writeFile(failingAutomationPath, "process.stderr.write('automation failed'); process.exit(1);", "utf8");
|
||
openerScript = await writeOpenMarkerScript(openerMarker);
|
||
const result = await runRuntime(
|
||
path.join(repoRoot, "scripts", "browser-control-smoke.mjs"),
|
||
{
|
||
requestKind: "browser_control",
|
||
requestId: "browser-automation-fallback",
|
||
objective: "打开 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([failingAutomationPath]),
|
||
BOSS_BROWSER_OPEN_COMMAND: process.execPath,
|
||
BOSS_BROWSER_OPEN_ARGS_JSON: JSON.stringify([openerScript.scriptPath]),
|
||
},
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.targetUrl, "https://example.com");
|
||
assert.match(result.executionSummary, /open_url_executed/);
|
||
assert.equal(await fs.readFile(openerMarker, "utf8"), "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("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,
|
||
BOSS_BROWSER_VISIBLE_OPEN_AFTER_AUTOMATION: "off",
|
||
},
|
||
},
|
||
);
|
||
|
||
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("browser smoke runtime opens Chrome when app control text asks for Chrome", async () => {
|
||
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-chrome-open-"));
|
||
const markerFile = path.join(marker, "open-args.json");
|
||
let openerCommand;
|
||
try {
|
||
openerCommand = await writeArgumentMarkerCommand(markerFile, "open");
|
||
const result = await runRuntime(
|
||
path.join(repoRoot, "scripts", "browser-control-smoke.mjs"),
|
||
{
|
||
requestKind: "browser_control",
|
||
requestId: "browser-open-chrome",
|
||
objective: "打开Chrome浏览器,访问 example.com,完成后回复一句任务小结。",
|
||
context: { dryRun: false },
|
||
},
|
||
{
|
||
env: {
|
||
BOSS_BROWSER_OPEN_COMMAND: openerCommand.scriptPath,
|
||
},
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.targetUrl, "https://example.com");
|
||
assert.deepEqual(JSON.parse(await fs.readFile(markerFile, "utf8")), [
|
||
"-a",
|
||
"Google Chrome",
|
||
"https://example.com",
|
||
]);
|
||
} finally {
|
||
if (openerCommand?.scriptDir) {
|
||
await fs.rm(openerCommand.scriptDir, { recursive: true, force: true });
|
||
}
|
||
await fs.rm(marker, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("browser smoke runtime uses osascript for real Chrome URL opens on macOS", async () => {
|
||
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-browser-chrome-osascript-"));
|
||
const osascriptMarker = path.join(marker, "osascript-args.json");
|
||
let osascriptCommand;
|
||
try {
|
||
osascriptCommand = await writeArgumentMarkerCommand(osascriptMarker, "osascript");
|
||
const failingOpenPath = path.join(osascriptCommand.scriptDir, "open");
|
||
await fs.writeFile(
|
||
failingOpenPath,
|
||
"#!/usr/bin/env node\nprocess.stderr.write('open should not be used for Chrome URL opens'); process.exit(1);\n",
|
||
"utf8",
|
||
);
|
||
await fs.chmod(failingOpenPath, 0o755);
|
||
|
||
const result = await runRuntime(
|
||
path.join(repoRoot, "scripts", "browser-control-smoke.mjs"),
|
||
{
|
||
requestKind: "browser_control",
|
||
requestId: "browser-open-chrome-osascript",
|
||
objective: "打开Chrome浏览器,访问 example.com,完成后回复一句任务小结。",
|
||
context: { dryRun: false },
|
||
},
|
||
{
|
||
env: {
|
||
PATH: `${osascriptCommand.scriptDir}:${process.env.PATH}`,
|
||
},
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.targetUrl, "https://example.com");
|
||
const args = JSON.parse(await fs.readFile(osascriptMarker, "utf8"));
|
||
assert.match(args.join(" "), /Google Chrome/);
|
||
assert.match(args.join(" "), /https:\/\/example\.com/);
|
||
} finally {
|
||
if (osascriptCommand?.scriptDir) {
|
||
await fs.rm(osascriptCommand.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 maps Chrome to Google Chrome for macOS app opens", async () => {
|
||
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-computer-open-chrome-"));
|
||
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-chrome",
|
||
objective: "打开Chrome浏览器",
|
||
context: { dryRun: false },
|
||
}),
|
||
);
|
||
child.stdin.end();
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.targetApp, "Chrome");
|
||
assert.equal(result.automationTargetApp, "Google Chrome");
|
||
const argv = JSON.parse(await fs.readFile(markerFile, "utf8"));
|
||
assert.deepEqual(argv, ["-a", "Google Chrome"]);
|
||
} 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 opens browser urls directly without keystroke automation", async () => {
|
||
const marker = await fs.mkdtemp(path.join(os.tmpdir(), "boss-computer-open-browser-url-"));
|
||
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: "osascript",
|
||
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-browser-url",
|
||
objective: "打开软件 Chrome,输入“https://example.com”,回车",
|
||
context: { dryRun: false },
|
||
}),
|
||
);
|
||
child.stdin.end();
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.targetApp, "Chrome");
|
||
assert.equal(result.automationTargetApp, "Google Chrome");
|
||
assert.equal(result.targetUrl, "https://example.com");
|
||
assert.match(result.executionSummary, /open_app_url_executed/);
|
||
const argv = JSON.parse(await fs.readFile(markerFile, "utf8"));
|
||
assert.deepEqual(argv, ["-a", "Google Chrome", "https://example.com"]);
|
||
} 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 });
|
||
}
|
||
});
|