feat: harden enterprise control plane
This commit is contained in:
@@ -434,6 +434,179 @@ test("POST /api/v1/projects/[projectId]/messages lets @主Agent create browser c
|
||||
assert.equal(task?.requiresUserConfirmation, undefined);
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages routes direct GUI browser commands to browser control", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, false);
|
||||
|
||||
const state = await readState();
|
||||
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
|
||||
assert.ok(targetDevice, "expected a seeded target device");
|
||||
targetDevice.preferredExecutionMode = "gui";
|
||||
targetDevice.capabilities = {
|
||||
...(targetDevice.capabilities ?? {}),
|
||||
browserAutomation: {
|
||||
...(targetDevice.capabilities?.browserAutomation ?? {}),
|
||||
connected: true,
|
||||
},
|
||||
};
|
||||
await writeState(state);
|
||||
|
||||
const response = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
{ body: "打开浏览器,用浏览器打开 YouTube,找一个 MV 播放" },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
replyPresenter?: "thread" | "master";
|
||||
task?: { taskId: string; taskType: string; status: string } | null;
|
||||
executionMode?: string;
|
||||
riskLevel?: string;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.replyPresenter, "master");
|
||||
assert.equal(payload.task?.taskType, "browser_control");
|
||||
assert.equal(payload.task?.status, "queued");
|
||||
assert.equal(payload.executionMode, "browser");
|
||||
assert.equal(payload.riskLevel, "medium");
|
||||
|
||||
const nextState = await readState();
|
||||
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
|
||||
assert.ok(task, "expected a queued browser_control task");
|
||||
assert.equal(task?.projectId, singleProject.id);
|
||||
assert.equal(task?.deviceId, singleProject.deviceIds[0]);
|
||||
assert.equal(task?.taskType, "browser_control");
|
||||
assert.equal(task?.intentCategory, "browser_control");
|
||||
assert.equal(task?.runtimeKind, "browser-automation-runtime");
|
||||
|
||||
const staleConversationTask = nextState.masterAgentTasks.find(
|
||||
(item) =>
|
||||
item.projectId === singleProject.id &&
|
||||
item.taskType === "conversation_reply" &&
|
||||
item.requestText === "打开浏览器,用浏览器打开 YouTube,找一个 MV 播放",
|
||||
);
|
||||
assert.equal(staleConversationTask, undefined, "direct GUI control should not queue an unclaimable thread task");
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages creates native remote progress for direct GUI control", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, false);
|
||||
|
||||
const state = await readState();
|
||||
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
|
||||
assert.ok(targetDevice, "expected a seeded target device");
|
||||
targetDevice.preferredExecutionMode = "gui";
|
||||
targetDevice.capabilities = {
|
||||
...(targetDevice.capabilities ?? {}),
|
||||
browserAutomation: {
|
||||
...(targetDevice.capabilities?.browserAutomation ?? {}),
|
||||
connected: true,
|
||||
},
|
||||
};
|
||||
await writeState(state);
|
||||
|
||||
const response = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
{ body: "打开浏览器,用浏览器打开 YouTube,找一个 MV 播放" },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as { task?: { taskId: string } | null };
|
||||
|
||||
const nextState = await readState();
|
||||
const progressMessage = nextState.projects
|
||||
.find((project) => project.id === singleProject.id)
|
||||
?.messages.find((message) => message.executionProgress?.taskId === payload.task?.taskId);
|
||||
const progress = progressMessage?.executionProgress as
|
||||
| (NonNullable<typeof progressMessage>["executionProgress"] & {
|
||||
controlMode?: string;
|
||||
runtimeKind?: string;
|
||||
controlPlatform?: string;
|
||||
computerUseProvider?: string;
|
||||
})
|
||||
| undefined;
|
||||
|
||||
assert.ok(progressMessage, "expected native control task to render its own progress card");
|
||||
assert.equal(progress?.title, "远程控制进度");
|
||||
assert.equal(progress?.controlMode, "native_remote_control");
|
||||
assert.equal(progress?.runtimeKind, "browser-automation-runtime");
|
||||
assert.equal(progress?.controlPlatform, "macos");
|
||||
assert.equal(progress?.computerUseProvider, "openai-computer-use");
|
||||
assert.ok(progress?.steps.some((step) => step.text.includes("连接目标电脑")));
|
||||
assert.ok(!progress?.steps.some((step) => /Codex|Git|线程记录/.test(step.text)));
|
||||
assert.equal(progress?.branch, undefined);
|
||||
assert.equal(progress?.agents, undefined);
|
||||
});
|
||||
|
||||
test("POST /api/v1/master-agent/tasks/[taskId]/complete appends a visible master summary after control tasks", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
|
||||
const task = await queueMasterAgentTask({
|
||||
projectId: singleProject.id,
|
||||
taskType: "browser_control",
|
||||
requestMessageId: "msg-browser-summary",
|
||||
requestText: "打开浏览器,用浏览器打开 YouTube,找一个周杰伦 MV 播放",
|
||||
executionPrompt: "打开浏览器并搜索周杰伦 MV",
|
||||
requestedBy: "Boss 超级管理员",
|
||||
requestedByAccount: TEST_ACCOUNT,
|
||||
deviceId: "mac-studio",
|
||||
accountLabel: "主 GPT",
|
||||
intentCategory: "browser_control",
|
||||
runtimeKind: "browser-automation-runtime",
|
||||
riskLevel: "medium",
|
||||
});
|
||||
|
||||
const response = await completeMasterTaskRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
|
||||
"POST",
|
||||
{
|
||||
deviceId: task.deviceId,
|
||||
status: "completed",
|
||||
replyBody: "浏览器控制已完成:打开浏览器,用浏览器打开 YouTube,找一个周杰伦 MV 播放",
|
||||
targetUrl: "https://www.youtube.com/results?search_query=%E5%91%A8%E6%9D%B0%E4%BC%A6%20MV",
|
||||
},
|
||||
),
|
||||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const nextState = await readState();
|
||||
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
|
||||
const controlSummary = updatedProject?.messages.find(
|
||||
(message) => message.kind === "control_summary" && message.executionProgress?.taskId !== task.taskId,
|
||||
);
|
||||
const visibleSummary = updatedProject?.messages.at(-1);
|
||||
|
||||
assert.equal(controlSummary?.body, "浏览器控制已完成:打开浏览器,用浏览器打开 YouTube,找一个周杰伦 MV 播放");
|
||||
assert.equal(visibleSummary?.sender, "master");
|
||||
assert.equal(visibleSummary?.senderLabel, "主 Agent · 主 GPT");
|
||||
assert.equal(visibleSummary?.kind, "text");
|
||||
assert.match(visibleSummary?.body ?? "", /任务小结:浏览器控制已完成/);
|
||||
assert.match(visibleSummary?.body ?? "", /周杰伦 MV/);
|
||||
assert.ok(visibleSummary?.body.includes("youtube.com/results"));
|
||||
assert.ok(!visibleSummary?.body.includes("\n"));
|
||||
assert.equal(updatedProject?.preview, visibleSummary?.body);
|
||||
assert.equal(updatedProject?.unreadCount, 1);
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages lets @主Agent trigger project summary sync that writes back to top entries", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
|
||||
Reference in New Issue
Block a user