Files
boss/tests/local-agent-server-computer-use-dialog-flow.test.mjs
2026-05-11 23:12:47 +08:00

168 lines
5.7 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { createServer } from "node:http";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
function listen(server, host = "127.0.0.1") {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, host, () => {
server.off("error", reject);
resolve(server.address().port);
});
});
}
function readJsonBody(request) {
return new Promise((resolve, reject) => {
let raw = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
raw += chunk;
});
request.on("end", () => {
try {
resolve(raw ? JSON.parse(raw) : {});
} catch (error) {
reject(error);
}
});
request.on("error", reject);
});
}
async function waitFor(predicate, timeoutMs = 5000) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
if (await predicate()) return;
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error("waitFor timeout");
}
test("local-agent forwards computer-use dialog intervention to control plane instead of completing task", async () => {
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-dialog-flow-"));
const skillsDir = path.join(runtimeRoot, "skills");
await mkdir(skillsDir, { recursive: true });
const completeBodies = [];
let claimCount = 0;
const controlPlane = createServer(async (request, response) => {
const url = request.url || "";
if (request.method === "POST" && url === "/api/v1/master-agent/tasks/claim") {
claimCount += 1;
response.writeHead(200, { "content-type": "application/json" });
response.end(
JSON.stringify({
ok: true,
task:
claimCount === 1
? {
taskId: "desktop-dialog-task",
taskType: "desktop_control",
projectId: "master-agent",
requestText: "打开系统设置并处理权限弹窗",
requestedByAccount: "krisolo",
deviceId: "mac-studio",
dispatchExecutionId: "dispatch-dialog-task",
targetThreadId: "thread-dialog",
requestedAt: "2026-05-11T10:00:00.000Z",
riskLevel: "high",
}
: null,
}),
);
return;
}
if (
request.method === "POST" &&
url === "/api/v1/master-agent/tasks/desktop-dialog-task/complete"
) {
completeBodies.push(await readJsonBody(request));
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.method === "POST" && url === "/api/device-heartbeat") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: true, token: "server-token" }));
return;
}
if (request.method === "POST" && url === "/api/v1/app-logs") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.method === "POST" && url === "/api/v1/devices/mac-studio/skills") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: true }));
return;
}
response.writeHead(404, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: false, message: "not_found", url }));
});
const controlPort = await listen(controlPlane);
const agentServer = createServer();
const agentPort = await listen(agentServer);
await new Promise((resolve) => agentServer.close(resolve));
const configPath = path.join(runtimeRoot, "local-agent-config.json");
await writeFile(
configPath,
JSON.stringify({
port: agentPort,
bindHost: "127.0.0.1",
controlPlaneUrl: `http://127.0.0.1:${controlPort}`,
deviceId: "mac-studio",
token: "local-token",
name: "Mac Studio",
account: "krisolo",
status: "online",
codexSessionDiscoveryEnabled: false,
skillsDir,
masterAgentEnabled: true,
masterAgentPollIntervalMs: 60_000,
heartbeatIntervalMs: 60_000,
skillLifecycleEnabled: false,
computerUseEnabled: true,
computerUseCommand: process.execPath,
computerUseArgs: ["tests/fixtures/computer-use-dialog-runtime.mjs"],
computerUseWorkdir: repoRoot,
computerUseTimeoutMs: 5000,
}),
);
const child = spawn(process.execPath, ["local-agent/server.mjs", configPath], {
cwd: repoRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
try {
await waitFor(() => completeBodies.length > 0);
const body = completeBodies.at(0);
assert.equal(body.status, "needs_user_action");
assert.equal(body.kind, "dialog_intervention_required");
assert.equal(body.dialogId, "dialog-system-permission");
assert.equal(body.appName, "System Settings");
assert.equal(body.platform, "darwin");
assert.equal(body.risk, "high");
assert.equal(body.summary, "System Settings 弹窗需要用户确认。");
assert.deepEqual(body.availableActions, ["handled_on_device", "cancel_task"]);
assert.equal(body.dispatchExecutionId, "dispatch-dialog-task");
assert.equal(body.targetThreadId, "thread-dialog");
assert.equal(body.replyBody, undefined);
} finally {
child.kill("SIGTERM");
controlPlane.close();
await rm(runtimeRoot, { recursive: true, force: true });
}
});