- busRecover: a live pid with the recorded start time is the recorded process, so it refuses
("bus host identity is uncertain") whatever its argv. It used to read a mismatching argv as a dead
host. The pinned recovery test now covers a reused pid (live, other start time) as the wrong
process, and a new test covers the refusal.
- busRecover clears through setAside: each inspected entry is renamed aside and checked by device
and inode; a swapped entry puts everything back and refuses before any receipt or deletion, and
the deletion names only the aside entries.
- mosaic bus recover refuses inside an agent run (refuseInsideAgent), like mosaic org reload. The
Console-start revive does nothing when the Console's own environment carries an agent marker.
- Tests: org-recovery (identity, agent refusal), org-revive (setAside swap), bus-revive (markers);
the other reviveBuses tests pass an explicit env.
96 lines
6.4 KiB
JavaScript
96 lines
6.4 KiB
JavaScript
// D2 (P0-3): the dev Console's start-up revive (bus-revive.mjs), on fakes. The real-host paths
|
|
// (a SIGKILLed host, a live host, a live writer lock) are in packages/cli/tests/org-revive.test.mjs.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { reviveBuses, revivePending } from '../src/bus-revive.mjs';
|
|
import { reviveStartOptions } from '../../cli/src/org-bus.mjs';
|
|
|
|
const cliError = (message, extra = {}) => Object.assign(new Error(message), { name: 'CliError', exitCode: 3 }, extra);
|
|
const orgs = ids => async () => ids.map(id => ({ id, state: 'active' }));
|
|
|
|
function run({ ids = ['alpha'], status, revive, trail, env = {} }) {
|
|
const calls = [], lines = [], trailed = [];
|
|
const done = reviveBuses({
|
|
ctx: {}, listOrgs: orgs(ids),
|
|
busStatus: async (ctx, id) => status(id),
|
|
busRevive: async (ctx, id, opts) => { calls.push([id, opts]); return revive(id); },
|
|
trail: trail ?? (e => trailed.push(e)),
|
|
log: line => lines.push(line), env,
|
|
});
|
|
return { done, calls, lines, trailed };
|
|
}
|
|
|
|
test('the start-up revive asks busRevive for pm:false, so no chief launches without a press', async () => {
|
|
const r = run({ status: () => { throw cliError('stale bus host lock'); }, revive: () => ({ pid: 77 }) });
|
|
assert.deepEqual(await r.done, [{ id: 'alpha', outcome: 'revived', pid: 77 }]);
|
|
assert.deepEqual(r.calls, [['alpha', { pm: false }]]);
|
|
});
|
|
|
|
test('reviveStartOptions keeps the recorded options, never wider, and pm only when both allow it', () => {
|
|
assert.deepEqual(reviveStartOptions({ devLocalOwner: true, pm: true, noLaunch: false }, { pm: false }), { detach: true, devLocalOwner: true, pm: false, noLaunch: false });
|
|
assert.deepEqual(reviveStartOptions({ devLocalOwner: true, pm: true, noLaunch: false }), { detach: true, devLocalOwner: true, pm: true, noLaunch: false });
|
|
assert.deepEqual(reviveStartOptions({ pm: false, noLaunch: true }, { pm: true }), { detach: true, devLocalOwner: false, pm: false, noLaunch: true });
|
|
// Anything but a literal true is false: a malformed record widens nothing.
|
|
assert.deepEqual(reviveStartOptions({ devLocalOwner: 'yes', pm: 1 }), { detach: true, devLocalOwner: false, pm: false, noLaunch: false });
|
|
assert.deepEqual(reviveStartOptions(null), { detach: true, devLocalOwner: false, pm: false, noLaunch: false });
|
|
});
|
|
|
|
test('a dropped --pm is logged with the way back', async () => {
|
|
const r = run({ status: () => { throw cliError('stale bus host lock'); }, revive: () => Object.defineProperty({ pid: 9 }, 'pmDropped', { value: true }) });
|
|
await r.done;
|
|
assert.ok(r.lines.some(l => l === 'bus revive: alpha: the dead host was started with --pm; the chief was not relaunched. mosaic bus stop alpha; mosaic bus start alpha --pm brings it back'), r.lines.join('\n'));
|
|
});
|
|
|
|
test('running, not-answering, stopped and unreadable buses are left alone and never trailed', async () => {
|
|
const states = { a: { healthy: true, pid: 1 }, b: { healthy: false, pid: 2 }, c: { healthy: false, pid: null } };
|
|
const r = run({ ids: ['a', 'b', 'c', 'd'], status: id => { if (id === 'd') throw new Error('EACCES /secret/path'); return states[id]; }, revive: () => assert.fail('revived') });
|
|
assert.deepEqual(await r.done, [{ id: 'a', outcome: 'running' }, { id: 'b', outcome: 'not-answering' }, { id: 'c', outcome: 'stopped' }, { id: 'd', outcome: 'uncertain' }]);
|
|
assert.deepEqual(r.trailed, []);
|
|
assert.ok(!r.lines.join('\n').includes('/secret/path'));
|
|
});
|
|
|
|
test('a trail that cannot be written skips the revive', async () => {
|
|
const r = run({ status: () => { throw cliError('stale bus host lock'); }, revive: () => assert.fail('revived without a trail line'), trail: () => { throw new Error('disk full'); } });
|
|
assert.deepEqual(await r.done, [{ id: 'alpha', outcome: 'trail-unavailable' }]);
|
|
assert.deepEqual(r.calls, []);
|
|
});
|
|
|
|
test('a start that fails after the dead state was cleared is "failed", not "refused"', async () => {
|
|
const r = run({ status: () => { throw cliError('stale bus host lock'); }, revive: () => { throw cliError('bus failed to start; see bus-host/host.log in the runtime directory of organization alpha', { recovered: { pid: 4 } }); } });
|
|
assert.deepEqual(await r.done, [{ id: 'alpha', outcome: 'failed' }]);
|
|
assert.deepEqual(r.trailed.map(e => [e.outcome, e.recovered]), [['requested', undefined], ['failed', true]]);
|
|
assert.match(r.lines.at(-1), /^bus revive: alpha: dead bus host cleared, but the bus did not start \(bus failed to start;/);
|
|
// A non-CliError shows its code, never its text.
|
|
const q = run({ status: () => { throw cliError('stale bus host lock'); }, revive: () => { throw Object.assign(new Error('EIO /home/someone/x'), { code: 'org-locked' }); } });
|
|
assert.deepEqual(await q.done, [{ id: 'alpha', outcome: 'refused' }]);
|
|
assert.equal(q.lines.at(-1), 'bus revive: alpha: refused (unexpected error org-locked); left for mosaic bus status alpha');
|
|
});
|
|
|
|
test('revivePending holds each organization until its own revive ends', async () => {
|
|
let release;
|
|
const gate = new Promise(resolve => { release = resolve; });
|
|
const r = run({ ids: ['alpha', 'beta'], status: id => { if (id === 'alpha') throw cliError('stale bus host lock'); return { healthy: true, pid: 3 }; }, revive: async () => { await gate; return { pid: 5 }; } });
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
const alpha = revivePending('alpha'), beta = revivePending('beta');
|
|
assert.ok(alpha instanceof Promise); assert.ok(beta instanceof Promise);
|
|
assert.equal(revivePending('gamma'), null);
|
|
let settled = false; alpha.then(() => { settled = true; });
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
assert.equal(settled, false);
|
|
release();
|
|
await r.done;
|
|
assert.equal(settled, true);
|
|
assert.equal(revivePending('alpha'), null); assert.equal(revivePending('beta'), null);
|
|
});
|
|
|
|
test('a Console that runs inside an agent run revives nothing and reads no organization', async () => {
|
|
for (const marker of ['MOSAIC_AGENT_RUN', 'CLAUDECODE', 'CODEX_THREAD_ID']) {
|
|
let listed = false;
|
|
const lines = [];
|
|
const results = await reviveBuses({ ctx: {}, listOrgs: async () => { listed = true; return []; }, busStatus: () => assert.fail('read'), busRevive: () => assert.fail('revived'), trail: () => assert.fail('trailed'), log: line => lines.push(line), env: { [marker]: '1' } });
|
|
assert.deepEqual(results, []);
|
|
assert.equal(listed, false);
|
|
assert.deepEqual(lines, ['bus revive: this Console runs inside an agent run; no bus was checked']);
|
|
}
|
|
});
|