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