@@ -231,6 +231,176 @@ func TestExecuteAgentHookSessionStartSkipsCaptureWhenPolicyUnreadable(t *testing
231231 require .True (t , os .IsNotExist (statErr ), "session-start must not claim the session when checkpoint policy is unreadable" )
232232}
233233
234+ // TestExecuteAgentHookShortCircuitsWhenDisabled is a regression test for #524:
235+ // a hook must not perform any dispatch/strategy work when Entire is
236+ // disabled. Asserted via the same "session was never claimed" signal the
237+ // checkpoint-policy tests above use, rather than a timing assertion.
238+ func TestExecuteAgentHookShortCircuitsWhenDisabled (t * testing.T ) {
239+ setupStopTestRepo (t )
240+ repoRoot := mustGetwd (t )
241+
242+ entireDir := filepath .Join (repoRoot , ".entire" )
243+ require .NoError (t , os .MkdirAll (entireDir , 0o750 ))
244+ require .NoError (t , os .WriteFile (filepath .Join (entireDir , "settings.json" ), []byte (`{"enabled":false}` ), 0o600 ))
245+
246+ sessionID := "disabled-session-start"
247+ payload , err := json .Marshal (map [string ]string {
248+ "session_id" : sessionID ,
249+ "transcript_path" : filepath .Join (repoRoot , "transcript.jsonl" ),
250+ })
251+ require .NoError (t , err )
252+
253+ cmd := & cobra.Command {}
254+ cmd .SetIn (bytes .NewReader (payload ))
255+ cmd .SetErr (& bytes.Buffer {})
256+ cmd .SetContext (context .Background ())
257+
258+ require .NoError (t , executeAgentHook (cmd , agent .AgentNameClaudeCode , claudecode .HookNameSessionStart , false ))
259+
260+ hintPath := filepath .Join (repoRoot , ".git" , session .SessionStateDirName , sessionID + ".agent" )
261+ _ , statErr := os .Stat (hintPath )
262+ require .True (t , os .IsNotExist (statErr ), "disabled hook must not dispatch or claim the session" )
263+ }
264+
265+ // TestExecuteAgentHookShortCircuitsWhenSettingsMissing is a regression test
266+ // for #524: a repo that was never `entire enable`d (no .entire/settings.json)
267+ // must short-circuit rather than falling through to full lifecycle dispatch.
268+ func TestExecuteAgentHookShortCircuitsWhenSettingsMissing (t * testing.T ) {
269+ setupStopTestRepo (t )
270+ repoRoot := mustGetwd (t )
271+ // Deliberately do NOT create .entire/settings.json.
272+
273+ sessionID := "missing-settings-session-start"
274+ payload , err := json .Marshal (map [string ]string {
275+ "session_id" : sessionID ,
276+ "transcript_path" : filepath .Join (repoRoot , "transcript.jsonl" ),
277+ })
278+ require .NoError (t , err )
279+
280+ cmd := & cobra.Command {}
281+ cmd .SetIn (bytes .NewReader (payload ))
282+ cmd .SetErr (& bytes.Buffer {})
283+ cmd .SetContext (context .Background ())
284+
285+ require .NoError (t , executeAgentHook (cmd , agent .AgentNameClaudeCode , claudecode .HookNameSessionStart , false ))
286+
287+ hintPath := filepath .Join (repoRoot , ".git" , session .SessionStateDirName , sessionID + ".agent" )
288+ _ , statErr := os .Stat (hintPath )
289+ require .True (t , os .IsNotExist (statErr ), "hook must not dispatch when Entire was never enabled in this repo" )
290+ }
291+
292+ // TestExecuteAgentHookShortCircuitsWhenSettingsCorrupted is a regression test
293+ // for #524. Before this fix, IsEnabled() failed OPEN on a settings.Load()
294+ // error (the caller's `err == nil && !enabled` check only short-circuited
295+ // when the read succeeded), so a corrupted settings file made every hook
296+ // invocation pay the full dispatch cost — including, for Stop hooks, a
297+ // multi-second wait on the transcript-flush sentinel (see
298+ // ClaudeCodeAgent.ParseHookEvent) — instead of exiting fast. The gate must
299+ // fail closed on any settings read error.
300+ func TestExecuteAgentHookShortCircuitsWhenSettingsCorrupted (t * testing.T ) {
301+ setupStopTestRepo (t )
302+ repoRoot := mustGetwd (t )
303+
304+ entireDir := filepath .Join (repoRoot , ".entire" )
305+ require .NoError (t , os .MkdirAll (entireDir , 0o750 ))
306+ require .NoError (t , os .WriteFile (filepath .Join (entireDir , "settings.json" ), []byte (`{ enabled: false, not valid json` ), 0o600 ))
307+
308+ sessionID := "corrupted-settings-session-start"
309+ payload , err := json .Marshal (map [string ]string {
310+ "session_id" : sessionID ,
311+ "transcript_path" : filepath .Join (repoRoot , "transcript.jsonl" ),
312+ })
313+ require .NoError (t , err )
314+
315+ cmd := & cobra.Command {}
316+ cmd .SetIn (bytes .NewReader (payload ))
317+ cmd .SetErr (& bytes.Buffer {})
318+ cmd .SetContext (context .Background ())
319+
320+ require .NoError (t , executeAgentHook (cmd , agent .AgentNameClaudeCode , claudecode .HookNameSessionStart , false ))
321+
322+ hintPath := filepath .Join (repoRoot , ".git" , session .SessionStateDirName , sessionID + ".agent" )
323+ _ , statErr := os .Stat (hintPath )
324+ require .True (t , os .IsNotExist (statErr ), "hook must fail closed (not dispatch) when settings are unreadable" )
325+ }
326+
327+ // TestExecuteAgentHookStopReturnsFastWhenSettingsCorrupted directly
328+ // regression-tests the reported symptom: `entire hooks claude-code stop`
329+ // against a corrupted settings file must return in well under the
330+ // multi-second transcript-flush-sentinel timeout it used to hit, not just
331+ // skip dispatch. The bound is intentionally generous (this repo has no
332+ // other timing-based tests to match precedent against) — it only needs to
333+ // distinguish "short-circuited" from "waited on the sentinel timeout".
334+ func TestExecuteAgentHookStopReturnsFastWhenSettingsCorrupted (t * testing.T ) {
335+ setupStopTestRepo (t )
336+ repoRoot := mustGetwd (t )
337+
338+ entireDir := filepath .Join (repoRoot , ".entire" )
339+ require .NoError (t , os .MkdirAll (entireDir , 0o750 ))
340+ require .NoError (t , os .WriteFile (filepath .Join (entireDir , "settings.json" ), []byte (`{ enabled: false, not valid json` ), 0o600 ))
341+
342+ transcriptPath := filepath .Join (repoRoot , "transcript.jsonl" )
343+ require .NoError (t , os .WriteFile (transcriptPath , []byte (`{"type":"user","message":{"content":"hi"}}` + "\n " ), 0o600 ))
344+
345+ payload , err := json .Marshal (map [string ]string {
346+ "session_id" : "corrupted-settings-stop" ,
347+ "transcript_path" : transcriptPath ,
348+ })
349+ require .NoError (t , err )
350+
351+ cmd := & cobra.Command {}
352+ cmd .SetIn (bytes .NewReader (payload ))
353+ cmd .SetErr (& bytes.Buffer {})
354+ cmd .SetContext (context .Background ())
355+
356+ start := time .Now ()
357+ require .NoError (t , executeAgentHook (cmd , agent .AgentNameClaudeCode , claudecode .HookNameStop , false ))
358+ elapsed := time .Since (start )
359+
360+ require .Lessf (t , elapsed , 1 * time .Second ,
361+ "stop hook took %s against a corrupted settings file; want a fast short-circuit, not the transcript-flush-sentinel timeout path" , elapsed )
362+ }
363+
364+ // TestExecuteAgentHookCapturesWhenEnabledViaLocalSettingsOnly guards against a
365+ // regression in the #524 fix: `entire enable --local` writes only
366+ // .entire/settings.local.json and never creates the base .entire/settings.json
367+ // (see determineSettingsTarget in setup.go). The disabled-hook gate must
368+ // recognize that local-only enablement — gating on the base file alone
369+ // (settings.IsSetUp) would silently no-op every agent hook for that repo and
370+ // drop all checkpoint capture. Asserted via the same "session was claimed"
371+ // signal (the .agent hint StoreAgentTypeHint writes during SessionStart
372+ // dispatch) the short-circuit tests above assert the *absence* of.
373+ func TestExecuteAgentHookCapturesWhenEnabledViaLocalSettingsOnly (t * testing.T ) {
374+ setupStopTestRepo (t )
375+ repoRoot := mustGetwd (t )
376+
377+ entireDir := filepath .Join (repoRoot , ".entire" )
378+ require .NoError (t , os .MkdirAll (entireDir , 0o750 ))
379+ // Local-only enablement: settings.local.json present, base settings.json absent.
380+ require .NoError (t , os .WriteFile (filepath .Join (entireDir , "settings.local.json" ), []byte (`{"enabled":true}` ), 0o600 ))
381+ require .NoFileExists (t , filepath .Join (entireDir , "settings.json" ))
382+
383+ transcriptPath := filepath .Join (repoRoot , "transcript.jsonl" )
384+ require .NoError (t , os .WriteFile (transcriptPath , []byte (`{"type":"user","message":{"content":"hi"}}` + "\n " ), 0o600 ))
385+
386+ sessionID := "local-only-session-start"
387+ payload , err := json .Marshal (map [string ]string {
388+ "session_id" : sessionID ,
389+ "transcript_path" : transcriptPath ,
390+ })
391+ require .NoError (t , err )
392+
393+ cmd := & cobra.Command {}
394+ cmd .SetIn (bytes .NewReader (payload ))
395+ cmd .SetErr (& bytes.Buffer {})
396+ cmd .SetContext (context .Background ())
397+
398+ require .NoError (t , executeAgentHook (cmd , agent .AgentNameClaudeCode , claudecode .HookNameSessionStart , false ))
399+
400+ hintPath := filepath .Join (repoRoot , ".git" , session .SessionStateDirName , sessionID + ".agent" )
401+ require .FileExists (t , hintPath , "SessionStart must dispatch and claim the session when Entire is enabled via settings.local.json only" )
402+ }
403+
234404func TestAgentHookPolicyFailsWhenRepoCannotOpen (t * testing.T ) {
235405 _ , err := agentHookPolicy (context .Background (), filepath .Join (t .TempDir (), "missing" ))
236406
0 commit comments