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 postRiskAction: (typeof import("../src/app/api/v1/admin/risks/actions/route"))["POST"]; let subscribeBossEvents: (typeof import("../src/lib/boss-events"))["subscribeBossEvents"]; let baseState: Awaited>; async function setup() { if (runtimeRoot) return; runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-admin-risk-actions-")); process.env.BOSS_RUNTIME_ROOT = runtimeRoot; process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); const [dataModule, authModule, routeModule, eventsModule] = await Promise.all([ import("../src/lib/boss-data.ts"), import("../src/lib/boss-auth.ts"), import("../src/app/api/v1/admin/risks/actions/route.ts"), import("../src/lib/boss-events.ts"), ]); data = dataModule; authCookie = authModule.AUTH_SESSION_COOKIE; postRiskAction = routeModule.POST; subscribeBossEvents = eventsModule.subscribeBossEvents; 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); const now = "2026-04-27T10:00:00+08:00"; state.authAccounts = [ { id: "account-owner", account: "owner@acme.com", passwordHash: "secret", displayName: "Acme 老板", role: "highest_admin", primaryDeviceId: "mac-1", createdAt: now, updatedAt: now, }, { id: "account-admin", account: "admin@acme.com", passwordHash: "secret", displayName: "Acme 管理员", role: "admin", primaryDeviceId: "mac-1", createdAt: now, updatedAt: now, }, ]; state.authSessions = []; state.devices = [ { id: "mac-1", name: "Acme Mac", avatar: "A", account: "owner@acme.com", source: "production", status: "online", projects: ["project-acme"], quota5h: 0, quota7d: 0, lastSeenAt: "2026-04-27T09:58:00+08:00", preferredExecutionMode: "cli", capabilities: { gui: { connected: true, lastSeenAt: "2026-04-27T09:58:00+08:00" }, cli: { connected: true, lastSeenAt: "2026-04-27T09:58:00+08:00" }, browserAutomation: { connected: false }, computerUse: { connected: false }, }, }, ]; state.projects = [ { id: "project-acme", name: "Acme 生产项目", pinned: false, deviceIds: ["mac-1"], preview: "", updatedAt: now, lastMessageAt: now, isGroup: false, threadMeta: { projectId: "project-acme", threadId: "thread-acme", threadDisplayName: "Acme 线程", folderName: "acme", activityIconCount: 0, updatedAt: now, }, groupMembers: [], createdByAgent: false, collaborationMode: "development", approvalState: "not_required", unreadCount: 0, riskLevel: "low", messages: [], goals: [], versions: [], }, ]; state.opsFaults = [ { faultId: "fault-1", faultKey: "LOCAL_AGENT.HEARTBEAT_FAILED", severity: "critical", status: "opened", nodeId: "mac-1", serviceName: "local-agent", projectId: "project-acme", traceId: "trace-1", runbookId: "runbook-agent", firstSeenAt: "2026-04-27T09:00:00+08:00", lastSeenAt: "2026-04-27T09:30:00+08:00", summary: "Mac 节点心跳失败", suggestedNextAction: "检查 local-agent", autoRepairable: true, }, ]; state.opsRepairTickets = []; state.opsRepairVerifications = []; state.threadContextAlerts = [ { alertId: "alert-1", projectId: "project-acme", threadId: "thread-acme", alertType: "context_critical", alertStatus: "opened", openedAt: "2026-04-27T09:20:00+08:00", summary: "线程上下文接近耗尽", masterActions: ["先固化版本记录"], }, ]; state.threadContextSnapshots = [ { snapshotId: "snapshot-1", projectId: "project-acme", taskId: "task-context-1", threadId: "thread-acme", title: "Acme 线程", summary: "上下文接近耗尽", nodeId: "mac-1", workerId: "worker-1", sourceKind: "codex_sdk", status: "running", contextBudgetRemainingPct: 8, contextBudgetLevel: "critical", mustFinishBeforeCompaction: true, estimatedRemainingTurns: 1, estimatedRemainingLargeMessages: 0, compactionCount: 0, patchPending: true, testsPending: true, evidencePending: false, checklist: ["固化项目目标"], capturedAt: "2026-04-27T09:19:00+08:00", }, ]; await data.writeState(state); }); async function authedRequest( account: string, role: "member" | "admin" | "highest_admin", body: Record, ) { const session = await data.createAuthSession({ account, role, displayName: account, loginMethod: "password", }); return new NextRequest("http://127.0.0.1:3000/api/v1/admin/risks/actions", { method: "POST", headers: { "content-type": "application/json", cookie: `${authCookie}=${session.sessionToken}`, }, body: JSON.stringify(body), }); } async function adminPost(body: Record) { return postRiskAction(await authedRequest("owner@acme.com", "highest_admin", body)); } test("admin risk actions require highest admin", async () => { await setup(); const unauthorized = await postRiskAction( new NextRequest("http://127.0.0.1:3000/api/v1/admin/risks/actions", { method: "POST", body: JSON.stringify({ riskId: "ops-fault:fault-1", action: "ack" }), }), ); assert.equal(unauthorized.status, 401); const forbidden = await postRiskAction( await authedRequest("admin@acme.com", "admin", { riskId: "ops-fault:fault-1", action: "ack" }), ); assert.equal(forbidden.status, 403); }); test("highest admin can ack and resolve an ops fault", async () => { const events: Array<{ event: string; payload: { projectId?: string; deviceId?: string; status?: string; note?: string } }> = []; const unsubscribe = subscribeBossEvents((event, payload) => { events.push({ event, payload }); }); const ackResponse = await adminPost({ riskId: "ops-fault:fault-1", action: "ack" }); assert.equal(ackResponse.status, 200); const ackPayload = await ackResponse.json(); assert.equal(ackPayload.ok, true); assert.equal(ackPayload.riskId, "ops-fault:fault-1"); assert.equal(ackPayload.action, "ack"); assert.equal(ackPayload.fault.status, "acked"); let state = await data.readState(); assert.equal(state.opsFaults.find((fault) => fault.faultId === "fault-1")?.status, "acked"); const ackEvent = events.at(-1); assert.equal(ackEvent?.event, "project.context_risk.updated"); assert.equal(ackEvent?.payload.projectId, "project-acme"); assert.equal(ackEvent?.payload.deviceId, "mac-1"); assert.equal(ackEvent?.payload.status, "acked"); assert.equal(ackEvent?.payload.note, "ops-fault:fault-1"); const resolveResponse = await adminPost({ riskId: "ops-fault:fault-1", action: "resolve" }); unsubscribe(); assert.equal(resolveResponse.status, 200); const resolvePayload = await resolveResponse.json(); assert.equal(resolvePayload.fault.status, "resolved"); state = await data.readState(); assert.equal(state.opsFaults.find((fault) => fault.faultId === "fault-1")?.status, "resolved"); }); test("highest admin can create and reuse an ops repair ticket for a fault", async () => { const firstResponse = await adminPost({ riskId: "ops-fault:fault-1", action: "create_repair_ticket" }); assert.equal(firstResponse.status, 200); const firstPayload = await firstResponse.json(); assert.equal(firstPayload.ok, true); assert.equal(firstPayload.ticket.faultId, "fault-1"); assert.equal(firstPayload.ticket.approvalStatus, "pending"); assert.equal(firstPayload.ticket.executionStatus, "queued"); assert.equal(firstPayload.ticket.targetNodeId, "mac-1"); let state = await data.readState(); assert.equal(state.opsRepairTickets.length, 1); assert.equal(state.opsRepairTickets[0]?.ticketId, firstPayload.ticket.ticketId); const secondResponse = await adminPost({ riskId: "ops-fault:fault-1", action: "create_repair_ticket" }); assert.equal(secondResponse.status, 200); const secondPayload = await secondResponse.json(); assert.equal(secondPayload.ticket.ticketId, firstPayload.ticket.ticketId); state = await data.readState(); assert.equal(state.opsRepairTickets.length, 1); }); test("highest admin can ack and resolve a thread context alert", async () => { const ackResponse = await adminPost({ riskId: "thread-alert:alert-1", action: "ack" }); assert.equal(ackResponse.status, 200); const ackPayload = await ackResponse.json(); assert.equal(ackPayload.alert.alertStatus, "acked"); assert.equal(ackPayload.alert.resolvedAt, undefined); let state = await data.readState(); assert.equal(state.threadContextAlerts.find((alert) => alert.alertId === "alert-1")?.alertStatus, "acked"); const resolveResponse = await adminPost({ riskId: "thread-alert:alert-1", action: "resolve" }); assert.equal(resolveResponse.status, 200); const resolvePayload = await resolveResponse.json(); assert.equal(resolvePayload.alert.alertStatus, "resolved"); assert.equal(typeof resolvePayload.alert.resolvedAt, "string"); state = await data.readState(); const alert = state.threadContextAlerts.find((item) => item.alertId === "alert-1"); assert.equal(alert?.alertStatus, "resolved"); assert.equal(typeof alert?.resolvedAt, "string"); }); test("highest admin can assign owners and SLA to risks", async () => { const assignFaultResponse = await adminPost({ riskId: "ops-fault:fault-1", action: "assign_owner", ownerAccount: "admin@acme.com", note: "请先处理心跳链路", }); assert.equal(assignFaultResponse.status, 200); const assignFaultPayload = await assignFaultResponse.json(); assert.equal(assignFaultPayload.ok, true); assert.equal(assignFaultPayload.fault.ownerAccount, "admin@acme.com"); assert.equal(assignFaultPayload.fault.riskNote, "请先处理心跳链路"); const setFaultSlaResponse = await adminPost({ riskId: "ops-fault:fault-1", action: "set_sla", slaDueAt: "2026-04-27T18:00:00+08:00", }); assert.equal(setFaultSlaResponse.status, 200); const setFaultSlaPayload = await setFaultSlaResponse.json(); assert.equal(setFaultSlaPayload.fault.slaDueAt, "2026-04-27T18:00:00+08:00"); const assignAlertResponse = await adminPost({ riskId: "thread-alert:alert-1", action: "assign_owner", ownerAccount: "admin@acme.com", }); assert.equal(assignAlertResponse.status, 200); const assignAlertPayload = await assignAlertResponse.json(); assert.equal(assignAlertPayload.alert.ownerAccount, "admin@acme.com"); const state = await data.readState(); assert.equal(state.opsFaults.find((fault) => fault.faultId === "fault-1")?.ownerAccount, "admin@acme.com"); assert.equal(state.opsFaults.find((fault) => fault.faultId === "fault-1")?.slaDueAt, "2026-04-27T18:00:00+08:00"); assert.equal(state.threadContextAlerts.find((alert) => alert.alertId === "alert-1")?.ownerAccount, "admin@acme.com"); }); test("risk owner and SLA actions require target fields", async () => { const assignResponse = await adminPost({ riskId: "ops-fault:fault-1", action: "assign_owner" }); assert.equal(assignResponse.status, 400); const assignPayload = await assignResponse.json(); assert.equal(assignPayload.message, "RISK_OWNER_REQUIRED"); const slaResponse = await adminPost({ riskId: "ops-fault:fault-1", action: "set_sla" }); assert.equal(slaResponse.status, 400); const slaPayload = await slaResponse.json(); assert.equal(slaPayload.message, "RISK_SLA_REQUIRED"); }); test("unsupported risk actions return 400", async () => { const response = await adminPost({ riskId: "device-offline:mac-1", action: "ack" }); assert.equal(response.status, 400); const payload = await response.json(); assert.equal(payload.ok, false); assert.equal(payload.message, "RISK_ACTION_UNSUPPORTED"); }); test("missing risk targets return 404", async () => { const response = await adminPost({ riskId: "ops-fault:missing", action: "ack" }); assert.equal(response.status, 404); const payload = await response.json(); assert.equal(payload.ok, false); });