test: harden remote control stress flow
This commit is contained in:
24
tests/fixtures/computer-use-dialog-runtime.mjs
vendored
Normal file
24
tests/fixtures/computer-use-dialog-runtime.mjs
vendored
Normal 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`,
|
||||
);
|
||||
});
|
||||
69
tests/local-agent-master-task-completion.test.mjs
Normal file
69
tests/local-agent-master-task-completion.test.mjs
Normal 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");
|
||||
});
|
||||
167
tests/local-agent-server-computer-use-dialog-flow.test.mjs
Normal file
167
tests/local-agent-server-computer-use-dialog-flow.test.mjs
Normal 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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user