import assert from "node:assert/strict"; import { mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { LEGACY_PERMISSION_POLICY_HASHES, migratePermissionPolicy, permissionPolicySemanticHash, } from "../scripts/migrate-permission-policy.mjs"; const knownLegacyHash = "9abf020e80fceae8c52de3e8b758822dfc86cb90e9083381e2569dde1e29400a"; async function fixture(t: test.TestContext) { const directory = await mkdtemp(join(tmpdir(), "permission-policy-migration-")); t.after(() => rm(directory, { recursive: true, force: true })); const policyPath = join(directory, "policy.jsonc"); const seedPath = join(directory, "seed.jsonc"); const seed = `{ // bundled policy "permission": { "*": "ask", "read": "allow" } }\n`; await writeFile(seedPath, seed, { mode: 0o600 }); return { directory, policyPath, seedPath, seed }; } test("recognizes the exact previously installed policy snapshot", () => { assert.equal(LEGACY_PERMISSION_POLICY_HASHES.has(knownLegacyHash), true); }); test("semantic hashes ignore JSONC comments, whitespace, and object key order", () => { const left = `{ // comment "permission": { "read": "allow", "*": "ask" }, "analysis": { "enabled": true } }`; const right = `{"analysis":{"enabled":true},"permission":{"*":"ask","read":"allow"}}`; assert.equal(permissionPolicySemanticHash(left), permissionPolicySemanticHash(right)); }); test("migrates only an exact legacy semantic snapshot and creates private files", async (t) => { const { directory, policyPath, seedPath, seed } = await fixture(t); const legacy = `{ // formatting does not matter "permission": { "*": "allow", "path": { "**/.env": "deny" } } }\n`; await writeFile(policyPath, legacy, { mode: 0o644 }); const result = await migratePermissionPolicy(policyPath, seedPath, { legacyHashes: new Set([permissionPolicySemanticHash(legacy)]), now: new Date("2026-09-02T03:04:05.000Z"), }); assert.equal(result.status, "migrated"); assert.equal(await readFile(policyPath, "utf8"), seed); assert.equal((await stat(policyPath)).mode & 0o777, 0o600); assert.ok(result.backupPath); assert.equal(await readFile(result.backupPath!, "utf8"), legacy); assert.equal((await stat(result.backupPath!)).mode & 0o777, 0o600); assert.equal( (await readdir(directory)).some((name) => name.includes(".tmp.")), false, ); }); test("preserves customized policy content", async (t) => { const { policyPath, seedPath } = await fixture(t); const custom = `{"permission":{"*":"deny"}}\n`; await writeFile(policyPath, custom, { mode: 0o600 }); const result = await migratePermissionPolicy(policyPath, seedPath); assert.deepEqual(result, { status: "custom" }); assert.equal(await readFile(policyPath, "utf8"), custom); }); test("treats the bundled seed as current and repairs its mode", async (t) => { const { policyPath, seedPath, seed } = await fixture(t); await writeFile(policyPath, seed, { mode: 0o644 }); const result = await migratePermissionPolicy(policyPath, seedPath); assert.deepEqual(result, { status: "current" }); assert.equal((await stat(policyPath)).mode & 0o777, 0o600); }); test("returns missing without creating a policy", async (t) => { const { policyPath, seedPath } = await fixture(t); assert.deepEqual(await migratePermissionPolicy(policyPath, seedPath), { status: "missing", }); }); test("rejects invalid current JSONC without replacing it", async (t) => { const { policyPath, seedPath } = await fixture(t); const invalid = "{ invalid\n"; await writeFile(policyPath, invalid, { mode: 0o600 }); await assert.rejects( migratePermissionPolicy(policyPath, seedPath), /invalid JSONC/, ); assert.equal(await readFile(policyPath, "utf8"), invalid); }); test("aborts if the policy changes before commit", async (t) => { const { directory, policyPath, seedPath } = await fixture(t); const legacy = `{"permission":{"*":"allow"}}\n`; const concurrent = `{"permission":{"*":"deny"}}\n`; await writeFile(policyPath, legacy, { mode: 0o600 }); await assert.rejects( migratePermissionPolicy(policyPath, seedPath, { legacyHashes: new Set([permissionPolicySemanticHash(legacy)]), beforeCommit: () => writeFile(policyPath, concurrent, "utf8"), }), /changed during migration/, ); assert.equal(await readFile(policyPath, "utf8"), concurrent); const entries = await readdir(directory); assert.equal(entries.some((name) => name.includes(".bak.")), true); assert.equal(entries.some((name) => name.includes(".tmp.")), false); });