lex-swarm-github 0.1.2 → 0.2.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: d40757aca13c9bab82f57b358f6206276198c6dfd04ffa3be18c5ad4e1a967e5
4
- data.tar.gz: d76193d6b724bca39020124a8131e57cc7a2746caa05237acb30c6060c39b1c0
3
+ metadata.gz: 164c564b62ef684027798c73f8c8137083a55e72df5f0d9204a627789a172472
4
+ data.tar.gz: 4368bd456b18a7af926460954c76ba8c4da25bbf2233dcdc1541c9fb6949e9ff
5
5
  SHA512:
6
- metadata.gz: ff0459d83eeb35e01951212ee17867bfe8ad7f4e38f6df0af3dde9a59a193c3e15cf86b790b4994bb3484bf70cd192d0e337c93043e89c2fe0f99fe8d327e29f
7
- data.tar.gz: 147be637157b8cddb282d39944ee03f5085c33ca52e2210c9788e1b8ae457b0978efd0bd29116dea418eb88a7ab8fc9e686907d7113b23f795d46c8f7d2e378d
6
+ metadata.gz: 022a87a111bda8aadf4517c53a70573f5cc9b95f53dad7eb9c4b7c870d695ce783084ff98e64e64b3d769ec8849e8c03112ea0f6ce55e59a9d2dd6f508a2bdb5
7
+ data.tar.gz: 60ac0a083878d8bfd2bce7513a91998cddb4c26e324a1160a8f9af1f6f62c05f040ebff300efef86fc399647bccad08561d4e1608e2b7a3db4420232b194f69a
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/subscription'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module SwarmGithub
8
+ module Actor
9
+ class PrWebhook < Legion::Extensions::Actors::Subscription
10
+ def runner_class
11
+ Legion::Extensions::SwarmGithub::Runners::PrPipeline
12
+ end
13
+
14
+ def runner_function
15
+ 'run_review_pipeline_from_webhook'
16
+ end
17
+
18
+ def check_subtask?
19
+ false
20
+ end
21
+
22
+ def generate_task?
23
+ false
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -3,12 +3,20 @@
3
3
  require 'legion/extensions/swarm_github/helpers/pipeline'
4
4
  require 'legion/extensions/swarm_github/helpers/issue_tracker'
5
5
  require 'legion/extensions/swarm_github/runners/github_swarm'
6
+ require 'legion/extensions/swarm_github/runners/pull_request_reviewer'
7
+ require 'legion/extensions/swarm_github/runners/review_poster'
8
+ require 'legion/extensions/swarm_github/runners/review_notifier'
9
+ require 'legion/extensions/swarm_github/runners/pr_pipeline'
6
10
 
7
11
  module Legion
8
12
  module Extensions
9
13
  module SwarmGithub
10
14
  class Client
11
15
  include Runners::GithubSwarm
16
+ include Runners::PullRequestReviewer
17
+ include Runners::ReviewPoster
18
+ include Runners::ReviewNotifier
19
+ include Runners::PrPipeline
12
20
 
13
21
  def initialize(**)
