lex-swarm-github 0.2.3 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dac167b9c8a974aa180eb0519a006ddd30a839ccd6e3a00ec42618d1d8f6021
4
- data.tar.gz: f298aaa736b6ab8ffddba8614c19ba31a7b16d23a0a95f51d4cae24b04e71e88
3
+ metadata.gz: a5b852399782262ebab25e020d689b147a55ce6f256134a9d53137b223e6ba8e
4
+ data.tar.gz: 8804deca49b8a4bd37a262d90e152ade0e2359fd065f40ea4d32604bb0f885bf
5
5
  SHA512:
6
- metadata.gz: 679c7437587c3e567d673ceab73c4c375c48671820433de46b4f569d9d6218ac9d9dd8bdff0815aba012a7a42ea768791a90797581e030393d1018af7cbd621c
7
- data.tar.gz: 2bf0b8ad3718eeb2291720e71c3cf1d809dd33fee39409d826c64b4f3fefe53b05004a7e77f20461e2a54f1955a18bb28799123ddac0da67c483cc3500492912
6
+ metadata.gz: 798c161250c26ba51ca15c0696d7f97dcc794d78b34fc1696b466e0aba16196368b96f2e5fc9351c0b34244efdbf3fe0c35b0a7cca4bc09d684d1fd014cec361
7
+ data.tar.gz: 5ad660b2154048f0974ff1ac444ac0a96f282c9ffa789455bb901a29b3c737cfe103ea0bfb8c177862daec6b11efe2d4606a221edcedb8689005b6c8dea76d38
@@ -10,8 +10,9 @@ module Legion
10
10
  REVIEWABLE_ACTIONS = %w[opened synchronize reopened].freeze
11
11
  DEFAULT_SLACK_CHANNEL = '#code-reviews'
12
12
 
13
- def run_review_pipeline(owner:, repo:, pull_number:, slack_channel: nil, **)
13
+ def run_review_pipeline(owner:, repo:, pull_number:, slack_channel: nil, **opts)
14
14
  channel = slack_channel || DEFAULT_SLACK_CHANNEL
15
+ issue_number = opts[:issue_number]
15
16
 
16
17
  review = review_pull_request(owner: owner, repo: repo, pull_number: pull_number)
17
18
  return { review: review, post: nil, notify: nil } unless review[:status] == 'reviewed'
@@ -20,7 +21,10 @@ module Legion
20
21
  notify = notify_review(channel: channel, pull_ref: "#{owner}/#{repo}##{pull_number}",
21
22
  review: review, post_result: post)
22
23
 
23
- { review: review, post: post, notify: notify }
24
+ result = { review: review, post: post, notify: notify }
25
+ bridge_review_to_issue(repo: "#{owner}/#{repo}", issue_number: issue_number,
26
+ review_result: result)
27
+ result
24
28
  end
25
29
 
26
30
  def run_review_pipeline_from_webhook(payload:, slack_channel: nil, **)
@@ -35,6 +39,18 @@ module Legion
35
39
  slack_channel: slack_channel)
36
40
  end
37
41
 
42
+ def bridge_review_to_issue(repo:, issue_number:, review_result:)
43
+ return unless issue_number
44
+
45
+ review_comments = review_result.dig(:review, :comments) || []
46
+ approved = review_comments.none? { |c| %w[critical high].include?(c[:severity]&.to_s&.downcase) }
47
+ @issue_tracker&.record_validation(
48
+ "#{repo}##{issue_number}",
49
+ validator: :code_review,
50
+ approved: approved
51
+ )
52
+ end
53
+
38
54
  def handle_mesh_review_request(payload:, charter_id: nil, **)
39
55
  owner = payload[:owner] || payload['owner']
40
56
  repo = payload[:repo] || payload['repo']
@@ -9,8 +9,7 @@ module Legion
9
9
  files = fetch_pr_files(owner: owner, repo: repo, pull_number: pull_number)
10
10
  return { status: 'skipped', reason: 'no files' } if files.empty?
11
11
 
12
- diff_text = files.map { |f| "#{f[:filename]}:\n#{f[:patch]}" }.join("\n\n")
13
- review = generate_review(diff_text)
12
+ review = generate_review(files)
14
13
 
