Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/agent-launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AgentLauncher as FrameworkAgentLauncher,
registerAgentBackendFactory,
isCopilotCliInvocationError,
trackProcess,
type BackendRuntimeConfig,
type BackendLoggerLike,
type AgentBackend,
Expand Down Expand Up @@ -277,6 +278,7 @@ function spawnAgent(command: string, args: string[], opts: { cwd: string; env: R
stdio: ['pipe', 'pipe', 'pipe'],
detached: true,
});
trackProcess(child);
child.unref();

const stdoutChunks: Buffer[] = [];
Expand Down
1 change: 1 addition & 0 deletions src/core/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,7 @@ export class MigrationRuntime {

process.on('SIGINT', () => void handler('SIGINT'));
process.on('SIGTERM', () => void handler('SIGTERM'));
process.on('SIGHUP', () => void handler('SIGHUP'));
process.on('exit', () => {
this.releaseRunLockSync();
});
Expand Down
10 changes: 7 additions & 3 deletions tests/core/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ describe('MigrationRuntime', () => {
await rm(root, { recursive: true, force: true });
});

it('setupShutdownHandlers registers SIGINT/SIGTERM handlers that flush and save state', async () => {
it('setupShutdownHandlers registers SIGINT/SIGTERM/SIGHUP handlers that flush and save state', async () => {
const runtime = new MigrationRuntime() as any;
const onSpy = vi.spyOn(process, 'on').mockReturnValue(process);
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);
Expand All @@ -846,18 +846,22 @@ describe('MigrationRuntime', () => {

const sigintHandler = onSpy.mock.calls.find(([signal]) => signal === 'SIGINT')?.[1] as (() => void) | undefined;
const sigtermHandler = onSpy.mock.calls.find(([signal]) => signal === 'SIGTERM')?.[1] as (() => void) | undefined;
const sighupHandler = onSpy.mock.calls.find(([signal]) => signal === 'SIGHUP')?.[1] as (() => void) | undefined;

expect(sigintHandler).toBeDefined();
expect(sigtermHandler).toBeDefined();
expect(sighupHandler).toBeDefined();

sigintHandler!();
sigtermHandler!();
sighupHandler!();
await new Promise(resolve => setTimeout(resolve, 0));

expect(runtime.logger.flush).toHaveBeenCalledTimes(2);
expect(runtime.checkpoint.save).toHaveBeenCalledTimes(2);
expect(runtime.logger.flush).toHaveBeenCalledTimes(3);
expect(runtime.checkpoint.save).toHaveBeenCalledTimes(3);
expect(runtime.progress.appendEvent).toHaveBeenCalledWith('Migration interrupted by SIGINT');
expect(runtime.progress.appendEvent).toHaveBeenCalledWith('Migration interrupted by SIGTERM');
expect(runtime.progress.appendEvent).toHaveBeenCalledWith('Migration interrupted by SIGHUP');
expect(exitSpy).toHaveBeenCalledWith(130);
expect(exitSpy).toHaveBeenCalledWith(143);

Expand Down