284 lines
9.2 KiB
TypeScript
284 lines
9.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { NextRequest } from "next/server";
|
|
|
|
let runtimeRoot = "";
|
|
let data: typeof import("../src/lib/boss-data");
|
|
let completeTaskRoute: typeof import("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route");
|
|
let decisionRoute: typeof import("../src/app/api/v1/dialog-guard/interventions/[interventionId]/decision/route");
|
|
let events: typeof import("../src/lib/boss-events");
|
|
let authCookie = "";
|
|
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data")["readState"]>>;
|
|
|
|
async function setup() {
|
|
if (runtimeRoot) return;
|
|
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-dialog-guard-backend-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
|
|
const [dataModule, completeRouteModule, decisionRouteModule, eventsModule, authModule] =
|
|
await Promise.all([
|
|
import("../src/lib/boss-data.ts"),
|
|
import("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts"),
|
|
import("../src/app/api/v1/dialog-guard/interventions/[interventionId]/decision/route.ts"),
|
|
import("../src/lib/boss-events.ts"),
|
|
import("../src/lib/boss-auth.ts"),
|
|
]);
|
|
|
|
data = dataModule;
|
|
completeTaskRoute = completeRouteModule;
|
|
decisionRoute = decisionRouteModule;
|
|
events = eventsModule;
|
|
authCookie = authModule.AUTH_SESSION_COOKIE;
|
|
baseState = structuredClone(await data.readState());
|
|
}
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) {
|
|
await rm(runtimeRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test.beforeEach(async () => {
|
|
await setup();
|
|
const state = structuredClone(baseState);
|
|
state.masterAgentTasks = [];
|
|
state.permissionAuditLogs = [];
|
|
state.dialogGuardInterventions = [];
|
|
await data.writeState(state);
|
|
});
|
|
|
|
function deviceRequest(taskId: string, body: Record<string, unknown>) {
|
|
return new NextRequest(
|
|
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${taskId}/complete`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-boss-device-token": "boss-mac-studio-token",
|
|
},
|
|
body: JSON.stringify(body),
|
|
},
|
|
);
|
|
}
|
|
|
|
async function authedDecisionRequest(interventionId: string, body: Record<string, unknown>) {
|
|
const session = await data.createAuthSession({
|
|
account: "krisolo",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
return new NextRequest(
|
|
`http://127.0.0.1:3000/api/v1/dialog-guard/interventions/${interventionId}/decision`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: `${authCookie}=${session.sessionToken}`,
|
|
},
|
|
body: JSON.stringify(body),
|
|
},
|
|
);
|
|
}
|
|
|
|
async function queueDesktopTask() {
|
|
const [requestMessage] = await data.appendProjectMessages({
|
|
projectId: "master-agent",
|
|
messages: [
|
|
{
|
|
senderLabel: "Boss 超级管理员",
|
|
body: "打开微信发送一句测试消息",
|
|
kind: "text",
|
|
},
|
|
],
|
|
});
|
|
|
|
return data.queueMasterAgentTask({
|
|
projectId: "master-agent",
|
|
taskType: "desktop_control",
|
|
requestMessageId: requestMessage.id,
|
|
requestText: "打开微信发送一句测试消息",
|
|
executionPrompt: "打开微信发送一句测试消息",
|
|
requestedBy: "Boss 超级管理员",
|
|
requestedByAccount: "krisolo",
|
|
deviceId: "mac-studio",
|
|
accountId: "openai-master",
|
|
accountLabel: "gpt-5.4-mini",
|
|
intentCategory: "desktop_control",
|
|
runtimeKind: "computer-use-runtime",
|
|
riskLevel: "medium",
|
|
confirmationPolicy: "light_confirm",
|
|
});
|
|
}
|
|
|
|
test("needs_user_action task complete creates pending dialog intervention audit log and realtime event", async () => {
|
|
await setup();
|
|
const task = await queueDesktopTask();
|
|
const seenEvents: Array<{
|
|
event: string;
|
|
payload: {
|
|
interventionId?: string;
|
|
projectId?: string;
|
|
appName?: string;
|
|
taskId?: string;
|
|
status?: string;
|
|
};
|
|
}> = [];
|
|
const unsubscribe = events.subscribeBossEvents((event, payload) => {
|
|
seenEvents.push({ event, payload });
|
|
});
|
|
|
|
try {
|
|
const response = await completeTaskRoute.POST(
|
|
deviceRequest(task.taskId, {
|
|
deviceId: "mac-studio",
|
|
status: "needs_user_action",
|
|
kind: "dialog_intervention_required",
|
|
requestId: "runtime-request-001",
|
|
dialogId: "dialog-wechat-send-confirm",
|
|
appName: "微信",
|
|
platform: "darwin",
|
|
risk: "high",
|
|
summary: "微信即将发送外部可见消息,需要用户确认。",
|
|
recommendedAction: "allow_once",
|
|
availableActions: ["allow_once", "deny", "cancel_task"],
|
|
}),
|
|
{ params: Promise.resolve({ taskId: task.taskId }) },
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.task.status, "needs_user_action");
|
|
|
|
const state = await data.readState();
|
|
const intervention = state.dialogGuardInterventions.at(0);
|
|
assert.ok(intervention, "expected dialog intervention to be persisted");
|
|
assert.equal(intervention.taskId, task.taskId);
|
|
assert.equal(intervention.dialogId, "dialog-wechat-send-confirm");
|
|
assert.equal(intervention.requestId, "runtime-request-001");
|
|
assert.equal(intervention.deviceId, "mac-studio");
|
|
assert.equal(intervention.projectId, "master-agent");
|
|
assert.equal(intervention.appName, "微信");
|
|
assert.equal(intervention.platform, "darwin");
|
|
assert.equal(intervention.risk, "high");
|
|
assert.equal(intervention.status, "pending");
|
|
assert.deepEqual(intervention.availableActions, ["allow_once", "deny", "cancel_task"]);
|
|
|
|
assert.equal(
|
|
state.permissionAuditLogs.some(
|
|
(log) =>
|
|
log.action === "dialog_guard.intervention_required" &&
|
|
log.deviceId === "mac-studio" &&
|
|
log.projectId === "master-agent" &&
|
|
log.requestId === "runtime-request-001",
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
seenEvents.some(
|
|
(item) =>
|
|
item.event === "desktop.dialog_guard.intervention_required" &&
|
|
item.payload.interventionId === intervention.interventionId &&
|
|
item.payload.projectId === "master-agent" &&
|
|
item.payload.appName === "微信" &&
|
|
item.payload.taskId === task.taskId &&
|
|
item.payload.status === "pending",
|
|
),
|
|
true,
|
|
);
|
|
} finally {
|
|
unsubscribe();
|
|
}
|
|
});
|
|
|
|
test("decision route resolves pending intervention writes audit log and emits resolved event", async () => {
|
|
await setup();
|
|
const task = await queueDesktopTask();
|
|
await completeTaskRoute.POST(
|
|
deviceRequest(task.taskId, {
|
|
deviceId: "mac-studio",
|
|
status: "needs_user_action",
|
|
kind: "dialog_intervention_required",
|
|
requestId: "runtime-request-002",
|
|
dialogId: "dialog-open-file",
|
|
appName: "Finder",
|
|
platform: "darwin",
|
|
risk: "medium",
|
|
summary: "Finder 要打开一个本地文件。",
|
|
recommendedAction: "allow_once",
|
|
availableActions: ["allow_once", "deny", "handled_on_device"],
|
|
}),
|
|
{ params: Promise.resolve({ taskId: task.taskId }) },
|
|
);
|
|
|
|
const pending = (await data.readState()).dialogGuardInterventions.at(0);
|
|
assert.ok(pending, "expected setup to create a pending intervention");
|
|
|
|
const seenEvents: Array<{
|
|
event: string;
|
|
payload: {
|
|
interventionId?: string;
|
|
projectId?: string;
|
|
decision?: string;
|
|
status?: string;
|
|
};
|
|
}> = [];
|
|
const unsubscribe = events.subscribeBossEvents((event, payload) => {
|
|
seenEvents.push({ event, payload });
|
|
});
|
|
|
|
try {
|
|
const response = await decisionRoute.POST(
|
|
await authedDecisionRequest(pending.interventionId, {
|
|
decision: "allow_once",
|
|
note: "本次允许。",
|
|
}),
|
|
{ params: Promise.resolve({ interventionId: pending.interventionId }) },
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.intervention.status, "resolved");
|
|
assert.equal(payload.intervention.decision, "allow_once");
|
|
|
|
const state = await data.readState();
|
|
const resolved = state.dialogGuardInterventions.find(
|
|
(item) => item.interventionId === pending.interventionId,
|
|
);
|
|
assert.equal(resolved?.status, "resolved");
|
|
assert.equal(resolved?.decision, "allow_once");
|
|
assert.ok(resolved?.resolvedAt);
|
|
assert.equal(
|
|
state.permissionAuditLogs.some(
|
|
(log) =>
|
|
log.action === "dialog_guard.intervention_resolved" &&
|
|
log.actorAccount === "krisolo" &&
|
|
log.deviceId === "mac-studio" &&
|
|
log.projectId === "master-agent" &&
|
|
log.requestId === "runtime-request-002",
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
seenEvents.some(
|
|
(item) =>
|
|
item.event === "desktop.dialog_guard.intervention_resolved" &&
|
|
item.payload.interventionId === pending.interventionId &&
|
|
item.payload.projectId === "master-agent" &&
|
|
item.payload.decision === "allow_once" &&
|
|
item.payload.status === "resolved",
|
|
),
|
|
true,
|
|
);
|
|
} finally {
|
|
unsubscribe();
|
|
}
|
|
});
|