main
rb 156 lines 5.9 KB
Raw
1 # frozen_string_literal: true
2
3 require "rails_helper"
4
5 RSpec.describe GithubPrReviewJob do
6 let(:installation) do
7 GithubAppInstallation.create!(installation_id: 555, account_login: "octocat")
8 end
9
10 let(:pull_request) do
11 { "number" => 7, "state" => "open", "draft" => false,
12 "title" => "Fix login", "body" => "Corrects the token check.",
13 "base" => { "ref" => "main" }, "head" => { "ref" => "fix-login", "sha" => "headsha1" } }
14 end
15
16 let(:files) do
17 [ { "filename" => "app/a.rb", "status" => "modified", "additions" => 1, "deletions" => 0,
18 "patch" => "@@ -1,2 +1,3 @@\n line one\n+line two\n line three" } ]
19 end
20
21 let(:model_reply) do
22 { "summary" => "Tightens the token check.",
23 "findings" => [
24 { "path" => "app/a.rb", "line" => 2, "severity" => "warning", "comment" => "Handle nil token." },
25 { "path" => "app/a.rb", "line" => 90, "severity" => "nit", "comment" => "Off the diff." }
26 ] }.to_json
27 end
28
29 def onde_response(content) = { "choices" => [ { "message" => { "content" => content } } ] }
30
31 def perform
32 described_class.perform_now(
33 installation_id: installation.id, repo_full_name: "octocat/hello",
34 pr_number: 7, head_sha: "headsha1"
35 )
36 end
37
38 before do
39 allow(GithubReviewConfig).to receive_messages(enabled?: true, model: "onde-large")
40 allow(GithubAppService).to receive_messages(
41 pull_request: pull_request, pull_request_files: files,
42 create_review: {}, create_issue_comment: {}
43 )
44 allow(OndeCloudService).to receive(:create).and_return(onde_response(model_reply))
45 end
46
47 it "posts one review with the summary and only diff-valid comments, then completes" do
48 perform
49
50 expect(GithubAppService).to have_received(:create_review) do |_inst, repo, number, commit_id:, body:, comments:|
51 expect([ repo, number, commit_id ]).to eq([ "octocat/hello", 7, "headsha1" ])
52 expect(body).to include("Tightens the token check.").and include("siGit Code")
53 expect(comments).to eq([ { "path" => "app/a.rb", "line" => 2, "side" => "RIGHT",
54 "body" => "**warning** — Handle nil token." } ])
55 end
56
57 review = GithubPrReview.find_by!(repo_full_name: "octocat/hello", pr_number: 7, head_sha: "headsha1")
58 expect(review).to have_attributes(status: "completed", comment_count: 1, model: "onde-large")
59 end
60
61 it "is idempotent: a second run for the same head SHA does nothing" do
62 perform
63 perform
64
65 expect(GithubAppService).to have_received(:create_review).once
66 end
67
68 it "no-ops when the kill switch is off, leaving the event reviewable later" do
69 allow(GithubReviewConfig).to receive(:enabled?).and_return(false)
70 perform
71
72 expect(OndeCloudService).not_to have_received(:create)
73 expect(GithubPrReview.count).to eq(0)
74 end
75
76 it "skips when a newer push superseded this SHA" do
77 pull_request["head"]["sha"] = "newersha"
78 perform
79
80 expect(OndeCloudService).not_to have_received(:create)
81 expect(GithubPrReview.last).to have_attributes(status: "skipped", skip_reason: "superseded")
82 end
83
84 it "skips a PR that turned draft after enqueue" do
85 pull_request["draft"] = true
86 perform
87
88 expect(GithubPrReview.last).to have_attributes(status: "skipped", skip_reason: "draft")
89 expect(OndeCloudService).not_to have_received(:create)
90 end
91
92 it "skips a PR that vanished (404)" do
93 allow(GithubAppService).to receive(:pull_request)
94 .and_raise(GithubAppService::ApiError.new("gone", status: 404))
95 perform
96
97 expect(GithubPrReview.last).to have_attributes(status: "skipped", skip_reason: "pr_gone")
98 end
99
100 it "posts a note instead of a review when the PR is over budget" do
101 too_many = (0..GithubReviewPrompt::MAX_FILES).map do |i|
102 { "filename" => "f#{i}.rb", "patch" => "@@ -1,1 +1,2 @@\n a\n+b" }
103 end
104 allow(GithubAppService).to receive(:pull_request_files).and_return(too_many)
105 perform
106
107 expect(GithubAppService).to have_received(:create_issue_comment)
108 expect(GithubAppService).not_to have_received(:create_review)
109 expect(OndeCloudService).not_to have_received(:create)
110 expect(GithubPrReview.last).to have_attributes(status: "skipped", skip_reason: "too_large")
111 end
112
113 it "degrades to summary-only, then an issue comment, when GitHub 422s the review" do
114 allow(GithubAppService).to receive(:create_review)
115 .and_raise(GithubAppService::ApiError.new("invalid", status: 422, body: "{}"))
116 perform
117
118 expect(GithubAppService).to have_received(:create_review).twice # full, then summary-only
119 expect(GithubAppService).to have_received(:create_issue_comment)
120 expect(GithubPrReview.last).to have_attributes(status: "completed")
121 end
122
123 it "retries once with a fresh token on 401" do
124 calls = 0
125 allow(GithubAppService).to receive(:pull_request) do
126 calls += 1
127 raise GithubAppService::ApiError.new("expired", status: 401) if calls == 1
128
129 pull_request
130 end
131 installation.update!(access_token: "ghs_stale", access_token_expires_at: 1.hour.from_now)
132
133 perform
134
135 expect(installation.reload.access_token).to be_nil
136 expect(GithubPrReview.last).to have_attributes(status: "completed")
137 end
138
139 it "marks the review failed and posts nothing when the model reply is unparseable" do
140 allow(OndeCloudService).to receive(:create).and_return(onde_response("not json at all"))
141 perform
142
143 expect(GithubAppService).not_to have_received(:create_review)
144 expect(GithubAppService).not_to have_received(:create_issue_comment)
145 expect(GithubPrReview.last.status).to eq("failed")
146 end
147
148 it "releases the claim and schedules a retry on a transient inference failure" do
149 ActiveJob::Base.queue_adapter = :test
150 allow(OndeCloudService).to receive(:create)
151 .and_raise(OndeCloudService::UpstreamError.new("bad gateway", status: 502))
152
153 expect { perform }.to have_enqueued_job(described_class) # retry_on re-enqueues
154 expect(GithubPrReview.last.status).to eq("pending") # claim released for the retry
155 end
156 end