Files
boss/tests/admin-risk-sla-notifications-route.test.ts
2026-06-08 12:22:50 +08:00

305 lines
10 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 authCookie = "";
let postScan: (typeof import("../src/app/api/v1/admin/risks/scan/route"))["POST"];
let getOverview: (typeof import("../src/app/api/v1/admin/overview/route"))["GET"];
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-risk-sla-notifications-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [dataModule, authModule, scanRoute, overviewRoute] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
import("../src/app/api/v1/admin/risks/scan/route.ts"),
import("../src/app/api/v1/admin/overview/route.ts"),
]);
data = dataModule;
authCookie = authModule.AUTH_SESSION_COOKIE;
postScan = scanRoute.POST;
getOverview = overviewRoute.GET;
}
test.after(async () => {
if (runtimeRoot) await rm(runtimeRoot, { recursive: true, force: true });
});
test.beforeEach(async () => {
await setup();
const now = "2026-04-27T17:30:00+08:00";
const state = await data.readState();
await data.writeState({
...state,
adminNotifications: [],
authAccounts: [
{
id: "account-owner",
account: "owner@example.com",
passwordHash: "hash",
displayName: "平台管理员",
role: "highest_admin",
createdAt: now,
updatedAt: now,
},
{
id: "account-customer",
account: "customer@example.com",
passwordHash: "hash",
displayName: "客户负责人",
role: "admin",
companyId: "tenant-a",
createdAt: now,
updatedAt: now,
},
],
devices: [
{
id: "mac-a",
name: "客户 Mac",
avatar: "M",
account: "customer@example.com",
companyId: "tenant-a",
source: "production",
status: "online",
projects: ["project-a"],
quota5h: 0,
quota7d: 0,
lastSeenAt: now,
},
],
projects: [],
opsFaults: [
{
faultId: "fault-overdue",
faultKey: "LOCAL_AGENT.OFFLINE",
severity: "critical",
status: "opened",
nodeId: "mac-a",
serviceName: "local-agent",
projectId: "project-a",
traceId: "trace-overdue",
runbookId: "runbook-local-agent",
firstSeenAt: "2026-04-27T12:00:00+08:00",
lastSeenAt: "2026-04-27T13:00:00+08:00",
ownerAccount: "customer@example.com",
slaDueAt: "2026-04-27T14:00:00+08:00",
summary: "local-agent 离线超过 SLA",
suggestedNextAction: "联系客户重启本地节点",
autoRepairable: false,
},
],
threadContextAlerts: [],
});
});
async function adminRequest(url: string, init: RequestInit = {}) {
const session = await data.createAuthSession({
account: "owner@example.com",
role: "highest_admin",
displayName: "平台管理员",
loginMethod: "password",
});
return new NextRequest(url, {
...init,
headers: {
...(init.headers ?? {}),
cookie: `${authCookie}=${session.sessionToken}`,
},
});
}
test("risk SLA scan creates idempotent overdue notifications", async () => {
const first = await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", {
method: "POST",
}));
assert.equal(first.status, 200);
const firstPayload = await first.json();
assert.equal(firstPayload.created.length, 1);
assert.equal(firstPayload.notifications[0].kind, "risk_sla_overdue");
assert.equal(firstPayload.notifications[0].companyId, "tenant-a");
const second = await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", {
method: "POST",
}));
assert.equal(second.status, 200);
const secondPayload = await second.json();
assert.equal(secondPayload.created.length, 0);
assert.equal(secondPayload.notifications.length, 1);
});
test("admin overview includes open risk notifications", async () => {
await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", { method: "POST" }));
const response = await getOverview(await adminRequest("http://127.0.0.1:3000/api/v1/admin/overview"));
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.summary.openNotifications, 1);
assert.equal(payload.notifications[0].riskId, "ops-fault:fault-overdue");
});
test("risk scan creates operational faults for computer use and boss-agent OTA failures", async () => {
const state = await data.readState();
await data.writeState({
...state,
adminNotifications: [],
opsFaults: [],
devices: [
{
id: "mac-a",
name: "客户 Mac",
avatar: "M",
account: "customer@example.com",
companyId: "tenant-a",
source: "production",
status: "online",
projects: ["project-a"],
quota5h: 0,
quota7d: 0,
lastSeenAt: "2026-04-27T17:20:00+08:00",
capabilities: {
gui: { connected: true },
cli: { connected: true },
browserAutomation: { connected: true },
computerUse: { connected: false },
codexAppServer: { connected: true },
},
},
],
appLogs: [
{
logId: "log-ota-failed",
deviceId: "mac-a",
level: "error",
source: "local_agent",
category: "local_agent.boss_agent_ota_failed",
message: "boss-agent OTA 更新失败",
detail: "BOSS_AGENT_OTA_CHECKSUM_MISMATCH",
mirroredToProject: false,
createdAt: "2026-04-27T17:21:00+08:00",
},
],
});
const response = await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", {
method: "POST",
}));
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.createdFaults.length, 2);
assert.equal(payload.createdFaults.some((fault: { faultKey: string }) => fault.faultKey === "BOSS.COMPUTER_USE.UNAVAILABLE"), true);
assert.equal(payload.createdFaults.some((fault: { faultKey: string }) => fault.faultKey === "BOSS_AGENT.OTA.FAILED"), true);
const nextState = await data.readState();
assert.equal(nextState.opsFaults.some((fault) => fault.faultKey === "BOSS.COMPUTER_USE.UNAVAILABLE"), true);
assert.equal(nextState.opsFaults.some((fault) => fault.faultKey === "BOSS_AGENT.OTA.FAILED"), true);
const second = await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", {
method: "POST",
}));
const secondPayload = await second.json();
assert.equal(secondPayload.createdFaults.length, 0);
});
test("risk scan creates SLA notifications for stuck master agent tasks", async () => {
const state = await data.readState();
await data.writeState({
...state,
adminNotifications: [],
masterAgentTasks: [
{
taskId: "task-stuck",
projectId: "project-a",
taskType: "conversation_reply",
requestMessageId: "msg-stuck",
requestText: "让线程继续执行",
executionPrompt: "继续执行并回写结果",
requestedBy: "客户负责人",
requestedByAccount: "customer@example.com",
deviceId: "mac-a",
status: "running",
phase: "awaiting_reply",
requestedAt: "2026-04-27T13:00:00+08:00",
claimedAt: "2026-04-27T13:01:00+08:00",
lastProgressAt: "2026-04-27T13:01:00+08:00",
leaseExpiresAt: "2026-04-27T13:16:00+08:00",
attemptCount: 1,
maxAttempts: 2,
},
],
});
const response = await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", {
method: "POST",
}));
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(
payload.created.some((notification: { riskId: string }) => notification.riskId === "master-task:task-stuck"),
true,
);
assert.equal(
payload.notifications.some((notification: { title: string }) => notification.title.includes("任务 SLA")),
true,
);
});
test("risk scan automatically requeues safely recoverable master agent tasks", async () => {
const state = await data.readState();
await data.writeState({
...state,
adminNotifications: [],
permissionAuditLogs: [],
adminRiskTimeline: [],
masterAgentTasks: [
{
taskId: "task-recoverable",
projectId: "project-a",
taskType: "conversation_reply",
requestMessageId: "msg-recoverable",
requestText: "继续处理",
executionPrompt: "继续处理并回写结果",
requestedBy: "客户负责人",
requestedByAccount: "customer@example.com",
deviceId: "mac-a",
status: "running",
phase: "recoverable_failed",
requestedAt: "2026-04-27T13:00:00+08:00",
claimedAt: "2026-04-27T13:01:00+08:00",
lastProgressAt: "2026-04-27T13:02:00+08:00",
leaseExpiresAt: "2026-04-27T13:16:00+08:00",
attemptCount: 1,
maxAttempts: 2,
recoverable: true,
nextRetryAt: "2026-04-27T13:03:00+08:00",
lastErrorCode: "RECOVERABLE_RUNTIME_FAILURE",
errorMessage: "CODEX_APP_SERVER_TIMEOUT",
},
],
});
const response = await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", {
method: "POST",
}));
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.autoRecovered.length, 1);
assert.equal(payload.autoRecovered[0].taskId, "task-recoverable");
const nextState = await data.readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === "task-recoverable");
assert.equal(task?.status, "queued");
assert.equal(task?.phase, "queued");
assert.equal(task?.recoverable, false);
assert.equal(nextState.permissionAuditLogs.some((log) => log.action === "master_agent.task_retried"), true);
assert.equal(nextState.adminRiskTimeline.some((event) => event.action === "task.auto_recovery_requeued"), true);
});