116 lines
7.5 KiB
JavaScript
116 lines
7.5 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');
|
|
});
|
|
|
|
test('CB4 archive and delete from the CLI hand the managed copy remote to the organization service', async t => {
|
|
const home = mkdtempSync(join(tmpdir(), 'cb4-cli-')), 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 };
|
|
await service.createOrg(ctx, 'alpha', await service.templateRender('alpha'));
|
|
const env = humanEnv({ HOME: home, MOSAIC_CONFIG: join(configDir, 'config.json') });
|
|
const asked = [];
|
|
const copyRemoteFor = async c => { asked.push([c.configDir, c.dataRoot]); return async () => 'revoked'; };
|
|
const run = words => orgCommand(words, io(env), { system: { dataRoot }, copyRemoteFor });
|
|
const revision = (await listOrgs(ctx)).find(e => e.id === 'alpha').revision;
|
|
await run(['archive', 'alpha', '--if-revision', String(revision), '--json']);
|
|
assert.deepEqual(asked, [[configDir, dataRoot]]);
|
|
const archived = (await listOrgs(ctx)).find(e => e.id === 'alpha').revision;
|
|
await run(['delete', 'alpha', '--confirm', 'alpha', '--if-revision', String(archived), '--json']);
|
|
assert.deepEqual(asked, [[configDir, dataRoot], [configDir, dataRoot]]);
|
|
assert.equal((await listOrgs(ctx)).find(e => e.id === 'alpha')?.state ?? 'deleted', 'deleted');
|
|
});
|