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.
93 lines
6.3 KiB
JavaScript
93 lines
6.3 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { reserveOrg, activateOrg } from '../../organization/src/org-registry.mjs';
|
|
import { businessDoc, rolesCopy, writeJson } from '../../organization/tests/helpers.mjs';
|
|
import { humanEnv, notifyConfig } from './helpers.mjs';
|
|
import { busStart, busStatus, busStop } from '../src/org-bus.mjs';
|
|
|
|
const REPO = new URL('../../../', import.meta.url).pathname;
|
|
|
|
test('two organization buses are isolated and refusals never select a sibling', { timeout: 120000 }, async t => {
|
|
const home = mkdtempSync(join(tmpdir(), 's1-'));
|
|
const env = humanEnv({ HOME: home, MOSAIC_CONFIG: join(home, '.config/mosaic-s1test/config.json') });
|
|
const setup = spawnSync(join(REPO, 'scripts/bootstrap.sh'), ['--instance', 's1test'], { env, encoding: 'utf8' });
|
|
assert.equal(setup.status, 0, setup.stderr);
|
|
const ctx = { configDir: join(home, '.config/mosaic-s1test'), dataRoot: join(home, '.mosaic-s1test'), instance: 's1test', env };
|
|
env.MOSAIC_ROLES_DIR = rolesCopy(home);
|
|
for (const id of ['alpha', 'beta']) {
|
|
const org = await reserveOrg(ctx, id);
|
|
await activateOrg(ctx, id);
|
|
writeJson(join(ctx.configDir, 'businesses', `${id}.json`), businessDoc(home, id));
|
|
notifyConfig(org.root, id, null);
|
|
}
|
|
t.after(async () => {
|
|
for (const id of ['alpha', 'beta']) { try { await busStop(ctx, id); } catch {} }
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
const incompatible = spawnSync(join(REPO, 'scripts/mosaic'), ['bus', 'start', 'alpha', '--pm', '--no-launch'], { env, encoding: 'utf8' });
|
|
assert.equal(incompatible.status, 4);
|
|
const start = spawnSync(join(REPO, 'scripts/mosaic'), ['bus', 'start', 'alpha', '--no-launch', '--detach'], { env, encoding: 'utf8', timeout: 30000 });
|
|
assert.equal(start.status, 0, start.stderr);
|
|
const alpha = JSON.parse(start.stdout);
|
|
const beta = await busStart(ctx, 'beta', { detach: true, devLocalOwner: true });
|
|
assert.notEqual(alpha.socket, beta.socket);
|
|
assert.notEqual(alpha.root, beta.root);
|
|
assert.equal((await busStatus(ctx, 'alpha')).healthy, true);
|
|
assert.equal((await busStatus(ctx, 'beta')).healthy, true);
|
|
const refused = spawnSync(join(REPO, 'scripts/mosaic'), ['bus', 'recover', 'alpha'], { env, encoding: 'utf8' });
|
|
assert.equal(refused.status, 3, refused.stderr);
|
|
assert.equal((await busStatus(ctx, 'alpha')).healthy, true);
|
|
await assert.rejects(busStatus(ctx, 'missing'), e => e.code === 'org-unknown');
|
|
const launches = spawnSync(join(REPO, 'scripts/mosaic'), ['launches', 'list', '--business', 'beta', '--json'], { env, encoding: 'utf8' });
|
|
assert.equal(launches.status, 0, launches.stderr);
|
|
assert.ok(Array.isArray(JSON.parse(launches.stdout)));
|
|
const inbox = spawnSync(join(REPO, 'scripts/mosaic'), ['inbox', '--business', 'alpha', '--json'], { env, encoding: 'utf8', timeout: 30000 });
|
|
assert.equal(inbox.status, 3, inbox.stderr);
|
|
assert.match(inbox.stderr, /human-required/); // The existing pinned-tree proof refused after reaching alpha.
|
|
|
|
const status = spawnSync(join(REPO, 'scripts/mosaic'), ['bus', 'status', 'alpha', '--json'], { env, encoding: 'utf8' });
|
|
assert.equal(status.status, 0, status.stderr);
|
|
assert.equal(JSON.parse(status.stdout).socket, alpha.socket);
|
|
const restart = spawnSync(join(REPO, 'scripts/mosaic'), ['bus', 'restart', 'alpha'], { env, encoding: 'utf8', timeout: 30000 });
|
|
assert.equal(restart.status, 0, restart.stderr);
|
|
assert.notEqual(JSON.parse(restart.stdout).pid, alpha.pid);
|
|
assert.equal(JSON.parse(readFileSync(join(alpha.root, 'bus-host/host.json'), 'utf8')).options.noLaunch, true);
|
|
assert.equal((await busStatus(ctx, 'beta')).healthy, true);
|
|
const stop = spawnSync(join(REPO, 'scripts/mosaic'), ['bus', 'stop', 'alpha'], { env, encoding: 'utf8', timeout: 30000 });
|
|
assert.equal(stop.status, 0, stop.stderr);
|
|
assert.equal(JSON.parse(stop.stdout).stopped, true);
|
|
assert.equal((await busStatus(ctx, 'alpha')).healthy, false);
|
|
assert.equal((await busStatus(ctx, 'beta')).healthy, true);
|
|
// Recovery removes dead host and broker locks, and a fresh start can take them.
|
|
const staleHost = { pid: 2147483647, startTime: '1', business: 'alpha', startedAt: '2026-01-01T00:00:00Z' };
|
|
writeFileSync(join(alpha.root, 'bus-host/host.json'), JSON.stringify(staleHost), { mode: 0o600 });
|
|
writeFileSync(join(alpha.root, 'bus-host/host.lock'), JSON.stringify(staleHost), { mode: 0o600 });
|
|
writeFileSync(join(alpha.root, 'bus/writer.lock'), JSON.stringify({ pid: 2147483647, at: '2026-01-01T00:00:00Z' }), { mode: 0o600 });
|
|
const recovery = spawnSync(join(REPO, 'scripts/mosaic'), ['bus', 'recover', 'alpha'], { env, encoding: 'utf8' });
|
|
assert.equal(recovery.status, 0, recovery.stderr);
|
|
const restarted = await busStart(ctx, 'alpha', { detach: true, noLaunch: true });
|
|
assert.ok(restarted.pid > 0);
|
|
assert.equal((await busStatus(ctx, 'alpha')).healthy, true);
|
|
await busStop(ctx, 'alpha');
|
|
// A forged alpha state cannot name the running beta host as alpha.
|
|
const alphaHost = join(alpha.root, 'bus-host');
|
|
const betaState = JSON.parse(readFileSync(join(beta.root, 'bus-host/host.json'), 'utf8'));
|
|
const forged = { ...betaState, business: 'alpha' };
|
|
writeFileSync(join(alphaHost, 'host.json'), JSON.stringify(forged), { mode: 0o600 });
|
|
writeFileSync(join(alphaHost, 'host.lock'), JSON.stringify(forged), { mode: 0o600 });
|
|
try { await assert.rejects(busStatus(ctx, 'alpha'), /mismatch|identity/i); }
|
|
finally { rmSync(join(alphaHost, 'host.json')); rmSync(join(alphaHost, 'host.lock')); }
|
|
assert.equal((await busStatus(ctx, 'beta')).healthy, true);
|
|
mkdirSync(join(alpha.root, 'bus'), { recursive: true, mode: 0o700 });
|
|
writeFileSync(join(alpha.root, 'bus/writer.lock'), JSON.stringify({ pid: 99999999 }), { mode: 0o600 });
|
|
await assert.rejects(busStart(ctx, 'alpha', { detach: true }), /lock/i);
|
|
await assert.rejects(busStatus(ctx, 'alpha'), /lock/i);
|
|
writeFileSync(join(alpha.root, '.mosaic-org.json'), JSON.stringify({ instance: 's1test', id: 'beta' }), { mode: 0o600 });
|
|
await assert.rejects(busStatus(ctx, 'alpha'), e => e.code === 'org-mismatch');
|
|
assert.equal((await busStatus(ctx, 'beta')).healthy, true);
|
|
});
|