|
| 1 | +package strategy |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "context" |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 8 | + "testing" |
| 9 | + |
| 10 | + "github.com/entireio/cli/cmd/entire/cli/agent" |
| 11 | + |
| 12 | + "github.com/stretchr/testify/assert" |
| 13 | + "github.com/stretchr/testify/require" |
| 14 | +) |
| 15 | + |
| 16 | +// Issue #1965: when a coding agent commits in an Entire-enabled repo but no |
| 17 | +// session anywhere in the repository has recorded recent activity (e.g. the |
| 18 | +// agent was launched from a bare-clone layout's shared root, outside any |
| 19 | +// worktree, so its hooks never fired), PrepareCommitMsg used to only log a |
| 20 | +// debug line invisible to a normal user before silently skipping the commit. |
| 21 | +// These tests exercise the real hook handler (not a hand-mocked call) end to |
| 22 | +// end and assert a visible stderr warning now appears. |
| 23 | + |
| 24 | +// writeEnabledSettings writes a minimal .entire/settings.json with |
| 25 | +// "enabled": true, matching what `entire enable` produces and what the |
| 26 | +// PersistentPreRun gate in hooks_git_cmd.go requires before any git-hook |
| 27 | +// command reaches the strategy layer in a real invocation. |
| 28 | +func writeEnabledSettings(t *testing.T, dir string) { |
| 29 | + t.Helper() |
| 30 | + entireDir := filepath.Join(dir, ".entire") |
| 31 | + require.NoError(t, os.MkdirAll(entireDir, 0o755)) |
| 32 | + require.NoError(t, os.WriteFile( |
| 33 | + filepath.Join(entireDir, "settings.json"), |
| 34 | + []byte(`{"enabled":true}`), |
| 35 | + 0o644, |
| 36 | + )) |
| 37 | +} |
| 38 | + |
| 39 | +// TestPrepareCommitMsg_AgentCommitNoSession_WarnsVisibly is the core |
| 40 | +// regression test for #1965. Setup: a real temp repo (testutil.InitRepo via |
| 41 | +// setupGitRepo), Entire enabled, CLAUDECODE=1 (the real env marker Claude |
| 42 | +// Code sets on every command it runs), and deliberately NO session state |
| 43 | +// anywhere (.git/entire-sessions/ is never populated - no InitializeSession |
| 44 | +// call). PrepareCommitMsg is invoked directly - the actual function a real |
| 45 | +// prepare-commit-msg git hook invocation reaches - with a real commit message |
| 46 | +// file, and stderr is captured through the package's real injectable |
| 47 | +// stderrWriter (the same seam warnStaleEndedSessions already uses). |
| 48 | +// |
| 49 | +// On the fixed code this must produce a visible warning. Run against the |
| 50 | +// pre-fix code (git stash the strategy fix), the captured buffer is empty: |
| 51 | +// the only trace was the debug-level "prepare-commit-msg: no active |
| 52 | +// sessions" log line, invisible to a user reading their terminal. |
| 53 | +func TestPrepareCommitMsg_AgentCommitNoSession_WarnsVisibly(t *testing.T) { |
| 54 | + dir := setupGitRepo(t) |
| 55 | + t.Chdir(dir) |
| 56 | + writeEnabledSettings(t, dir) |
| 57 | + t.Setenv("CLAUDECODE", "1") |
| 58 | + |
| 59 | + s := &ManualCommitStrategy{} |
| 60 | + |
| 61 | + commitMsgFile := filepath.Join(t.TempDir(), "COMMIT_EDITMSG") |
| 62 | + require.NoError(t, os.WriteFile(commitMsgFile, []byte("fix: something\n"), 0o644)) |
| 63 | + |
| 64 | + var buf bytes.Buffer |
| 65 | + oldWriter := stderrWriter |
| 66 | + stderrWriter = &buf |
| 67 | + defer func() { stderrWriter = oldWriter }() |
| 68 | + |
| 69 | + err := s.PrepareCommitMsg(context.Background(), commitMsgFile, "") |
| 70 | + require.NoError(t, err) |
| 71 | + |
| 72 | + t.Logf("captured stderr: %q", buf.String()) |
| 73 | + |
| 74 | + assert.Contains(t, buf.String(), "no active Entire session", |
| 75 | + "a coding-agent commit with no session recorded anywhere in the repo must print a visible stderr warning (issue #1965)") |
| 76 | + assert.Contains(t, buf.String(), "entire doctor", |
| 77 | + "the warning should point the user at `entire doctor`") |
| 78 | +} |
| 79 | + |
| 80 | +// TestPrepareCommitMsg_HumanCommitNoSession_NoWarn pins the other half of the |
| 81 | +// enabled-but-no-session vs no-agent-at-all distinction: with Entire enabled, |
| 82 | +// no session anywhere, but NO agent env marker set (an ordinary human running |
| 83 | +// `git commit` by hand), the warning must stay silent. This is |
| 84 | +// indistinguishable from "no agent running at all" and must never nag a |
| 85 | +// human's own commit. |
| 86 | +func TestPrepareCommitMsg_HumanCommitNoSession_NoWarn(t *testing.T) { |
| 87 | + dir := setupGitRepo(t) |
| 88 | + t.Chdir(dir) |
| 89 | + writeEnabledSettings(t, dir) |
| 90 | + // Force-clear every known agent env marker: this test asserts the |
| 91 | + // no-agent-at-all case, and the test process itself may be running |
| 92 | + // inside an actual agent session (e.g. this very fix was developed under |
| 93 | + // Claude Code, which sets CLAUDECODE=1 in its own subprocess env) - that |
| 94 | + // ambient marker must not leak into "no agent" test coverage. |
| 95 | + t.Setenv("CLAUDECODE", "") |
| 96 | + t.Setenv("GEMINI_CLI", "") |
| 97 | + t.Setenv("COPILOT_CLI", "") |
| 98 | + t.Setenv("PI_CODING_AGENT", "") |
| 99 | + |
| 100 | + s := &ManualCommitStrategy{} |
| 101 | + |
| 102 | + commitMsgFile := filepath.Join(t.TempDir(), "COMMIT_EDITMSG") |
| 103 | + require.NoError(t, os.WriteFile(commitMsgFile, []byte("fix: something\n"), 0o644)) |
| 104 | + |
| 105 | + var buf bytes.Buffer |
| 106 | + oldWriter := stderrWriter |
| 107 | + stderrWriter = &buf |
| 108 | + defer func() { stderrWriter = oldWriter }() |
| 109 | + |
| 110 | + err := s.PrepareCommitMsg(context.Background(), commitMsgFile, "") |
| 111 | + require.NoError(t, err) |
| 112 | + |
| 113 | + t.Logf("captured stderr: %q", buf.String()) |
| 114 | + |
| 115 | + assert.Empty(t, buf.String(), |
| 116 | + "a human commit with no agent env marker must never warn, even with no session recorded") |
| 117 | +} |
| 118 | + |
| 119 | +// TestPrepareCommitMsg_AgentCommitWithSession_NoWarn is the mandatory |
| 120 | +// regression guard: a real session with recent activity in THIS worktree |
| 121 | +// (initialized via the real InitializeSession turn-start path, not a |
| 122 | +// hand-built state) must suppress the warning entirely - PrepareCommitMsg |
| 123 | +// takes the "sessions found" branch and never reaches the no-session warning |
| 124 | +// at all. |
| 125 | +func TestPrepareCommitMsg_AgentCommitWithSession_NoWarn(t *testing.T) { |
| 126 | + dir := setupGitRepo(t) |
| 127 | + t.Chdir(dir) |
| 128 | + writeEnabledSettings(t, dir) |
| 129 | + t.Setenv("CLAUDECODE", "1") |
| 130 | + |
| 131 | + s := &ManualCommitStrategy{} |
| 132 | + |
| 133 | + sessionID := "test-session-1965-present" |
| 134 | + require.NoError(t, s.InitializeSession(context.Background(), sessionID, agent.AgentTypeClaudeCode, "", "working on a fix", "")) |
| 135 | + |
| 136 | + state, err := s.loadSessionState(context.Background(), sessionID) |
| 137 | + require.NoError(t, err) |
| 138 | + require.NotNil(t, state.LastInteractionTime, "InitializeSession's turn-start transition should stamp LastInteractionTime") |
| 139 | + |
| 140 | + commitMsgFile := filepath.Join(t.TempDir(), "COMMIT_EDITMSG") |
| 141 | + require.NoError(t, os.WriteFile(commitMsgFile, []byte("fix: something\n"), 0o644)) |
| 142 | + |
| 143 | + var buf bytes.Buffer |
| 144 | + oldWriter := stderrWriter |
| 145 | + stderrWriter = &buf |
| 146 | + defer func() { stderrWriter = oldWriter }() |
| 147 | + |
| 148 | + err = s.PrepareCommitMsg(context.Background(), commitMsgFile, "") |
| 149 | + require.NoError(t, err) |
| 150 | + |
| 151 | + t.Logf("captured stderr: %q", buf.String()) |
| 152 | + |
| 153 | + assert.Empty(t, buf.String(), |
| 154 | + "an agent commit with a recently-active session present must not warn") |
| 155 | +} |
| 156 | + |
| 157 | +// TestWarnIfAgentCommitHasNoSession_RateLimit pins the sentinel-file rate |
| 158 | +// limit, mirroring TestWarnStaleEndedSessions_RateLimit's pattern. |
| 159 | +func TestWarnIfAgentCommitHasNoSession_RateLimit(t *testing.T) { |
| 160 | + dir := setupGitRepo(t) |
| 161 | + t.Chdir(dir) |
| 162 | + t.Setenv("CLAUDECODE", "1") |
| 163 | + ctx := context.Background() |
| 164 | + |
| 165 | + s := &ManualCommitStrategy{} |
| 166 | + |
| 167 | + var buf bytes.Buffer |
| 168 | + s.warnIfAgentCommitHasNoSessionTo(ctx, &buf) |
| 169 | + assert.Contains(t, buf.String(), "no active Entire session") |
| 170 | + |
| 171 | + buf.Reset() |
| 172 | + s.warnIfAgentCommitHasNoSessionTo(ctx, &buf) |
| 173 | + assert.Empty(t, buf.String(), "second call within window must be suppressed") |
| 174 | +} |
0 commit comments