Files
stack/packages/organization/tests/org-archive.test.mjs
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
git mv packages/business -> packages/organization (@mosaic/organization). The
old package stays as one-line re-export shims of each moved module, with
src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs
forwarding to the moved executable. BusinessError is OrganizationConfigError
(one constructor, name kept BusinessError); OrganizationError stays distinct.
The bus adapter gains organization-context.mjs (busOrganization), with
business.mjs (busBusiness) as the same function.

Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh
COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure
walker (organization-imports.mjs, both roots required) move to the new path.
v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage
A contract tests stay where they were and pass as before.
2026-11-09 11:47:48 -06:00

71 lines
4.9 KiB
JavaScript

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);
});