Files
boss/tests/admin-backoffice-bff-route.test.ts
2026-06-08 12:22:50 +08:00

431 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.ts");
let authCookie = "";
let getBackoffice: (typeof import("../src/app/api/v1/admin/backoffice/route.ts"))["GET"];
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data.ts")["readState"]>>;
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-admin-backoffice-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [dataModule, authModule, routeModule] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
import("../src/app/api/v1/admin/backoffice/route.ts"),
]);
data = dataModule;
authCookie = authModule.AUTH_SESSION_COOKIE;
getBackoffice = routeModule.GET;
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-30T10:00:00+08:00";
state.adminCompanies = [
{
companyId: "acme",
name: "Acme 科技",
ownerAccount: "owner@acme.com",
successOwnerAccount: "cs@boss.com",
planTier: "enterprise",
contractExpiresAt: "2027-04-30T00:00:00+08:00",
status: "active",
createdAt: now,
updatedAt: now,
},
{
companyId: "otherco",
name: "OtherCo 制造",
ownerAccount: "owner@otherco.com",
successOwnerAccount: "cs2@boss.com",
planTier: "standard",
contractExpiresAt: "2027-04-30T00:00:00+08:00",
status: "active",
createdAt: now,
updatedAt: now,
},
];
state.authAccounts = [
{
id: "account-owner",
account: "owner@acme.com",
passwordHash: "do-not-leak-owner-password-hash",
displayName: "Acme 老板",
role: "highest_admin",
status: "active",
companyId: "acme",
mfaSecret: "do-not-leak-mfa-secret",
primaryDeviceId: "mac-1",
createdAt: now,
updatedAt: now,
lastLoginAt: now,
},
{
id: "account-dev",
account: "dev@acme.com",
passwordHash: "do-not-leak-dev-password-hash",
displayName: "开发同事",
role: "member",
status: "active",
companyId: "acme",
primaryDeviceId: "win-1",
createdAt: now,
updatedAt: now,
},
{
id: "account-other",
account: "owner@otherco.com",
passwordHash: "do-not-leak-other-password-hash",
displayName: "OtherCo 老板",
role: "admin",
status: "active",
companyId: "otherco",
createdAt: now,
updatedAt: now,
},
];
state.authSessions = [];
state.devices = [
{
id: "mac-1",
name: "Acme Mac Studio",
avatar: "A",
account: "owner@acme.com",
companyId: "acme",
source: "production",
status: "online",
projects: ["project-acme"],
quota5h: 0,
quota7d: 0,
lastSeenAt: "2026-04-30T09:58:00+08:00",
preferredExecutionMode: "cli",
capabilities: {
gui: { connected: true, lastSeenAt: "2026-04-30T09:58:00+08:00" },
cli: { connected: true, lastSeenAt: "2026-04-30T09:58:00+08:00" },
browserAutomation: { connected: true, lastSeenAt: "2026-04-30T09:57:00+08:00" },
computerUse: { connected: true, lastSeenAt: "2026-04-30T09:57:00+08:00" },
},
},
{
id: "win-1",
name: "Acme Windows",
avatar: "W",
account: "dev@acme.com",
companyId: "acme",
source: "production",
status: "offline",
projects: ["project-acme"],
quota5h: 0,
quota7d: 0,
lastSeenAt: "2026-04-30T08:00:00+08:00",
preferredExecutionMode: "gui",
capabilities: {
gui: { connected: false },
cli: { connected: false },
browserAutomation: { connected: false },
computerUse: { connected: false },
},
},
{
id: "other-mac",
name: "OtherCo Mac",
avatar: "O",
account: "owner@otherco.com",
companyId: "otherco",
source: "production",
status: "online",
projects: ["project-other"],
quota5h: 0,
quota7d: 0,
lastSeenAt: "2026-04-30T09:58:00+08:00",
preferredExecutionMode: "cli",
capabilities: {
gui: { connected: true },
cli: { connected: true },
browserAutomation: { connected: true },
computerUse: { connected: true },
},
},
];
state.projects = [
{
id: "project-acme",
name: "Acme 生产项目",
pinned: false,
deviceIds: ["mac-1", "win-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: [],
},
{
id: "project-other",
name: "OtherCo 私有项目",
pinned: false,
deviceIds: ["other-mac"],
preview: "不应出现在 Acme 企业后台",
updatedAt: now,
lastMessageAt: now,
isGroup: false,
threadMeta: {
projectId: "project-other",
threadId: "thread-other",
threadDisplayName: "OtherCo 线程",
folderName: "otherco",
activityIconCount: 0,
updatedAt: now,
},
groupMembers: [],
createdByAgent: false,
collaborationMode: "development",
approvalState: "not_required",
unreadCount: 0,
riskLevel: "low",
messages: [],
goals: [],
versions: [],
},
];
state.deviceSkills = [
{
skillId: "mac-1:boss-server-debug",
deviceId: "mac-1",
name: "boss-server-debug",
description: "Boss 服务器调试",
path: "/Users/kris/.codex/skills/boss-server-debug/SKILL.md",
invocation: "$boss-server-debug",
category: "运维",
updatedAt: now,
},
];
state.opsFaults = [
{
faultId: "fault-1",
faultKey: "LOCAL_AGENT.HEARTBEAT_FAILED",
severity: "warning",
status: "opened",
nodeId: "win-1",
serviceName: "local-agent",
projectId: "project-acme",
traceId: "trace-1",
runbookId: "runbook-agent",
firstSeenAt: "2026-04-30T08:10:00+08:00",
lastSeenAt: "2026-04-30T08:30:00+08:00",
summary: "Windows 节点心跳失败",
suggestedNextAction: "检查 local-agent",
autoRepairable: true,
},
];
state.permissionAuditLogs = [
{
auditId: "audit-1",
actorAccount: "owner@acme.com",
action: "grant.created",
targetAccount: "dev@acme.com",
deviceId: "mac-1",
permissions: ["device.view"],
detail: "授权设备只读",
createdAt: now,
},
];
state.adminRiskTimeline = [
{
eventId: "risk-event-1",
riskId: "ops-fault:fault-1",
companyId: "acme",
action: "risk.created",
actorAccount: "system",
note: "发现节点风险",
createdAt: now,
},
];
state.masterAgentTasks = [
{
taskId: "task-stale",
projectId: "project-acme",
taskType: "conversation_reply",
requestMessageId: "message-stale-request",
requestText: "请继续处理 Acme 生产项目的等待回复。",
executionPrompt: "继续 Acme 生产项目的 conversation_reply并回写安全摘要。",
requestedBy: "开发同事",
requestedByAccount: "dev@acme.com",
deviceId: "win-1",
status: "running",
phase: "awaiting_reply",
requestedAt: "2026-04-30T08:00:00+08:00",
claimedAt: "2026-04-30T08:01:00+08:00",
lastProgressAt: "2026-04-30T08:01:00+08:00",
leaseExpiresAt: "2026-04-30T08:02:00+08:00",
attemptCount: 1,
maxAttempts: 2,
},
];
await data.writeState(state);
});
async function authedRequest(account: string, role: "member" | "admin" | "highest_admin") {
const session = await data.createAuthSession({
account,
role,
displayName: account,
loginMethod: "password",
});
return new NextRequest("http://127.0.0.1:3000/api/v1/admin/backoffice", {
headers: {
cookie: `${authCookie}=${session.sessionToken}`,
},
});
}
async function authedScopedRequest(account: string, role: "member" | "admin" | "highest_admin", scope: string) {
const session = await data.createAuthSession({
account,
role,
displayName: account,
loginMethod: "password",
});
return new NextRequest(`http://127.0.0.1:3000/api/v1/admin/backoffice?scope=${scope}`, {
headers: {
cookie: `${authCookie}=${session.sessionToken}`,
},
});
}
test("backoffice bff rejects non highest admin accounts", async () => {
await setup();
const response = await getBackoffice(await authedRequest("dev@acme.com", "member"));
assert.equal(response.status, 403);
});
test("enterprise backoffice allows company admins and filters to their company", async () => {
await setup();
const response = await getBackoffice(await authedScopedRequest("owner@acme.com", "admin", "enterprise"));
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.ok, true);
assert.equal(payload.surface, "enterprise");
assert.equal(payload.currentCompany.companyId, "acme");
assert.deepEqual(
payload.menuTree.map((item: { label: string }) => item.label),
["企业总览", "组织与成员", "设备与项目", "Agent 与流程", "Skill 中心", "风险与审计", "备份与回退", "企业设置"],
);
assert.deepEqual(
payload.insights.agentFlowSteps,
["主 Agent", "项目 Agent", "本地 Agent", "Codex / Computer Use / Skill"],
);
assert.deepEqual(
payload.insights.recoveryActions,
["消息恢复", "项目目标恢复", "权限撤销", "Skill 回滚", "Codex checkpoint"],
);
assert.equal(payload.insights.organizationUnits.includes("研发部"), true);
assert.equal(payload.tenants.length, 1);
assert.equal(payload.tenants[0].companyId, "acme");
assert.equal(payload.users.every((user: { companyId: string }) => user.companyId === "acme"), true);
assert.equal(payload.resourceGroups.devices.some((device: { id: string }) => device.id === "other-mac"), false);
assert.equal(
payload.resourceGroups.projects.some((project: { name: string }) => project.name === "OtherCo 私有项目"),
false,
);
assert.equal(JSON.stringify(payload).includes("do-not-leak-other-password-hash"), false);
});
test("enterprise backoffice rejects normal members", async () => {
await setup();
const response = await getBackoffice(await authedScopedRequest("dev@acme.com", "member", "enterprise"));
assert.equal(response.status, 403);
});
test("backoffice bff exposes yudao style management contract without secrets", async () => {
await setup();
const response = await getBackoffice(await authedRequest("owner@acme.com", "highest_admin"));
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.ok, true);
assert.deepEqual(
payload.menuTree.map((item: { label: string }) => item.label),
["平台总览", "企业开通", "客户与套餐", "全局设备", "全局风险", "客户成功", "系统审计", "计费与授权", "平台设置"],
);
assert.equal(payload.surface, "platform");
assert.deepEqual(
payload.insights.onboardingSteps,
["企业信息", "老板账号", "套餐授权", "设备与交付"],
);
assert.deepEqual(
payload.insights.serviceStatuses.map((item: { label: string }) => item.label),
["Boss API", "OTA", "Codex Provider", "Computer Use", "Skill Hub"],
);
assert.equal(payload.insights.riskAggregates.some((item: { label: string }) => item.label === "设备离线"), true);
assert.equal(payload.insights.dataSafetySummary.restorePointCount >= 0, true);
assert.match(payload.insights.dataSafetySummary.rpoLabel, /文件 MVP|企业标准/);
assert.equal(Array.isArray(payload.insights.taskRiskSummary.rows), true);
assert.equal(typeof payload.insights.taskRiskSummary.counts.stale, "number");
const staleTask = payload.insights.taskRiskSummary.rows.find((row: { taskId: string }) => row.taskId === "task-stale");
assert.equal(staleTask?.stale, true);
assert.equal(staleTask?.phase, "awaiting_reply");
assert.equal(Array.isArray(payload.insights.taskSlaPanel.rows), true);
assert.equal(payload.insights.taskSlaPanel.summary.breached >= 1, true);
const breachedTask = payload.insights.taskSlaPanel.rows.find((row: { taskId: string }) => row.taskId === "task-stale");
assert.equal(breachedTask?.slaLevel, "breached");
assert.equal(breachedTask?.riskId, "master-task:task-stale");
assert.equal(typeof breachedTask?.recommendedAction, "string");
assert.equal(payload.yudaoMapping.tenant, "adminCompanies");
assert.equal(payload.yudaoMapping.user, "authAccounts");
assert.equal(payload.yudaoMapping.role, "BOSS_PERMISSION_TEMPLATES");
assert.equal(payload.workbench.summary.companies >= 1, true);
assert.equal(
payload.tenants.some((tenant: { name: string }) => tenant.name === "Acme 科技"),
true,
);
assert.equal(payload.users[0].passwordHash, undefined);
assert.equal(payload.users[0].mfaSecret, undefined);
assert.equal(payload.roles.permissionTemplates.length >= 3, true);
assert.equal(payload.resourceGroups.devices.length, 3);
assert.equal(
payload.resourceGroups.projects.some((project: { name: string }) => project.name === "Acme 生产项目"),
true,
);
assert.equal(payload.resourceGroups.skills[0].name, "boss-server-debug");
assert.equal(payload.audit.permissionLogs.length, 1);
assert.equal(payload.audit.risks.length >= 1, true);
const serialized = JSON.stringify(payload);
assert.equal(serialized.includes("do-not-leak-owner-password-hash"), false);
assert.equal(serialized.includes("do-not-leak-mfa-secret"), false);
});