package operations import ( "context" "errors" "strings" "testing" "time" "orchestra/internal/domain" "orchestra/internal/human" "orchestra/internal/registry" "orchestra/internal/review" "orchestra/internal/store" ) var operatorTrust = human.Trust{Accepted: []string{"kami"}, Ignored: []string{"orchestra-bot", "gitea-actions"}} // submitted walks a task all the way to in_review at shaA. func submitted(t *testing.T) (*store.Store, string, registry.Project) { t.Helper() s, id, project := reviewed(t) plan, err := PrepareSubmission(s, project, id, shaA, gate(shaA), Notes{}) if err != nil { t.Fatal(err) } if _, err := ExecuteSubmission(context.Background(), s, plan, &fakePublisher{}, head(shaA)); err != nil { t.Fatal(err) } if got, _ := s.Task(id); got.State != domain.StateInReview { t.Fatalf("state = %s", got.State) } return s, id, project } func submittedAt(t *testing.T, s *store.Store, id string) time.Time { t.Helper() got, _ := s.Task(id) at, _, ok := submissionRecord(s, got) if !ok { t.Fatal("no submission event") } return at } func prState(id, headSHA, state string, comments ...human.Input) human.PullRequestState { return human.PullRequestState{ID: id, HeadSHA: headSHA, State: state, Comments: comments} } // Trusted feedback after submission reopens the task without any lease being // involved, and the old submission stays as history. func TestTrustedFeedbackReopensTheTask(t *testing.T) { s, id, project := submitted(t) after := submittedAt(t, s, id).Add(time.Minute) events, err := ReflectSubmission(s, project, id, prState("142", shaA, "open", human.Input{Provider: "gitea:p", ExternalID: "c9", Author: "kami", At: after, Body: "change x to y"}, ), operatorTrust) if err != nil { t.Fatal(err) } if len(events) != 2 { t.Fatalf("events = %d, want a decision and a changes-requested", len(events)) } got, _ := s.Task(id) if got.State != domain.StateQueued { t.Fatalf("state = %s, want queued so the router can lease it", got.State) } if got.WorkPhase != domain.WorkPhaseImplement { t.Fatalf("phase = %q, want implement", got.WorkPhase) } if got.Submission == nil || got.Submission.ResultSHA != shaA { t.Fatalf("the submission must remain as history: %+v", got.Submission) } // The feedback is standing authority. intent, err := s.EffectiveIntent(id) if err != nil { t.Fatal(err) } if len(intent.Decisions) != 1 || intent.Decisions[0].Value != "change x to y" { t.Fatalf("decisions = %+v", intent.Decisions) } // The old review and submission satisfy nothing at a new commit. if domain.CheckSubmission(got, shaB, gate(shaB)).Eligible { t.Fatal("a new commit inherited the old review") } // Polling again with the same comment changes nothing. before := len(s.Events(0)) if events, err := ReflectSubmission(s, project, id, prState("142", shaA, "open", human.Input{Provider: "gitea:p", ExternalID: "c9", Author: "kami", At: after, Body: "change x to y"}, ), operatorTrust); err != nil || len(events) != 0 { t.Fatalf("duplicate poll: events=%d err=%v", len(events), err) } if len(s.Events(0)) != before { t.Fatal("a duplicate comment appended events") } } // Bots, Orchestra itself, and comments from before the submission cannot // reopen finished work. func TestUntrustedAndStaleCommentsDoNotReopen(t *testing.T) { s, id, project := submitted(t) at := submittedAt(t, s, id) cases := map[string]human.Input{ "bot": {Provider: "gitea:p", ExternalID: "b1", Author: "gitea-actions", At: at.Add(time.Minute), Body: "build passed"}, "orchestra itself": {Provider: "gitea:p", ExternalID: "b2", Author: "orchestra-bot", At: at.Add(time.Minute), Body: "submitted"}, "unknown actor": {Provider: "gitea:p", ExternalID: "b3", Author: "passer-by", At: at.Add(time.Minute), Body: "nice"}, "before submission": {Provider: "gitea:p", ExternalID: "b4", Author: "kami", At: at.Add(-time.Hour), Body: "looks good so far"}, "at submission": {Provider: "gitea:p", ExternalID: "b5", Author: "kami", At: at, Body: "same instant"}, "empty": {Provider: "gitea:p", ExternalID: "b6", Author: "kami", At: at.Add(time.Minute), Body: " "}, } for name, in := range cases { events, err := ReflectSubmission(s, project, id, prState("142", shaA, "open", in), operatorTrust) if err != nil || len(events) != 0 { t.Fatalf("%s: events=%d err=%v", name, len(events), err) } if got, _ := s.Task(id); got.State != domain.StateInReview { t.Fatalf("%s: reopened the task", name) } } } // A merged pull request completes the task, with the receipt bound to the exact // submission. Merge strategy is not assumed. func TestMergedPullRequestCompletes(t *testing.T) { s, id, project := submitted(t) state := human.PullRequestState{ ID: "142", HeadSHA: shaA, State: "merged", MergeSHA: "9999999999999999999999999999999999999999", MergedAt: time.Unix(1700000000, 0).UTC(), } events, err := ReflectSubmission(s, project, id, state, operatorTrust) if err != nil { t.Fatal(err) } if len(events) != 1 || events[0].Type != "TaskCompleted" { t.Fatalf("events = %+v", events) } got, _ := s.Task(id) if got.State != domain.StateCompleted { t.Fatalf("state = %s", got.State) } // The receipt names the submission, the pull request, both commits, and // when it merged. var payload struct { ReportRef string `json:"report_ref"` Receipt domain.CompletionReceipt `json:"receipt"` } if err := unmarshal(events[0].Payload, &payload); err != nil { t.Fatal(err) } r := payload.Receipt if r.SubmittedSHA != shaA || r.MergeSHA != "9999999999999999999999999999999999999999" { t.Fatalf("receipt = %+v", r) } if r.PR.ID != "142" || r.SubmissionRef == "" || r.MergedAt.IsZero() { t.Fatalf("receipt = %+v", r) } if _, err := s.Artifact(payload.ReportRef); err != nil { t.Fatalf("receipt artifact missing: %v", err) } // Reflecting again is idempotent. if events, err := ReflectSubmission(s, project, id, state, operatorTrust); err != nil || len(events) != 0 { t.Fatalf("second merge reflection: events=%d err=%v", len(events), err) } } // A stale observation cannot complete a task, and neither can another task's // pull request. func TestForeignOrStaleObservationCannotComplete(t *testing.T) { s, id, project := submitted(t) // Another pull request entirely. if _, err := ReflectSubmission(s, project, id, human.PullRequestState{ID: "999", HeadSHA: shaA, State: "merged"}, operatorTrust); !errors.Is(err, ErrForeignPullRequest) { t.Fatalf("want ErrForeignPullRequest, got %v", err) } // The right pull request, but carrying a commit that was never submitted. if _, err := ReflectSubmission(s, project, id, human.PullRequestState{ID: "142", HeadSHA: shaB, State: "merged"}, operatorTrust); !errors.Is(err, ErrForeignPullRequest) { t.Fatalf("want ErrForeignPullRequest, got %v", err) } if got, _ := s.Task(id); got.State != domain.StateInReview { t.Fatalf("state = %s, a rejected observation must change nothing", got.State) } } // Closed without merging is an operator question, not a failure. func TestClosedWithoutMergeAsksTheOperator(t *testing.T) { s, id, project := submitted(t) events, err := ReflectSubmission(s, project, id, human.PullRequestState{ID: "142", HeadSHA: shaA, State: "closed"}, operatorTrust) if err != nil { t.Fatal(err) } if len(events) != 1 { t.Fatalf("events = %+v", events) } got, _ := s.Task(id) if got.State == domain.StateFailed || got.State == domain.StateCompleted { t.Fatalf("state = %s, a closed pull request must not decide the task", got.State) } if got.BlockReason != domain.BlockReasonOperator { t.Fatalf("block reason = %q", got.BlockReason) } if !strings.Contains(got.Blocker, "closed without merging") { t.Fatalf("blocker = %q", got.Blocker) } // And it does not repeat on the next poll. if events, err := ReflectSubmission(s, project, id, human.PullRequestState{ID: "142", HeadSHA: shaA, State: "closed"}, operatorTrust); err != nil || len(events) != 0 { t.Fatalf("repeated: events=%d err=%v", len(events), err) } } // A review with changes_requested and no body still reopens the task. func TestChangesRequestedReviewWithNoBodyReopens(t *testing.T) { s, id, project := submitted(t) after := submittedAt(t, s, id).Add(time.Minute) state := human.PullRequestState{ID: "142", HeadSHA: shaA, State: "open", Reviews: []human.ReviewObservation{ {Actor: "kami", State: "changes_requested", At: after}, }} events, err := ReflectSubmission(s, project, id, state, operatorTrust) if err != nil { t.Fatal(err) } if len(events) != 2 { t.Fatalf("events = %d", len(events)) } if got, _ := s.Task(id); got.State != domain.StateQueued { t.Fatalf("state = %s", got.State) } } // A reflector outage leaves the task exactly as it was. func TestReflectorOutageChangesNothing(t *testing.T) { s, id, project := submitted(t) before, _ := s.Task(id) beforeEvents := len(s.Events(0)) // A poll that never happened is simply a poll with no observation. The // caller records its own error; the task must not move. if _, err := ReflectSubmission(s, project, id, prState("142", shaA, "open"), operatorTrust); err != nil { t.Fatal(err) } after, _ := s.Task(id) if after.State != before.State || after.Version != before.Version || len(s.Events(0)) != beforeEvents { t.Fatalf("an empty observation changed state: %s -> %s", before.State, after.State) } } // The full loop: rejected at A, fixed at B, reviewed again, resubmitted to the // same pull request, then merged. func TestFullHumanLoopFromRejectionToMerge(t *testing.T) { s, id, project := submitted(t) after := submittedAt(t, s, id).Add(time.Minute) if _, err := ReflectSubmission(s, project, id, prState("142", shaA, "open", human.Input{Provider: "gitea:p", ExternalID: "c9", Author: "kami", At: after, Body: "rename the variable"}, ), operatorTrust); err != nil { t.Fatal(err) } // A fresh gate and a fresh review at the new commit. if _, err := EnterReview(s, project, id, evidence(shaB)); err != nil { t.Fatal(err) } if _, err := RecordReview(s, project, id, review.Result{ResultSHA: shaB, Findings: []review.Finding{finding("f1", review.Minor)}}); err != nil { t.Fatal(err) } plan, err := PrepareSubmission(s, project, id, shaB, gate(shaB), Notes{}) if err != nil { t.Fatal(err) } pub := &fakePublisher{pr: domain.ExternalRef{Provider: "gitea:p", ID: "142", URL: "https://git/pulls/142"}} if _, err := ExecuteSubmission(context.Background(), s, plan, pub, head(shaB)); err != nil { t.Fatal(err) } resubmitted, _ := s.Task(id) if resubmitted.Submission.ResultSHA != shaB || resubmitted.Submission.PR.ID != "142" { t.Fatalf("submission = %+v, the same pull request must be refreshed", resubmitted.Submission) } // Feedback on the old submission cannot reopen the new one. if _, err := ReflectSubmission(s, project, id, prState("142", shaA, "merged"), operatorTrust); !errors.Is(err, ErrForeignPullRequest) { t.Fatalf("a stale observation completed a newer submission: %v", err) } // The human merges what they reviewed. if _, err := ReflectSubmission(s, project, id, human.PullRequestState{ ID: "142", HeadSHA: shaB, State: "merged", MergeSHA: "8888888888888888888888888888888888888888", MergedAt: time.Unix(1700009999, 0).UTC(), }, operatorTrust); err != nil { t.Fatal(err) } final, _ := s.Task(id) if final.State != domain.StateCompleted { t.Fatalf("state = %s", final.State) } }