import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import * as org from '../src/org-service.mjs'; import { listOrgs } from '../src/org-registry.mjs'; import { busStart, busStopForDelete } from '../../cli/src/org-bus.mjs'; async function fixture() { const root = await mkdtemp(join(tmpdir(), 'org-archive-')); const ctx = { configDir: join(root, 'config'), dataRoot: join(root, 'data'), instance: 'test', principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } }; await mkdir(ctx.configDir, { mode: 0o700 }); await mkdir(ctx.dataRoot, { mode: 0o700 }); await org.createOrg(ctx, 'alpha', await org.templateRender('alpha')); return ctx; } const stopped = async () => ({ stopped: false, reason: 'no host runs' }); test('archive and restore preserve files, increment revisions and append lifecycle log', async () => { assert.equal(typeof org.archiveOrg, 'function'); assert.equal(typeof org.restoreOrg, 'function'); const c = await fixture(), path = join(c.dataRoot, 'org-runtime', 'alpha', 'evidence'); await writeFile(path, 'immutable evidence'); const docFile = join(c.configDir, 'businesses', 'alpha.json'), original = await readFile(docFile); let calls = 0; const archive = await org.archiveOrg(c, 'alpha', { ifRevision: 1, busStop: async (...args) => { calls++; return stopped(...args); } }); assert.equal(archive.outcome, 'done'); assert.equal(archive.step, 'archive'); assert.equal(archive.revision, 2); assert.equal(calls, 1); assert.equal((await listOrgs(c))[0].state, 'archived'); assert.deepEqual(await readFile(docFile), original); assert.equal(await readFile(path, 'utf8'), 'immutable evidence'); const restored = await org.restoreOrg(c, 'alpha', { ifRevision: 2 }); assert.equal(restored.outcome, 'done'); assert.equal(restored.step, 'restore'); assert.equal(restored.revision, 3); assert.equal(calls, 1); assert.equal((await listOrgs(c))[0].state, 'active'); const lines = (await readFile(join(c.dataRoot, 'org-lifecycle.log'), 'utf8')).trim().split('\n').map(JSON.parse); assert.deepEqual(lines.map(e => [e.op, e.org, e.revision]), [['org.archive', 'alpha', 2], ['org.restore', 'alpha', 3]]); assert.deepEqual(lines[0].bus, { stopped: false, reason: 'no host runs' }); assert.ok(lines.every(e => e.actor.sub === 'local-dev-owner' && e.time)); }); test('stale revision and uncertain stop leave the organization active', async () => { const c = await fixture(); let called = 0; await assert.rejects(org.archiveOrg(c, 'alpha', { ifRevision: 2, busStop: async () => { called++; } }), { code: 'revision-conflict' }); assert.equal(called, 0); await assert.rejects(org.archiveOrg(c, 'alpha', { ifRevision: 1, busStop: async () => { throw Error('uncertain stop'); } }), /uncertain stop/); assert.equal((await listOrgs(c))[0].state, 'active'); }); test('archived org refuses bus start, update and setup, and may be deleted', async () => { const c = await fixture(), doc = (await org.showOrg(c, 'alpha')).doc; await org.archiveOrg(c, 'alpha', { ifRevision: 1, busStop: stopped }); await assert.rejects(busStart(c, 'alpha'), /org-archived/); await assert.rejects(org.updateOrg(c, 'alpha', doc, { ifRevision: 2 }), { code: 'org-archived' }); await assert.rejects(org.queueInit(c, 'alpha', { mode: 'local-only', acceptLocalLossRisk: true }), { code: 'org-archived' }); await assert.rejects(org.projectRepoInit(c, 'alpha', 'work', { mode: 'local-only', acceptLocalLossRisk: true }), { code: 'org-archived' }); await org.deleteOrg(c, 'alpha', { confirm: 'alpha', ifRevision: 2, busStop: busStopForDelete }); assert.deepEqual(await listOrgs(c), []); }); test('concurrent archive and delete share the lifecycle lock', async () => { const c = await fixture(); let entered, release; const ready = new Promise(r => { entered = r; }), wait = new Promise(r => { release = r; }); const pending = org.archiveOrg(c, 'alpha', { ifRevision: 1, busStop: async () => { entered(); await wait; return stopped(); } }); await ready; try { await assert.rejects(org.deleteOrg(c, 'alpha', { confirm: 'alpha', ifRevision: 1, busStop: stopped }), { code: 'org-locked' }); } finally { release(); await pending; } assert.equal((await listOrgs(c))[0].state, 'archived'); }); test('archive refuses an unreadable stop result and a revision changed while stopping', async () => { const c = await fixture(), doc = (await org.showOrg(c, 'alpha')).doc; await assert.rejects(org.archiveOrg(c, 'alpha', { ifRevision: 1, busStop: async () => null }), /uncertain/i); await assert.rejects(org.archiveOrg(c, 'alpha', { ifRevision: 1, busStop: async () => { await org.updateOrg(c, 'alpha', doc, { ifRevision: 1 }); return stopped(); } }), { code: 'revision-conflict' }); const entry = (await listOrgs(c))[0]; assert.equal(entry.state, 'active'); assert.equal(entry.revision, 2); });