import test from "node:test"; import assert from "node:assert/strict"; import os from "node:os"; import path from "node:path"; import { mkdtemp, rm, readFile } from "node:fs/promises"; let runtimeRoot = ""; let data: typeof import("../src/lib/boss-data"); async function setup() { if (runtimeRoot) return; runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-state-migrations-")); process.env.BOSS_RUNTIME_ROOT = runtimeRoot; process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); data = await import("../src/lib/boss-data.ts"); } test.after(async () => { if (runtimeRoot) await rm(runtimeRoot, { recursive: true, force: true }); }); test("migrates legacy unversioned state into the current schema metadata and RBAC/Skill arrays", async () => { await setup(); const migrated = data.migrateBossState({ accountDeviceGrants: [ { account: "worker@example.com", deviceId: "mac-studio", permissions: ["device.view", "device.view", "invalid.permission"], }, ], accountProjectGrants: [ { account: "worker@example.com", projectId: "master-agent", permissions: ["project.view", "thread.chat"], }, { account: "broken@example.com", projectId: "master-agent", permissions: ["not-real"], }, ], accountSkillGrants: undefined, skillLifecycleRequests: [ { deviceId: "mac-studio", sourceUrl: "https://example.com/skills/demo.git", action: "not-a-real-action", status: "not-a-real-status", }, ], permissionAuditLogs: [ { actorAccount: "krisolo", action: "unexpected-action", targetAccount: "worker@example.com", permissions: ["device.view", "bad.permission"], }, ], } as unknown as Partial); assert.equal(migrated.schemaVersion, data.CURRENT_BOSS_STATE_SCHEMA_VERSION); assert.match(migrated.migratedAt, /^\d{4}-\d{2}-\d{2}T/); assert.deepEqual(migrated.accountDeviceGrants[0]?.permissions, ["device.view"]); assert.deepEqual(migrated.accountProjectGrants.map((grant) => grant.account), ["worker@example.com"]); assert.deepEqual(migrated.accountSkillGrants, []); assert.equal(migrated.skillLifecycleRequests[0]?.action, "install"); assert.equal(migrated.skillLifecycleRequests[0]?.status, "pending"); assert.equal(migrated.permissionAuditLogs[0]?.action, "grant.updated"); assert.deepEqual(migrated.permissionAuditLogs[0]?.permissions, ["device.view"]); }); test("preserves current schema migration metadata instead of rewriting it on every normalize", async () => { await setup(); const migrated = data.migrateBossState({ schemaVersion: data.CURRENT_BOSS_STATE_SCHEMA_VERSION, migratedAt: "2026-04-20T08:00:00.000Z", accountDeviceGrants: [], accountProjectGrants: [], accountSkillGrants: [], skillLifecycleRequests: [], permissionAuditLogs: [], } as Partial); assert.equal(migrated.schemaVersion, data.CURRENT_BOSS_STATE_SCHEMA_VERSION); assert.equal(migrated.migratedAt, "2026-04-20T08:00:00.000Z"); }); test("writeState persists schema metadata while keeping the state file JSON-compatible", async () => { await setup(); const state = await data.readState(); assert.equal(state.schemaVersion, data.CURRENT_BOSS_STATE_SCHEMA_VERSION); assert.match(state.migratedAt, /^\d{4}-\d{2}-\d{2}T/); await data.writeState(state); const persisted = JSON.parse(await readFile(process.env.BOSS_STATE_FILE as string, "utf8")); assert.equal(persisted.schemaVersion, data.CURRENT_BOSS_STATE_SCHEMA_VERSION); assert.equal(persisted.migratedAt, state.migratedAt); assert.ok(Array.isArray(persisted.accountDeviceGrants)); assert.ok(Array.isArray(persisted.accountProjectGrants)); assert.ok(Array.isArray(persisted.accountSkillGrants)); assert.ok(Array.isArray(persisted.skillLifecycleRequests)); assert.ok(Array.isArray(persisted.permissionAuditLogs)); });