Files
stack/packages/cli/tests/org-restore-bus.test.mjs

95 lines
6.0 KiB
JavaScript

// E1 (P1-4): `mosaic org restore` brings the organization back running. It starts the bus as
// `mosaic bus start ID --dev-local-owner --detach` would; a bus that does not start is said so with
// that verb, and the restore stands. On an instance where that start is refused, the restore is
// refused first and changes nothing. `--no-bus` keeps the old restore. The bus start itself is a
// stand-in here; agent-guard.test.mjs and org.test.mjs start a real one.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import * as service from '../../organization/src/org-service.mjs';
import { listOrgs } from '../../organization/src/org-registry.mjs';
import { DEV_OWNER } from '../src/dev-local-owner.mjs';
import { orgCommand } from '../src/org.mjs';
import { io, humanEnv } from './helpers.mjs';
// A named instance under a temporary HOME (the dev-owner checks read the instance from HOME), with
// one archived organization.
async function archived(t) {
const home = mkdtempSync(join(tmpdir(), 'e1-restore-')), saved = process.env.HOME;
process.env.HOME = home;
t.after(() => { process.env.HOME = saved; rmSync(home, { recursive: true, force: true }); });
const configDir = join(home, '.config', 'mosaic-e1test'), dataRoot = join(home, 'data');
mkdirSync(configDir, { recursive: true, mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 });
const ctx = { configDir, dataRoot, instance: 'e1test', principal: DEV_OWNER };
const created = await service.createOrg(ctx, 'alpha', await service.templateRender('alpha'));
const gone = await service.archiveOrg(ctx, 'alpha', { ifRevision: created.revision, busStop: async () => ({ stopped: false }) });
const env = humanEnv({ HOME: home, MOSAIC_CONFIG: join(configDir, 'config.json') });
const state = async () => (await listOrgs(ctx)).find(e => e.id === 'alpha').state;
return { ctx, env, revision: gone.revision, state, system: { dataRoot } };
}
const restore = (f, deps, ...extra) => { const x = io(f.env); return orgCommand(['restore', 'alpha', '--if-revision', String(f.revision), '--json', ...extra], x, { system: f.system, ...deps }).then(() => x, e => { e.io = x; throw e; }); };
test('E1: restore starts the bus as the dev owner and its receipt names the bus', async t => {
const f = await archived(t), calls = [];
const x = await restore(f, { busStart: async (ctx, id, opts) => { calls.push([ctx.dataRoot, ctx.env === f.env, id, opts]); return { org: id, pid: 4242, socket: '/s', root: '/r' }; } });
assert.deepEqual(calls, [[f.ctx.dataRoot, true, 'alpha', { detach: true, devLocalOwner: true, pm: false }]]);
const receipt = JSON.parse(x.out.text);
assert.equal(receipt.revision, f.revision + 1);
assert.deepEqual(receipt.bus, { outcome: 'started', pid: 4242 });
assert.equal(await f.state(), 'active');
});
test('E1: a bus that does not start after a good restore is said so, with the verb, and the restore stands', async t => {
const f = await archived(t);
const { CliError } = await import('../src/errors.mjs');
const busStart = async () => { throw new CliError('bus failed to start; see the bus log', 3); };
await assert.rejects(restore(f, { busStart }), e => {
assert.equal(e.exitCode, 3);
assert.equal(e.message, 'alpha was restored, but its bus did not start: bus failed to start; see the bus log; start it with mosaic bus start alpha --dev-local-owner --detach');
// The restore receipt is printed before the refusal.
const receipt = JSON.parse(e.io.out.text);
assert.equal(receipt.revision, f.revision + 1);
assert.deepEqual(receipt.bus, { outcome: 'failed', message: 'bus failed to start; see the bus log', next: 'mosaic bus start alpha --dev-local-owner --detach' });
return true;
});
assert.equal(await f.state(), 'active');
});
test('E1: a bus host that died leaves a stale lock; the receipt names bus recover before the start', async t => {
const f = await archived(t);
const { CliError } = await import('../src/errors.mjs');
const busStart = async () => { throw new CliError('stale bus host lock', 3); };
await assert.rejects(restore(f, { busStart }), e => {
assert.equal(e.exitCode, 3);
assert.equal(e.message, 'alpha was restored, but its bus did not start: stale bus host lock; clear the dead host with mosaic bus recover alpha, then start it with mosaic bus start alpha --dev-local-owner --detach');
assert.deepEqual(JSON.parse(e.io.out.text).bus, { outcome: 'failed', message: 'stale bus host lock', next: 'mosaic bus recover alpha; mosaic bus start alpha --dev-local-owner --detach' });
return true;
});
assert.equal(await f.state(), 'active');
});
test('E1: where a dev-owner bus start is refused, restore is refused before it changes anything, and --no-bus restores', async t => {
const f = await archived(t); let starts = 0;
const busStart = async () => { starts++; return { pid: 1 }; };
writeFileSync(join(f.ctx.dataRoot, '.deployed'), '{}\n');
await assert.rejects(restore(f, { busStart }), e => e.exitCode === 3 && /^restore starts the bus as the local dev owner, and --dev-local-owner refuses \.deployed; restore with --no-bus, then start the bus with mosaic bus start$/.test(e.message) && e.io.out.text === '');
assert.equal(await f.state(), 'archived');
const x = await restore(f, { busStart }, '--no-bus');
assert.equal(JSON.parse(x.out.text).bus, null);
assert.equal(starts, 0);
assert.equal(await f.state(), 'active');
});
test('E1: an agent run may restore only with --no-bus', async t => {
const f = await archived(t); let starts = 0;
const busStart = async () => { starts++; return { pid: 1 }; };
f.env = { ...f.env, MOSAIC_AGENT_RUN: '1' };
await assert.rejects(restore(f, { busStart }), e => e.exitCode === 3 && /MOSAIC_AGENT_RUN/.test(e.message));
assert.equal(await f.state(), 'archived');
await restore(f, { busStart }, '--no-bus');
assert.equal(starts, 0);
assert.equal(await f.state(), 'active');
});