fix(dispose): improve hook disposal and plugin cleanup

🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
YeonGyu-Kim
2026-03-31 17:33:19 -07:00
parent 9795cc5b3d
commit 990095d22e
3 changed files with 78 additions and 4 deletions
+4
View File
@@ -14,12 +14,16 @@ export type CreatedHooks = ReturnType<typeof createHooks>
type DisposableHook = { dispose?: () => void } | null | undefined type DisposableHook = { dispose?: () => void } | null | undefined
export type DisposableCreatedHooks = { export type DisposableCreatedHooks = {
claudeCodeHooks?: DisposableHook
commentChecker?: DisposableHook
runtimeFallback?: DisposableHook runtimeFallback?: DisposableHook
todoContinuationEnforcer?: DisposableHook todoContinuationEnforcer?: DisposableHook
autoSlashCommand?: DisposableHook autoSlashCommand?: DisposableHook
} }
export function disposeCreatedHooks(hooks: DisposableCreatedHooks): void { export function disposeCreatedHooks(hooks: DisposableCreatedHooks): void {
hooks.claudeCodeHooks?.dispose?.()
hooks.commentChecker?.dispose?.()
hooks.runtimeFallback?.dispose?.() hooks.runtimeFallback?.dispose?.()
hooks.todoContinuationEnforcer?.dispose?.() hooks.todoContinuationEnforcer?.dispose?.()
hooks.autoSlashCommand?.dispose?.() hooks.autoSlashCommand?.dispose?.()
+65 -3
View File
@@ -12,10 +12,14 @@ describe("createPluginDispose", () => {
const skillMcpManager = { const skillMcpManager = {
disconnectAll: async (): Promise<void> => {}, disconnectAll: async (): Promise<void> => {},
} }
const lspManager = {
stopAll: async (): Promise<void> => {},
}
const shutdownSpy = spyOn(backgroundManager, "shutdown") const shutdownSpy = spyOn(backgroundManager, "shutdown")
const dispose = createPluginDispose({ const dispose = createPluginDispose({
backgroundManager, backgroundManager,
skillMcpManager, skillMcpManager,
lspManager,
disposeHooks: (): void => {}, disposeHooks: (): void => {},
}) })
@@ -34,10 +38,14 @@ describe("createPluginDispose", () => {
const skillMcpManager = { const skillMcpManager = {
disconnectAll: async (): Promise<void> => {}, disconnectAll: async (): Promise<void> => {},
} }
const lspManager = {
stopAll: async (): Promise<void> => {},
}
const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll") const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll")
const dispose = createPluginDispose({ const dispose = createPluginDispose({
backgroundManager, backgroundManager,
skillMcpManager, skillMcpManager,
lspManager,
disposeHooks: (): void => {}, disposeHooks: (): void => {},
}) })
@@ -50,6 +58,12 @@ describe("createPluginDispose", () => {
test("#given plugin with hooks that have dispose #when dispose() is called #then each hook's dispose is called", async () => { test("#given plugin with hooks that have dispose #when dispose() is called #then each hook's dispose is called", async () => {
// given // given
const claudeCodeHooks = {
dispose: (): void => {},
}
const commentChecker = {
dispose: (): void => {},
}
const runtimeFallback = { const runtimeFallback = {
dispose: (): void => {}, dispose: (): void => {},
} }
@@ -59,6 +73,11 @@ describe("createPluginDispose", () => {
const autoSlashCommand = { const autoSlashCommand = {
dispose: (): void => {}, dispose: (): void => {},
} }
const lspManager = {
stopAll: async (): Promise<void> => {},
}
const claudeCodeHooksDisposeSpy = spyOn(claudeCodeHooks, "dispose")
const commentCheckerDisposeSpy = spyOn(commentChecker, "dispose")
const runtimeFallbackDisposeSpy = spyOn(runtimeFallback, "dispose") const runtimeFallbackDisposeSpy = spyOn(runtimeFallback, "dispose")
const todoContinuationEnforcerDisposeSpy = spyOn(todoContinuationEnforcer, "dispose") const todoContinuationEnforcerDisposeSpy = spyOn(todoContinuationEnforcer, "dispose")
const autoSlashCommandDisposeSpy = spyOn(autoSlashCommand, "dispose") const autoSlashCommandDisposeSpy = spyOn(autoSlashCommand, "dispose")
@@ -69,8 +88,11 @@ describe("createPluginDispose", () => {
skillMcpManager: { skillMcpManager: {
disconnectAll: async (): Promise<void> => {}, disconnectAll: async (): Promise<void> => {},
}, },
lspManager,
disposeHooks: (): void => { disposeHooks: (): void => {
disposeCreatedHooks({ disposeCreatedHooks({
claudeCodeHooks,
commentChecker,
runtimeFallback, runtimeFallback,
todoContinuationEnforcer, todoContinuationEnforcer,
autoSlashCommand, autoSlashCommand,
@@ -82,6 +104,8 @@ describe("createPluginDispose", () => {
await dispose() await dispose()
// then // then
expect(claudeCodeHooksDisposeSpy).toHaveBeenCalledTimes(1)
expect(commentCheckerDisposeSpy).toHaveBeenCalledTimes(1)
expect(runtimeFallbackDisposeSpy).toHaveBeenCalledTimes(1) expect(runtimeFallbackDisposeSpy).toHaveBeenCalledTimes(1)
expect(todoContinuationEnforcerDisposeSpy).toHaveBeenCalledTimes(1) expect(todoContinuationEnforcerDisposeSpy).toHaveBeenCalledTimes(1)
expect(autoSlashCommandDisposeSpy).toHaveBeenCalledTimes(1) expect(autoSlashCommandDisposeSpy).toHaveBeenCalledTimes(1)
@@ -95,15 +119,20 @@ describe("createPluginDispose", () => {
const skillMcpManager = { const skillMcpManager = {
disconnectAll: async (): Promise<void> => {}, disconnectAll: async (): Promise<void> => {},
} }
const lspManager = {
stopAll: async (): Promise<void> => {},
}
const disposeHooks = { const disposeHooks = {
run: (): void => {}, run: (): void => {},
} }
const shutdownSpy = spyOn(backgroundManager, "shutdown") const shutdownSpy = spyOn(backgroundManager, "shutdown")
const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll") const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll")
const stopAllSpy = spyOn(lspManager, "stopAll")
const disposeHooksSpy = spyOn(disposeHooks, "run") const disposeHooksSpy = spyOn(disposeHooks, "run")
const dispose = createPluginDispose({ const dispose = createPluginDispose({
backgroundManager, backgroundManager,
skillMcpManager, skillMcpManager,
lspManager,
disposeHooks: disposeHooks.run, disposeHooks: disposeHooks.run,
}) })
@@ -112,9 +141,10 @@ describe("createPluginDispose", () => {
await dispose() await dispose()
// then // then
expect(shutdownSpy).toHaveBeenCalledTimes(1) expect(shutdownSpy).toHaveBeenCalledTimes(1)
expect(disconnectAllSpy).toHaveBeenCalledTimes(1) expect(disconnectAllSpy).toHaveBeenCalledTimes(1)
expect(disposeHooksSpy).toHaveBeenCalledTimes(1) expect(stopAllSpy).toHaveBeenCalledTimes(1)
expect(disposeHooksSpy).toHaveBeenCalledTimes(1)
}) })
test("#given backgroundManager.shutdown() throws #when dispose() is called #then skillMcpManager.disconnectAll() and disposeHooks() are still called", async () => { test("#given backgroundManager.shutdown() throws #when dispose() is called #then skillMcpManager.disconnectAll() and disposeHooks() are still called", async () => {
@@ -127,11 +157,15 @@ describe("createPluginDispose", () => {
const skillMcpManager = { const skillMcpManager = {
disconnectAll: async (): Promise<void> => {}, disconnectAll: async (): Promise<void> => {},
} }
const lspManager = {
stopAll: async (): Promise<void> => {},
}
const disposeHooksCalls: number[] = [] const disposeHooksCalls: number[] = []
const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll") const disconnectAllSpy = spyOn(skillMcpManager, "disconnectAll")
const dispose = createPluginDispose({ const dispose = createPluginDispose({
backgroundManager, backgroundManager,
skillMcpManager, skillMcpManager,
lspManager,
disposeHooks: (): void => { disposeHooks: (): void => {
disposeHooksCalls.push(1) disposeHooksCalls.push(1)
}, },
@@ -155,11 +189,15 @@ describe("createPluginDispose", () => {
throw new Error("disconnectAll failed") throw new Error("disconnectAll failed")
}, },
} }
const lspManager = {
stopAll: async (): Promise<void> => {},
}
const disposeHooksCalls: number[] = [] const disposeHooksCalls: number[] = []
const shutdownSpy = spyOn(backgroundManager, "shutdown") const shutdownSpy = spyOn(backgroundManager, "shutdown")
const dispose = createPluginDispose({ const dispose = createPluginDispose({
backgroundManager, backgroundManager,
skillMcpManager, skillMcpManager,
lspManager,
disposeHooks: (): void => { disposeHooks: (): void => {
disposeHooksCalls.push(1) disposeHooksCalls.push(1)
}, },
@@ -172,4 +210,28 @@ describe("createPluginDispose", () => {
expect(shutdownSpy).toHaveBeenCalledTimes(1) expect(shutdownSpy).toHaveBeenCalledTimes(1)
expect(disposeHooksCalls).toHaveLength(1) expect(disposeHooksCalls).toHaveLength(1)
}) })
test("#given active LSP clients #when dispose runs #then lsp manager is stopped", async () => {
// given
const lspManager = {
stopAll: async (): Promise<void> => {},
}
const stopAllSpy = spyOn(lspManager, "stopAll")
const dispose = createPluginDispose({
backgroundManager: {
shutdown: async (): Promise<void> => {},
},
skillMcpManager: {
disconnectAll: async (): Promise<void> => {},
},
lspManager,
disposeHooks: (): void => {},
})
// when
await dispose()
// then
expect(stopAllSpy).toHaveBeenCalledTimes(1)
})
}) })
+9 -1
View File
@@ -9,9 +9,12 @@ export function createPluginDispose(args: {
skillMcpManager: { skillMcpManager: {
disconnectAll: () => Promise<void> disconnectAll: () => Promise<void>
} }
lspManager: {
stopAll: () => Promise<void>
}
disposeHooks: () => void disposeHooks: () => void
}): PluginDispose { }): PluginDispose {
const { backgroundManager, skillMcpManager, disposeHooks } = args const { backgroundManager, skillMcpManager, lspManager, disposeHooks } = args
let disposePromise: Promise<void> | null = null let disposePromise: Promise<void> | null = null
return async (): Promise<void> => { return async (): Promise<void> => {
@@ -31,6 +34,11 @@ export function createPluginDispose(args: {
} catch (error) { } catch (error) {
log("[plugin-dispose] skillMcpManager.disconnectAll() error:", error) log("[plugin-dispose] skillMcpManager.disconnectAll() error:", error)
} }
try {
await lspManager.stopAll()
} catch (error) {
log("[plugin-dispose] lspManager.stopAll() error:", error)
}
try { try {
disposeHooks() disposeHooks()
} catch (error) { } catch (error) {