15
14
  {
16
15
  status: 'reviewed',
@@ -33,24 +32,43 @@ module Legion
33
32
  []
34
33
  end
35
34
 
36
- def generate_review(diff_text)
37
- return { summary: 'LLM unavailable', comments: [] } unless defined?(Legion::LLM)
38
-
39
- result = Legion::LLM.chat(
40
- message: "#{code_review_prompt}\n\n#{diff_text[0..12_000]}",
41
- caller: { extension: 'lex-swarm-github' }
42
- )
43
- ::JSON.parse(result[:content] || '{}', symbolize_names: true)
35
+ def generate_review(files)
36
+ chunks = Helpers::DiffChunker.chunk_files(files)
37
+ reviews = chunks.map do |chunk|
38
+ diff_text = chunk.map { |f| "--- #{f[:filename]} ---\n#{f[:patch]}" }.join("\n\n")
39
+ prompt = code_review_prompt(diff_text)
40
+ response = Legion::LLM.chat(message: prompt, caller: { extension: 'lex-swarm-github' })
41
+ parse_review_response(response)
42
+ end
43
+ merge_chunk_reviews(reviews)
44
44
  rescue StandardError => e
45
- { summary: "Review failed: #{e.message}", comments: [] }
45
+ Legion::Logging::Logger.warn "Review generation failed: #{e.message}"
46
+ { summary: 'Review generation failed', comments: [] }
47
+ end
48
+
49
+ def merge_chunk_reviews(reviews)
50
+ merged_comments = reviews.flat_map { |r| r[:comments] || [] }
51
+ summaries = reviews.map { |r| r[:summary] }.compact
52
+ {
53
+ summary: summaries.join("\n\n"),
54
+ comments: merged_comments
55
+ }
46
56
  end
47
57
 
48
- def code_review_prompt
58
+ def parse_review_response(response)
59
+ Legion::JSON.load(response)
60
+ rescue StandardError
61
+ { summary: response.to_s, comments: [] }
62
+ end
63
+
64
+ def code_review_prompt(diff_text)
49
65
  <<~PROMPT
50
66
  Review this code diff. Return JSON with:
51
67
  - "summary": 1-2 sentence overall assessment
52
68
  - "comments": array of {"file", "line", "severity", "message"}
53
69
  Severity: info, warning, error. Focus on bugs and security issues.
70
+
71
+ #{diff_text}
54
72
  PROMPT
55
73
  end
56
74
  end
@@ -11,7 +11,7 @@ module Legion
11
11
  return { notified: false, reason: 'lex-slack not available' } unless defined?(Legion::Extensions::Slack::Client)
12
12
 
13
13
  message = format_slack_message(pull_ref: pull_ref, review: review, post_result: post_result)
14
- result = Legion::Extensions::Slack::Client.new(**).post_message(
14
+ result = Legion::Extensions::Slack::Client.new.post_message(
15
15
  channel: channel, text: message
16
16
  )
17
17
 
@@ -15,7 +15,8 @@ module Legion
15
15
 
16
16
  result = Legion::Extensions::Github::Client.new.create_review(
17
17
  owner: owner, repo: repo, pull_number: pull_number,
18
- body: body, comments: inline_comments
18
+ body: body, comments: inline_comments,
19
+ event: review_event(review[:comments])
19
20
  )
20
21
 
21
22
  review_id = result.dig(:result, 'id') || result.dig(:result, :id)
@@ -26,6 +27,13 @@ module Legion
26
27
 
27
28
  private
28
29
 
30
+ def review_event(comments)
31
+ severities = (comments || []).map { |c| c[:severity]&.to_s&.downcase }
32
+ return 'REQUEST_CHANGES' if severities.any? { |s| %w[critical high].include?(s) }
33
+
34
+ 'APPROVE'
35
+ end
36
+
29
37
  def format_review_body(review)
30
38
  parts = ["**Legion AI Review** (#{review[:files_reviewed]} file#{'s' if review[:files_reviewed] != 1} reviewed)"]
31
39
  parts << ''
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module SwarmGithub
6
- VERSION = '0.2.3'
6
+ VERSION = '0.3.0'
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'legion/extensions/swarm_github/client'
4
5
  require 'legion/extensions/swarm_github/helpers/pipeline'
5
6
  require 'legion/extensions/swarm_github/helpers/mesh_integration'
6
7
  require 'legion/extensions/swarm_github/runners/pull_request_reviewer'
@@ -75,6 +76,29 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::PrPipeline do
75
76
  end
76
77
  end
77
78
 
79
+ describe '#run_review_pipeline with issue bridge' do
80
+ let(:client) { Legion::Extensions::SwarmGithub::Client.new }
81
+
82
+ before do
83
+ client.ingest_issue(repo: 'owner/repo', issue_number: 5, title: 'Fix bug')
84
+ client.claim_issue(key: 'owner/repo#5')
85
+ client.start_fix(key: 'owner/repo#5')
86
+ allow(client).to receive(:review_pull_request).and_return(
87
+ { status: 'reviewed', files_reviewed: 1, summary: 'OK', comments: [] }
88
+ )
89
+ allow(client).to receive(:post_review).and_return({ posted: true, review_id: 1, comments_count: 0 })
90
+ allow(client).to receive(:notify_review).and_return({ notified: false, reason: 'lex-slack not available' })
91
+ end
92
+
93
+ context 'when PR is linked to a tracked issue' do
94
+ it 'records review as validation on the linked issue' do
95
+ client.run_review_pipeline(owner: 'owner', repo: 'repo', pull_number: 1, issue_number: 5)
96
+ issue = client.get_issue(key: 'owner/repo#5')[:issue]
97
+ expect(issue[:validations]).not_to be_empty
98
+ end
99
+ end
100
+ end
101
+
78
102
  describe '#run_review_pipeline_from_webhook' do
79
103
  let(:review_result) do
80
104
  { status: 'reviewed', pr: 'LegionIO/core#7', files_reviewed: 2,
@@ -45,6 +45,26 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::PullRequestReviewer do
45
45
  end
46
46
  end
47
47
 
48
+ context 'when diff exceeds max_chars' do
49
+ let(:large_files) do
50
+ [
51
+ { filename: 'a.rb', patch: 'x' * 7000 },
52
+ { filename: 'b.rb', patch: 'y' * 7000 }
53
+ ]
54
+ end
55
+
56
+ before do
57
+ stub_const('Legion::LLM', Module.new)
58
+ allow(reviewer).to receive(:fetch_pr_files).and_return(large_files)
59
+ allow(Legion::LLM).to receive(:chat).and_return('{"summary":"ok","comments":[]}')
60
+ end
61
+
62
+ it 'calls LLM multiple times for chunked diffs' do
63
+ reviewer.review_pull_request(owner: 'owner', repo: 'repo', pull_number: 1)
64
+ expect(Legion::LLM).to have_received(:chat).at_least(2).times
65
+ end
66
+ end
67
+
48
68
  context 'when PR has multiple files with comments' do
49
69
  let(:files) do
50
70
  [
@@ -68,6 +68,26 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ReviewNotifier do
68
68
  end
69
69
  end
70
70
 
71
+ context 'when instantiating Slack client' do
72
+ let(:slack_client) { double('Slack::Client') }
73
+ let(:slack_client_class) { Class.new }
74
+
75
+ before do
76
+ stub_const('Legion::Extensions::Slack::Client', slack_client_class)
77
+ allow(slack_client_class).to receive(:new).and_return(slack_client)
78
+ allow(slack_client).to receive(:post_message).and_return({ ok: true, ts: '1' })
79
+ end
80
+
81
+ it 'instantiates Slack client with no arguments' do
82
+ notifier.notify_review(
83
+ channel: '#reviews', pull_ref: 'owner/repo#1',
84
+ review: review_result, post_result: post_result,
85
+ extra_kwarg: 'value'
86
+ )
87
+ expect(slack_client_class).to have_received(:new).with(no_args)
88
+ end
89
+ end
90
+
71
91
  context 'when lex-slack is not available' do
72
92
  it 'returns not_available' do
73
93
  result = notifier.notify_review(
@@ -61,6 +61,42 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ReviewPoster do
61
61
  end
62
62
  end
63
63
 
64
+ context 'when review has no critical or high severity comments' do
65
+ let(:github_client) { double('Github::Client') }
66
+ let(:review) { { status: 'reviewed', files_reviewed: 1, summary: 'Looks good', comments: [{ severity: 'info', message: 'nit' }] } }
67
+
68
+ before do
69
+ stub_const('Legion::Extensions::Github::Client', Class.new)
70
+ allow(Legion::Extensions::Github::Client).to receive(:new).and_return(github_client)
71
+ allow(github_client).to receive(:create_review).and_return({ result: { 'id' => 1 } })
72
+ end
73
+
74
+ it 'posts with APPROVE event' do
75
+ poster.post_review(owner: 'owner', repo: 'repo', pull_number: 1, review: review)
76
+ expect(github_client).to have_received(:create_review).with(
77
+ hash_including(event: 'APPROVE')
78
+ )
79
+ end
80
+ end
81
+
82
+ context 'when review has critical severity comments' do
83
+ let(:github_client) { double('Github::Client') }
84
+ let(:review) { { status: 'reviewed', files_reviewed: 1, summary: 'Issues found', comments: [{ severity: 'critical', message: 'bug' }] } }
85
+
86
+ before do
87
+ stub_const('Legion::Extensions::Github::Client', Class.new)
88
+ allow(Legion::Extensions::Github::Client).to receive(:new).and_return(github_client)
89
+ allow(github_client).to receive(:create_review).and_return({ result: { 'id' => 2 } })
90
+ end
91
+
92
+ it 'posts with REQUEST_CHANGES event' do
93
+ poster.post_review(owner: 'owner', repo: 'repo', pull_number: 1, review: review)
94
+ expect(github_client).to have_received(:create_review).with(
95
+ hash_including(event: 'REQUEST_CHANGES')
96
+ )
97
+ end
98
+ end
99
+
64
100
  context 'when lex-github is not available' do
65
101
  it 'returns not_available error' do
66
102
  result = poster.post_review(owner: 'org', repo: 'repo', pull_number: 42, review: review_result)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-swarm-github
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity