diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index 4bd0faae7..4cd296492 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -421,14 +421,17 @@ export async function handleMetaCommand( } case 'stop': { - await shutdown(); + // Return the acknowledgement before closing the listener. Shutting down + // inline resets the CLI's fetch, which it reasonably interprets as a + // crash and then restarts the daemon it was asked to stop. + setTimeout(() => { void shutdown(); }, 25).unref?.(); return 'Server stopped'; } case 'restart': { // Signal that we want a restart — the CLI will detect exit and restart console.log('[browse] Restart requested. Exiting for CLI to restart.'); - await shutdown(); + setTimeout(() => { void shutdown(); }, 25).unref?.(); return 'Restarting...'; } diff --git a/browse/test/stop-ack-before-shutdown.test.ts b/browse/test/stop-ack-before-shutdown.test.ts new file mode 100644 index 000000000..05c761675 --- /dev/null +++ b/browse/test/stop-ack-before-shutdown.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'bun:test'; +import { handleMetaCommand } from '../src/meta-commands'; + +describe('server control acknowledgement ordering', () => { + for (const [command, acknowledgement] of [ + ['stop', 'Server stopped'], + ['restart', 'Restarting...'], + ] as const) { + test(`${command} acknowledges before closing the listener`, async () => { + let shutdownCalls = 0; + const manager = { getActiveSession: () => ({}) } as any; + + const result = await handleMetaCommand(command, [], manager, async () => { + shutdownCalls += 1; + }); + + expect(result).toBe(acknowledgement); + expect(shutdownCalls).toBe(0); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(shutdownCalls).toBe(1); + }); + } +});