diff --git a/src/hooks/src/hooks/read-once-shared.ts b/src/hooks/src/hooks/read-once-shared.ts index 6c0c6956..821ed0ef 100644 --- a/src/hooks/src/hooks/read-once-shared.ts +++ b/src/hooks/src/hooks/read-once-shared.ts @@ -64,11 +64,16 @@ export interface ReadOnceConfig { disabled: boolean; } -export const getReadOnceConfig = (): ReadOnceConfig => ({ - mode: process.env.READ_ONCE_MODE === 'deny' ? 'deny' : 'warn', - ttlMs: Math.max(1, parseInt(process.env.READ_ONCE_TTL ?? '', 10) || DEFAULT_TTL_MS / 1000) * 1000, - disabled: process.env.READ_ONCE_DISABLED === '1', -}); +export const getReadOnceConfig = (): ReadOnceConfig => { + const ttlSecs = Number.isNaN(parseInt(process.env.READ_ONCE_TTL ?? '', 10)) + ? DEFAULT_TTL_MS / 1000 + : parseInt(process.env.READ_ONCE_TTL ?? '', 10); + return { + mode: process.env.READ_ONCE_MODE === 'deny' ? 'deny' : 'warn', + ttlMs: Math.max(1, ttlSecs) * 1000, + disabled: process.env.READ_ONCE_DISABLED === '1', + }; +}; export const isFullRead = (ctx: HookContext): boolean => !('offset' in ctx.toolInput) && !('limit' in ctx.toolInput); diff --git a/src/hooks/tests/read-once.test.ts b/src/hooks/tests/read-once.test.ts index 916845d5..7f9c77b4 100644 --- a/src/hooks/tests/read-once.test.ts +++ b/src/hooks/tests/read-once.test.ts @@ -36,7 +36,11 @@ vi.mock('../src/runtime/state-store', () => ({ import { runHook } from '../src/runtime/run-hook'; import { readOnceHook } from '../src/hooks/read-once'; import { readOnceResetHook } from '../src/hooks/read-once-reset'; -import { readReadOnceState, READ_ONCE_NAMESPACE } from '../src/hooks/read-once-shared'; +import { + getReadOnceConfig, + readReadOnceState, + READ_ONCE_NAMESPACE, +} from '../src/hooks/read-once-shared'; const makeClaudeRead = ( filePath: string, @@ -172,6 +176,16 @@ describe('read-once', () => { expect(result).toBeNull(); }); + test('zero TTL is honoured as the floored minimum, not the default', () => { + process.env.READ_ONCE_TTL = '0'; + expect(getReadOnceConfig().ttlMs).toBe(1000); + }); + + test('missing TTL falls back to the default', () => { + delete process.env.READ_ONCE_TTL; + expect(getReadOnceConfig().ttlMs).toBe(20 * 60 * 1000); + }); + test('preCompact reset clears the ledger for the current session', async () => { await runHookWithRaw(readOnceHook, makeClaudeRead(filePath)); await runHookWithRaw(readOnceResetHook, makeClaudePreCompact('claude-session-001'));