fix(console): Stop bus review round: trail and stopped flag (#1873)

A stop that may already have signalled the host (a timeout or an
unexpected error) is trailed as failed, not refused; only exit 3 comes
before the signal. The response's stopped flag follows what busStop
reports, so a host that exited on its own between the read and the stop
is not reported as stopped by this press.
This commit was merged in pull request #1941.
This commit is contained in:
jason.woltje committed 2026-11-11 16:10:48 -06:00
1 parent ce066123b0
commit a5f227e9fc
2 files changed
+15 -3

No files matched your search

+4 -2
View File
@@ -655,7 +655,8 @@ async function stopBus(req, res, { sub, write, id, ctx, service, admin, log, tra
let receipt;
try { receipt = await bus.busStop(ctx, id); }
catch (e) {
note('refused');
// Only exit 3 comes before the signal; a timeout or an unexpected error may follow SIGTERM.
note(isCliError(e) && e.exitCode === 3 ? 'refused' : 'failed');
if (!isCliError(e)) return refusal(res, e, { step, id, unknownOk: true });
return json(res, 409, { error: e.exitCode === 3 ? 'bus-refused' : 'bus-failed', step, message: `${plainMessage(ctx, e.message)}. Inspect it with mosaic bus status ${id}.` });
}
@@ -669,7 +670,8 @@ async function stopBus(req, res, { sub, write, id, ctx, service, admin, log, tra
}
note('done');
log(`org-admin: ${id} bus stopped`);
return json(res, 200, { outcome: 'done', step, stopped: true, state: 'stopped', receipt: forReceipt(ctx, { org: id, ...receipt }), verb: verbFor('bus-stop', id) });
// A host that exited on its own between the read and the stop was not stopped by this press.
return json(res, 200, { outcome: 'done', step, stopped: receipt?.stopped === true, state: 'stopped', receipt: forReceipt(ctx, { org: id, ...receipt }), verb: verbFor('bus-stop', id) });
}
// The dev-local owner exactly (packages/cli/src/dev-local-owner.mjs). A signed-in session carries
+11 -1
View File
@@ -840,6 +840,10 @@ test('stop: a stop that is refused, times out or leaves state behind is a visibl
bus.busStop = async () => { throw Object.assign(new Error('bus host pid 4242 did not stop within 60 s'), { name: 'CliError', exitCode: 1 }); };
r = await c.call('POST', STOP, {});
assert.equal(r.status, 409); assert.equal(r.body.error, 'bus-failed');
// An unexpected error may come after the signal: the outcome is unknown and the trail says failed.
bus.busStop = async () => { throw new Error('EPERM'); };
r = await c.call('POST', STOP, {});
assert.equal(r.status, 502, r.raw); assert.equal(r.body.error, 'outcome-unknown');
// The host was signalled and exited, but a lock is left, or the host is still recorded.
let after;
bus.busStop = async () => { bus.busStatus = after; return { stopped: true, business: 'acme', pid: 4242 }; };
@@ -851,7 +855,13 @@ test('stop: a stop that is refused, times out or leaves state behind is a visibl
r = await c.call('POST', STOP, {});
assert.equal(r.status, 409); assert.equal(r.body.error, 'bus-failed');
assert.match(r.body.message, /^the bus host was stopped, but a bus host still runs\./);
assert.deepEqual(tr.lines().map(e => e.outcome), ['requested', 'refused', 'requested', 'refused', 'requested', 'failed', 'requested', 'failed']);
assert.deepEqual(tr.lines().map(e => e.outcome), ['requested', 'refused', 'requested', 'failed', 'requested', 'failed', 'requested', 'failed', 'requested', 'failed']);
// The host exited on its own between the read and the stop: done, but this press stopped nothing.
bus.busStatus = live;
bus.busStop = async () => { bus.busStatus = async () => ({ org: 'acme', healthy: false, pid: null }); return { stopped: false, reason: 'no host runs' }; };
r = await c.call('POST', STOP, {});
assert.equal(r.status, 200, r.raw);
assert.deepEqual([r.body.outcome, r.body.state, r.body.stopped, r.body.receipt.stopped], ['done', 'stopped', false, false]);
});
test('stop: an archived organization, or a Console whose bus control has no stop, refuses', async t => {