Files
Agent-System/internal/docrevision/commit_idempotent_test.go
weichenandCursor 9a9ffa48cc fix(docrevision): allow idempotent commit retry after ledger commit
When a candidate is already committed, matching expected hashes return the
current revision state so projection and MarkProposalApplied can finish;
reject on an already-committed candidate stays an error.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 10:09:18 +00:00

75 lines
2.4 KiB
Go

package docrevision_test
import (
"context"
"testing"
"chat-agent-system/internal/docrevision"
)
func TestCommitAgentVersion_IdempotentWhenAlreadyCommitted(t *testing.T) {
ctx := context.Background()
svc := docrevision.NewService(nil)
docID := "doc_commit_idem"
base := "# base\n\n法规 GB9706\n"
st, err := svc.OpenTemplateVersion(ctx, docID, base, docrevision.ActorSnapshot{DisplayName: "t"}, 1, "")
if err != nil {
t.Fatal(err)
}
cand, _, err := svc.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
DocID: docID, RunID: "idem-1", InputSnapshotID: st.HeadSnapshotID,
OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad,
})
if err != nil && cand == nil {
t.Fatal(err)
}
if cand.Status != docrevision.CandidateNeedsConfirmation {
t.Fatalf("want needs_confirmation got %s", cand.Status)
}
st1, err := svc.CommitAgentVersion(ctx, docID, cand.ID, docrevision.CommitAgentDecision{
Decision: "commit", Confirmed: true, ActorUserID: 1,
ExpectedInputSHA: st.HeadSHA256,
})
if err != nil {
t.Fatalf("first commit: %v", err)
}
got, err := svc.GetCandidate(ctx, docID, cand.ID)
if err != nil || got.Status != docrevision.CandidateCommitted {
t.Fatalf("want committed got %+v err=%v", got, err)
}
// Retry with same hashes must succeed (projection/governance compensation path).
st2, err := svc.CommitAgentVersion(ctx, docID, cand.ID, docrevision.CommitAgentDecision{
Decision: "commit", Confirmed: true, ActorUserID: 1,
ExpectedInputSHA: st.HeadSHA256,
})
if err != nil {
t.Fatalf("idempotent recommit: %v", err)
}
if st2 == nil || st2.HeadSHA256 == "" || st2.HeadSHA256 != st1.HeadSHA256 {
t.Fatalf("head mismatch after idempotent commit: %#v vs %#v", st2, st1)
}
// Wrong expected output hash must not silently succeed.
outSHA := ""
if got.OutputSnapshotID != nil {
// CommitAgentVersion validates ExpectedOutputSHA against output snapshot.
// Use a bogus hash.
outSHA = "deadbeef"
}
_, err = svc.CommitAgentVersion(ctx, docID, cand.ID, docrevision.CommitAgentDecision{
Decision: "commit", Confirmed: true, ActorUserID: 1,
ExpectedInputSHA: st.HeadSHA256, ExpectedOutputSHA: outSHA,
})
if err == nil {
t.Fatal("mismatched expected_output_sha must fail on idempotent path")
}
_, err = svc.CommitAgentVersion(ctx, docID, cand.ID, docrevision.CommitAgentDecision{
Decision: "reject", Confirmed: true, ActorUserID: 1,
})
if err == nil {
t.Fatal("reject after commit must fail")
}
}