feat(vcs): add CommitID to ReviewRequest (#115)
CI / test (pull_request) Successful in 20s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 27s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 49s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m15s

Add CommitID string field to vcs.ReviewRequest so both platform
adapters can thread the commit anchor through the abstraction layer.

Changes:
- vcs/types.go: Add CommitID field with json tag commit_id,omitempty
- gitea/client.go: Re-add commitID parameter to PostReview (was
  removed during PR #112 refactoring)
- gitea/adapter.go: Forward req.CommitID to underlying client
- github/review.go: Use req.CommitID as primary anchor, fall back to
  comment-derived CommitID when empty, reject on conflict
- cmd/review-bot/main.go: Set ReviewRequest.CommitID = evaluatedSHA

Fixes #115
This commit is contained in:
claw
2026-05-13 13:35:58 -07:00
parent a32a5b694b
commit 6d08236773
9 changed files with 243 additions and 13 deletions
+122
View File
@@ -389,3 +389,125 @@ func TestPostReview_ConflictingCommitIDs(t *testing.T) {
t.Errorf("expected ErrConflictingCommitIDs, got: %v", err)
}
}
func TestPostReview_CommitIDFromRequest(t *testing.T) {
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req postReviewRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("unmarshal request: %v", err)
}
if req.CommitID != "req-level-sha" {
t.Errorf("expected commit_id %q, got %q", "req-level-sha", req.CommitID)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 200,
"body": "ok",
"state": "APPROVED",
"commit_id": "req-level-sha",
"user": map[string]string{"login": "bot"},
})
})
// CommitID set at request level, no comments
review, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
Body: "ok",
Event: vcs.ReviewEventApprove,
CommitID: "req-level-sha",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if review.CommitID != "req-level-sha" {
t.Errorf("expected commit_id %q, got %q", "req-level-sha", review.CommitID)
}
}
func TestPostReview_CommitIDFallbackToComment(t *testing.T) {
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req postReviewRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("unmarshal request: %v", err)
}
// When req.CommitID is empty, should fall back to comment's CommitID
if req.CommitID != "comment-sha" {
t.Errorf("expected commit_id %q (from comment fallback), got %q", "comment-sha", req.CommitID)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 201,
"body": "ok",
"state": "APPROVED",
"commit_id": "comment-sha",
"user": map[string]string{"login": "bot"},
})
})
review, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
Body: "ok",
Event: vcs.ReviewEventApprove,
// CommitID intentionally empty — should fall back to comment
Comments: []vcs.ReviewComment{
{Path: "main.go", Position: 1, CommitID: "comment-sha", Body: "nit"},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if review.CommitID != "comment-sha" {
t.Errorf("expected commit_id %q, got %q", "comment-sha", review.CommitID)
}
}
func TestPostReview_CommitIDConflictBetweenRequestAndComment(t *testing.T) {
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("request should not have been sent")
})
_, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
Body: "ok",
Event: vcs.ReviewEventApprove,
CommitID: "req-sha",
Comments: []vcs.ReviewComment{
{Path: "main.go", Position: 1, CommitID: "different-sha", Body: "nit"},
},
})
if !errors.Is(err, ErrConflictingCommitIDs) {
t.Errorf("expected ErrConflictingCommitIDs, got %v", err)
}
}
func TestPostReview_CommitIDMatchBetweenRequestAndComment(t *testing.T) {
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req postReviewRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("unmarshal request: %v", err)
}
if req.CommitID != "same-sha" {
t.Errorf("expected commit_id %q, got %q", "same-sha", req.CommitID)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 202,
"body": "ok",
"state": "APPROVED",
"commit_id": "same-sha",
"user": map[string]string{"login": "bot"},
})
})
_, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
Body: "ok",
Event: vcs.ReviewEventApprove,
CommitID: "same-sha",
Comments: []vcs.ReviewComment{
{Path: "main.go", Position: 1, CommitID: "same-sha", Body: "nit"},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}