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.
44 lines
4.3 KiB
JavaScript
44 lines
4.3 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { spawn } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import { reserveOrg, activateOrg, resolveOrg, orgRoot, withRegistryWriter } from '../src/org-registry.mjs';
|
|
|
|
function fixture() { const root = mkdtempSync(join(tmpdir(), 'org-')); const configDir = join(root, 'config'); const dataRoot = join(root, 'data'); mkdirSync(configDir, { mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 }); return { configDir, dataRoot, instance: 'test' }; }
|
|
test('reserves and verifies an organization marker', async () => { const c = fixture(); await reserveOrg(c, 'alpha'); await activateOrg(c, 'alpha'); assert.equal((await resolveOrg(c, 'alpha')).root, orgRoot(c.dataRoot, 'alpha')); writeFileSync(join(orgRoot(c.dataRoot, 'alpha'), '.mosaic-org.json'), '{"instance":"other","id":"alpha"}'); await assert.rejects(resolveOrg(c, 'alpha'), { code: 'org-mismatch' }); });
|
|
test('activation records the digest of an existing organization document', async () => {
|
|
const c = fixture(), dir = join(c.configDir, 'businesses'), raw = '{"id":"alpha"}\n';
|
|
mkdirSync(dir, { mode: 0o700 }); writeFileSync(join(dir, 'alpha.json'), raw);
|
|
await reserveOrg(c, 'alpha');
|
|
const active = await activateOrg(c, 'alpha');
|
|
assert.equal(active.digest, createHash('sha256').update(raw).digest('hex'));
|
|
});
|
|
test('activation refuses a symlinked organization document', async () => {
|
|
const c = fixture(), dir = join(c.configDir, 'businesses');
|
|
mkdirSync(dir, { mode: 0o700 }); writeFileSync(join(dir, 'other.json'), '{}');
|
|
symlinkSync(join(dir, 'other.json'), join(dir, 'alpha.json'));
|
|
await reserveOrg(c, 'alpha');
|
|
await assert.rejects(activateOrg(c, 'alpha'));
|
|
assert.equal((await resolveOrg(c, 'alpha')).state, 'reserved');
|
|
});
|
|
test('the first registry reservation waits for a legacy writer already holding the lock', async () => {
|
|
const c = fixture();
|
|
let release;
|
|
const hold = new Promise(resolve => { release = resolve; });
|
|
const writer = withRegistryWriter(c.configDir, () => hold);
|
|
let reserved = false;
|
|
const reservation = reserveOrg(c, 'alpha').then(value => { reserved = true; return value; });
|
|
try {
|
|
await new Promise(resolve => setTimeout(resolve, 80));
|
|
assert.equal(reserved, false);
|
|
} finally { release(); await writer; await reservation; }
|
|
});
|
|
test('refuses existing and symlink roots', async () => { const c = fixture(); mkdirSync(join(c.dataRoot, 'org-runtime'), { mode: 0o700 }); mkdirSync(orgRoot(c.dataRoot, 'alpha')); await assert.rejects(reserveOrg(c, 'alpha')); symlinkSync(orgRoot(c.dataRoot, 'alpha'), orgRoot(c.dataRoot, 'beta')); await assert.rejects(reserveOrg(c, 'beta')); });
|
|
test('two processes cannot reserve the same id', async () => { const c = fixture(); const url = new URL('../src/org-registry.mjs', import.meta.url).href; const code = `import {reserveOrg} from ${JSON.stringify(url)}; await reserveOrg(${JSON.stringify(c)}, 'alpha');`; const run = () => new Promise(resolve => { const p = spawn(process.execPath, ['--input-type=module', '-e', code], { stdio: 'ignore' }); p.on('close', resolve); }); const results = await Promise.all([run(), run()]); assert.deepEqual(results.sort(), [0, 1]); });
|
|
test('rejects a symlink in the data root path', async () => { const c = fixture(); symlinkSync(c.dataRoot, c.dataRoot + '-link'); c.dataRoot += '-link'; await assert.rejects(reserveOrg(c, 'alpha'), { code: 'org-mismatch' }); });
|
|
test('rejects an overlapping registry root', async () => { const c = fixture(); writeFileSync(join(c.configDir, 'org-runtime.json'), JSON.stringify({version: 1, instance: c.instance, orgs: { other: { id: 'other', root: join(c.dataRoot, 'org-runtime'), state: 'reserved', created: new Date().toISOString(), revision: 1 } } }), { mode: 0o600 }); await assert.rejects(reserveOrg(c, 'alpha'), { code: 'org-overlap' }); });
|
|
test('refuses loose ownership modes and a missing data root', async () => { const c = fixture(); const { chmodSync, rmSync } = await import('node:fs'); chmodSync(c.dataRoot, 0o755); await assert.rejects(reserveOrg(c, 'alpha'), { code: 'org-mismatch' }); chmodSync(c.dataRoot, 0o700); rmSync(c.dataRoot, { recursive: true }); await assert.rejects(reserveOrg(c, 'alpha')); });
|