Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion internal/cmd/reviewcmd/reviewcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ const reviewLong = `Run an automated pull-request review.
Live review checks local and host state before starting the reviewer loop. By
default, if the posting identity has already approved the PR, cr exits before
any LLM classifier or reviewer work, even if newer commits made that approval
stale. Use --rerun to bypass these local gates and force a new live review.
stale. A newer COMMENTED review from the posting identity supersedes that fast
path so thread-response activity can be followed by a fresh verdict. Use
--rerun to bypass these local gates and force a new live review.

Session reuse is independent of local review gates. Plain follow-up reviews and
--rerun reuse the PR's original reviewer cohort and each reviewer's provider
Expand Down
24 changes: 17 additions & 7 deletions internal/gateio/gateio.go
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,14 @@ func readGateHostStateWithReviews(ctx context.Context, provider outbox.LiveProvi

func summarizePRFromHost(host gateHostState, req Request) gate.PRSummary {
records := markerActionRecords(host, req.PostingIdentity)
return classifyMarkers(records, req.PR.Head.SHA, req.PR.Base.SHA)
summary := classifyMarkers(records, req.PR.Head.SHA, req.PR.Base.SHA)
if summary.State == gate.PRStateCompleteReview {
latest, found := latestVerdictReviewByPostingIdentity(host.reviews, req.PostingIdentity)
if found && latest.State == gitprovider.ReviewStateCommented {
return gate.PRSummary{State: gate.PRStateFresh}
}
}
return summary
}

func markerActionRecords(host gateHostState, posting gitprovider.Identity) []markerRecord {
Expand Down Expand Up @@ -990,6 +997,11 @@ func latestCodereviewMarkerAt(host gateHostState, posting gitprovider.Identity)
}

func activeApprovalByPostingIdentity(reviews []gitprovider.Review, posting gitprovider.Identity) bool {
selected, found := latestVerdictReviewByPostingIdentity(reviews, posting)
return found && selected.State == gitprovider.ReviewStateApproved
}

func latestVerdictReviewByPostingIdentity(reviews []gitprovider.Review, posting gitprovider.Identity) (gitprovider.Review, bool) {
var (
selected gitprovider.Review
found bool
Expand All @@ -999,21 +1011,19 @@ func activeApprovalByPostingIdentity(reviews []gitprovider.Review, posting gitpr
continue
}
switch review.State {
case gitprovider.ReviewStateApproved, gitprovider.ReviewStateChangesRequested:
case gitprovider.ReviewStateCommented, gitprovider.ReviewStateDismissed, gitprovider.ReviewStatePending:
case gitprovider.ReviewStateApproved, gitprovider.ReviewStateChangesRequested, gitprovider.ReviewStateCommented:
case gitprovider.ReviewStateDismissed, gitprovider.ReviewStatePending:
continue
default:
continue
}
if !found || review.SubmittedAt.After(selected.SubmittedAt) ||
(review.SubmittedAt.Equal(selected.SubmittedAt) &&
selected.State == gitprovider.ReviewStateApproved &&
review.State == gitprovider.ReviewStateChangesRequested) {
(review.SubmittedAt.Equal(selected.SubmittedAt) && string(review.ID) > string(selected.ID)) {
selected = review
found = true
}
}
return found && selected.State == gitprovider.ReviewStateApproved
return selected, found
}

func maybeExecuteApprovalOverride(ctx context.Context, opts Options, req Request, host *gateHostState) (Result, bool, error) {
Expand Down
35 changes: 35 additions & 0 deletions internal/gateio/gateio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,41 @@ func TestEvaluateActivePostingIdentityApprovalExitsBeforeOverrideReads(t *testin
}
}

func TestEvaluateNewerCommentedReviewDoesNotUseApprovalFastPath(t *testing.T) {
fixture := newFixture(t)
submit := mustRenderAction(t, marker.ActionMarker{
RunID: "run-approved",
ActionID: "submit-1",
Kind: marker.ActionKindSubmitReview,
SHA: testHeadSHA,
BaseSHA: testBaseSHA,
})
setReviews(t, fixture, []gitprovider.Review{
{
ID: "review-approved",
Author: fixture.req.PostingIdentity,
Body: submit,
State: gitprovider.ReviewStateApproved,
SubmittedAt: testNow.Add(-time.Minute),
},
{
ID: "review-commented",
Author: fixture.req.PostingIdentity,
State: gitprovider.ReviewStateCommented,
SubmittedAt: testNow,
},
})

result, err := Evaluate(context.Background(), fixture.opts(), fixture.req)
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
defer releaseResultLock(t, result)
if result.Status != StatusContinue || result.Decision.Kind != gate.DecisionFresh {
t.Fatalf("Evaluate = %#v, want fresh review after newer commented review", result)
}
}

func TestEvaluateRetryPostsIgnoresActiveApprovalAndOverride(t *testing.T) {
fixture := newFixture(t)
run := fixture.allocateRun(t, "run-retry", testBaseSHA, ledger.PostModeLive)
Expand Down
Loading