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 getBackups: (typeof import("../src/app/api/v1/admin/backups/route.ts"))["GET"]; let postBackups: (typeof import("../src/app/api/v1/admin/backups/route.ts"))["POST"]; let baseState: Awaited>; async function setup() { if (runtimeRoot) return; runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-admin-backups-")); process.env.BOSS_RUNTIME_ROOT = runtimeRoot; process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); process.env.BOSS_STATE_BACKUP_DIR = path.join(runtimeRoot, "state-backups"); process.env.BOSS_STATE_AUTO_BACKUP_ENABLED = "1"; process.env.BOSS_STATE_AUTO_BACKUP_INTERVAL_MS = "0"; 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/backups/route.ts"), ]); data = dataModule; authCookie = authModule.AUTH_SESSION_COOKIE; getBackups = routeModule.GET; postBackups = routeModule.POST; 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.authAccounts = [ { id: "account-owner", account: "owner@boss.com", passwordHash: "secret", displayName: "平台管理员", role: "highest_admin", createdAt: "2026-05-16T10:00:00+08:00", updatedAt: "2026-05-16T10:00:00+08:00", }, { id: "account-member", account: "member@boss.com", passwordHash: "secret", displayName: "普通成员", role: "member", createdAt: "2026-05-16T10:00:00+08:00", updatedAt: "2026-05-16T10:00:00+08:00", }, ]; state.projects = [ { id: "project-before", name: "备份前项目", pinned: false, deviceIds: [], preview: "before", updatedAt: "2026-05-16T10:00:00+08:00", lastMessageAt: "2026-05-16T10:00:00+08:00", isGroup: false, threadMeta: { projectId: "project-before", threadId: "thread-before", threadDisplayName: "备份前线程", folderName: "before", activityIconCount: 0, updatedAt: "2026-05-16T10:00:00+08:00", }, groupMembers: [], createdByAgent: false, collaborationMode: "development", approvalState: "not_required", unreadCount: 0, riskLevel: "low", messages: [], goals: [], versions: [], }, ]; state.authSessions = []; await data.writeState(state); }); async function requestFor(account: string, role: "member" | "admin" | "highest_admin", init: RequestInit = {}) { const session = await data.createAuthSession({ account, role, displayName: account, loginMethod: "password", }); return new NextRequest("http://127.0.0.1:3000/api/v1/admin/backups", { ...init, headers: { "content-type": "application/json", ...(init.headers ?? {}), cookie: `${authCookie}=${session.sessionToken}`, }, }); } test("admin backups require highest admin", async () => { await setup(); const unauthenticated = await getBackups(new NextRequest("http://127.0.0.1:3000/api/v1/admin/backups")); assert.equal(unauthenticated.status, 401); const forbidden = await getBackups(await requestFor("member@boss.com", "member")); assert.equal(forbidden.status, 403); }); test("highest admin can create, list, and restore a state snapshot", async () => { const createResponse = await postBackups( await requestFor("owner@boss.com", "highest_admin", { method: "POST", body: JSON.stringify({ action: "create_snapshot", reason: "before risky operation" }), }), ); assert.equal(createResponse.status, 200); const createPayload = await createResponse.json(); assert.equal(createPayload.ok, true); assert.equal(createPayload.snapshot.reason, "before risky operation"); assert.equal(createPayload.snapshot.actorAccount, "owner@boss.com"); assert.match(createPayload.snapshot.snapshotId, /^state-snapshot-/); const mutated = await data.readState(); mutated.projects[0]!.name = "误操作后的项目"; await data.writeState(mutated); const listResponse = await getBackups(await requestFor("owner@boss.com", "highest_admin")); assert.equal(listResponse.status, 200); const listPayload = await listResponse.json(); assert.equal(listPayload.ok, true); assert.equal(listPayload.status.restorePointCount >= 1, true); assert.equal(listPayload.snapshots.some((snapshot: { snapshotId: string }) => snapshot.snapshotId === createPayload.snapshot.snapshotId), true); const restoreResponse = await postBackups( await requestFor("owner@boss.com", "highest_admin", { method: "POST", body: JSON.stringify({ action: "restore_snapshot", snapshotId: createPayload.snapshot.snapshotId }), }), ); assert.equal(restoreResponse.status, 200); const restorePayload = await restoreResponse.json(); assert.equal(restorePayload.ok, true); assert.equal(restorePayload.restored.snapshotId, createPayload.snapshot.snapshotId); assert.match(restorePayload.preRestoreSnapshot.snapshotId, /^state-snapshot-/); const restored = await data.readState(); assert.equal(restored.projects.find((project) => project.id === "project-before")?.name, "备份前项目"); }); test("state writes create automatic restore points", async () => { const state = await data.readState(); state.projects[0]!.name = "自动备份触发项目"; await data.writeState(state); const listResponse = await getBackups(await requestFor("owner@boss.com", "highest_admin")); assert.equal(listResponse.status, 200); const listPayload = await listResponse.json(); assert.equal(listPayload.ok, true); assert.equal( listPayload.snapshots.some((snapshot: { reason?: string; actorAccount?: string }) => snapshot.reason === "auto:writeState" && snapshot.actorAccount === "system", ), true, ); });