refactor(debug): replace ConsolePatcher with debugLogger and update error reporting

- Replace ConsolePatcher with centralized debugLogger utility
- Refactor errorReporting to use debugLogger instead of file-based reporting
- Remove user-facing console message components:
  - Delete ConsolePatcher.ts, useConsoleMessages.ts/hook
  - Delete ConsoleSummaryDisplay.tsx, DetailedMessagesDisplay.tsx
- Update all tests in packages/core and packages/cli:
  - Mock debugLogger where needed
  - Remove assertions for console output on non-critical errors
  - Keep debugLogger assertions for fatal/network errors
  - Use HOME directory mocking for hermetic file system tests

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-02-02 17:37:54 +08:00
parent 135df54f27
commit 89e3c2cd7a
64 changed files with 1240 additions and 2416 deletions

View file

@ -457,9 +457,6 @@ describe('ControlDispatcher', () => {
it('should handle response for non-existent request in debug mode', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
const response: CLIControlResponse = {
@ -471,15 +468,10 @@ describe('ControlDispatcher', () => {
},
};
dispatcherWithDebug.handleControlResponse(response);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'[ControlDispatcher] No pending outgoing request for: non-existent',
),
);
consoleSpy.mockRestore();
// Should not throw in debug mode
expect(() =>
dispatcherWithDebug.handleControlResponse(response),
).not.toThrow();
});
});
@ -599,11 +591,8 @@ describe('ControlDispatcher', () => {
expect(() => dispatcher.handleCancel('non-existent')).not.toThrow();
});
it('should log cancellation in debug mode', () => {
it('should cancel request in debug mode without throwing', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
const requestId = 'cancel-req-debug';
@ -626,15 +615,7 @@ describe('ControlDispatcher', () => {
timeoutId,
);
dispatcherWithDebug.handleCancel(requestId);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'[ControlDispatcher] Cancelled incoming request: cancel-req-debug',
),
);
consoleSpy.mockRestore();
expect(() => dispatcherWithDebug.handleCancel(requestId)).not.toThrow();
});
});
@ -720,11 +701,8 @@ describe('ControlDispatcher', () => {
expect(secondRejectCount).toBe(firstRejectCount);
});
it('should log input closure in debug mode', () => {
it('should mark input closed in debug mode without throwing', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
const requestId = 'reject-req-debug';
@ -750,15 +728,7 @@ describe('ControlDispatcher', () => {
timeoutId,
);
dispatcherWithDebug.markInputClosed();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'[ControlDispatcher] Input closed, rejecting 1 pending outgoing requests',
),
);
consoleSpy.mockRestore();
expect(() => dispatcherWithDebug.markInputClosed()).not.toThrow();
});
});
@ -848,21 +818,12 @@ describe('ControlDispatcher', () => {
expect(mockSystemController.cleanup).toHaveBeenCalled();
});
it('should log shutdown in debug mode', () => {
it('should shutdown in debug mode without throwing', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
dispatcherWithDebug.shutdown();
expect(consoleSpy).toHaveBeenCalledWith(
'[ControlDispatcher] Shutting down',
);
consoleSpy.mockRestore();
expect(() => dispatcherWithDebug.shutdown()).not.toThrow();
});
});

View file

@ -219,7 +219,7 @@ export class ControlDispatcher implements IPendingRequestRegistry {
const requestIds = Array.from(this.pendingOutgoingRequests.keys());
if (this.context.debugMode) {
console.error(
debugLogger.debug(
`[ControlDispatcher] Input closed, rejecting ${requestIds.length} pending outgoing requests`,
);
}

View file

@ -18,7 +18,6 @@ import { StreamJsonOutputAdapter } from './io/StreamJsonOutputAdapter.js';
import { ControlDispatcher } from './control/ControlDispatcher.js';
import { ControlContext } from './control/ControlContext.js';
import { ControlService } from './control/ControlService.js';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
const runNonInteractiveMock = vi.fn();
@ -47,10 +46,6 @@ vi.mock('./control/ControlService.js', () => ({
ControlService: vi.fn(),
}));
vi.mock('../ui/utils/ConsolePatcher.js', () => ({
ConsolePatcher: vi.fn(),
}));
interface ConfigOverrides {
getSessionId?: () => string;
getModel?: () => string;
@ -160,24 +155,11 @@ describe('runNonInteractiveStreamJson', () => {
createSendSdkMcpMessage: ReturnType<typeof vi.fn>;
};
};
let mockConsolePatcher: {
patch: ReturnType<typeof vi.fn>;
cleanup: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
config = createConfig();
runNonInteractiveMock.mockReset();
// Setup mocks
mockConsolePatcher = {
patch: vi.fn(),
cleanup: vi.fn(),
};
(ConsolePatcher as unknown as ReturnType<typeof vi.fn>).mockImplementation(
() => mockConsolePatcher,
);
mockOutputAdapter = {
emitResult: vi.fn(),
} as {
@ -236,9 +218,7 @@ describe('runNonInteractiveStreamJson', () => {
await runNonInteractiveStreamJson(config, '');
expect(mockConsolePatcher.patch).toHaveBeenCalledTimes(1);
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(initRequest);
expect(mockConsolePatcher.cleanup).toHaveBeenCalledTimes(1);
});
it('processes user message when received as first message', async () => {
@ -489,8 +469,6 @@ describe('runNonInteractiveStreamJson', () => {
await expect(runNonInteractiveStreamJson(config, '')).rejects.toThrow(
'Stream error',
);
expect(mockConsolePatcher.cleanup).toHaveBeenCalled();
});
it('stops processing when abort signal is triggered', async () => {
@ -567,9 +545,6 @@ describe('runNonInteractiveStreamJson', () => {
};
await runNonInteractiveStreamJson(config, '');
expect(mockConsolePatcher.patch).toHaveBeenCalledTimes(1);
expect(mockConsolePatcher.cleanup).toHaveBeenCalledTimes(1);
});
it('cleans up output adapter on completion', async () => {
@ -598,7 +573,5 @@ describe('runNonInteractiveStreamJson', () => {
};
await runNonInteractiveStreamJson(config, '');
expect(mockConsolePatcher.cleanup).toHaveBeenCalled();
});
});

View file

@ -33,7 +33,6 @@ import {
} from './types.js';
import { createMinimalSettings } from '../config/settings.js';
import { runNonInteractive } from '../nonInteractiveCli.js';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
const debugLogger = createDebugLogger('NON_INTERACTIVE_SESSION');
@ -607,29 +606,20 @@ export async function runNonInteractiveStreamJson(
config: Config,
input: string,
): Promise<void> {
const consolePatcher = new ConsolePatcher({
debugMode: config.getDebugMode(),
});
consolePatcher.patch();
try {
let initialPrompt: CLIUserMessage | undefined = undefined;
if (input && input.trim().length > 0) {
const sessionId = config.getSessionId();
initialPrompt = {
type: 'user',
session_id: sessionId,
message: {
role: 'user',
content: input.trim(),
},
parent_tool_use_id: null,
};
}
const manager = new Session(config, initialPrompt);
await manager.run();
} finally {
consolePatcher.cleanup();
let initialPrompt: CLIUserMessage | undefined = undefined;
if (input && input.trim().length > 0) {
const sessionId = config.getSessionId();
initialPrompt = {
type: 'user',
session_id: sessionId,
message: {
role: 'user',
content: input.trim(),
},
parent_tool_use_id: null,
};
}
const manager = new Session(config, initialPrompt);
await manager.run();
}