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 }); } });