fix(compaction): isolate preservation hook failures
This commit is contained in:
@@ -1,46 +1,9 @@
|
||||
import { describe, expect, it, mock } from "bun:test"
|
||||
|
||||
function createCompactingHandler(hooks: {
|
||||
compactionContextInjector?: {
|
||||
capture: (sessionID: string) => Promise<void>
|
||||
inject: (sessionID: string) => string
|
||||
}
|
||||
compactionTodoPreserver?: { capture: (sessionID: string) => Promise<void> }
|
||||
claudeCodeHooks?: {
|
||||
"experimental.session.compacting"?: (
|
||||
input: { sessionID: string },
|
||||
output: { context: string[] },
|
||||
) => Promise<void>
|
||||
}
|
||||
}) {
|
||||
return async (
|
||||
input: { sessionID: string },
|
||||
output: { context: string[] },
|
||||
): Promise<void> => {
|
||||
await hooks.compactionContextInjector?.capture(input.sessionID)
|
||||
await hooks.compactionTodoPreserver?.capture(input.sessionID)
|
||||
await hooks.claudeCodeHooks?.["experimental.session.compacting"]?.(
|
||||
input,
|
||||
output,
|
||||
)
|
||||
if (hooks.compactionContextInjector) {
|
||||
output.context.push(hooks.compactionContextInjector.inject(input.sessionID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createCompactionAutocontinueHandler(hooks: {
|
||||
compactionContextInjector?: { restore: (sessionID: string) => Promise<boolean> }
|
||||
compactionTodoPreserver?: { restore: (sessionID: string) => Promise<void> }
|
||||
}) {
|
||||
return async (
|
||||
input: { sessionID: string },
|
||||
_output: { enabled: boolean },
|
||||
): Promise<void> => {
|
||||
await hooks.compactionContextInjector?.restore(input.sessionID)
|
||||
await hooks.compactionTodoPreserver?.restore(input.sessionID)
|
||||
}
|
||||
}
|
||||
import {
|
||||
createCompactionAutocontinueHandler,
|
||||
createSessionCompactingHandler,
|
||||
} from "./plugin/session-compacting"
|
||||
|
||||
describe("experimental.session.compacting handler", () => {
|
||||
//#given all three hooks are present
|
||||
@@ -49,7 +12,7 @@ describe("experimental.session.compacting handler", () => {
|
||||
it("calls claudeCodeHooks PreCompact alongside other hooks", async () => {
|
||||
const callOrder: string[] = []
|
||||
|
||||
const handler = createCompactingHandler({
|
||||
const handler = createSessionCompactingHandler({
|
||||
compactionContextInjector: {
|
||||
capture: mock(async () => {
|
||||
callOrder.push("checkpointCapture")
|
||||
@@ -71,7 +34,7 @@ describe("experimental.session.compacting handler", () => {
|
||||
},
|
||||
})
|
||||
|
||||
const output = { context: [] as string[] }
|
||||
const output = { context: [] as string[], prompt: undefined as string | undefined }
|
||||
await handler({ sessionID: "ses_test" }, output)
|
||||
|
||||
expect(callOrder).toEqual([
|
||||
@@ -87,7 +50,7 @@ describe("experimental.session.compacting handler", () => {
|
||||
//#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({
|
||||
const handler = createSessionCompactingHandler({
|
||||
claudeCodeHooks: {
|
||||
"experimental.session.compacting": async (_input, output) => {
|
||||
output.context.push("precompact-injected-context")
|
||||
@@ -95,7 +58,7 @@ describe("experimental.session.compacting handler", () => {
|
||||
},
|
||||
})
|
||||
|
||||
const output = { context: [] as string[] }
|
||||
const output = { context: [] as string[], prompt: undefined as string | undefined }
|
||||
await handler({ sessionID: "ses_test" }, output)
|
||||
|
||||
expect(output.context).toContain("precompact-injected-context")
|
||||
@@ -109,7 +72,7 @@ describe("experimental.session.compacting handler", () => {
|
||||
const checkpointCaptureMock = mock(async () => {})
|
||||
const contextMock = mock(() => "injected-context")
|
||||
|
||||
const handler = createCompactingHandler({
|
||||
const handler = createSessionCompactingHandler({
|
||||
compactionContextInjector: {
|
||||
capture: checkpointCaptureMock,
|
||||
inject: contextMock,
|
||||
@@ -118,7 +81,7 @@ describe("experimental.session.compacting handler", () => {
|
||||
claudeCodeHooks: undefined,
|
||||
})
|
||||
|
||||
const output = { context: [] as string[] }
|
||||
const output = { context: [] as string[], prompt: undefined as string | undefined }
|
||||
await handler({ sessionID: "ses_test" }, output)
|
||||
|
||||
expect(checkpointCaptureMock).toHaveBeenCalledWith("ses_test")
|
||||
@@ -133,19 +96,67 @@ describe("experimental.session.compacting handler", () => {
|
||||
it("does not early-return when compactionContextInjector is null", async () => {
|
||||
const preCompactMock = mock(async () => {})
|
||||
|
||||
const handler = createCompactingHandler({
|
||||
const handler = createSessionCompactingHandler({
|
||||
claudeCodeHooks: {
|
||||
"experimental.session.compacting": preCompactMock,
|
||||
},
|
||||
compactionContextInjector: undefined,
|
||||
})
|
||||
|
||||
const output = { context: [] as string[] }
|
||||
const output = { context: [] as string[], prompt: undefined as string | undefined }
|
||||
await handler({ sessionID: "ses_test" }, output)
|
||||
|
||||
expect(preCompactMock).toHaveBeenCalled()
|
||||
expect(output.context).toEqual([])
|
||||
})
|
||||
|
||||
//#given a preservation hook throws while OpenCode is compacting
|
||||
//#when compacting handler is invoked
|
||||
//#then compaction still continues so the user does not see a failed compact
|
||||
it("continues compaction when an internal preservation hook throws", async () => {
|
||||
const preCompactMock = mock(async (_input, output: { context: string[] }) => {
|
||||
output.context.push("precompact-context")
|
||||
})
|
||||
|
||||
const handler = createSessionCompactingHandler({
|
||||
compactionContextInjector: {
|
||||
capture: mock(async () => {
|
||||
throw new Error("checkpoint api down")
|
||||
}),
|
||||
inject: mock(() => "injected-context"),
|
||||
},
|
||||
compactionTodoPreserver: {
|
||||
capture: mock(async () => {}),
|
||||
},
|
||||
claudeCodeHooks: {
|
||||
"experimental.session.compacting": preCompactMock,
|
||||
},
|
||||
})
|
||||
|
||||
const output = { context: [] as string[], prompt: undefined as string | undefined }
|
||||
|
||||
await expect(handler({ sessionID: "ses_test" }, output)).resolves.toBeUndefined()
|
||||
expect(preCompactMock).toHaveBeenCalled()
|
||||
expect(output.context).toContain("precompact-context")
|
||||
})
|
||||
|
||||
//#given a PreCompact hook replaces the OpenCode compaction prompt
|
||||
//#when compacting handler is invoked
|
||||
//#then the prompt replacement is preserved for OpenCode
|
||||
it("preserves prompt replacement from PreCompact hooks", async () => {
|
||||
const handler = createSessionCompactingHandler({
|
||||
claudeCodeHooks: {
|
||||
"experimental.session.compacting": mock(async (_input, output) => {
|
||||
output.prompt = "custom compaction prompt"
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const output = { context: [] as string[], prompt: undefined as string | undefined }
|
||||
await handler({ sessionID: "ses_prompt" }, output)
|
||||
|
||||
expect(output.prompt).toBe("custom compaction prompt")
|
||||
})
|
||||
})
|
||||
|
||||
describe("experimental.compaction.autocontinue handler", () => {
|
||||
@@ -177,4 +188,25 @@ describe("experimental.compaction.autocontinue handler", () => {
|
||||
expect(callOrder).toEqual(["context", "todos:ses_autocontinue"])
|
||||
expect(output.enabled).toBe(true)
|
||||
})
|
||||
|
||||
it("continues autocontinue restore when one restore hook throws", async () => {
|
||||
//#given
|
||||
const restoreMock = mock(async () => {})
|
||||
const handler = createCompactionAutocontinueHandler({
|
||||
compactionContextInjector: {
|
||||
restore: mock(async () => {
|
||||
throw new Error("checkpoint restore failed")
|
||||
}),
|
||||
},
|
||||
compactionTodoPreserver: { restore: restoreMock },
|
||||
})
|
||||
const output = { enabled: true }
|
||||
|
||||
//#when
|
||||
await expect(handler({ sessionID: "ses_autocontinue" }, output)).resolves.toBeUndefined()
|
||||
|
||||
//#then
|
||||
expect(restoreMock).toHaveBeenCalledWith("ses_autocontinue")
|
||||
expect(output.enabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user