diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts index 6f4dbcb3715..b8d66dbd1df 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts @@ -58,6 +58,20 @@ describe('a worker whose process exits while its own stop is in flight', () => { expect(receipt.state).toBe('stopped') }) + it('accepts an exit observed while inspecting the process before close', async () => { + const { dispatchId } = await h.startWorker() + vi.mocked(h.runtime.showTerminal).mockImplementation(async (handle) => { + fireExit(handle) + return { handle, connected: false } as never + }) + vi.spyOn(h.runtime, 'getTerminalLivenessVerdict').mockReturnValue({ status: 'exited' }) + + await expect( + h.call('orchestration.workerStop', { dispatch: dispatchId }) + ).resolves.toMatchObject({ state: 'stopped', processAction: 'none' }) + expect(h.runtime.closeTerminal).not.toHaveBeenCalled() + }) + it('leaves an exit with no stop in flight failing the dispatch', async () => { const { dispatchId } = await h.startWorker() fireExit('term_worker') diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts index 6438040e1bf..b0cc88c51c1 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts @@ -127,6 +127,16 @@ export const ORCHESTRATION_WORKER_STOP_METHODS: RpcMethod[] = [ ) } const observation = await inspectWorkerTerminal(runtime, db, params.dispatch) + // The host exit can settle this stop while terminal inspection is awaiting inventory. + if (db.getWorkerDispatch(params.dispatch)?.state === 'stopped') { + runtime.notifyMessageArrived(`dispatch:${params.dispatch}`, 'status') + return { + dispatchId: params.dispatch, + state: 'stopped', + alreadySettled: false, + processAction: 'none' + } + } // Why `unverifiable` still proceeds: losing contact is a reason to report // the outcome honestly, never a reason to stop trying to stop the worker. if (