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 getSkillRequests: (typeof import("../src/app/api/v1/admin/skills/requests/route"))["GET"]; let postSkillRequest: (typeof import("../src/app/api/v1/admin/skills/requests/route"))["POST"]; let claimSkillRequest: (typeof import("../src/app/api/v1/devices/[deviceId]/skill-requests/claim/route"))["POST"]; let completeSkillRequest: (typeof import("../src/app/api/v1/devices/[deviceId]/skill-requests/[requestId]/complete/route"))["POST"]; let baseState: Awaited>; async function setup() { if (runtimeRoot) return; runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-skill-lifecycle-")); process.env.BOSS_RUNTIME_ROOT = runtimeRoot; process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); const [dataModule, authModule, routeModule, claimRouteModule, completeRouteModule] = await Promise.all([ import("../src/lib/boss-data.ts"), import("../src/lib/boss-auth.ts"), import("../src/app/api/v1/admin/skills/requests/route.ts"), import("../src/app/api/v1/devices/[deviceId]/skill-requests/claim/route.ts"), import("../src/app/api/v1/devices/[deviceId]/skill-requests/[requestId]/complete/route.ts"), ]); data = dataModule; authCookie = authModule.AUTH_SESSION_COOKIE; getSkillRequests = routeModule.GET; postSkillRequest = routeModule.POST; claimSkillRequest = claimRouteModule.POST; completeSkillRequest = completeRouteModule.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.skillLifecycleRequests = []; state.deviceSkills = [ { skillId: "mac-studio:boss-server-debug", deviceId: "mac-studio", name: "boss-server-debug", description: "服务器调试", path: "/Users/kris/.codex/skills/boss-server-debug/SKILL.md", invocation: "$boss-server-debug", category: "Mac Studio", updatedAt: "2026-04-26T12:00:00+08:00", }, ]; await data.writeState(state); }); async function authedRequest( account: string, role: "member" | "admin" | "highest_admin", url: string, init: RequestInit = {}, ) { const session = await data.createAuthSession({ account, role, displayName: account, loginMethod: "password", }); return new NextRequest(url, { ...init, headers: { ...(init.headers ?? {}), cookie: `${authCookie}=${session.sessionToken}`, }, }); } async function adminPost(body: Record) { return postSkillRequest( await authedRequest("krisolo", "highest_admin", "http://127.0.0.1:3000/api/v1/admin/skills/requests", { method: "POST", body: JSON.stringify(body), }), ); } async function devicePost( deviceId: string, url: string, body: Record = {}, ) { return new NextRequest(url, { method: "POST", headers: { "Content-Type": "application/json", "x-boss-device-token": deviceId === "mac-studio" ? "boss-mac-studio-token" : `${deviceId}-token`, }, body: JSON.stringify(body), }); } test("member cannot create or list skill lifecycle requests", async () => { const getResponse = await getSkillRequests( await authedRequest("worker@example.com", "member", "http://127.0.0.1:3000/api/v1/admin/skills/requests"), ); assert.equal(getResponse.status, 403); const postResponse = await postSkillRequest( await authedRequest("worker@example.com", "member", "http://127.0.0.1:3000/api/v1/admin/skills/requests", { method: "POST", body: JSON.stringify({ action: "install", deviceId: "mac-studio", sourceUrl: "https://git.example.com/org/skill.git", }), }), ); assert.equal(postResponse.status, 403); }); test("highest admin can create install update uninstall rollback and version lock requests", async () => { const cases = [ { action: "install", deviceId: "mac-studio", sourceUrl: "https://git.example.com/org/new-skill.git" }, { action: "update", deviceId: "mac-studio", skillId: "mac-studio:boss-server-debug", targetVersion: "1.2.0" }, { action: "uninstall", deviceId: "mac-studio", skillId: "mac-studio:boss-server-debug" }, { action: "rollback", deviceId: "mac-studio", skillId: "mac-studio:boss-server-debug", rollbackToVersion: "1.1.0" }, { action: "version_lock", deviceId: "mac-studio", skillId: "mac-studio:boss-server-debug", lockedVersion: "1.1.0" }, ]; for (const item of cases) { const response = await adminPost(item); assert.equal(response.status, 200); const payload = await response.json(); assert.equal(payload.ok, true); assert.equal(payload.request.status, "pending"); assert.equal(payload.request.deviceId, "mac-studio"); assert.equal(payload.request.requestedBy, "krisolo"); } const listResponse = await getSkillRequests( await authedRequest("krisolo", "highest_admin", "http://127.0.0.1:3000/api/v1/admin/skills/requests"), ); assert.equal(listResponse.status, 200); const listPayload = await listResponse.json(); assert.deepEqual( listPayload.requests.map((request: { action: string }) => request.action), ["version_lock", "rollback", "uninstall", "update", "install"], ); const state = await data.readState(); assert.equal(state.skillLifecycleRequests.length, 5); assert.equal( state.permissionAuditLogs.filter((log) => log.action === "skill.lifecycle.requested").length, 5, ); }); test("skill lifecycle request must bind a device and a skill id or source url", async () => { const missingDevice = await adminPost({ action: "install", sourceUrl: "https://git.example.com/org/new-skill.git", }); assert.equal(missingDevice.status, 400); const missingTarget = await adminPost({ action: "install", deviceId: "mac-studio", }); assert.equal(missingTarget.status, 400); const invalidAction = await adminPost({ action: "enable", deviceId: "mac-studio", skillId: "mac-studio:boss-server-debug", }); assert.equal(invalidAction.status, 400); }); test("skill lifecycle request preserves trusted source and checksum for device claim", async () => { const createResponse = await adminPost({ action: "install", deviceId: "mac-studio", trustedSourceId: "company-skillhub", expectedChecksum: "abc123", }); assert.equal(createResponse.status, 200); const createPayload = await createResponse.json(); assert.equal(createPayload.request.trustedSourceId, "company-skillhub"); assert.equal(createPayload.request.expectedChecksum, "abc123"); const claimResponse = await claimSkillRequest( await devicePost("mac-studio", "http://127.0.0.1:3000/api/v1/devices/mac-studio/skill-requests/claim"), { params: Promise.resolve({ deviceId: "mac-studio" }) }, ); assert.equal(claimResponse.status, 200); const claimPayload = await claimResponse.json(); assert.equal(claimPayload.request.trustedSourceId, "company-skillhub"); assert.equal(claimPayload.request.expectedChecksum, "abc123"); }); test("skill lifecycle request rejects unknown devices and existing skill mismatches", async () => { const missingDevice = await adminPost({ action: "install", deviceId: "missing-device", sourceUrl: "https://git.example.com/org/new-skill.git", }); assert.equal(missingDevice.status, 404); assert.equal((await missingDevice.json()).message, "DEVICE_NOT_FOUND"); const missingSkill = await adminPost({ action: "update", deviceId: "mac-studio", skillId: "mac-studio:missing-skill", }); assert.equal(missingSkill.status, 404); assert.equal((await missingSkill.json()).message, "SKILL_NOT_FOUND"); }); test("device can claim and complete only its own skill lifecycle requests", async () => { const createResponse = await adminPost({ action: "update", deviceId: "mac-studio", skillId: "mac-studio:boss-server-debug", targetVersion: "1.2.0", }); assert.equal(createResponse.status, 200); const claimResponse = await claimSkillRequest( await devicePost("mac-studio", "http://127.0.0.1:3000/api/v1/devices/mac-studio/skill-requests/claim"), { params: Promise.resolve({ deviceId: "mac-studio" }) }, ); assert.equal(claimResponse.status, 200); const claimPayload = await claimResponse.json(); assert.equal(claimPayload.ok, true); assert.equal(claimPayload.request.action, "update"); assert.equal(claimPayload.request.status, "running"); assert.equal(claimPayload.request.claimedByDeviceId, "mac-studio"); const emptyClaimResponse = await claimSkillRequest( await devicePost("mac-studio", "http://127.0.0.1:3000/api/v1/devices/mac-studio/skill-requests/claim"), { params: Promise.resolve({ deviceId: "mac-studio" }) }, ); assert.equal(emptyClaimResponse.status, 200); assert.equal((await emptyClaimResponse.json()).request, null); const completeResponse = await completeSkillRequest( await devicePost( "mac-studio", `http://127.0.0.1:3000/api/v1/devices/mac-studio/skill-requests/${claimPayload.request.requestId}/complete`, { status: "completed", resultSummary: "Skill 已更新到 1.2.0", }, ), { params: Promise.resolve({ deviceId: "mac-studio", requestId: claimPayload.request.requestId, }), }, ); assert.equal(completeResponse.status, 200); const completePayload = await completeResponse.json(); assert.equal(completePayload.ok, true); assert.equal(completePayload.request.status, "completed"); assert.equal(completePayload.request.resultSummary, "Skill 已更新到 1.2.0"); const state = await data.readState(); assert.equal(state.skillLifecycleRequests[0]?.status, "completed"); assert.equal( state.permissionAuditLogs.some((log) => log.action === "skill.lifecycle.completed"), true, ); });