From 48e08032a23b5025ce3ab9aef1607e0879ee246d Mon Sep 17 00:00:00 2001 From: cat Date: Tue, 21 Jul 2026 18:07:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(engine):=20=E9=98=B2=E6=AD=A2=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=A4=8D=E6=B4=BB=E6=97=A7=20backfill=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/engine/backfill_test.go | 84 ++++++++++++++++++++++++++++++++ internal/engine/merge.go | 9 +++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/internal/engine/backfill_test.go b/internal/engine/backfill_test.go index 94dd7ea8..fade92f4 100644 --- a/internal/engine/backfill_test.go +++ b/internal/engine/backfill_test.go @@ -224,3 +224,87 @@ func TestBackfill_PinIdempotentPastLookback(t *testing.T) { } } } + +func TestBackfill_MergedIntentDoesNotReviveRewrittenBackfillCommit(t *testing.T) { + dir, cleanup := testRepo(t) + defer cleanup() + svc := NewServiceFromRoot(dir) + if _, err := svc.Init("agent"); err != nil { + t.Fatalf("init: %v", err) + } + gitCmd(t, dir, "checkout", "main") + + initial := svc.Git.ReadRef("refs/heads/main") + writeFile(t, dir, "rewritten.go", "package main\n") + gitCmd(t, dir, "add", "rewritten.go") + gitCmd(t, dir, "commit", "-m", "old rewritten backfill target") + oldCommit := svc.Git.ReadRef("refs/heads/main") + gitCmd(t, dir, "branch", "old-main", oldCommit) + + gitCmd(t, dir, "reset", "--hard", initial) + writeFile(t, dir, "rewritten.go", "package main\n") + gitCmd(t, dir, "add", "rewritten.go") + gitCmd(t, dir, "commit", "-m", "new rewritten backfill target") + newCommit := svc.Git.ReadRef("refs/heads/main") + if newCommit == oldCommit { + t.Fatal("test setup should create a rewritten commit") + } + + intentID := "int_rewritten_backfill" + note := domain.CommitNote{ + SchemaVersion: 1, + Kind: "mainline.commit_note", + Intents: []domain.IntentReference{ + {IntentID: intentID, SealResultHash: "sha256:test"}, + }, + AddedAt: "2026-06-22T00:00:00Z", + AddedBy: "actor_test", + Via: "pin_auto", + MatchStrategy: "tree_hash", + } + if err := upsertCommitNote(svc.Git, newCommit, note); err != nil { + t.Fatalf("write new merge note: %v", err) + } + + view := &domain.MainlineView{ + SchemaVersion: 1, + MainBranch: "main", + Intents: []domain.IntentView{ + { + IntentID: intentID, + SchemaVersion: 1, + Status: domain.StatusMerged, + Goal: "rewritten backfill intent", + CodeCommit: oldCommit, + BackfillCommits: []string{oldCommit}, + StatusEvidence: domain.StatusEvidence{ + MergedMainCommit: newCommit, + MergedVia: "pin_backfill", + EvidenceComplete: true, + }, + }, + }, + } + if err := svc.Store.WriteMainlineView(view); err != nil { + t.Fatalf("write view: %v", err) + } + if raw, _ := svc.Git.NotesShow(oldCommit); raw != "" { + t.Fatalf("old commit should start without a note, got %q", raw) + } + + pinResult, err := svc.Pin() + if err != nil { + t.Fatalf("pin: %v", err) + } + for _, link := range pinResult.Links { + if link.IntentID == intentID && link.Commit == oldCommit { + t.Fatalf("pin should not revive rewritten backfill commit, got %+v", pinResult.Links) + } + } + if raw, _ := svc.Git.NotesShow(oldCommit); raw != "" { + t.Fatalf("old commit note should stay absent, got %q", raw) + } + if raw, _ := svc.Git.NotesShow(newCommit); raw == "" { + t.Fatal("new merged commit note should stay present") + } +} diff --git a/internal/engine/merge.go b/internal/engine/merge.go index cabbc951..06f42b12 100644 --- a/internal/engine/merge.go +++ b/internal/engine/merge.go @@ -288,7 +288,14 @@ func (s *Service) Pin() (*PinResult, error) { continue } - if len(iv.BackfillCommits) > 0 { + useBackfill := len(iv.BackfillCommits) > 0 + if useBackfill && iv.Status == domain.StatusMerged && iv.StatusEvidence.MergedMainCommit != "" { + // Notes recovery may move merge evidence to a rewritten main commit + // while the immutable seal event still names old backfill commits. + // Do not resurrect notes on those stale commits. + useBackfill = false + } + if useBackfill { pinnedAny := false for _, target := range iv.BackfillCommits { resolved := s.resolvePinCommit(target)