test: harden remote control stress flow

This commit is contained in:
AI Bot
2026-05-11 23:12:47 +08:00
parent a311280238
commit 9c8ffebb92
7 changed files with 884 additions and 40 deletions

View File

@@ -0,0 +1,24 @@
let input = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
input += chunk;
});
process.stdin.on("end", () => {
const payload = JSON.parse(input || "{}");
process.stdout.write(
`${JSON.stringify({
status: "needs_user_action",
requestId: payload.requestId,
kind: "dialog_intervention_required",
dialogId: "dialog-system-permission",
appName: "System Settings",
platform: "darwin",
risk: "high",
summary: "System Settings 弹窗需要用户确认。",
recommendedAction: "handled_on_device",
availableActions: ["handled_on_device", "cancel_task"],
})}\n`,
);
});

View File

@@ -0,0 +1,69 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
buildComputerUseCompletionPayload,
buildMasterAgentTaskCompletionRequestBody,
buildRemoteExecutionCompletionPayload,
} from "../local-agent/master-task-completion.mjs";
test("computer use needs_user_action is preserved for server dialog guard completion", () => {
const task = {
taskId: "desktop-task-dialog",
taskType: "desktop_control",
projectId: "master-agent",
targetThreadId: "thread-1",
dispatchExecutionId: "dispatch-1",
};
const runtimeResult = {
status: "needs_user_action",
requestId: "runtime-request-1",
kind: "dialog_intervention_required",
dialogId: "dialog-permission-1",
appName: "System Settings",
platform: "darwin",
risk: "high",
summary: "System Settings 弹窗需要用户确认。",
recommendedAction: "handled_on_device",
availableActions: ["handled_on_device", "cancel_task"],
};
const completion = buildComputerUseCompletionPayload(task, runtimeResult);
assert.equal(completion.status, "needs_user_action");
assert.equal(completion.taskId, "desktop-task-dialog");
assert.equal(completion.kind, "dialog_intervention_required");
assert.equal(completion.dialogId, "dialog-permission-1");
assert.equal(completion.summary, "System Settings 弹窗需要用户确认。");
assert.deepEqual(completion.availableActions, ["handled_on_device", "cancel_task"]);
const requestBody = buildMasterAgentTaskCompletionRequestBody(
{ deviceId: "mac-studio" },
completion,
);
assert.equal(requestBody.deviceId, "mac-studio");
assert.equal(requestBody.status, "needs_user_action");
assert.equal(requestBody.kind, "dialog_intervention_required");
assert.equal(requestBody.dialogId, "dialog-permission-1");
assert.equal(requestBody.appName, "System Settings");
assert.equal(requestBody.platform, "darwin");
assert.equal(requestBody.risk, "high");
assert.deepEqual(requestBody.availableActions, ["handled_on_device", "cancel_task"]);
});
test("remote execution completion payload does not coerce waiting state into completed", () => {
const payload = buildRemoteExecutionCompletionPayload(
{ taskId: "desktop-task-dialog" },
{
status: "needs_user_action",
requestId: "runtime-request-2",
kind: "dialog_intervention_required",
dialogId: "dialog-2",
summary: "需要确认。",
},
);
assert.equal(payload.status, "needs_user_action");
assert.equal(payload.kind, "dialog_intervention_required");
assert.equal(payload.dialogId, "dialog-2");
});

View File

@@ -0,0 +1,167 @@
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 });
}
});