From 5c13a637580fa86be0fbd2cd97a8ec10f689cc98 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 17 Feb 2026 02:14:01 +0900 Subject: [PATCH] fix: invoke claude-code-hooks PreCompact in session compacting handler The experimental.session.compacting handler was not delegating to claudeCodeHooks, making PreCompact hooks from .claude/settings.json dead code. Also fixed premature early-return when compactionContextInjector was null which would skip any subsequent hooks. --- src/index.test.ts | 117 +++++++++++++++++++++++++++++++++++++++++++++- src/index.ts | 9 ++-- 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 8d2c6d976..c0ca3212f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,4 +1,119 @@ -import { describe, expect, it } from "bun:test" +import { describe, expect, it, mock } from "bun:test" + +describe("experimental.session.compacting handler", () => { + function createCompactingHandler(hooks: { + compactionTodoPreserver?: { capture: (sessionID: string) => Promise } + claudeCodeHooks?: { + "experimental.session.compacting"?: ( + input: { sessionID: string }, + output: { context: string[] }, + ) => Promise + } + compactionContextInjector?: (sessionID: string) => string + }) { + return async ( + _input: { sessionID: string }, + output: { context: string[] }, + ): Promise => { + await hooks.compactionTodoPreserver?.capture(_input.sessionID) + await hooks.claudeCodeHooks?.["experimental.session.compacting"]?.( + _input, + output, + ) + if (hooks.compactionContextInjector) { + output.context.push(hooks.compactionContextInjector(_input.sessionID)) + } + } + } + + //#given all three hooks are present + //#when compacting handler is invoked + //#then all hooks are called in order: capture → PreCompact → contextInjector + it("calls claudeCodeHooks PreCompact alongside other hooks", async () => { + const callOrder: string[] = [] + + const handler = createCompactingHandler({ + compactionTodoPreserver: { + capture: mock(async () => { callOrder.push("capture") }), + }, + claudeCodeHooks: { + "experimental.session.compacting": mock(async () => { + callOrder.push("preCompact") + }), + }, + compactionContextInjector: mock((sessionID: string) => { + callOrder.push("contextInjector") + return `context-for-${sessionID}` + }), + }) + + const output = { context: [] as string[] } + await handler({ sessionID: "ses_test" }, output) + + expect(callOrder).toEqual(["capture", "preCompact", "contextInjector"]) + expect(output.context).toEqual(["context-for-ses_test"]) + }) + + //#given claudeCodeHooks injects context during PreCompact + //#when compacting handler is invoked + //#then injected context from PreCompact is preserved in output + it("preserves context injected by PreCompact hooks", async () => { + const handler = createCompactingHandler({ + claudeCodeHooks: { + "experimental.session.compacting": async (_input, output) => { + output.context.push("precompact-injected-context") + }, + }, + }) + + const output = { context: [] as string[] } + await handler({ sessionID: "ses_test" }, output) + + expect(output.context).toContain("precompact-injected-context") + }) + + //#given claudeCodeHooks is null (no claude code hooks configured) + //#when compacting handler is invoked + //#then handler completes without error and other hooks still run + it("handles null claudeCodeHooks gracefully", async () => { + const captureMock = mock(async () => {}) + const contextMock = mock(() => "injected-context") + + const handler = createCompactingHandler({ + compactionTodoPreserver: { capture: captureMock }, + claudeCodeHooks: undefined, + compactionContextInjector: contextMock, + }) + + const output = { context: [] as string[] } + await handler({ sessionID: "ses_test" }, output) + + expect(captureMock).toHaveBeenCalledWith("ses_test") + expect(contextMock).toHaveBeenCalledWith("ses_test") + expect(output.context).toEqual(["injected-context"]) + }) + + //#given compactionContextInjector is null + //#when compacting handler is invoked + //#then handler does not early-return, PreCompact hooks still execute + it("does not early-return when compactionContextInjector is null", async () => { + const preCompactMock = mock(async () => {}) + + const handler = createCompactingHandler({ + claudeCodeHooks: { + "experimental.session.compacting": preCompactMock, + }, + compactionContextInjector: undefined, + }) + + const output = { context: [] as string[] } + await handler({ sessionID: "ses_test" }, output) + + expect(preCompactMock).toHaveBeenCalled() + expect(output.context).toEqual([]) + }) +}) + /** * Tests for conditional tool registration logic in index.ts * diff --git a/src/index.ts b/src/index.ts index a444e5128..2555a9319 100644 --- a/src/index.ts +++ b/src/index.ts @@ -80,10 +80,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { output: { context: string[] }, ): Promise => { await hooks.compactionTodoPreserver?.capture(_input.sessionID) - if (!hooks.compactionContextInjector) { - return + await hooks.claudeCodeHooks?.["experimental.session.compacting"]?.( + _input, + output, + ) + if (hooks.compactionContextInjector) { + output.context.push(hooks.compactionContextInjector(_input.sessionID)) } - output.context.push(hooks.compactionContextInjector(_input.sessionID)) }, } }