fix(cli): clean up slash command IDE listeners (#24397)

Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
Jason Matthew Suhari
2026-04-16 03:41:50 +08:00
committed by GitHub
parent cb289e0724
commit cb35ee6710
2 changed files with 78 additions and 5 deletions

View File

@@ -858,11 +858,81 @@ describe('useSlashCommandProcessor', () => {
});
describe('Lifecycle', () => {
it('removes the IDE status listener on unmount after async initialization', async () => {
let resolveIdeClient:
| ((client: {
addStatusChangeListener: (listener: () => void) => void;
removeStatusChangeListener: (listener: () => void) => void;
}) => void)
| undefined;
const addStatusChangeListener = vi.fn();
const removeStatusChangeListener = vi.fn();
mockIdeClientGetInstance.mockImplementation(
() =>
new Promise((resolve) => {
resolveIdeClient = resolve;
}),
);
const result = await setupProcessorHook();
await act(async () => {
resolveIdeClient?.({
addStatusChangeListener,
removeStatusChangeListener,
});
});
result.unmount();
unmountHook = undefined;
expect(addStatusChangeListener).toHaveBeenCalledTimes(1);
expect(removeStatusChangeListener).toHaveBeenCalledTimes(1);
expect(removeStatusChangeListener).toHaveBeenCalledWith(
addStatusChangeListener.mock.calls[0]?.[0],
);
});
it('does not register an IDE status listener if unmounted before async initialization resolves', async () => {
let resolveIdeClient:
| ((client: {
addStatusChangeListener: (listener: () => void) => void;
removeStatusChangeListener: (listener: () => void) => void;
}) => void)
| undefined;
const addStatusChangeListener = vi.fn();
const removeStatusChangeListener = vi.fn();
mockIdeClientGetInstance.mockImplementation(
() =>
new Promise((resolve) => {
resolveIdeClient = resolve;
}),
);
const result = await setupProcessorHook();
result.unmount();
unmountHook = undefined;
await act(async () => {
resolveIdeClient?.({
addStatusChangeListener,
removeStatusChangeListener,
});
});
expect(addStatusChangeListener).not.toHaveBeenCalled();
expect(removeStatusChangeListener).not.toHaveBeenCalled();
});
it('should abort command loading when the hook unmounts', async () => {
const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
const { unmount } = await setupProcessorHook();
unmount();
unmountHook = undefined;
expect(abortSpy).toHaveBeenCalledTimes(1);
});

View File

@@ -281,10 +281,16 @@ export const useSlashCommandProcessor = (
const listener = () => {
reloadCommands();
};
let isActive = true;
let activeIdeClient: IdeClient | undefined;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
const ideClient = await IdeClient.getInstance();
if (!isActive) {
return;
}
activeIdeClient = ideClient;
ideClient.addStatusChangeListener(listener);
})();
@@ -307,11 +313,8 @@ export const useSlashCommandProcessor = (
coreEvents.on('extensionsStopping', extensionEventListener);
return () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
const ideClient = await IdeClient.getInstance();
ideClient.removeStatusChangeListener(listener);
})();
isActive = false;
activeIdeClient?.removeStatusChangeListener(listener);
removeMCPStatusChangeListener(listener);
coreEvents.off('extensionsStarting', extensionEventListener);
coreEvents.off('extensionsStopping', extensionEventListener);