From 770caba3ff48f8fa974563f87b3bd26cd0e809ac Mon Sep 17 00:00:00 2001 From: Peyton Montei Date: Fri, 20 Mar 2026 17:39:08 -0700 Subject: [PATCH 1/5] perf: warn when stale ENDED sessions accumulate Add detection and rate-limited warning for non-FullyCondensed ENDED sessions that incur PostCommit processing cost on every commit. When >= 3 stale sessions are found, emit a stderr warning pointing users to 'entire clean --force'. Warning is rate-limited to once per 24h via a sentinel file in .git/entire-sessions/. Co-Authored-By: Claude Opus 4.6 (1M context) Entire-Checkpoint: 7841b7892888 --- .../cli/strategy/manual_commit_hooks.go | 40 ++++++++++ .../cli/strategy/manual_commit_session.go | 13 ++++ .../cli/strategy/phase_postcommit_test.go | 78 +++++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/cmd/entire/cli/strategy/manual_commit_hooks.go b/cmd/entire/cli/strategy/manual_commit_hooks.go index c90c3bd6e..01a08b1d3 100644 --- a/cmd/entire/cli/strategy/manual_commit_hooks.go +++ b/cmd/entire/cli/strategy/manual_commit_hooks.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "io" "log/slog" "os" "os/exec" @@ -730,6 +731,41 @@ func (h *postCommitActionHandler) shouldCondenseWithOverlapCheck(isActive bool, }) } +const ( + staleEndedSessionWarnThreshold = 3 // warn when ≥ this many stale sessions + staleEndedSessionWarnInterval = 24 * time.Hour // rate-limit window + staleEndedSessionWarnFile = ".warn-stale-ended" // sentinel file name in entire-sessions/ +) + +// warnStaleEndedSessions emits a rate-limited warning to stderr when too many +// non-FullyCondensed ENDED sessions are accumulating. +func warnStaleEndedSessions(ctx context.Context, count int) { + warnStaleEndedSessionsTo(ctx, count, os.Stderr) +} + +func warnStaleEndedSessionsTo(ctx context.Context, count int, w io.Writer) { + commonDir, err := GetGitCommonDir(ctx) + if err != nil { + return // fail-open + } + warnDir := filepath.Join(commonDir, session.SessionStateDirName) + warnFile := filepath.Join(warnDir, staleEndedSessionWarnFile) + if info, statErr := os.Stat(warnFile); statErr == nil { + if time.Since(info.ModTime()) < staleEndedSessionWarnInterval { + return // rate-limited + } + } + //nolint:errcheck,gosec // G104: Best-effort warning — fail-open if file ops fail + os.MkdirAll(warnDir, 0o755) + //nolint:errcheck,gosec // G104: Best-effort sentinel file write + os.WriteFile(warnFile, []byte{}, 0o644) + fmt.Fprintf(w, + "\nentire: %d ended session(s) are accumulating and slowing down commits.\n"+ + "Run 'entire clean --force' to remove them and restore commit performance.\n\n", + count, + ) +} + // activeSessionInteractionThreshold is the maximum age of LastInteractionTime // for an ACTIVE session to be considered genuinely active. 24h is generous // because LastInteractionTime only updates at TurnStart, not per-tool-call. @@ -809,6 +845,10 @@ func (s *ManualCommitStrategy) PostCommit(ctx context.Context) error { //nolint: findSessionsSpan.RecordError(err) findSessionsSpan.End() + if stale := countStaleEndedSessions(sessions); stale >= staleEndedSessionWarnThreshold { + warnStaleEndedSessions(ctx, stale) + } + if err != nil || len(sessions) == 0 { logging.Warn(logCtx, "post-commit: no active sessions despite trailer", slog.String("strategy", "manual-commit"), diff --git a/cmd/entire/cli/strategy/manual_commit_session.go b/cmd/entire/cli/strategy/manual_commit_session.go index 9658bc499..efc44b50e 100644 --- a/cmd/entire/cli/strategy/manual_commit_session.go +++ b/cmd/entire/cli/strategy/manual_commit_session.go @@ -9,6 +9,7 @@ import ( "github.com/entireio/cli/cmd/entire/cli/checkpoint" "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" "github.com/entireio/cli/cmd/entire/cli/paths" + "github.com/entireio/cli/cmd/entire/cli/session" "github.com/entireio/cli/cmd/entire/cli/versioninfo" "github.com/go-git/go-git/v6" @@ -101,6 +102,18 @@ func (s *ManualCommitStrategy) listAllSessionStates(ctx context.Context) ([]*Ses return states, nil } +// countStaleEndedSessions returns the number of ENDED sessions that have not +// been fully condensed. These sessions still incur PostCommit processing cost. +func countStaleEndedSessions(sessions []*SessionState) int { + n := 0 + for _, s := range sessions { + if s.Phase == session.PhaseEnded && !s.FullyCondensed { + n++ + } + } + return n +} + // findSessionsForWorktree finds all sessions for the given worktree path. func (s *ManualCommitStrategy) findSessionsForWorktree(ctx context.Context, worktreePath string) ([]*SessionState, error) { allStates, err := s.listAllSessionStates(ctx) diff --git a/cmd/entire/cli/strategy/phase_postcommit_test.go b/cmd/entire/cli/strategy/phase_postcommit_test.go index 9f1d0135c..f69608750 100644 --- a/cmd/entire/cli/strategy/phase_postcommit_test.go +++ b/cmd/entire/cli/strategy/phase_postcommit_test.go @@ -1,6 +1,7 @@ package strategy import ( + "bytes" "context" "os" "path/filepath" @@ -2390,3 +2391,80 @@ func TestPostCommit_ActiveSession_DifferentFilesThanCommit_ShouldCondense(t *tes require.NoError(t, err, "entire/checkpoints/v1 should exist — ACTIVE session with different files must still condense") } + +// TestCountStaleEndedSessions verifies detection of non-FullyCondensed ENDED sessions. +// Safe to parallelize — no CWD dependency. +func TestCountStaleEndedSessions(t *testing.T) { + t.Parallel() + cases := []struct { + name string + sessions []*SessionState + want int + }{ + { + name: "empty", + sessions: nil, + want: 0, + }, + { + name: "all active — no stale", + sessions: []*SessionState{ + {Phase: session.PhaseActive, FullyCondensed: false}, + {Phase: session.PhaseIdle, FullyCondensed: false}, + }, + want: 0, + }, + { + name: "ended but FullyCondensed — not stale", + sessions: []*SessionState{ + {Phase: session.PhaseEnded, FullyCondensed: true}, + }, + want: 0, + }, + { + name: "two stale ended sessions", + sessions: []*SessionState{ + {Phase: session.PhaseEnded, FullyCondensed: false}, + {Phase: session.PhaseEnded, FullyCondensed: false}, + {Phase: session.PhaseIdle, FullyCondensed: false}, + }, + want: 2, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := countStaleEndedSessions(tc.sessions) + assert.Equal(t, tc.want, got) + }) + } +} + +// TestWarnStaleEndedSessions_RateLimit verifies the 24h sentinel file gate. +// Uses t.Chdir — do NOT add t.Parallel(). +func TestWarnStaleEndedSessions_RateLimit(t *testing.T) { + dir := setupGitRepo(t) + t.Chdir(dir) + ctx := context.Background() + + // First call: no sentinel file → should write to stderr + var buf bytes.Buffer + warnStaleEndedSessionsTo(ctx, 5, &buf) + assert.Contains(t, buf.String(), "entire clean --force") + + // Sentinel file now exists with current mtime → second call suppressed + buf.Reset() + warnStaleEndedSessionsTo(ctx, 5, &buf) + assert.Empty(t, buf.String(), "second call within window must be suppressed") + + // Backdate sentinel file by 25h → call should warn again + commonDir, err := GetGitCommonDir(ctx) + require.NoError(t, err) + warnFile := filepath.Join(commonDir, session.SessionStateDirName, staleEndedSessionWarnFile) + past := time.Now().Add(-25 * time.Hour) + require.NoError(t, os.Chtimes(warnFile, past, past)) + + buf.Reset() + warnStaleEndedSessionsTo(ctx, 5, &buf) + assert.Contains(t, buf.String(), "entire clean --force") +} From f8c73c723e5f98284e5c5d8b96cf98e82d858483 Mon Sep 17 00:00:00 2001 From: Peyton Montei Date: Fri, 20 Mar 2026 17:54:25 -0700 Subject: [PATCH 2/5] fix: point stale session warning to 'entire doctor' entire doctor handles ENDED sessions with uncondensed checkpoint data (condenses them to entire/checkpoints/v1). entire clean only removes orphaned sessions with no shadow branch, which wouldn't help here. Co-Authored-By: Claude Opus 4.6 (1M context) Entire-Checkpoint: cf6c460dff65 --- cmd/entire/cli/strategy/manual_commit_hooks.go | 2 +- cmd/entire/cli/strategy/phase_postcommit_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/entire/cli/strategy/manual_commit_hooks.go b/cmd/entire/cli/strategy/manual_commit_hooks.go index 01a08b1d3..1e569bb93 100644 --- a/cmd/entire/cli/strategy/manual_commit_hooks.go +++ b/cmd/entire/cli/strategy/manual_commit_hooks.go @@ -761,7 +761,7 @@ func warnStaleEndedSessionsTo(ctx context.Context, count int, w io.Writer) { os.WriteFile(warnFile, []byte{}, 0o644) fmt.Fprintf(w, "\nentire: %d ended session(s) are accumulating and slowing down commits.\n"+ - "Run 'entire clean --force' to remove them and restore commit performance.\n\n", + "Run 'entire doctor' to condense them and restore commit performance.\n\n", count, ) } diff --git a/cmd/entire/cli/strategy/phase_postcommit_test.go b/cmd/entire/cli/strategy/phase_postcommit_test.go index f69608750..9092e1bae 100644 --- a/cmd/entire/cli/strategy/phase_postcommit_test.go +++ b/cmd/entire/cli/strategy/phase_postcommit_test.go @@ -2450,7 +2450,7 @@ func TestWarnStaleEndedSessions_RateLimit(t *testing.T) { // First call: no sentinel file → should write to stderr var buf bytes.Buffer warnStaleEndedSessionsTo(ctx, 5, &buf) - assert.Contains(t, buf.String(), "entire clean --force") + assert.Contains(t, buf.String(), "entire doctor") // Sentinel file now exists with current mtime → second call suppressed buf.Reset() @@ -2466,5 +2466,5 @@ func TestWarnStaleEndedSessions_RateLimit(t *testing.T) { buf.Reset() warnStaleEndedSessionsTo(ctx, 5, &buf) - assert.Contains(t, buf.String(), "entire clean --force") + assert.Contains(t, buf.String(), "entire doctor") } From 33761982193d75b46e84ec23f1d113be27085e92 Mon Sep 17 00:00:00 2001 From: Peyton Montei Date: Mon, 23 Mar 2026 17:48:06 -0700 Subject: [PATCH 3/5] align stale ended-session warning with doctor and post-commit cleanup --- .../cli/strategy/manual_commit_hooks.go | 8 +- .../cli/strategy/manual_commit_session.go | 23 ++- .../cli/strategy/phase_postcommit_test.go | 170 ++++++++++++++---- 3 files changed, 156 insertions(+), 45 deletions(-) diff --git a/cmd/entire/cli/strategy/manual_commit_hooks.go b/cmd/entire/cli/strategy/manual_commit_hooks.go index 1e569bb93..187523e89 100644 --- a/cmd/entire/cli/strategy/manual_commit_hooks.go +++ b/cmd/entire/cli/strategy/manual_commit_hooks.go @@ -845,10 +845,6 @@ func (s *ManualCommitStrategy) PostCommit(ctx context.Context) error { //nolint: findSessionsSpan.RecordError(err) findSessionsSpan.End() - if stale := countStaleEndedSessions(sessions); stale >= staleEndedSessionWarnThreshold { - warnStaleEndedSessions(ctx, stale) - } - if err != nil || len(sessions) == 0 { logging.Warn(logCtx, "post-commit: no active sessions despite trailer", slog.String("strategy", "manual-commit"), @@ -935,6 +931,10 @@ func (s *ManualCommitStrategy) PostCommit(ctx context.Context) error { //nolint: } cleanupBranchesSpan.End() + if stale := countWarnableStaleEndedSessions(repo, sessions); stale >= staleEndedSessionWarnThreshold { + warnStaleEndedSessions(ctx, stale) + } + return nil } diff --git a/cmd/entire/cli/strategy/manual_commit_session.go b/cmd/entire/cli/strategy/manual_commit_session.go index efc44b50e..d28df3a23 100644 --- a/cmd/entire/cli/strategy/manual_commit_session.go +++ b/cmd/entire/cli/strategy/manual_commit_session.go @@ -102,12 +102,25 @@ func (s *ManualCommitStrategy) listAllSessionStates(ctx context.Context) ([]*Ses return states, nil } -// countStaleEndedSessions returns the number of ENDED sessions that have not -// been fully condensed. These sessions still incur PostCommit processing cost. -func countStaleEndedSessions(sessions []*SessionState) int { +// isWarnableStaleEndedSession reports whether an ENDED session is still both +// expensive in PostCommit and actionable via 'entire doctor'. +func isWarnableStaleEndedSession(repo *git.Repository, state *SessionState) bool { + if state.Phase != session.PhaseEnded || state.FullyCondensed || state.StepCount <= 0 { + return false + } + + shadowBranch := getShadowBranchNameForCommit(state.BaseCommit, state.WorktreeID) + refName := plumbing.NewBranchReferenceName(shadowBranch) + _, err := repo.Reference(refName, true) + return err == nil +} + +// countWarnableStaleEndedSessions returns the number of ENDED sessions that +// still remain slow and fixable after PostCommit finishes processing. +func countWarnableStaleEndedSessions(repo *git.Repository, sessions []*SessionState) int { n := 0 - for _, s := range sessions { - if s.Phase == session.PhaseEnded && !s.FullyCondensed { + for _, state := range sessions { + if isWarnableStaleEndedSession(repo, state) { n++ } } diff --git a/cmd/entire/cli/strategy/phase_postcommit_test.go b/cmd/entire/cli/strategy/phase_postcommit_test.go index 9092e1bae..1c4479f28 100644 --- a/cmd/entire/cli/strategy/phase_postcommit_test.go +++ b/cmd/entire/cli/strategy/phase_postcommit_test.go @@ -3,6 +3,7 @@ package strategy import ( "bytes" "context" + "io" "os" "path/filepath" "testing" @@ -1410,6 +1411,41 @@ func setupSessionWithCheckpoint(t *testing.T, s *ManualCommitStrategy, _ *git.Re require.NoError(t, err, "SaveStep should succeed to create shadow branch content") } +// setupSessionWithCheckpointAndFile initializes a session with a checkpoint for +// a caller-provided new file. This lets tests create multiple independent +// sessions that all overlap with the same commit. +func setupSessionWithCheckpointAndFile(t *testing.T, s *ManualCommitStrategy, dir, sessionID, fileName string) { + t.Helper() + + filePath := filepath.Join(dir, fileName) + fileContent := "agent content for " + fileName + require.NoError(t, os.WriteFile(filePath, []byte(fileContent), 0o644)) + + metadataDir := ".entire/metadata/" + sessionID + metadataDirAbs := filepath.Join(dir, metadataDir) + require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755)) + + transcript := `{"type":"human","message":{"content":"test prompt"}} +{"type":"assistant","message":{"content":"test response"}} +` + require.NoError(t, os.WriteFile( + filepath.Join(metadataDirAbs, paths.TranscriptFileName), + []byte(transcript), 0o644)) + + err := s.SaveStep(context.Background(), StepContext{ + SessionID: sessionID, + ModifiedFiles: []string{}, + NewFiles: []string{fileName}, + DeletedFiles: []string{}, + MetadataDir: metadataDir, + MetadataDirAbs: metadataDirAbs, + CommitMessage: "Checkpoint 1", + AuthorName: "Test", + AuthorEmail: "test@test.com", + }) + require.NoError(t, err, "SaveStep should succeed to create shadow branch content") +} + // shadowTranscriptSize returns the byte size of the transcript blob on the shadow branch. // Used in tests to set CheckpointTranscriptSize without hardcoding sizes. func shadowTranscriptSize(t *testing.T, repo *git.Repository, state *SessionState) int64 { @@ -2392,52 +2428,114 @@ func TestPostCommit_ActiveSession_DifferentFilesThanCommit_ShouldCondense(t *tes "entire/checkpoints/v1 should exist — ACTIVE session with different files must still condense") } -// TestCountStaleEndedSessions verifies detection of non-FullyCondensed ENDED sessions. -// Safe to parallelize — no CWD dependency. -func TestCountStaleEndedSessions(t *testing.T) { - t.Parallel() - cases := []struct { - name string - sessions []*SessionState - want int - }{ +// TestCountWarnableStaleEndedSessions verifies that the warning only counts the +// same ENDED sessions that 'entire doctor' can actually condense. +func TestCountWarnableStaleEndedSessions(t *testing.T) { + dir := setupGitRepo(t) + t.Chdir(dir) + + repo, err := git.PlainOpen(dir) + require.NoError(t, err) + + s := &ManualCommitStrategy{} + setupSessionWithCheckpoint(t, s, repo, dir, "warnable-session") + + warnableState, err := s.loadSessionState(context.Background(), "warnable-session") + require.NoError(t, err) + warnableState.Phase = session.PhaseEnded + warnableState.FullyCondensed = false + require.NoError(t, s.saveSessionState(context.Background(), warnableState)) + + sessions := []*SessionState{ + warnableState, { - name: "empty", - sessions: nil, - want: 0, + SessionID: "no-shadow-branch", + BaseCommit: "1234567890abcdef1234567890abcdef12345678", + WorktreeID: warnableState.WorktreeID, + Phase: session.PhaseEnded, + FullyCondensed: false, + StepCount: 3, }, { - name: "all active — no stale", - sessions: []*SessionState{ - {Phase: session.PhaseActive, FullyCondensed: false}, - {Phase: session.PhaseIdle, FullyCondensed: false}, - }, - want: 0, + SessionID: "zero-steps", + BaseCommit: warnableState.BaseCommit, + WorktreeID: warnableState.WorktreeID, + Phase: session.PhaseEnded, + FullyCondensed: false, + StepCount: 0, }, { - name: "ended but FullyCondensed — not stale", - sessions: []*SessionState{ - {Phase: session.PhaseEnded, FullyCondensed: true}, - }, - want: 0, + SessionID: "fully-condensed", + BaseCommit: warnableState.BaseCommit, + WorktreeID: warnableState.WorktreeID, + Phase: session.PhaseEnded, + FullyCondensed: true, + StepCount: 3, }, { - name: "two stale ended sessions", - sessions: []*SessionState{ - {Phase: session.PhaseEnded, FullyCondensed: false}, - {Phase: session.PhaseEnded, FullyCondensed: false}, - {Phase: session.PhaseIdle, FullyCondensed: false}, - }, - want: 2, + SessionID: "idle-session", + BaseCommit: warnableState.BaseCommit, + WorktreeID: warnableState.WorktreeID, + Phase: session.PhaseIdle, + FullyCondensed: false, + StepCount: 3, }, } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - got := countStaleEndedSessions(tc.sessions) - assert.Equal(t, tc.want, got) - }) + + assert.Equal(t, 1, countWarnableStaleEndedSessions(repo, sessions)) +} + +// TestPostCommit_WarnStaleEndedSessions_AfterProcessing verifies that the +// warning is emitted only for sessions that remain stale AFTER the current +// commit is processed. +func TestPostCommit_WarnStaleEndedSessions_AfterProcessing(t *testing.T) { + dir := setupGitRepo(t) + t.Chdir(dir) + + repo, err := git.PlainOpen(dir) + require.NoError(t, err) + + s := &ManualCommitStrategy{} + sessionFiles := map[string]string{ + "ended-a": "stale-a.txt", + "ended-b": "stale-b.txt", + "ended-c": "stale-c.txt", + } + + filesToCommit := make([]string, 0, len(sessionFiles)) + for sessionID, fileName := range sessionFiles { + setupSessionWithCheckpointAndFile(t, s, dir, sessionID, fileName) + + state, loadErr := s.loadSessionState(context.Background(), sessionID) + require.NoError(t, loadErr) + now := time.Now() + state.Phase = session.PhaseEnded + state.EndedAt = &now + state.FilesTouched = []string{fileName} + require.NoError(t, s.saveSessionState(context.Background(), state)) + + filesToCommit = append(filesToCommit, fileName) } + + commitFilesWithTrailer(t, repo, dir, "abc123def456", filesToCommit...) + + oldStderr := os.Stderr + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stderr = w + defer func() { + os.Stderr = oldStderr + }() + + err = s.PostCommit(context.Background()) + require.NoError(t, err) + + require.NoError(t, w.Close()) + stderr, err := io.ReadAll(r) + require.NoError(t, err) + + assert.NotContains(t, string(stderr), "entire doctor", + "warning should be suppressed when this commit already condensed the stale ended sessions") } // TestWarnStaleEndedSessions_RateLimit verifies the 24h sentinel file gate. From 61322ccd4e489443866a8260b01de7ae0067f9d5 Mon Sep 17 00:00:00 2001 From: Peyton Montei Date: Mon, 23 Mar 2026 18:32:29 -0700 Subject: [PATCH 4/5] fix: address code review findings for stale session warning - Replace os.Stderr mutation with injectable stderrWriter package var - Add comment explaining intentional shadow branch re-check in isWarnableStaleEndedSession (PostCommit deletes branches mid-execution) - Replace map with slice in test for deterministic iteration order - Extract testTranscript constant to fix goconst lint - Add t.Chdir warning comments to non-parallel tests Co-Authored-By: Claude Opus 4.6 (1M context) Entire-Checkpoint: d924cd6bb74b --- .../cli/strategy/manual_commit_hooks.go | 7 ++- .../cli/strategy/manual_commit_session.go | 5 ++ cmd/entire/cli/strategy/manual_commit_test.go | 5 +- .../cli/strategy/phase_postcommit_test.go | 54 +++++++++---------- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/cmd/entire/cli/strategy/manual_commit_hooks.go b/cmd/entire/cli/strategy/manual_commit_hooks.go index 187523e89..9ab4de815 100644 --- a/cmd/entire/cli/strategy/manual_commit_hooks.go +++ b/cmd/entire/cli/strategy/manual_commit_hooks.go @@ -737,10 +737,15 @@ const ( staleEndedSessionWarnFile = ".warn-stale-ended" // sentinel file name in entire-sessions/ ) +// stderrWriter is the destination for user-facing warnings emitted outside of +// Cobra command output (e.g. from PostCommit). Tests can swap this to capture +// output without mutating the process-global os.Stderr. +var stderrWriter io.Writer = os.Stderr + // warnStaleEndedSessions emits a rate-limited warning to stderr when too many // non-FullyCondensed ENDED sessions are accumulating. func warnStaleEndedSessions(ctx context.Context, count int) { - warnStaleEndedSessionsTo(ctx, count, os.Stderr) + warnStaleEndedSessionsTo(ctx, count, stderrWriter) } func warnStaleEndedSessionsTo(ctx context.Context, count int, w io.Writer) { diff --git a/cmd/entire/cli/strategy/manual_commit_session.go b/cmd/entire/cli/strategy/manual_commit_session.go index d28df3a23..ad4d852fd 100644 --- a/cmd/entire/cli/strategy/manual_commit_session.go +++ b/cmd/entire/cli/strategy/manual_commit_session.go @@ -109,6 +109,11 @@ func isWarnableStaleEndedSession(repo *git.Repository, state *SessionState) bool return false } + // Re-check shadow branch existence even though listAllSessionStates already + // filters orphaned sessions. This is intentional: PostCommit deletes shadow + // branches during condensation, so a branch that existed at list-load time + // may be gone by the time we reach the warning check. Without this re-check + // we would warn about sessions that this commit just cleaned up. shadowBranch := getShadowBranchNameForCommit(state.BaseCommit, state.WorktreeID) refName := plumbing.NewBranchReferenceName(shadowBranch) _, err := repo.Reference(refName, true) diff --git a/cmd/entire/cli/strategy/manual_commit_test.go b/cmd/entire/cli/strategy/manual_commit_test.go index 4b4e9294e..e1587beb3 100644 --- a/cmd/entire/cli/strategy/manual_commit_test.go +++ b/cmd/entire/cli/strategy/manual_commit_test.go @@ -1501,10 +1501,7 @@ func TestShadowStrategy_CondenseSession_EphemeralBranchTrailer(t *testing.T) { t.Fatalf("failed to create metadata dir: %v", err) } - transcript := `{"type":"human","message":{"content":"test prompt"}} -{"type":"assistant","message":{"content":"test response"}} -` - if err := os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName), []byte(transcript), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName), []byte(testTranscript), 0o644); err != nil { t.Fatalf("failed to write transcript: %v", err) } diff --git a/cmd/entire/cli/strategy/phase_postcommit_test.go b/cmd/entire/cli/strategy/phase_postcommit_test.go index 1c4479f28..ef96fcb31 100644 --- a/cmd/entire/cli/strategy/phase_postcommit_test.go +++ b/cmd/entire/cli/strategy/phase_postcommit_test.go @@ -3,7 +3,6 @@ package strategy import ( "bytes" "context" - "io" "os" "path/filepath" "testing" @@ -1388,12 +1387,9 @@ func setupSessionWithCheckpoint(t *testing.T, s *ManualCommitStrategy, _ *git.Re metadataDirAbs := filepath.Join(dir, metadataDir) require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755)) - transcript := `{"type":"human","message":{"content":"test prompt"}} -{"type":"assistant","message":{"content":"test response"}} -` require.NoError(t, os.WriteFile( filepath.Join(metadataDirAbs, paths.TranscriptFileName), - []byte(transcript), 0o644)) + []byte(testTranscript), 0o644)) // SaveStep creates the shadow branch and checkpoint // Include test.txt as a modified file so it's saved to the shadow branch @@ -1425,12 +1421,9 @@ func setupSessionWithCheckpointAndFile(t *testing.T, s *ManualCommitStrategy, di metadataDirAbs := filepath.Join(dir, metadataDir) require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755)) - transcript := `{"type":"human","message":{"content":"test prompt"}} -{"type":"assistant","message":{"content":"test response"}} -` require.NoError(t, os.WriteFile( filepath.Join(metadataDirAbs, paths.TranscriptFileName), - []byte(transcript), 0o644)) + []byte(testTranscript), 0o644)) err := s.SaveStep(context.Background(), StepContext{ SessionID: sessionID, @@ -2430,6 +2423,7 @@ func TestPostCommit_ActiveSession_DifferentFilesThanCommit_ShouldCondense(t *tes // TestCountWarnableStaleEndedSessions verifies that the warning only counts the // same ENDED sessions that 'entire doctor' can actually condense. +// Uses t.Chdir — do NOT add t.Parallel(). func TestCountWarnableStaleEndedSessions(t *testing.T) { dir := setupGitRepo(t) t.Chdir(dir) @@ -2488,6 +2482,7 @@ func TestCountWarnableStaleEndedSessions(t *testing.T) { // TestPostCommit_WarnStaleEndedSessions_AfterProcessing verifies that the // warning is emitted only for sessions that remain stale AFTER the current // commit is processed. +// Uses t.Chdir — do NOT add t.Parallel(). func TestPostCommit_WarnStaleEndedSessions_AfterProcessing(t *testing.T) { dir := setupGitRepo(t) t.Chdir(dir) @@ -2496,45 +2491,44 @@ func TestPostCommit_WarnStaleEndedSessions_AfterProcessing(t *testing.T) { require.NoError(t, err) s := &ManualCommitStrategy{} - sessionFiles := map[string]string{ - "ended-a": "stale-a.txt", - "ended-b": "stale-b.txt", - "ended-c": "stale-c.txt", + type sessionFile struct { + sessionID string + fileName string + } + sessionFiles := []sessionFile{ + {"ended-a", "stale-a.txt"}, + {"ended-b", "stale-b.txt"}, + {"ended-c", "stale-c.txt"}, } filesToCommit := make([]string, 0, len(sessionFiles)) - for sessionID, fileName := range sessionFiles { - setupSessionWithCheckpointAndFile(t, s, dir, sessionID, fileName) + for _, sf := range sessionFiles { + setupSessionWithCheckpointAndFile(t, s, dir, sf.sessionID, sf.fileName) - state, loadErr := s.loadSessionState(context.Background(), sessionID) + state, loadErr := s.loadSessionState(context.Background(), sf.sessionID) require.NoError(t, loadErr) now := time.Now() state.Phase = session.PhaseEnded state.EndedAt = &now - state.FilesTouched = []string{fileName} + state.FilesTouched = []string{sf.fileName} require.NoError(t, s.saveSessionState(context.Background(), state)) - filesToCommit = append(filesToCommit, fileName) + filesToCommit = append(filesToCommit, sf.fileName) } commitFilesWithTrailer(t, repo, dir, "abc123def456", filesToCommit...) - oldStderr := os.Stderr - r, w, err := os.Pipe() - require.NoError(t, err) - os.Stderr = w - defer func() { - os.Stderr = oldStderr - }() + // Capture warning output via the injectable stderrWriter instead of + // mutating the process-global os.Stderr. + var buf bytes.Buffer + oldWriter := stderrWriter + stderrWriter = &buf + defer func() { stderrWriter = oldWriter }() err = s.PostCommit(context.Background()) require.NoError(t, err) - require.NoError(t, w.Close()) - stderr, err := io.ReadAll(r) - require.NoError(t, err) - - assert.NotContains(t, string(stderr), "entire doctor", + assert.NotContains(t, buf.String(), "entire doctor", "warning should be suppressed when this commit already condensed the stale ended sessions") } From e52ce3f8903c213b2894d540d3a4881efc7f9c9f Mon Sep 17 00:00:00 2001 From: peyton-alt Date: Mon, 23 Mar 2026 18:53:50 -0700 Subject: [PATCH 5/5] Update cmd/entire/cli/strategy/manual_commit_hooks.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/entire/cli/strategy/manual_commit_hooks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/entire/cli/strategy/manual_commit_hooks.go b/cmd/entire/cli/strategy/manual_commit_hooks.go index 9ab4de815..440add114 100644 --- a/cmd/entire/cli/strategy/manual_commit_hooks.go +++ b/cmd/entire/cli/strategy/manual_commit_hooks.go @@ -761,7 +761,7 @@ func warnStaleEndedSessionsTo(ctx context.Context, count int, w io.Writer) { } } //nolint:errcheck,gosec // G104: Best-effort warning — fail-open if file ops fail - os.MkdirAll(warnDir, 0o755) + os.MkdirAll(warnDir, 0o750) //nolint:errcheck,gosec // G104: Best-effort sentinel file write os.WriteFile(warnFile, []byte{}, 0o644) fmt.Fprintf(w,