14
22
  @issue_tracker = Helpers::IssueTracker.new
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SwarmGithub
6
+ module Helpers
7
+ module DiffChunker
8
+ DEFAULT_MAX_CHARS = 12_000
9
+
10
+ module_function
11
+
12
+ def chunk_files(files, max_chars: DEFAULT_MAX_CHARS)
13
+ files = files.select { |f| f[:patch] && !f[:patch].empty? }
14
+ return [] if files.empty?
15
+
16
+ chunks = []
17
+ current_chunk = []
18
+ current_size = 0
19
+
20
+ files.each do |file|
21
+ patch = file[:patch]
22
+ patch = patch[0, max_chars] if patch.length > max_chars
23
+ entry = { filename: file[:filename], patch: patch }
24
+
25
+ if current_size + patch.length > max_chars && !current_chunk.empty?
26
+ chunks << current_chunk
27
+ current_chunk = []
28
+ current_size = 0
29
+ end
30
+
31
+ current_chunk << entry
32
+ current_size += patch.length
33
+ end
34
+
35
+ chunks << current_chunk unless current_chunk.empty?
36
+ chunks
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SwarmGithub
6
+ module Runners
7
+ module PrPipeline
8
+ REVIEWABLE_ACTIONS = %w[opened synchronize reopened].freeze
9
+ DEFAULT_SLACK_CHANNEL = '#code-reviews'
10
+
11
+ def run_review_pipeline(owner:, repo:, pull_number:, slack_channel: nil, **)
12
+ channel = slack_channel || DEFAULT_SLACK_CHANNEL
13
+
14
+ review = review_pull_request(owner: owner, repo: repo, pull_number: pull_number)
15
+ return { review: review, post: nil, notify: nil } unless review[:status] == 'reviewed'
16
+
17
+ post = post_review(owner: owner, repo: repo, pull_number: pull_number, review: review)
18
+ notify = notify_review(channel: channel, pull_ref: "#{owner}/#{repo}##{pull_number}",
19
+ review: review, post_result: post)
20
+
21
+ { review: review, post: post, notify: notify }
22
+ end
23
+
24
+ def run_review_pipeline_from_webhook(payload:, slack_channel: nil, **)
25
+ action = payload['action']
26
+ return { skipped: true, reason: 'action not reviewable' } unless REVIEWABLE_ACTIONS.include?(action)
27
+
28
+ full_name = payload.dig('repository', 'full_name') || ''
29
+ owner, repo = full_name.split('/', 2)
30
+ pull_number = payload.dig('pull_request', 'number')
31
+
32
+ run_review_pipeline(owner: owner, repo: repo, pull_number: pull_number,
33
+ slack_channel: slack_channel)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SwarmGithub
6
+ module Runners
7
+ module ReviewNotifier
8
+ def notify_review(channel:, pull_ref:, review:, post_result:, **)
9
+ return { notified: false, reason: 'review not posted' } unless post_result[:posted]
10
+
11
+ return { notified: false, reason: 'lex-slack not available' } unless defined?(Legion::Extensions::Slack::Client)
12
+
13
+ message = format_slack_message(pull_ref: pull_ref, review: review, post_result: post_result)
14
+ result = Legion::Extensions::Slack::Client.new(**).post_message(
15
+ channel: channel, text: message
16
+ )
17
+
18
+ { notified: result[:ok] == true, channel: channel, ts: result[:ts] }
19
+ rescue StandardError => e
20
+ { notified: false, reason: "slack error: #{e.message}" }
21
+ end
22
+
23
+ private
24
+
25
+ def format_slack_message(pull_ref:, review:, post_result:)
26
+ comments_count = post_result[:comments_count] || 0
27
+ files = review[:files_reviewed] || 0
28
+
29
+ parts = [":mag: *AI Code Review* for `#{pull_ref}` (#{files} file#{'s' if files != 1})"]
30
+ parts << review[:summary] if review[:summary]
31
+
32
+ parts << if comments_count.positive?
33
+ ":memo: #{comments_count} comment#{'s' if comments_count != 1} posted"
34
+ else
35
+ ':white_check_mark: No issues found'
36
+ end
37
+
38
+ parts.join("\n")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SwarmGithub
6
+ module Runners
7
+ module ReviewPoster
8
+ def post_review(owner:, repo:, pull_number:, review:, **)
9
+ return { posted: false, reason: 'review was skipped' } unless review[:status] == 'reviewed'
10
+
11
+ return { posted: false, reason: 'lex-github not available' } unless defined?(Legion::Extensions::Github::Client)
12
+
13
+ body = format_review_body(review)
14
+ inline_comments = format_inline_comments(review[:comments] || [])
15
+
16
+ result = Legion::Extensions::Github::Client.new.create_review(
17
+ owner: owner, repo: repo, pull_number: pull_number,
18
+ body: body, comments: inline_comments
19
+ )
20
+
21
+ review_id = result.dig(:result, 'id') || result.dig(:result, :id)
22
+ { posted: true, review_id: review_id, comments_count: inline_comments.size }
23
+ rescue StandardError => e
24
+ { posted: false, reason: "post failed: #{e.message}" }
25
+ end
26
+
27
+ private
28
+
29
+ def format_review_body(review)
30
+ parts = ["**Legion AI Review** (#{review[:files_reviewed]} file#{'s' if review[:files_reviewed] != 1} reviewed)"]
31
+ parts << ''
32
+ parts << review[:summary] if review[:summary]
33
+
34
+ comments = review[:comments] || []
35
+ if comments.any?
36
+ by_severity = comments.group_by { |c| c[:severity] || 'info' }
37
+ counts = by_severity.map { |sev, list| "#{list.size} #{sev}" }.join(', ')
38
+ parts << ''
39
+ parts << "Findings: #{counts}"
40
+ end
41
+
42
+ parts.join("\n")
43
+ end
44
+
45
+ def format_inline_comments(comments)
46
+ comments.filter_map do |c|
47
+ next unless c[:file] && c[:message]
48
+
49
+ severity_icon = case c[:severity]
50
+ when 'error' then '**Error**'
51
+ when 'warning' then 'Warning'
52
+ else 'Note'
53
+ end
54
+
55
+ {
56
+ path: c[:file],
57
+ position: c[:line] || 1,
58
+ body: "#{severity_icon}: #{c[:message]}"
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module SwarmGithub
6
- VERSION = '0.1.2'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
9
9
  end
@@ -5,6 +5,10 @@ require 'legion/extensions/swarm_github/helpers/pipeline'
5
5
  require 'legion/extensions/swarm_github/helpers/issue_tracker'
6
6
  require 'legion/extensions/swarm_github/runners/github_swarm'
7
7
  require 'legion/extensions/swarm_github/runners/pull_request_reviewer'
8
+ require 'legion/extensions/swarm_github/helpers/diff_chunker'
9
+ require 'legion/extensions/swarm_github/runners/review_poster'
10
+ require 'legion/extensions/swarm_github/runners/review_notifier'
11
+ require 'legion/extensions/swarm_github/runners/pr_pipeline'
8
12
 
9
13
  module Legion
10
14
  module Extensions
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # Stub base class for actor
6
+ unless defined?(Legion::Extensions::Actors::Subscription)
7
+ module Legion
8
+ module Extensions
9
+ module Actors
10
+ class Subscription
11
+ def initialize(**); end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ $LOADED_FEATURES << 'legion/extensions/actors/subscription'
17
+ end
18
+
19
+ require 'legion/extensions/swarm_github/actors/pr_webhook'
20
+
21
+ RSpec.describe Legion::Extensions::SwarmGithub::Actor::PrWebhook do
22
+ let(:actor) { described_class.allocate }
23
+
24
+ it 'defines runner_class as PrPipeline' do
25
+ expect(actor.runner_class).to eq(Legion::Extensions::SwarmGithub::Runners::PrPipeline)
26
+ end
27
+
28
+ it 'defines runner_function as run_review_pipeline_from_webhook' do
29
+ expect(actor.runner_function).to eq('run_review_pipeline_from_webhook')
30
+ end
31
+
32
+ it 'does not check subtasks' do
33
+ expect(actor.check_subtask?).to be false
34
+ end
35
+
36
+ it 'does not generate tasks' do
37
+ expect(actor.generate_task?).to be false
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/swarm_github/helpers/diff_chunker'
5
+
6
+ RSpec.describe Legion::Extensions::SwarmGithub::Helpers::DiffChunker do
7
+ describe '.chunk_files' do
8
+ let(:small_file) { { filename: 'a.rb', patch: '+ line1' } }
9
+ let(:large_patch) { "+ #{'x' * 6000}" }
10
+ let(:large_file) { { filename: 'b.rb', patch: large_patch } }
11
+
12
+ it 'returns a single chunk when files fit within limit' do
13
+ chunks = described_class.chunk_files([small_file], max_chars: 4000)
14
+ expect(chunks.size).to eq(1)
15
+ expect(chunks.first.size).to eq(1)
16
+ expect(chunks.first.first[:filename]).to eq('a.rb')
17
+ end
18
+
19
+ it 'splits files into multiple chunks when total exceeds limit' do
20
+ files = [small_file, large_file]
21
+ chunks = described_class.chunk_files(files, max_chars: 4000)
22
+ expect(chunks.size).to eq(2)
23
+ end
24
+
25
+ it 'truncates a single file patch that exceeds the limit' do
26
+ chunks = described_class.chunk_files([large_file], max_chars: 1000)
27
+ expect(chunks.size).to eq(1)
28
+ expect(chunks.first.first[:patch].length).to be <= 1000
29
+ end
30
+
31
+ it 'preserves filename in each chunk entry' do
32
+ chunks = described_class.chunk_files([small_file, large_file], max_chars: 4000)
33
+ all_files = chunks.flatten.map { |f| f[:filename] }
34
+ expect(all_files).to include('a.rb', 'b.rb')
35
+ end
36
+
37
+ it 'uses default max_chars of 12000' do
38
+ files = Array.new(5) { |i| { filename: "f#{i}.rb", patch: "+ #{'y' * 3000}" } }
39
+ chunks = described_class.chunk_files(files)
40
+ expect(chunks.size).to be > 1
41
+ end
42
+
43
+ it 'handles empty file list' do
44
+ chunks = described_class.chunk_files([])
45
+ expect(chunks).to eq([])
46
+ end
47
+
48
+ it 'skips files with nil patch' do
49
+ nil_patch = { filename: 'binary.png', patch: nil }
50
+ chunks = described_class.chunk_files([nil_patch, small_file])
51
+ all_files = chunks.flatten.map { |f| f[:filename] }
52
+ expect(all_files).not_to include('binary.png')
53
+ expect(all_files).to include('a.rb')
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/swarm_github/helpers/pipeline'
5
+ require 'legion/extensions/swarm_github/runners/pull_request_reviewer'
6
+ require 'legion/extensions/swarm_github/runners/review_poster'
7
+ require 'legion/extensions/swarm_github/runners/review_notifier'
8
+ require 'legion/extensions/swarm_github/runners/pr_pipeline'
9
+
10
+ RSpec.describe Legion::Extensions::SwarmGithub::Runners::PrPipeline do
11
+ let(:pipeline) do
12
+ klass = Class.new do
13
+ include Legion::Extensions::SwarmGithub::Runners::PullRequestReviewer
14
+ include Legion::Extensions::SwarmGithub::Runners::ReviewPoster
15
+ include Legion::Extensions::SwarmGithub::Runners::ReviewNotifier
16
+ include Legion::Extensions::SwarmGithub::Runners::PrPipeline
17
+ end
18
+ klass.new
19
+ end
20
+
21
+ describe '#run_review_pipeline' do
22
+ let(:review_result) do
23
+ { status: 'reviewed', pr: 'org/repo#1', files_reviewed: 2,
24
+ summary: 'OK', comments: [] }
25
+ end
26
+ let(:post_result) { { posted: true, review_id: 1, comments_count: 0 } }
27
+ let(:notify_result) { { notified: true } }
28
+
29
+ before do
30
+ allow(pipeline).to receive(:review_pull_request).and_return(review_result)
31
+ allow(pipeline).to receive(:post_review).and_return(post_result)
32
+ allow(pipeline).to receive(:notify_review).and_return(notify_result)
33
+ end
34
+
35
+ it 'calls review, post, notify in sequence' do
36
+ expect(pipeline).to receive(:review_pull_request).ordered
37
+ expect(pipeline).to receive(:post_review).ordered
38
+ expect(pipeline).to receive(:notify_review).ordered
39
+
40
+ pipeline.run_review_pipeline(owner: 'org', repo: 'repo', pull_number: 1)
41
+ end
42
+
43
+ it 'returns combined result' do
44
+ result = pipeline.run_review_pipeline(owner: 'org', repo: 'repo', pull_number: 1)
45
+ expect(result[:review][:status]).to eq('reviewed')
46
+ expect(result[:post][:posted]).to be true
47
+ expect(result[:notify][:notified]).to be true
48
+ end
49
+
50
+ it 'passes slack_channel to notify' do
51
+ expect(pipeline).to receive(:notify_review).with(hash_including(channel: '#my-reviews'))
52
+
53
+ pipeline.run_review_pipeline(owner: 'org', repo: 'repo', pull_number: 1,
54
+ slack_channel: '#my-reviews')
55
+ end
56
+
57
+ it 'uses default channel when none provided' do
58
+ expect(pipeline).to receive(:notify_review).with(hash_including(channel: '#code-reviews'))
59
+
60
+ pipeline.run_review_pipeline(owner: 'org', repo: 'repo', pull_number: 1)
61
+ end
62
+
63
+ it 'skips post and notify when review is skipped' do
64
+ allow(pipeline).to receive(:review_pull_request)
65
+ .and_return({ status: 'skipped', reason: 'no files' })
66
+
67
+ expect(pipeline).not_to receive(:post_review)
68
+ expect(pipeline).not_to receive(:notify_review)
69
+
70
+ result = pipeline.run_review_pipeline(owner: 'org', repo: 'repo', pull_number: 1)
71
+ expect(result[:review][:status]).to eq('skipped')
72
+ expect(result[:post]).to be_nil
73
+ expect(result[:notify]).to be_nil
74
+ end
75
+ end
76
+
77
+ describe '#run_review_pipeline_from_webhook' do
78
+ let(:review_result) do
79
+ { status: 'reviewed', pr: 'LegionIO/core#7', files_reviewed: 2,
80
+ summary: 'OK', comments: [] }
81
+ end
82
+
83
+ before do
84
+ allow(pipeline).to receive(:review_pull_request).and_return(review_result)
85
+ allow(pipeline).to receive(:post_review).and_return({ posted: true, review_id: 1, comments_count: 0 })
86
+ allow(pipeline).to receive(:notify_review).and_return({ notified: true })
87
+ end
88
+
89
+ it 'extracts owner, repo, pull_number from pull_request event' do
90
+ payload = {
91
+ 'action' => 'opened',
92
+ 'pull_request' => { 'number' => 7 },
93
+ 'repository' => { 'full_name' => 'LegionIO/core' }
94
+ }
95
+
96
+ expect(pipeline).to receive(:review_pull_request)
97
+ .with(hash_including(owner: 'LegionIO', repo: 'core', pull_number: 7))
98
+ .and_return(review_result)
99
+
100
+ pipeline.run_review_pipeline_from_webhook(payload: payload)
101
+ end
102
+
103
+ it 'ignores non-reviewable actions' do
104
+ payload = { 'action' => 'closed', 'pull_request' => { 'number' => 1 },
105
+ 'repository' => { 'full_name' => 'a/b' } }
106
+
107
+ result = pipeline.run_review_pipeline_from_webhook(payload: payload)
108
+ expect(result[:skipped]).to be true
109
+ expect(result[:reason]).to eq('action not reviewable')
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/swarm_github/runners/review_notifier'
5
+
6
+ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ReviewNotifier do
7
+ let(:notifier) { Class.new { include Legion::Extensions::SwarmGithub::Runners::ReviewNotifier }.new }
8
+
9
+ describe '#notify_review' do
10
+ let(:review_result) do
11
+ {
12
+ status: 'reviewed', pr: 'org/repo#42', files_reviewed: 3,
13
+ summary: 'Found 1 issue',
14
+ comments: [{ file: 'lib/foo.rb', severity: 'error', message: 'Bug' }]
15
+ }
16
+ end
17
+
18
+ let(:post_result) { { posted: true, review_id: 999, comments_count: 1 } }
19
+
20
+ context 'when lex-slack is available' do
21
+ let(:slack_client) { double('Slack::Client') }
22
+
23
+ before do
24
+ stub_const('Legion::Extensions::Slack::Client', Class.new)
25
+ allow(Legion::Extensions::Slack::Client).to receive(:new).and_return(slack_client)
26
+ end
27
+
28
+ it 'posts a summary to the configured channel' do
29
+ expect(slack_client).to receive(:post_message) do |args|
30
+ expect(args[:channel]).to eq('#code-review')
31
+ expect(args[:text]).to include('org/repo#42')
32
+ expect(args[:text]).to include('3 file')
33
+ { ok: true, ts: '123.456' }
34
+ end
35
+
36
+ result = notifier.notify_review(
37
+ channel: '#code-review', pull_ref: 'org/repo#42',
38
+ review: review_result, post_result: post_result
39
+ )
40
+ expect(result[:notified]).to be true
41
+ end
42
+
43
+ it 'includes comment count in message' do
44
+ expect(slack_client).to receive(:post_message) do |args|
45
+ expect(args[:text]).to include('1 comment')
46
+ { ok: true, ts: '1' }
47
+ end
48
+
49
+ notifier.notify_review(
50
+ channel: '#reviews', pull_ref: 'org/repo#42',
51
+ review: review_result, post_result: post_result
52
+ )
53
+ end
54
+
55
+ it 'sends clean message when review had no issues' do
56
+ clean_review = review_result.merge(comments: [], summary: 'All clear')
57
+ clean_post = post_result.merge(comments_count: 0)
58
+
59
+ expect(slack_client).to receive(:post_message) do |args|
60
+ expect(args[:text]).to include('No issues found')
61
+ { ok: true, ts: '1' }
62
+ end
63
+
64
+ notifier.notify_review(
65
+ channel: '#reviews', pull_ref: 'org/repo#42',
66
+ review: clean_review, post_result: clean_post
67
+ )
68
+ end
69
+ end
70
+
71
+ context 'when lex-slack is not available' do
72
+ it 'returns not_available' do
73
+ result = notifier.notify_review(
74
+ channel: '#reviews', pull_ref: 'org/repo#42',
75
+ review: review_result, post_result: post_result
76
+ )
77
+ expect(result[:notified]).to be false
78
+ expect(result[:reason]).to eq('lex-slack not available')
79
+ end
80
+ end
81
+
82
+ context 'when review was not posted' do
83
+ let(:failed_post) { { posted: false, reason: 'lex-github not available' } }
84
+
85
+ it 'skips notification' do
86
+ result = notifier.notify_review(
87
+ channel: '#reviews', pull_ref: 'org/repo#42',
88
+ review: review_result, post_result: failed_post
89
+ )
90
+ expect(result[:notified]).to be false
91
+ expect(result[:reason]).to eq('review not posted')
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/swarm_github/runners/review_poster'
5
+
6
+ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ReviewPoster do
7
+ let(:poster) { Class.new { include Legion::Extensions::SwarmGithub::Runners::ReviewPoster }.new }
8
+
9
+ describe '#post_review' do
10
+ let(:review_result) do
11
+ {
12
+ status: 'reviewed', pr: 'org/repo#42', files_reviewed: 3,
13
+ summary: 'Found 1 issue',
14
+ comments: [{ file: 'lib/foo.rb', line: 10, severity: 'error', message: 'SQL injection' }]
15
+ }
16
+ end
17
+
18
+ context 'when lex-github is available' do
19
+ let(:github_client) { double('Github::Client') }
20
+
21
+ before do
22
+ stub_const('Legion::Extensions::Github::Client', Class.new)
23
+ allow(Legion::Extensions::Github::Client).to receive(:new).and_return(github_client)
24
+ end
25
+
26
+ it 'posts a review to GitHub with formatted body' do
27
+ expect(github_client).to receive(:create_review) do |args|
28
+ expect(args[:owner]).to eq('org')
29
+ expect(args[:repo]).to eq('repo')
30
+ expect(args[:pull_number]).to eq(42)
31
+ expect(args[:body]).to include('Found 1 issue')
32
+ expect(args[:body]).to include('3 file')
33
+ { result: { 'id' => 999 } }
34
+ end
35
+
36
+ result = poster.post_review(owner: 'org', repo: 'repo', pull_number: 42, review: review_result)
37
+ expect(result[:posted]).to be true
38
+ expect(result[:review_id]).to eq(999)
39
+ end
40
+
41
+ it 'converts comments to GitHub inline format' do
42
+ expect(github_client).to receive(:create_review) do |args|
43
+ expect(args[:comments].size).to eq(1)
44
+ expect(args[:comments].first[:path]).to eq('lib/foo.rb')
45
+ expect(args[:comments].first[:body]).to include('SQL injection')
46
+ { result: { 'id' => 999 } }
47
+ end
48
+
49
+ poster.post_review(owner: 'org', repo: 'repo', pull_number: 42, review: review_result)
50
+ end
51
+
52
+ it 'handles review with no comments' do
53
+ no_comments = review_result.merge(comments: [], summary: 'All clear')
54
+ expect(github_client).to receive(:create_review) do |args|
55
+ expect(args[:comments]).to eq([])
56
+ { result: { 'id' => 1 } }
57
+ end
58
+
59
+ result = poster.post_review(owner: 'org', repo: 'repo', pull_number: 42, review: no_comments)
60
+ expect(result[:posted]).to be true
61
+ end
62
+ end
63
+
64
+ context 'when lex-github is not available' do
65
+ it 'returns not_available error' do
66
+ result = poster.post_review(owner: 'org', repo: 'repo', pull_number: 42, review: review_result)
67
+ expect(result[:posted]).to be false
68
+ expect(result[:reason]).to eq('lex-github not available')
69
+ end
70
+ end
71
+
72
+ context 'when review was skipped' do
73
+ let(:skipped) { { status: 'skipped', reason: 'no files' } }
74
+
75
+ it 'returns skipped without posting' do
76
+ result = poster.post_review(owner: 'org', repo: 'repo', pull_number: 1, review: skipped)
77
+ expect(result[:posted]).to be false
78
+ expect(result[:reason]).to eq('review was skipped')
79
+ end
80
+ end
81
+ end
82
+ end
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.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -20,19 +20,29 @@ files:
20
20
  - Gemfile
21
21
  - lex-swarm-github.gemspec
22
22
  - lib/legion/extensions/swarm_github.rb
23
+ - lib/legion/extensions/swarm_github/actors/pr_webhook.rb
23
24
  - lib/legion/extensions/swarm_github/actors/stale_issues.rb
24
25
  - lib/legion/extensions/swarm_github/client.rb
26
+ - lib/legion/extensions/swarm_github/helpers/diff_chunker.rb
25
27
  - lib/legion/extensions/swarm_github/helpers/issue_tracker.rb
26
28
  - lib/legion/extensions/swarm_github/helpers/pipeline.rb
27
29
  - lib/legion/extensions/swarm_github/runners/github_swarm.rb
30
+ - lib/legion/extensions/swarm_github/runners/pr_pipeline.rb
28
31
  - lib/legion/extensions/swarm_github/runners/pull_request_reviewer.rb
32
+ - lib/legion/extensions/swarm_github/runners/review_notifier.rb
33
+ - lib/legion/extensions/swarm_github/runners/review_poster.rb
29
34
  - lib/legion/extensions/swarm_github/version.rb
35
+ - spec/legion/extensions/swarm_github/actors/pr_webhook_spec.rb
30
36
  - spec/legion/extensions/swarm_github/actors/stale_issues_spec.rb
31
37
  - spec/legion/extensions/swarm_github/client_spec.rb
38
+ - spec/legion/extensions/swarm_github/helpers/diff_chunker_spec.rb
32
39
  - spec/legion/extensions/swarm_github/helpers/issue_tracker_spec.rb
33
40
  - spec/legion/extensions/swarm_github/helpers/pipeline_spec.rb
34
41
  - spec/legion/extensions/swarm_github/runners/github_swarm_spec.rb
42
+ - spec/legion/extensions/swarm_github/runners/pr_pipeline_spec.rb
35
43
  - spec/legion/extensions/swarm_github/runners/pull_request_reviewer_spec.rb
44
+ - spec/legion/extensions/swarm_github/runners/review_notifier_spec.rb
45
+ - spec/legion/extensions/swarm_github/runners/review_poster_spec.rb
36
46
  - spec/spec_helper.rb
37
47
  homepage: https://github.com/LegionIO/lex-swarm-github
38
48
  licenses: