Files
boss/tests/boss-state-migrations.test.ts

110 lines
3.9 KiB
TypeScript

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<data.BossState>);
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<data.BossState>);
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));
});