fix(todo-continuation-enforcer): expose prune interval for cleanup

Prune interval created inside hook was not exposed for disposal,
preventing cleanup on plugin unload.

- Add dispose() method that clears the prune interval
- Export dispose in hook return type

Tests: 2 pass, 6 expects
This commit is contained in:
YeonGyu-Kim
2026-03-11 20:09:34 +09:00
parent 2d2ca863f1
commit a2f030e699
3 changed files with 103 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
declare module "bun:test" {
export interface Matchers {
toBeDefined(): void
toBeUndefined(): void
toHaveLength(expected: number): void
}
}
import { afterAll, afterEach, describe, expect, it, mock } from "bun:test"
import * as actualSessionStateModule from "./session-state"
import type { SessionStateStore } from "./session-state"
let createdSessionStateStore: SessionStateStore | undefined
const createActualSessionStateStore = actualSessionStateModule.createSessionStateStore
const mockModule = mock as typeof mock & {
module: (specifier: string, factory: () => unknown) => void
}
mockModule.module("./session-state", () => ({
...actualSessionStateModule,
createSessionStateStore: () => {
const sessionStateStore = createActualSessionStateStore()
createdSessionStateStore = sessionStateStore
return sessionStateStore
},
}))
const { createTodoContinuationEnforcer } = await import(".")
type PluginInput = Parameters<typeof createTodoContinuationEnforcer>[0]
function createMockPluginInput(): PluginInput {
return {
directory: "/tmp/test",
} as PluginInput
}
function getCreatedSessionStateStore(): SessionStateStore {
if (!createdSessionStateStore) {
throw new Error("expected session state store to be created")
}
return createdSessionStateStore
}
describe("todo-continuation-enforcer dispose", () => {
afterEach(() => {
createdSessionStateStore?.shutdown()
createdSessionStateStore = undefined
})
afterAll(() => {
mockModule.module("./session-state", () => actualSessionStateModule)
})
it("#given todo-continuation-enforcer created #when dispose exists on return value #then it is a function", () => {
// given
const enforcer = createTodoContinuationEnforcer(createMockPluginInput())
// when
const { dispose } = enforcer
// then
expect(typeof dispose).toBe("function")
enforcer.dispose()
})
it("#given enforcer with active session states #when dispose is called #then internal session state store is shut down", () => {
// given
const originalClearInterval = globalThis.clearInterval
const clearIntervalCalls: Array<Parameters<typeof clearInterval>[0]> = []
globalThis.clearInterval = ((timer?: Parameters<typeof clearInterval>[0]) => {
clearIntervalCalls.push(timer)
return originalClearInterval(timer)
}) as typeof clearInterval
try {
const enforcer = createTodoContinuationEnforcer(createMockPluginInput())
const sessionStateStore = getCreatedSessionStateStore()
enforcer.markRecovering("session-1")
enforcer.markRecovering("session-2")
expect(sessionStateStore.getExistingState("session-1")).toBeDefined()
expect(sessionStateStore.getExistingState("session-2")).toBeDefined()
// when
enforcer.dispose()
// then
expect(clearIntervalCalls).toHaveLength(1)
expect(sessionStateStore.getExistingState("session-1")).toBeUndefined()
expect(sessionStateStore.getExistingState("session-2")).toBeUndefined()
} finally {
globalThis.clearInterval = originalClearInterval
}
})
})

View File

@@ -56,5 +56,6 @@ export function createTodoContinuationEnforcer(
markRecovering,
markRecoveryComplete,
cancelAllCountdowns,
dispose: () => sessionStateStore.shutdown(),
}
}

View File

@@ -13,6 +13,7 @@ export interface TodoContinuationEnforcer {
markRecovering: (sessionID: string) => void
markRecoveryComplete: (sessionID: string) => void
cancelAllCountdowns: () => void
dispose: () => void
}
export interface Todo {