mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
fix(targets): harden adapter lifecycle and droid model edge cases
This commit is contained in:
1 parent
02af8d5737
commit
3da3407f9a
12 files changed
+556
-101
No files matched your search
@@ -0,0 +1,148 @@
|
||||
import { describe, it, expect, jest } from 'bun:test';
|
||||
import { EventEmitter } from 'events';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
import { forwardSignals, wireChildProcessSignals } from '../../../src/utils/signal-forwarder';
|
||||
|
||||
type MockChildProcess = EventEmitter & {
|
||||
killed: boolean;
|
||||
kill: ChildProcess['kill'];
|
||||
};
|
||||
|
||||
function createMockChildProcess(): ChildProcess {
|
||||
const child = new EventEmitter() as MockChildProcess;
|
||||
child.killed = false;
|
||||
child.kill = jest.fn(() => true) as ChildProcess['kill'];
|
||||
return child as ChildProcess;
|
||||
}
|
||||
|
||||
function getSignalListenerCounts(): Record<'SIGINT' | 'SIGTERM' | 'SIGHUP', number> {
|
||||
return {
|
||||
SIGINT: process.listenerCount('SIGINT'),
|
||||
SIGTERM: process.listenerCount('SIGTERM'),
|
||||
SIGHUP: process.listenerCount('SIGHUP'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('signal-forwarder', () => {
|
||||
it('forwardSignals should register and cleanup listeners', () => {
|
||||
const child = createMockChildProcess();
|
||||
const before = getSignalListenerCounts();
|
||||
|
||||
const cleanup = forwardSignals(child);
|
||||
|
||||
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT + 1);
|
||||
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM + 1);
|
||||
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP + 1);
|
||||
|
||||
cleanup();
|
||||
|
||||
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT);
|
||||
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM);
|
||||
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP);
|
||||
});
|
||||
|
||||
it('wireChildProcessSignals should use default exit behavior for exit code', () => {
|
||||
const child = createMockChildProcess();
|
||||
const before = getSignalListenerCounts();
|
||||
|
||||
const exitSpy = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((() => undefined as never) as typeof process.exit);
|
||||
const killSpy = jest
|
||||
.spyOn(process, 'kill')
|
||||
.mockImplementation((() => true) as typeof process.kill);
|
||||
|
||||
try {
|
||||
wireChildProcessSignals(child, () => {});
|
||||
child.emit('exit', 7, null);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(7);
|
||||
expect(killSpy).not.toHaveBeenCalled();
|
||||
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT);
|
||||
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM);
|
||||
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP);
|
||||
} finally {
|
||||
exitSpy.mockRestore();
|
||||
killSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('wireChildProcessSignals should use default exit behavior for signal', () => {
|
||||
const child = createMockChildProcess();
|
||||
const before = getSignalListenerCounts();
|
||||
|
||||
const exitSpy = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((() => undefined as never) as typeof process.exit);
|
||||
const killSpy = jest
|
||||
.spyOn(process, 'kill')
|
||||
.mockImplementation((() => true) as typeof process.kill);
|
||||
|
||||
try {
|
||||
wireChildProcessSignals(child, () => {});
|
||||
child.emit('exit', null, 'SIGTERM');
|
||||
|
||||
expect(killSpy).toHaveBeenCalledWith(process.pid, 'SIGTERM');
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT);
|
||||
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM);
|
||||
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP);
|
||||
} finally {
|
||||
exitSpy.mockRestore();
|
||||
killSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('wireChildProcessSignals should invoke onError and cleanup listeners', async () => {
|
||||
const child = createMockChildProcess();
|
||||
const before = getSignalListenerCounts();
|
||||
const onError = jest.fn(async () => {});
|
||||
const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException;
|
||||
|
||||
wireChildProcessSignals(child, onError);
|
||||
child.emit('error', err);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(err);
|
||||
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT);
|
||||
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM);
|
||||
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP);
|
||||
});
|
||||
|
||||
it('wireChildProcessSignals should run only one terminal callback when error is followed by exit', async () => {
|
||||
const child = createMockChildProcess();
|
||||
const onError = jest.fn(async () => {});
|
||||
const onExit = jest.fn();
|
||||
const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException;
|
||||
|
||||
wireChildProcessSignals(child, onError, onExit);
|
||||
child.emit('error', err);
|
||||
child.emit('exit', 1, null);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('wireChildProcessSignals should exit with code 1 when onError throws', async () => {
|
||||
const child = createMockChildProcess();
|
||||
const onError = jest.fn(async () => {
|
||||
throw new Error('handler exploded');
|
||||
});
|
||||
const exitSpy = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((() => undefined as never) as typeof process.exit);
|
||||
|
||||
try {
|
||||
const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException;
|
||||
wireChildProcessSignals(child, onError);
|
||||
child.emit('error', err);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
} finally {
|
||||
exitSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user