fix: cli input stream handling and error management, improve e2e and unit tests

This commit is contained in:
mingholy.lmh 2026-01-23 13:56:38 +08:00
parent 6eb16c0bcf
commit f578ff07a2
13 changed files with 741 additions and 150 deletions

View file

@ -42,6 +42,7 @@ function createMockContext(debugMode: boolean = false): IControlContext {
permissionMode: 'default',
sdkMcpServers: new Set<string>(),
mcpClients: new Map(),
inputClosed: false,
};
}
@ -637,6 +638,130 @@ describe('ControlDispatcher', () => {
});
});
describe('markInputClosed', () => {
it('should reject all pending outgoing requests when input closes', () => {
const requestId1 = 'reject-req-1';
const requestId2 = 'reject-req-2';
const resolve1 = vi.fn();
const resolve2 = vi.fn();
const reject1 = vi.fn();
const reject2 = vi.fn();
const timeoutId1 = setTimeout(() => {}, 1000);
const timeoutId2 = setTimeout(() => {}, 1000);
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
const register = (
dispatcher as unknown as {
registerOutgoingRequest: (
id: string,
controller: string,
resolve: (response: ControlResponse) => void,
reject: (error: Error) => void,
timeoutId: NodeJS.Timeout,
) => void;
}
).registerOutgoingRequest.bind(dispatcher);
register(requestId1, 'SystemController', resolve1, reject1, timeoutId1);
register(requestId2, 'SystemController', resolve2, reject2, timeoutId2);
dispatcher.markInputClosed();
expect(reject1).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Input closed' }),
);
expect(reject2).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Input closed' }),
);
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutId1);
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutId2);
});
it('should mark input as closed on context', () => {
dispatcher.markInputClosed();
expect(mockContext.inputClosed).toBe(true);
});
it('should handle empty pending requests gracefully', () => {
expect(() => dispatcher.markInputClosed()).not.toThrow();
});
it('should be idempotent when called multiple times', () => {
const requestId = 'idempotent-req';
const resolve = vi.fn();
const reject = vi.fn();
const timeoutId = setTimeout(() => {}, 1000);
(
dispatcher as unknown as {
registerOutgoingRequest: (
id: string,
controller: string,
resolve: (response: ControlResponse) => void,
reject: (error: Error) => void,
timeoutId: NodeJS.Timeout,
) => void;
}
).registerOutgoingRequest(
requestId,
'SystemController',
resolve,
reject,
timeoutId,
);
dispatcher.markInputClosed();
const firstRejectCount = vi.mocked(reject).mock.calls.length;
// Call again - should not reject again
dispatcher.markInputClosed();
const secondRejectCount = vi.mocked(reject).mock.calls.length;
expect(secondRejectCount).toBe(firstRejectCount);
});
it('should log input closure in debug mode', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
const requestId = 'reject-req-debug';
const resolve = vi.fn();
const reject = vi.fn();
const timeoutId = setTimeout(() => {}, 1000);
(
dispatcherWithDebug as unknown as {
registerOutgoingRequest: (
id: string,
controller: string,
resolve: (response: ControlResponse) => void,
reject: (error: Error) => void,
timeoutId: NodeJS.Timeout,
) => void;
}
).registerOutgoingRequest(
requestId,
'SystemController',
resolve,
reject,
timeoutId,
);
dispatcherWithDebug.markInputClosed();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'[ControlDispatcher] Input closed, rejecting 1 pending outgoing requests',
),
);
consoleSpy.mockRestore();
});
});
describe('shutdown', () => {
it('should cancel all pending incoming requests', () => {
const requestId1 = 'shutdown-req-1';