diff --git a/src/hooks/todo-continuation-enforcer/compaction-guard.regression.test.ts b/src/hooks/todo-continuation-enforcer/compaction-guard.regression.test.ts new file mode 100644 index 000000000..f5ab2c3d0 --- /dev/null +++ b/src/hooks/todo-continuation-enforcer/compaction-guard.regression.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it as test } from "bun:test" + +import { COMPACTION_GUARD_MS } from "./constants" +import { + acknowledgeCompactionGuard, + armCompactionGuard, + isCompactionGuardActive, +} from "./compaction-guard" +import type { SessionState } from "./types" + +function createSessionState(): SessionState { + return { + stagnationCount: 0, + consecutiveFailures: 0, + } +} + +describe("compaction guard regressions", () => { + describe("#given a compaction epoch was already acknowledged", () => { + describe("#when a newer compaction epoch is armed", () => { + test("#then the guard re-arms for the newer epoch", () => { + const state = createSessionState() + + const firstEpoch = armCompactionGuard(state, 1_000) + expect(acknowledgeCompactionGuard(state, firstEpoch)).toBe(true) + expect(isCompactionGuardActive(state, 1_001)).toBe(false) + + const secondEpoch = armCompactionGuard(state, 2_000) + + expect(secondEpoch).toBe(firstEpoch + 1) + expect(state.recentCompactionEpoch).toBe(secondEpoch) + expect(isCompactionGuardActive(state, 2_001)).toBe(true) + }) + }) + }) + + describe("#given a newer compaction epoch is armed before an older idle check finishes", () => { + describe("#when the older epoch tries to acknowledge the guard", () => { + test("#then it does not clear the newer epoch", () => { + const state = createSessionState() + + const firstEpoch = armCompactionGuard(state, 1_000) + const secondEpoch = armCompactionGuard(state, 2_000) + + expect(acknowledgeCompactionGuard(state, firstEpoch)).toBe(false) + expect(state.acknowledgedCompactionEpoch).toBeUndefined() + expect(state.recentCompactionEpoch).toBe(secondEpoch) + expect(isCompactionGuardActive(state, 2_001)).toBe(true) + }) + }) + }) + + describe("#given the current compaction epoch is still inside the guard window", () => { + describe("#when that same epoch is acknowledged", () => { + test("#then continuation can proceed again without waiting for the window to expire", () => { + const state = createSessionState() + + const currentEpoch = armCompactionGuard(state, 1_000) + + expect(isCompactionGuardActive(state, 1_000 + COMPACTION_GUARD_MS - 1)).toBe(true) + expect(acknowledgeCompactionGuard(state, currentEpoch)).toBe(true) + expect(isCompactionGuardActive(state, 1_001)).toBe(false) + expect(isCompactionGuardActive(state, 1_000 + COMPACTION_GUARD_MS - 1)).toBe(false) + }) + }) + }) +}) diff --git a/src/hooks/todo-continuation-enforcer/compaction-guard.ts b/src/hooks/todo-continuation-enforcer/compaction-guard.ts index 38f3d640a..5711dde39 100644 --- a/src/hooks/todo-continuation-enforcer/compaction-guard.ts +++ b/src/hooks/todo-continuation-enforcer/compaction-guard.ts @@ -1,8 +1,37 @@ import { COMPACTION_GUARD_MS } from "./constants" import type { SessionState } from "./types" +export function armCompactionGuard(state: SessionState, now: number): number { + const nextEpoch = (state.recentCompactionEpoch ?? 0) + 1 + + state.recentCompactionAt = now + state.recentCompactionEpoch = nextEpoch + + return nextEpoch +} + +export function acknowledgeCompactionGuard( + state: SessionState, + compactionEpoch: number | undefined +): boolean { + if (compactionEpoch === undefined) { + return false + } + + if (state.recentCompactionEpoch !== compactionEpoch) { + return false + } + + state.acknowledgedCompactionEpoch = compactionEpoch + return true +} + export function isCompactionGuardActive(state: SessionState, now: number): boolean { - if (!state.recentCompactionAt) { + if (state.recentCompactionAt === undefined || state.recentCompactionEpoch === undefined) { + return false + } + + if (state.acknowledgedCompactionEpoch === state.recentCompactionEpoch) { return false }