Files
stack/packages/cli/tests/org-archive-race.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

58 lines
5.6 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { mkdtemp, writeFile, rm, access } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import * as org from '../../organization/src/org-service.mjs';
import { listOrgs } from '../../organization/src/org-registry.mjs';
import { busStart, busStopForDelete } from '../src/org-bus.mjs';
import { readHostState } from '../src/host.mjs';
const repo = resolve(import.meta.dirname, '../../..');
async function until(fn) { const end = Date.now() + 20000; while (Date.now() < end) { if (await fn()) return; await new Promise(r => setTimeout(r, 20)); } throw Error('probe condition timed out'); }
test('internal host holds lifecycle fence from admission through readiness', { timeout: 60000 }, async t => {
const home = await mkdtemp(join(tmpdir(), 'r-'));
const env = { ...process.env, HOME: home, MOSAIC_CONFIG: join(home, '.config/mosaic-rv/config.json') };
const boot = spawnSync(join(repo, 'scripts/bootstrap.sh'), ['--instance', 'rv'], { env, encoding: 'utf8' }); assert.equal(boot.status, 0, boot.stderr);
const ctx = { configDir: join(home, '.config/mosaic-rv'), dataRoot: join(home, '.mosaic-rv'), instance: 'rv', env, principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } };
await org.createOrg(ctx, 'alpha', await org.templateRender('alpha', { fixture: 'local' }));
const gate = join(home, 'gate'), preload = join(home, 'pause.mjs');
await writeFile(preload, `import fs from 'node:fs';import {syncBuiltinESMExports} from 'node:module';const append=fs.appendFileSync;let once=false;fs.appendFileSync=function(file,...args){if(!once&&String(file).endsWith('/bus-host/boot-events.jsonl')){once=true;fs.writeFileSync(${JSON.stringify(gate+'.ready')},'ready');const end=Date.now()+15000;while(!fs.existsSync(${JSON.stringify(gate+'.go')})){if(Date.now()>end)throw Error('gate timed out');Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10);}}return append.call(this,file,...args)};syncBuiltinESMExports();`);
const child = spawn(process.execPath, ['--import', preload, join(repo, 'packages/cli/src/cli.mjs'), 'bus', 'start', 'alpha', '--internal-host', '--dev-local-owner', '--no-launch'], { env, stdio: ['ignore', 'pipe', 'pipe', 'ipc'] });
let output = '', ready = false; child.stdout.on('data', d => { output += d; }); child.stderr.on('data', d => { output += d; }); child.on('message', m => { if (m?.orgReady === 'alpha') ready = true; });
const ended = new Promise(r => child.once('exit', r));
t.after(async () => { await writeFile(gate+'.go', 'go'); if (child.exitCode === null && child.signalCode === null) { child.kill('SIGTERM'); await ended; } await rm(home, { recursive: true, force: true }); });
await until(async () => { try { await access(gate+'.ready'); return true; } catch { return child.exitCode !== null; } });
assert.equal(child.exitCode, null, output); assert.equal(readHostState(join(ctx.dataRoot, 'org-runtime', 'alpha')), null);
await assert.rejects(org.archiveOrg(ctx, 'alpha', { ifRevision: 1, busStop: busStopForDelete }), { code: 'org-locked' });
assert.equal((await listOrgs(ctx))[0].state, 'active');
await writeFile(gate+'.go', 'go'); await until(() => ready || child.exitCode !== null || child.signalCode !== null);
assert.equal(ready, true, output); assert.equal(readHostState(join(ctx.dataRoot, 'org-runtime', 'alpha'))?.live, true);
await busStopForDelete(ctx, 'alpha'); await ended;
const result = await org.archiveOrg(ctx, 'alpha', { ifRevision: 1, busStop: busStopForDelete });
assert.equal(result.outcome, 'done'); assert.equal((await listOrgs(ctx))[0].state, 'archived');
});
test('archive between parent preparation and child admission prevents the bus from starting', { timeout: 60000 }, async t => {
const home = await mkdtemp(join(tmpdir(), 'r-'));
const env = { ...process.env, HOME: home, MOSAIC_CONFIG: join(home, '.config/mosaic-rv/config.json') };
const boot = spawnSync(join(repo, 'scripts/bootstrap.sh'), ['--instance', 'rv'], { env, encoding: 'utf8' }); assert.equal(boot.status, 0, boot.stderr);
const ctx = { configDir: join(home, '.config/mosaic-rv'), dataRoot: join(home, '.mosaic-rv'), instance: 'rv', env, principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } };
await org.createOrg(ctx, 'alpha', await org.templateRender('alpha', { fixture: 'local' }));
const gate = join(home, 'gate'), preload = join(home, 'pause.mjs');
await writeFile(preload, `import fs from 'node:fs';fs.writeFileSync(${JSON.stringify(gate+'.ready')},'ready');const end=Date.now()+15000;while(!fs.existsSync(${JSON.stringify(gate+'.go')})){if(Date.now()>end)throw Error('gate timed out');Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10);}`);
env.NODE_OPTIONS = `--import=${preload}`;
const pending = busStart(ctx, 'alpha', { detach: true, devLocalOwner: true, noLaunch: true }).then(value => ({ value }), error => ({ error }));
try {
await until(async () => { try { await access(gate+'.ready'); return true; } catch { return false; } });
const archived = await org.archiveOrg(ctx, 'alpha', { ifRevision: 1, busStop: busStopForDelete });
assert.equal(archived.outcome, 'done');
await writeFile(gate+'.go', 'go');
const result = await pending; assert.match(result.error?.message ?? '', /bus failed to start/);
assert.equal((await listOrgs(ctx))[0].state, 'archived');
assert.equal(readHostState(join(ctx.dataRoot, 'org-runtime', 'alpha'))?.live ?? false, false);
} finally { await writeFile(gate+'.go', 'go'); await pending; await rm(home, { recursive: true, force: true }); }
});