diff --git a/internal/agent/agent.go b/internal/agent/agent.go index f7402a9f..b24003a5 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -775,7 +775,11 @@ func (a *Agent) reviewMode() string { } func reviewItemFingerprint(mode string, d model.Diff) string { - sum := sha256.Sum256([]byte(mode + "\x00" + d.OldPath + "\x00" + d.NewPath + "\x00" + d.Diff)) + // The patch splitter can leave extra line endings on the final file in a + // multi-file patch. Unified diff content lines always carry a marker, so + // trimming CR/LF here removes only that position-dependent delimiter. + diffText := strings.TrimRight(d.Diff, "\r\n") + sum := sha256.Sum256([]byte(mode + "\x00" + d.OldPath + "\x00" + d.NewPath + "\x00" + diffText)) return fmt.Sprintf("%x", sum) } diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index c0692693..70921dd1 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -559,6 +559,35 @@ func TestFilterLargeDiffs_ZeroMaxTokens(t *testing.T) { } } +func TestReviewItemFingerprintIgnoresTrailingLineEndings(t *testing.T) { + base := model.Diff{ + OldPath: "main.go", + NewPath: "main.go", + Diff: "@@ -1 +1 @@\n-old\n+new", + } + want := reviewItemFingerprint(session.ReviewModeRange, base) + + for name, suffix := range map[string]string{ + "lf": "\n", + "crlf": "\r\n", + "extra blank line": "\n\n", + } { + t.Run(name, func(t *testing.T) { + d := base + d.Diff += suffix + if got := reviewItemFingerprint(session.ReviewModeRange, d); got != want { + t.Errorf("fingerprint = %q, want %q", got, want) + } + }) + } + + withContextLine := base + withContextLine.Diff += "\n " + if got := reviewItemFingerprint(session.ReviewModeRange, withContextLine); got == want { + t.Error("fingerprint ignored a real trailing context line") + } +} + func TestApplyResumeReusesCompletedItemsAcrossModels(t *testing.T) { diffs := []model.Diff{ {OldPath: "a.go", NewPath: "a.go", Diff: "+a", Insertions: 1},