Files
stack/packages/organization/tests/org-delete.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

197 lines
12 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, readFile, lstat, symlink } 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 { busStopForDelete as busStop } from '../../cli/src/org-bus.mjs';
async function fixture() {
const root = await mkdtemp(join(tmpdir(), 'org-delete-'));
const c = { configDir: join(root, 'config'), dataRoot: join(root, 'data'), instance: 'test', principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } };
await mkdir(c.configDir, { mode: 0o700 }); await mkdir(c.dataRoot, { mode: 0o700 });
await org.createOrg(c, 'alpha', await org.templateRender('alpha'));
return c;
}
test('delete refuses without a bus stop function', async () => {
const c = await fixture();
await assert.rejects(org.deleteOrg(c, 'alpha', { confirm: 'alpha', ifRevision: 1 }), { code: 'org-invalid' });
assert.equal((await listOrgs(c)).find(e => e.id === 'alpha').state, 'active');
});
test('delete fences confirmation and revision before moving any files', async () => {
const c = await fixture();
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'other', ifRevision: 1 }), { code: 'org-confirm' });
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 2 }), { code: 'revision-conflict' });
assert.equal((await org.showOrg(c, 'alpha')).revision, 1);
});
test('delete preserves owned trees, records a receipt, and permits reuse', async () => {
const c = await fixture();
for (const kind of ['agents', 'grants', 'hosts']) {
const dir = join(c.configDir, 'secrets', kind, 'alpha');
await mkdir(dir, { recursive: true, mode: 0o700 }); await writeFile(join(dir, 'dummy'), 'secret', { mode: 0o600 });
}
const history = join(c.configDir, 'businesses', '.history', 'alpha'); await mkdir(history, { recursive: true, mode: 0o700 }); await writeFile(join(history, 'changes.jsonl'), 'evidence', { mode: 0o600 });
const receipt = await org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 });
assert.equal(await readFile(join(receipt.destination, 'history', 'changes.jsonl'), 'utf8'), 'evidence');
assert.equal((await lstat(receipt.destination)).mode & 0o777, 0o700);
assert.equal(receipt.secrets.length, 3); assert.ok(receipt.secrets.every(s => s.entryCount === 1));
assert.equal((await listOrgs(c)).length, 0);
const stored = JSON.parse(await readFile(join(receipt.destination, 'receipt.json'), 'utf8'));
assert.equal(stored.registry, 'removed');
assert.equal(stored.org, 'alpha'); assert.ok(!JSON.stringify(stored).includes('dummy'));
assert.equal((await readFile(join(c.dataRoot, 'deleted', 'deleted.log'), 'utf8')).trim().split('\n').length, 1);
await org.createOrg(c, 'alpha', await org.templateRender('alpha'));
});
test('delete refuses an owned symlink without moving anything', async () => {
const c = await fixture(); await symlink('/tmp', join(c.dataRoot, 'org-runtime', 'alpha', 'link'));
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), { code: 'org-mismatch' });
assert.equal((await listOrgs(c))[0].state, 'deleting');
});
test('delete refuses a group-readable organization secret directory', async () => {
const c = await fixture(); const dir = join(c.configDir, 'secrets', 'agents', 'alpha');
await mkdir(dir, { recursive: true, mode: 0o700 });
const { chmod } = await import('node:fs/promises'); await chmod(dir, 0o750);
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), { code: 'org-mismatch' });
assert.equal((await listOrgs(c))[0].state, 'deleting');
});
test('delete refuses a symlink within an organization secret directory', async () => {
const c = await fixture(); const dir = join(c.configDir, 'secrets', 'hosts', 'alpha');
await mkdir(dir, { recursive: true, mode: 0o700 }); await symlink('/tmp', join(dir, 'link'));
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), { code: 'org-mismatch' });
assert.equal((await listOrgs(c))[0].state, 'deleting');
});
test('a refused deletion resumes without a revision after the unsafe path is fixed', async () => {
const c = await fixture(); const link = join(c.dataRoot, 'org-runtime', 'alpha', 'link');
await symlink(c.dataRoot, link);
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }));
const { unlink } = await import('node:fs/promises'); await unlink(link);
await org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha' });
assert.deepEqual(await listOrgs(c), []);
});
test('deleting state fences bus start, and an uncertain bus stop retains the organization', async () => {
const c = await fixture();
const hostDir = join(c.dataRoot, 'org-runtime', 'alpha', 'bus-host'); await mkdir(hostDir, { mode: 0o700 });
await writeFile(join(hostDir, 'host.lock'), JSON.stringify({ pid: 99999999, startTime: '0', business: 'alpha' }), { mode: 0o600 });
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), /lock/i);
assert.equal((await listOrgs(c))[0].state, 'deleting');
const { busStart } = await import('../../cli/src/org-bus.mjs');
await assert.rejects(busStart(c, 'alpha'), /being deleted/);
const { unlink } = await import('node:fs/promises'); await unlink(join(hostDir, 'host.lock'));
await org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha' });
});
test('deletion refuses to move files while an in-flight bus start holds its lifecycle fence', async () => {
const c = await fixture(); const lock = join(c.configDir, 'org-lifecycle-alpha.lock');
await writeFile(lock, 'in flight', { mode: 0o600 });
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), /lifecycle/i);
assert.equal((await listOrgs(c))[0].state, 'active');
await lstat(join(c.dataRoot, 'org-runtime', 'alpha'));
const { unlink } = await import('node:fs/promises'); await unlink(lock);
await org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 });
});
test('resume refuses a replaced data root symlink', async () => {
const c = await fixture(), link = join(c.dataRoot, 'org-runtime', 'alpha', 'link');
await symlink(c.configDir, link);
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }));
const { rename, unlink } = await import('node:fs/promises'); await unlink(link); await rename(c.dataRoot, c.dataRoot + '-real'); await symlink(c.dataRoot + '-real', c.dataRoot);
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha' }), { code: 'org-mismatch' });
});
test('bus verification refuses a symlinked host state before reading it', async () => {
const c = await fixture(), host = join(c.dataRoot, 'org-runtime', 'alpha', 'bus-host');
await mkdir(host, { mode: 0o700 });
await symlink(join(c.configDir, 'missing'), join(host, 'host.json'));
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), /symlink/i);
assert.equal((await listOrgs(c))[0].state, 'deleting');
});
test('bus verification refuses a non-file host state', async () => {
const c = await fixture(), host = join(c.dataRoot, 'org-runtime', 'alpha', 'bus-host');
await mkdir(host, { mode: 0o700 }); await mkdir(join(host, 'host.json'), { mode: 0o700 });
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), /unsafe bus state/i);
});
test('delete refuses an answering bus socket even without a recorded host', async t => {
const c = await fixture(), dir = join(c.dataRoot, 'org-runtime', 'alpha', 'bus');
await mkdir(dir, { mode: 0o700 });
const { createServer } = await import('node:net'); const server = createServer(socket => socket.end());
await new Promise((resolve, reject) => server.once('error', reject).listen(join(dir, 'broker.sock'), resolve));
t.after(() => new Promise(resolve => server.close(resolve)));
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), /socket still answers/i);
assert.equal((await listOrgs(c))[0].state, 'deleting');
});
test('a refused move reports the completed bus step and the point of refusal', async () => {
const c = await fixture(); await symlink(c.configDir, join(c.dataRoot, 'org-runtime', 'alpha', 'link'));
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }), e => {
assert.equal(e.deleteStep, 'inspect'); assert.equal(e.deleteReceipt.bus.reason, 'no host runs'); assert.deepEqual(e.deleteReceipt.moves, []); return true;
});
});
test('resume records a move completed before its receipt could be updated', async () => {
const c = await fixture(), root = join(c.dataRoot, 'org-runtime', 'alpha'), link = join(root, 'link');
await symlink(c.configDir, link);
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }));
const entry = (await listOrgs(c))[0];
const { unlink, rename } = await import('node:fs/promises'); await unlink(link);
await rename(root, join(c.dataRoot, 'deleted', entry.deletion.folder, 'runtime'));
const receipt = await org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha' });
assert.ok(receipt.moves.some(move => move.from === root)); assert.equal(receipt.registry, 'removed');
});
test('deleting another organization appends without rewriting prior log entries', async () => {
const c = await fixture();
await org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 });
const file = join(c.dataRoot, 'deleted', 'deleted.log'), before = await readFile(file, 'utf8');
await org.createOrg(c, 'beta', await org.templateRender('beta'));
await org.deleteOrg(c, 'beta', { busStop, confirm: 'beta', ifRevision: 1 });
const after = await readFile(file, 'utf8');
assert.ok(after.startsWith(before)); assert.equal(after.trim().split('\n').length, 2);
});
test('resume refuses an instance rebound to a different data root', async () => {
const c = await fixture(), link = join(c.dataRoot, 'org-runtime', 'alpha', 'link');
await symlink(c.configDir, link);
await assert.rejects(org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }));
const other = c.dataRoot + '-other'; await mkdir(other, { mode: 0o700 });
await assert.rejects(org.deleteOrg({ ...c, dataRoot: other }, 'alpha', { busStop, confirm: 'alpha' }), { code: 'org-mismatch' });
await assert.rejects(lstat(join(other, 'deleted')), { code: 'ENOENT' });
});
// Hold the first request immediately before its bus receipt is published.
test('overlapping deletes refuse before touching the receipt', async () => {
const c = await fixture();
const fs = (await import('node:fs/promises')).default;
const { syncBuiltinESMExports } = await import('node:module');
let entered, release;
const paused = new Promise(resolve => { entered = resolve; });
const proceed = new Promise(resolve => { release = resolve; });
const original = fs.rename; let pausedOnce = false;
fs.rename = async (from, to) => {
if (!pausedOnce && to.endsWith('/receipt.json')) {
const value = JSON.parse(await readFile(from, 'utf8'));
if (value.bus && value.moves.length === 0) { pausedOnce = true; entered(); await proceed; }
}
return original(from, to);
};
syncBuiltinESMExports();
let first;
try {
first = org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 });
await paused;
const second = await org.deleteOrg(c, 'alpha', { busStop, confirm: 'alpha', ifRevision: 1 }).then(value => ({ value }), error => ({ error }));
assert.equal(second.error?.code, 'org-locked');
} finally {
release();
try { await first; } catch { /* preserve the assertion failure */ }
fs.rename = original; syncBuiltinESMExports();
}
const result = await first;
const stored = JSON.parse(await readFile(join(result.destination, 'receipt.json'), 'utf8'));
assert.equal(stored.registry, 'removed'); assert.equal(stored.moves.length, 2);
});