lex-swarm-github 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/legion/extensions/swarm_github/actors/lifecycle_subscriber.rb +17 -3
- data/lib/legion/extensions/swarm_github/runners/extension_lifecycle.rb +115 -4
- data/lib/legion/extensions/swarm_github/runners/pull_request_reviewer.rb +9 -4
- data/lib/legion/extensions/swarm_github/version.rb +1 -1
- data/spec/legion/extensions/swarm_github/actors/lifecycle_subscriber_spec.rb +12 -2
- data/spec/legion/extensions/swarm_github/runners/extension_lifecycle_spec.rb +139 -0
- data/spec/legion/extensions/swarm_github/runners/pull_request_reviewer_spec.rb +27 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afb02c0714b2eeeb371b7d0890c04b6a5637be7b8584f62872cec6337baa980e
|
|
4
|
+
data.tar.gz: 794646c0b9897d25e18fb22cf156c3f7ee2a5a07408aa23876b17cef00ece198
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c20383d9e86e5db3135a6cca8af1b1b226348f9cada58232425e7ae8064c17c2366f6aab167404b2081a567e95e238cfd37fc23cebf338fedb9078c35d5591c
|
|
7
|
+
data.tar.gz: 9d494a1404085b965f8bf6d272b9b875fc4bd3c21c291d9d07fdf45452fe356d6e3966f4e5fe6f42c142f5557b18ea80d68092fe33dbc0cd1d5c9628eea9e68e
|
|
@@ -19,10 +19,14 @@ module Legion
|
|
|
19
19
|
return { skipped: true, reason: verdict == 'approve' ? :github_disabled : :not_approved }
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
generation = payload[:generation] || {}
|
|
23
|
-
review = payload.except(:generation)
|
|
22
|
+
generation = payload[:generation] || payload['generation'] || {}
|
|
23
|
+
review = payload.except(:generation, 'generation')
|
|
24
|
+
review_k = payload[:review_k] || payload['review_k']
|
|
25
|
+
raw_models = payload[:review_models] || payload['review_models']
|
|
26
|
+
review_models = normalize_review_models(raw_models)
|
|
24
27
|
|
|
25
|
-
Runners::ExtensionLifecycle.run_lifecycle(generation: generation, review: review
|
|
28
|
+
Runners::ExtensionLifecycle.run_lifecycle(generation: generation, review: review,
|
|
29
|
+
review_k: review_k, review_models: review_models)
|
|
26
30
|
rescue StandardError => e
|
|
27
31
|
log.warn("LifecycleSubscriber failed: #{e.message}")
|
|
28
32
|
{ success: false, error: e.message }
|
|
@@ -30,6 +34,16 @@ module Legion
|
|
|
30
34
|
|
|
31
35
|
private
|
|
32
36
|
|
|
37
|
+
def normalize_review_models(raw)
|
|
38
|
+
return nil unless raw.is_a?(Array)
|
|
39
|
+
|
|
40
|
+
raw.filter_map do |spec|
|
|
41
|
+
next unless spec.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
spec.transform_keys { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
33
47
|
def github_lifecycle_enabled?
|
|
34
48
|
return false unless defined?(Legion::Settings)
|
|
35
49
|
|
|
@@ -7,7 +7,7 @@ module Legion
|
|
|
7
7
|
module ExtensionLifecycle
|
|
8
8
|
extend self
|
|
9
9
|
|
|
10
|
-
def run_lifecycle(generation:, review:)
|
|
10
|
+
def run_lifecycle(generation:, review:, review_k: nil, review_models: nil)
|
|
11
11
|
config = github_config
|
|
12
12
|
return { success: false, error: :github_not_enabled } unless config[:enabled]
|
|
13
13
|
return { success: false, error: :target_repo_missing } unless config[:target_repo]
|
|
@@ -31,11 +31,17 @@ module Legion
|
|
|
31
31
|
label_pull_request(owner: owner, repo: repo, pull_number: pr[:pull_number],
|
|
32
32
|
labels: config[:pr_labels])
|
|
33
33
|
|
|
34
|
+
k = review_k || default_review_k
|
|
35
|
+
models = review_models || default_review_models
|
|
36
|
+
review_result = run_adversarial_review(owner: owner, repo: repo,
|
|
37
|
+
pull_number: pr[:pull_number], k: k, models: models)
|
|
38
|
+
|
|
34
39
|
handle_auto_merge(owner: owner, repo: repo, pull_number: pr[:pull_number],
|
|
35
|
-
config: config, review: review)
|
|
40
|
+
config: config, review: review, review_result: review_result)
|
|
36
41
|
|
|
37
42
|
{ success: true, pull_number: pr[:pull_number], html_url: pr[:html_url],
|
|
38
|
-
branch: branch_name, generation_id: generation[:generation_id]
|
|
43
|
+
branch: branch_name, generation_id: generation[:generation_id],
|
|
44
|
+
review_consensus: review_result&.dig(:consensus), review_k: k }
|
|
39
45
|
rescue StandardError => e
|
|
40
46
|
log.warn("ExtensionLifecycle failed: #{e.message}")
|
|
41
47
|
{ success: false, error: e.message }
|
|
@@ -84,8 +90,109 @@ module Legion
|
|
|
84
90
|
{ success: false, error: e.message }
|
|
85
91
|
end
|
|
86
92
|
|
|
87
|
-
def
|
|
93
|
+
def default_review_k
|
|
94
|
+
return 1 unless defined?(Legion::Settings)
|
|
95
|
+
|
|
96
|
+
Legion::Settings.dig(:codegen, :self_generate, :github, :review_k) || 1
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
log.warn(e.message)
|
|
99
|
+
1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def default_review_models
|
|
103
|
+
return [] unless defined?(Legion::Settings)
|
|
104
|
+
|
|
105
|
+
Legion::Settings.dig(:codegen, :self_generate, :github, :review_models) || []
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
log.warn(e.message)
|
|
108
|
+
[]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def provider_available?(provider_sym)
|
|
112
|
+
return false unless defined?(Legion::Settings)
|
|
113
|
+
|
|
114
|
+
Legion::Settings.dig(:llm, :providers, provider_sym, :enabled) == true
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
log.warn(e.message)
|
|
117
|
+
false
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_model_assignments(count, models)
|
|
121
|
+
return Array.new(count) { nil } if models.nil?
|
|
122
|
+
|
|
123
|
+
unless models.is_a?(Array)
|
|
124
|
+
log.warn("review_models must be an Array, got #{models.class}; using defaults")
|
|
125
|
+
return Array.new(count) { nil }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
return Array.new(count) { nil } if models.empty?
|
|
129
|
+
|
|
130
|
+
available = models.filter_map do |raw_spec|
|
|
131
|
+
unless raw_spec.is_a?(Hash)
|
|
132
|
+
log.warn('review model spec is not a Hash, skipping')
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
spec = raw_spec.transform_keys { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
|
|
137
|
+
provider_value = spec[:provider]
|
|
138
|
+
|
|
139
|
+
if provider_value && !provider_value.respond_to?(:to_sym)
|
|
140
|
+
log.warn("review provider value #{provider_value.inspect} (#{provider_value.class}) cannot be symbolized, skipping")
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
provider_sym = provider_value&.to_sym
|
|
145
|
+
spec[:provider] = provider_sym if provider_sym
|
|
146
|
+
|
|
147
|
+
if provider_sym && !provider_available?(provider_sym)
|
|
148
|
+
log.warn("review provider #{provider_sym} not available, skipping")
|
|
149
|
+
next
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
spec
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
return Array.new(count) { nil } if available.empty?
|
|
156
|
+
|
|
157
|
+
assignments = available.first(count)
|
|
158
|
+
assignments + Array.new([count - assignments.length, 0].max) { nil }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def run_adversarial_review(owner:, repo:, pull_number:, k:, models: []) # rubocop:disable Naming/MethodParameterName
|
|
162
|
+
return { success: true, skipped: true, reason: :reviewer_unavailable } unless pr_reviewer_available?
|
|
163
|
+
|
|
164
|
+
assignments = build_model_assignments(k, models)
|
|
165
|
+
|
|
166
|
+
reviews = assignments.map do |spec|
|
|
167
|
+
kwargs = { owner: owner, repo: repo, pull_number: pull_number }
|
|
168
|
+
if spec
|
|
169
|
+
kwargs[:model] = spec[:model] if spec[:model]
|
|
170
|
+
kwargs[:provider] = spec[:provider] if spec[:provider]
|
|
171
|
+
end
|
|
172
|
+
Legion::Extensions::SwarmGithub::Runners::PullRequestReviewer.review_pull_request(**kwargs)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
approvals = reviews.count do |r|
|
|
176
|
+
r[:status] == 'reviewed' && (r[:comments] || []).none? { |c| %w[error critical].include?(c[:severity]&.to_s) }
|
|
177
|
+
end
|
|
178
|
+
rejections = k - approvals
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
success: true,
|
|
182
|
+
consensus: approvals > rejections ? :approve : :request_changes,
|
|
183
|
+
k: k,
|
|
184
|
+
approvals: approvals,
|
|
185
|
+
rejections: rejections,
|
|
186
|
+
reviews: reviews
|
|
187
|
+
}
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
log.warn("adversarial PR review failed: #{e.message}")
|
|
190
|
+
{ success: true, skipped: true, reason: :review_error }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def handle_auto_merge(owner:, repo:, pull_number:, config:, review:, review_result: nil) # rubocop:disable Metrics/ParameterLists
|
|
88
194
|
return unless config[:auto_merge] && review[:verdict]&.to_sym == :approve
|
|
195
|
+
return if review_result && review_result[:consensus] == :request_changes
|
|
89
196
|
|
|
90
197
|
github_client.merge_pull_request(
|
|
91
198
|
owner: owner, repo: repo, pull_number: pull_number,
|
|
@@ -150,6 +257,10 @@ module Legion
|
|
|
150
257
|
defined?(Legion::Extensions::Github::Client)
|
|
151
258
|
end
|
|
152
259
|
|
|
260
|
+
def pr_reviewer_available?
|
|
261
|
+
defined?(Legion::Extensions::SwarmGithub::Runners::PullRequestReviewer)
|
|
262
|
+
end
|
|
263
|
+
|
|
153
264
|
def github_client
|
|
154
265
|
@github_client ||= Legion::Extensions::Github::Client.new(**github_connection_opts)
|
|
155
266
|
end
|
|
@@ -5,11 +5,13 @@ module Legion
|
|
|
5
5
|
module SwarmGithub
|
|
6
6
|
module Runners
|
|
7
7
|
module PullRequestReviewer
|
|
8
|
-
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def review_pull_request(owner:, repo:, pull_number:, model: nil, provider: nil)
|
|
9
11
|
files = fetch_pr_files(owner: owner, repo: repo, pull_number: pull_number)
|
|
10
12
|
return { status: 'skipped', reason: 'no files' } if files.empty?
|
|
11
13
|
|
|
12
|
-
review = generate_review(files)
|
|
14
|
+
review = generate_review(files, model: model, provider: provider)
|
|
13
15
|
|
|
14
16
|
{
|
|
15
17
|
status: 'reviewed',
|
|
@@ -33,12 +35,15 @@ module Legion
|
|
|
33
35
|
[]
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
def generate_review(files)
|
|
38
|
+
def generate_review(files, model: nil, provider: nil)
|
|
37
39
|
chunks = Helpers::DiffChunker.chunk_files(files)
|
|
38
40
|
reviews = chunks.map do |chunk|
|
|
39
41
|
diff_text = chunk.map { |f| "--- #{f[:filename]} ---\n#{f[:patch]}" }.join("\n\n")
|
|
40
42
|
prompt = code_review_prompt(diff_text)
|
|
41
|
-
|
|
43
|
+
llm_kwargs = { message: prompt, caller: { extension: 'lex-swarm-github' } }
|
|
44
|
+
llm_kwargs[:model] = model if model
|
|
45
|
+
llm_kwargs[:provider] = provider if provider
|
|
46
|
+
response = Legion::LLM.chat(**llm_kwargs)
|
|
42
47
|
parse_review_response(response)
|
|
43
48
|
end
|
|
44
49
|
merge_chunk_reviews(reviews)
|
|
@@ -80,11 +80,21 @@ RSpec.describe Legion::Extensions::SwarmGithub::Actor::LifecycleSubscriber do
|
|
|
80
80
|
actor.action(payload)
|
|
81
81
|
expect(Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle)
|
|
82
82
|
.to have_received(:run_lifecycle).with(
|
|
83
|
-
generation:
|
|
84
|
-
review:
|
|
83
|
+
generation: generation,
|
|
84
|
+
review: hash_including(verdict: 'approve'),
|
|
85
|
+
review_k: nil,
|
|
86
|
+
review_models: nil
|
|
85
87
|
)
|
|
86
88
|
end
|
|
87
89
|
|
|
90
|
+
it 'forwards review_models from payload' do
|
|
91
|
+
models = [{ provider: :bedrock, model: 'claude' }]
|
|
92
|
+
payload = { verdict: 'approve', generation: generation, review_models: models }
|
|
93
|
+
actor.action(payload)
|
|
94
|
+
expect(Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle)
|
|
95
|
+
.to have_received(:run_lifecycle).with(hash_including(review_models: models))
|
|
96
|
+
end
|
|
97
|
+
|
|
88
98
|
it 'returns the lifecycle result' do
|
|
89
99
|
result = actor.action({ verdict: 'approve', generation: generation })
|
|
90
100
|
expect(result).to eq(lifecycle_result)
|
|
@@ -79,6 +79,7 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle do
|
|
|
79
79
|
{ result: { 'number' => 42, 'html_url' => 'https://github.com/org/repo/pull/42' } }
|
|
80
80
|
)
|
|
81
81
|
allow(github_client).to receive(:add_labels).and_return({ success: true })
|
|
82
|
+
allow(runner).to receive(:pr_reviewer_available?).and_return(false)
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
it 'returns success with PR details' do
|
|
@@ -154,6 +155,7 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle do
|
|
|
154
155
|
{ result: { 'number' => 7, 'html_url' => 'https://github.com/org/repo/pull/7' } }
|
|
155
156
|
)
|
|
156
157
|
allow(github_client).to receive(:merge_pull_request).and_return({ success: true })
|
|
158
|
+
allow(runner).to receive(:pr_reviewer_available?).and_return(false)
|
|
157
159
|
end
|
|
158
160
|
|
|
159
161
|
it 'calls merge_pull_request' do
|
|
@@ -175,5 +177,142 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle do
|
|
|
175
177
|
expect(result[:error]).to eq('unexpected failure')
|
|
176
178
|
end
|
|
177
179
|
end
|
|
180
|
+
|
|
181
|
+
describe 'adversarial PR review' do
|
|
182
|
+
let(:mod) { described_class }
|
|
183
|
+
|
|
184
|
+
before do
|
|
185
|
+
allow(mod).to receive(:github_config).and_return(
|
|
186
|
+
enabled: true, target_repo: 'Test/repo', target_branch: 'main',
|
|
187
|
+
auto_merge: false, pr_labels: [], branch_prefix: 'feature/auto-gen'
|
|
188
|
+
)
|
|
189
|
+
allow(mod).to receive(:create_lifecycle_branch).and_return({ success: true })
|
|
190
|
+
allow(mod).to receive(:commit_generated_files).and_return({ success: true })
|
|
191
|
+
allow(mod).to receive(:open_pull_request).and_return({ success: true, pull_number: 1, html_url: 'url' })
|
|
192
|
+
allow(mod).to receive(:label_pull_request).and_return({ success: true })
|
|
193
|
+
allow(mod).to receive(:run_adversarial_review).and_return({ success: true, consensus: :approve, k: 3 })
|
|
194
|
+
allow(mod).to receive(:handle_auto_merge)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'passes review_k through to adversarial review' do
|
|
198
|
+
mod.run_lifecycle(generation: generation, review: review, review_k: 3)
|
|
199
|
+
expect(mod).to have_received(:run_adversarial_review).with(hash_including(k: 3))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'defaults review_k to 1' do
|
|
203
|
+
mod.run_lifecycle(generation: generation, review: review)
|
|
204
|
+
expect(mod).to have_received(:run_adversarial_review).with(hash_including(k: 1))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'includes review consensus in result' do
|
|
208
|
+
result = mod.run_lifecycle(generation: generation, review: review, review_k: 3)
|
|
209
|
+
expect(result[:review_consensus]).to eq(:approve)
|
|
210
|
+
expect(result[:review_k]).to eq(3)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
describe 'multi-provider adversarial review' do
|
|
215
|
+
let(:mod) { described_class }
|
|
216
|
+
|
|
217
|
+
before do
|
|
218
|
+
allow(mod).to receive(:github_config).and_return(
|
|
219
|
+
enabled: true, target_repo: 'Test/repo', target_branch: 'main',
|
|
220
|
+
auto_merge: false, pr_labels: [], branch_prefix: 'feature/auto-gen'
|
|
221
|
+
)
|
|
222
|
+
allow(mod).to receive(:create_lifecycle_branch).and_return({ success: true })
|
|
223
|
+
allow(mod).to receive(:commit_generated_files).and_return({ success: true })
|
|
224
|
+
allow(mod).to receive(:open_pull_request).and_return({ success: true, pull_number: 1, html_url: 'url' })
|
|
225
|
+
allow(mod).to receive(:label_pull_request).and_return({ success: true })
|
|
226
|
+
allow(mod).to receive(:handle_auto_merge)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'forwards review_models to adversarial review' do
|
|
230
|
+
allow(mod).to receive(:run_adversarial_review).and_return({ success: true, consensus: :approve, k: 2 })
|
|
231
|
+
models = [{ provider: :bedrock, model: 'claude' }]
|
|
232
|
+
mod.run_lifecycle(generation: generation, review: review, review_k: 2, review_models: models)
|
|
233
|
+
expect(mod).to have_received(:run_adversarial_review).with(hash_including(models: models))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'uses default_review_models when review_models is nil' do
|
|
237
|
+
allow(mod).to receive(:run_adversarial_review).and_return({ success: true, consensus: :approve, k: 1 })
|
|
238
|
+
allow(mod).to receive(:default_review_models).and_return([])
|
|
239
|
+
mod.run_lifecycle(generation: generation, review: review)
|
|
240
|
+
expect(mod).to have_received(:default_review_models)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
describe '#build_model_assignments' do
|
|
245
|
+
let(:mod) { described_class }
|
|
246
|
+
|
|
247
|
+
it 'returns all nils when models is nil' do
|
|
248
|
+
result = mod.send(:build_model_assignments, 3, nil)
|
|
249
|
+
expect(result).to eq([nil, nil, nil])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it 'returns all nils when models is empty' do
|
|
253
|
+
result = mod.send(:build_model_assignments, 2, [])
|
|
254
|
+
expect(result).to eq([nil, nil])
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'uses each available model at most once, then fills remaining slots with nil' do
|
|
258
|
+
allow(mod).to receive(:provider_available?).and_return(true)
|
|
259
|
+
models = [{ provider: :bedrock, model: 'a' }, { provider: :openai, model: 'b' }]
|
|
260
|
+
result = mod.send(:build_model_assignments, 3, models)
|
|
261
|
+
expect(result).to eq([{ provider: :bedrock, model: 'a' }, { provider: :openai, model: 'b' }, nil])
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it 'skips unavailable providers and falls back to nil assignments' do
|
|
265
|
+
allow(mod).to receive(:provider_available?).and_return(false)
|
|
266
|
+
models = [{ provider: :unavailable, model: 'x' }]
|
|
267
|
+
result = mod.send(:build_model_assignments, 2, models)
|
|
268
|
+
expect(result).to eq([nil, nil])
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it 'returns all nils and warns when models is a String' do
|
|
272
|
+
expect(mod).to receive(:log).at_least(:once).and_return(double(warn: nil))
|
|
273
|
+
result = mod.send(:build_model_assignments, 2, 'not-an-array')
|
|
274
|
+
expect(result).to eq([nil, nil])
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'returns all nils and warns when models is a Hash' do
|
|
278
|
+
expect(mod).to receive(:log).at_least(:once).and_return(double(warn: nil))
|
|
279
|
+
result = mod.send(:build_model_assignments, 2, { provider: :bedrock, model: 'claude' })
|
|
280
|
+
expect(result).to eq([nil, nil])
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it 'returns all nils and warns when models is an Integer' do
|
|
284
|
+
expect(mod).to receive(:log).at_least(:once).and_return(double(warn: nil))
|
|
285
|
+
result = mod.send(:build_model_assignments, 2, 42)
|
|
286
|
+
expect(result).to eq([nil, nil])
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it 'skips specs with a non-symbolizable provider value and warns' do
|
|
290
|
+
logger = double(warn: nil)
|
|
291
|
+
allow(mod).to receive(:log).and_return(logger)
|
|
292
|
+
models = [{ provider: [1, 2], model: 'x' }, { provider: :openai, model: 'y' }]
|
|
293
|
+
allow(mod).to receive(:provider_available?).with(:openai).and_return(true)
|
|
294
|
+
result = mod.send(:build_model_assignments, 2, models)
|
|
295
|
+
expect(logger).to have_received(:warn).with(/cannot be symbolized/)
|
|
296
|
+
expect(result).to eq([{ provider: :openai, model: 'y' }, nil])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
describe '#provider_available?' do
|
|
301
|
+
let(:mod) { described_class }
|
|
302
|
+
|
|
303
|
+
it 'returns false when Legion::Settings is not defined' do
|
|
304
|
+
hide_const('Legion::Settings') if defined?(Legion::Settings)
|
|
305
|
+
expect(mod.send(:provider_available?, :bedrock)).to be false
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
describe '#default_review_models' do
|
|
310
|
+
let(:mod) { described_class }
|
|
311
|
+
|
|
312
|
+
it 'returns empty array when Legion::Settings is not defined' do
|
|
313
|
+
hide_const('Legion::Settings') if defined?(Legion::Settings)
|
|
314
|
+
expect(mod.send(:default_review_models)).to eq([])
|
|
315
|
+
end
|
|
316
|
+
end
|
|
178
317
|
end
|
|
179
318
|
end
|
|
@@ -98,5 +98,32 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::PullRequestReviewer do
|
|
|
98
98
|
expect(result[:comments].first[:severity]).to eq('error')
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
|
+
|
|
102
|
+
context 'with model: and provider: kwargs' do
|
|
103
|
+
let(:client) { Class.new { include Legion::Extensions::SwarmGithub::Runners::PullRequestReviewer }.new }
|
|
104
|
+
|
|
105
|
+
before do
|
|
106
|
+
stub_const('Legion::LLM', Module.new)
|
|
107
|
+
allow(Legion::LLM).to receive(:chat).and_return('{"summary":"ok","comments":[]}')
|
|
108
|
+
allow(client).to receive(:fetch_pr_files).and_return(
|
|
109
|
+
[{ filename: 'test.rb', patch: '+x' }]
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'forwards model and provider to Legion::LLM.chat' do
|
|
114
|
+
client.review_pull_request(owner: 'o', repo: 'r', pull_number: 1, model: 'gpt-4o', provider: :openai)
|
|
115
|
+
expect(Legion::LLM).to have_received(:chat).with(hash_including(model: 'gpt-4o', provider: :openai))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'omits model key when model is nil' do
|
|
119
|
+
client.review_pull_request(owner: 'o', repo: 'r', pull_number: 1)
|
|
120
|
+
expect(Legion::LLM).to have_received(:chat).with(hash_not_including(:model))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'omits provider key when provider is nil' do
|
|
124
|
+
client.review_pull_request(owner: 'o', repo: 'r', pull_number: 1)
|
|
125
|
+
expect(Legion::LLM).to have_received(:chat).with(hash_not_including(:provider))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
101
128
|
end
|
|
102
129
|
end
|