feat(claude-code-hooks): improve session handling and add tests

🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
YeonGyu-Kim
2026-03-31 17:33:29 -07:00
parent a720ef5334
commit 439c9a6499
7 changed files with 205 additions and 13 deletions
@@ -0,0 +1,73 @@
import { describe, expect, test } from "bun:test"
import { ContextCollector } from "../../../features/context-injector"
import { cacheToolInput, getToolInput, stopToolInputCacheCleanup } from "../tool-input-cache"
import { buildTranscriptFromSession, hasTranscriptCacheEntry } from "../transcript"
import { createSessionEventHandler, disposeSessionEventHandler } from "./session-event-handler"
function createMockClient() {
return {
session: {
get: async () => ({ data: {} }),
prompt: async () => undefined,
messages: async () => ({ data: [] }),
},
}
}
describe("createSessionEventHandler", () => {
test("#given deleted session with retained caches #when session deleted arrives #then per-session resources are cleared", async () => {
//#given
const collector = new ContextCollector()
collector.register("ses_cleanup", {
id: "hook-context",
source: "custom",
content: "pending hook context",
})
cacheToolInput("ses_cleanup", "Read", "call-1", { path: "/tmp/a" })
await buildTranscriptFromSession(createMockClient(), "ses_cleanup", "/tmp", "Read", { path: "/tmp/a" })
const handler = createSessionEventHandler(createMockClient() as never, {}, collector)
//#when
await handler({
event: { type: "session.deleted", properties: { info: { id: "ses_cleanup" } } },
})
//#then
expect(collector.hasPending("ses_cleanup")).toBe(false)
expect(getToolInput("ses_cleanup", "Read", "call-1")).toBeNull()
expect(hasTranscriptCacheEntry("ses_cleanup")).toBe(false)
})
test("#given active singleton state #when dispose runs #then all shared caches are cleared", async () => {
//#given
const collector = new ContextCollector()
collector.register("ses_one", {
id: "ctx-1",
source: "custom",
content: "one",
})
collector.register("ses_two", {
id: "ctx-2",
source: "custom",
content: "two",
})
cacheToolInput("ses_one", "Read", "call-1", { path: "/tmp/one" })
cacheToolInput("ses_two", "Read", "call-2", { path: "/tmp/two" })
await buildTranscriptFromSession(createMockClient(), "ses_one", "/tmp", "Read", { path: "/tmp/one" })
await buildTranscriptFromSession(createMockClient(), "ses_two", "/tmp", "Read", { path: "/tmp/two" })
//#when
disposeSessionEventHandler(collector)
//#then
expect(collector.hasPending("ses_one")).toBe(false)
expect(collector.hasPending("ses_two")).toBe(false)
expect(getToolInput("ses_one", "Read", "call-1")).toBeNull()
expect(getToolInput("ses_two", "Read", "call-2")).toBeNull()
expect(hasTranscriptCacheEntry("ses_one")).toBe(false)
expect(hasTranscriptCacheEntry("ses_two")).toBe(false)
stopToolInputCacheCleanup()
})
})
@@ -1,16 +1,24 @@
import type { PluginInput } from "@opencode-ai/plugin"
import type { ContextCollector } from "../../../features/context-injector"
import { loadClaudeHooksConfig } from "../config"
import { loadPluginExtendedConfig } from "../config-loader"
import { executeStopHooks, type StopContext } from "../stop"
import { clearTranscriptCache } from "../transcript"
import { clearToolInputCache, stopToolInputCacheCleanup } from "../tool-input-cache"
import type { PluginConfig } from "../types"
import { createInternalAgentTextPart, isHookDisabled, log } from "../../../shared"
import {
clearAllSessionHookState,
clearSessionHookState,
sessionErrorState,
sessionInterruptState,
} from "../session-hook-state"
export function createSessionEventHandler(ctx: PluginInput, config: PluginConfig) {
export function createSessionEventHandler(
ctx: PluginInput,
config: PluginConfig,
contextCollector?: ContextCollector,
) {
return async (input: { event: { type: string; properties?: unknown } }) => {
const { event } = input
@@ -30,6 +38,9 @@ export function createSessionEventHandler(ctx: PluginInput, config: PluginConfig
const props = event.properties as Record<string, unknown> | undefined
const sessionInfo = props?.info as { id?: string } | undefined
if (sessionInfo?.id) {
clearTranscriptCache(sessionInfo.id)
clearToolInputCache(sessionInfo.id)
contextCollector?.clear(sessionInfo.id)
clearSessionHookState(sessionInfo.id)
}
return
@@ -109,3 +120,10 @@ export function createSessionEventHandler(ctx: PluginInput, config: PluginConfig
clearSessionHookState(sessionID)
}
}
export function disposeSessionEventHandler(contextCollector?: ContextCollector): void {
clearTranscriptCache()
stopToolInputCacheCleanup()
contextCollector?.clearAll()
clearAllSessionHookState()
}