fix(cli): prevent informational logs from polluting json output (#26264)

This commit is contained in:
Coco Sheng
2026-04-30 14:34:25 -04:00
committed by GitHub
parent 0af13141b2
commit 8c1e255ac0
4 changed files with 219 additions and 24 deletions
+56
View File
@@ -9,6 +9,7 @@ import {
CoreEventEmitter,
CoreEvent,
coreEvents,
type CoreEvents,
type UserFeedbackPayload,
type McpProgressPayload,
} from './events.js';
@@ -268,6 +269,61 @@ describe('CoreEventEmitter', () => {
});
});
describe('drainBacklogs Transformation', () => {
it('should transform events during drain', () => {
const listener = vi.fn();
events.emitOutput(false, 'stdout chunk');
events.emitFeedback('info', 'info message');
events.on(CoreEvent.Output, listener);
events.on(CoreEvent.UserFeedback, listener);
events.drainBacklogs(
<K extends keyof CoreEvents>(event: K, args: CoreEvents[K]) => {
if (event === (CoreEvent.Output as string)) {
const payload = args[0] as { isStderr: boolean; chunk: string };
return {
event,
args: [
{ ...payload, isStderr: true },
] as unknown as CoreEvents[K],
};
}
return { event, args };
},
);
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({ isStderr: true, chunk: 'stdout chunk' }),
);
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({ message: 'info message' }),
);
});
it('should drop events when transform returns undefined', () => {
const listener = vi.fn();
events.emitOutput(false, 'drop me');
events.emitFeedback('info', 'keep me');
events.on(CoreEvent.Output, listener);
events.on(CoreEvent.UserFeedback, listener);
events.drainBacklogs((event, args) => {
if (event === CoreEvent.Output) {
return undefined;
}
return { event, args };
});
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({ message: 'keep me' }),
);
});
});
describe('ModelChanged Event', () => {
it('should emit ModelChanged event with correct payload', () => {
const listener = vi.fn();
+21 -3
View File
@@ -422,8 +422,15 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
/**
* Flushes buffered messages. Call this immediately after primary UI listener
* subscribes.
*
* @param transform - Optional function to transform events before they are emitted.
*/
drainBacklogs(): void {
drainBacklogs(
transform?: <K extends keyof CoreEvents>(
event: K,
args: CoreEvents[K],
) => { event: K; args: CoreEvents[K] } | undefined,
): void {
const backlog = this._eventBacklog;
const head = this._backlogHead;
this._eventBacklog = [];
@@ -431,10 +438,21 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
for (let i = head; i < backlog.length; i++) {
const item = backlog[i];
if (item === undefined) continue;
let eventToEmit = item.event;
let argsToEmit = item.args;
if (transform) {
const transformed = transform(item.event, item.args);
if (!transformed) continue;
eventToEmit = transformed.event;
argsToEmit = transformed.args;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(this.emit as (event: keyof CoreEvents, ...args: unknown[]) => boolean)(
item.event,
...item.args,
eventToEmit,
...argsToEmit,
);
}
}