coverband 6.1.6 → 6.1.7
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/.github/workflows/main.yml +4 -4
- data/README.md +123 -0
- data/agents.md +217 -0
- data/bin/coverband-mcp +42 -0
- data/changes.md +18 -0
- data/coverband/log.272267 +1 -0
- data/coverband.gemspec +3 -1
- data/lib/coverband/collectors/route_tracker.rb +1 -1
- data/lib/coverband/collectors/view_tracker.rb +21 -13
- data/lib/coverband/configuration.rb +57 -18
- data/lib/coverband/integrations/resque.rb +2 -2
- data/lib/coverband/mcp/http_handler.rb +118 -0
- data/lib/coverband/mcp/server.rb +116 -0
- data/lib/coverband/mcp/tools/get_coverage_summary.rb +41 -0
- data/lib/coverband/mcp/tools/get_dead_methods.rb +69 -0
- data/lib/coverband/mcp/tools/get_file_coverage.rb +72 -0
- data/lib/coverband/mcp/tools/get_route_tracker_data.rb +60 -0
- data/lib/coverband/mcp/tools/get_translation_tracker_data.rb +60 -0
- data/lib/coverband/mcp/tools/get_uncovered_files.rb +73 -0
- data/lib/coverband/mcp/tools/get_view_tracker_data.rb +60 -0
- data/lib/coverband/mcp.rb +27 -0
- data/lib/coverband/reporters/base.rb +2 -4
- data/lib/coverband/reporters/web.rb +17 -14
- data/lib/coverband/utils/lines_classifier.rb +1 -1
- data/lib/coverband/utils/result.rb +2 -1
- data/lib/coverband/utils/source_file.rb +5 -5
- data/lib/coverband/utils/tasks.rb +31 -0
- data/lib/coverband/version.rb +1 -1
- data/lib/coverband.rb +2 -2
- data/test/coverband/file_store_integration_test.rb +72 -0
- data/test/coverband/file_store_redis_error_test.rb +56 -0
- data/test/coverband/github_issue_586_test.rb +46 -0
- data/test/coverband/initialization_timing_test.rb +71 -0
- data/test/coverband/mcp/http_handler_test.rb +159 -0
- data/test/coverband/mcp/security_test.rb +145 -0
- data/test/coverband/mcp/server_test.rb +125 -0
- data/test/coverband/mcp/tools/get_coverage_summary_test.rb +75 -0
- data/test/coverband/mcp/tools/get_dead_methods_test.rb +162 -0
- data/test/coverband/mcp/tools/get_file_coverage_test.rb +159 -0
- data/test/coverband/mcp/tools/get_route_tracker_data_test.rb +122 -0
- data/test/coverband/mcp/tools/get_translation_tracker_data_test.rb +122 -0
- data/test/coverband/mcp/tools/get_uncovered_files_test.rb +177 -0
- data/test/coverband/mcp/tools/get_view_tracker_data_test.rb +122 -0
- data/test/coverband/reporters/web_test.rb +5 -0
- data/test/coverband/tracker_initialization_test.rb +75 -0
- data/test/coverband/user_environment_simulation_test.rb +75 -0
- data/test/coverband/utils/lines_classifier_test.rb +1 -1
- data/test/integration/mcp_integration_test.rb +175 -0
- data/test/test_helper.rb +4 -5
- metadata +65 -6
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise LoadError, <<~MSG
|
|
7
|
+
The 'mcp' gem is required for MCP server support.
|
|
8
|
+
Add `gem 'mcp'` to your Gemfile and run `bundle install`.
|
|
9
|
+
MSG
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require "coverband"
|
|
13
|
+
require "coverband/utils/html_formatter"
|
|
14
|
+
require "coverband/utils/result"
|
|
15
|
+
require "coverband/utils/file_list"
|
|
16
|
+
require "coverband/utils/source_file"
|
|
17
|
+
require "coverband/utils/lines_classifier"
|
|
18
|
+
require "coverband/utils/results"
|
|
19
|
+
require "coverband/reporters/json_report"
|
|
20
|
+
|
|
21
|
+
# Load dead methods support if available
|
|
22
|
+
if defined?(RubyVM::AbstractSyntaxTree)
|
|
23
|
+
require "coverband/utils/dead_methods"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require_relative "mcp/server"
|
|
27
|
+
require_relative "mcp/http_handler"
|
|
@@ -55,13 +55,11 @@ module Coverband
|
|
|
55
55
|
fixed_report[name] = {}
|
|
56
56
|
report.each_pair do |key, vals|
|
|
57
57
|
filename = Coverband::Utils::AbsoluteFileConverter.convert(key)
|
|
58
|
-
|
|
58
|
+
if fixed_report[name].key?(filename) && fixed_report[name][filename][DATA_KEY] && vals[DATA_KEY]
|
|
59
59
|
merged_data = merge_arrays(fixed_report[name][filename][DATA_KEY], vals[DATA_KEY])
|
|
60
60
|
vals[DATA_KEY] = merged_data
|
|
61
|
-
vals
|
|
62
|
-
else
|
|
63
|
-
vals
|
|
64
61
|
end
|
|
62
|
+
fixed_report[name][filename] = vals
|
|
65
63
|
end
|
|
66
64
|
end
|
|
67
65
|
end
|
|
@@ -62,9 +62,9 @@ module Coverband
|
|
|
62
62
|
Coverband.configuration.trackers.each do |tracker|
|
|
63
63
|
if request_path_info.match(tracker.class::REPORT_ROUTE)
|
|
64
64
|
tracker_route = true
|
|
65
|
-
if request_path_info =~ %r{
|
|
65
|
+
if request_path_info =~ %r{/clear_.*_key}
|
|
66
66
|
return clear_abstract_tracking_key(tracker)
|
|
67
|
-
elsif request_path_info =~ %r{
|
|
67
|
+
elsif request_path_info =~ %r{/clear_.*}
|
|
68
68
|
return clear_abstract_tracking(tracker)
|
|
69
69
|
else
|
|
70
70
|
return [200, {"content-type" => "text/html"}, [display_abstract_tracker(tracker)]]
|
|
@@ -75,9 +75,9 @@ module Coverband
|
|
|
75
75
|
unless tracker_route
|
|
76
76
|
if request.post?
|
|
77
77
|
case request_path_info
|
|
78
|
-
when %r{
|
|
78
|
+
when %r{/clear_file}
|
|
79
79
|
clear_file
|
|
80
|
-
when %r{
|
|
80
|
+
when %r{/clear}
|
|
81
81
|
clear
|
|
82
82
|
else
|
|
83
83
|
[404, coverband_headers, ["404 error!"]]
|
|
@@ -86,21 +86,21 @@ module Coverband
|
|
|
86
86
|
case request_path_info
|
|
87
87
|
when /.*\.(css|js|gif|png)/
|
|
88
88
|
@static.call(env)
|
|
89
|
-
when %r{
|
|
89
|
+
when %r{/settings}
|
|
90
90
|
[200, coverband_headers, [settings]]
|
|
91
|
-
when %r{
|
|
91
|
+
when %r{/view_tracker_data}
|
|
92
92
|
[200, coverband_headers(content_type: "text/json"), [view_tracker_data]]
|
|
93
|
-
when %r{
|
|
93
|
+
when %r{/enriched_debug_data}
|
|
94
94
|
[200, coverband_headers(content_type: "text/json"), [enriched_debug_data]]
|
|
95
|
-
when %r{
|
|
95
|
+
when %r{/debug_data}
|
|
96
96
|
[200, coverband_headers(content_type: "text/json"), [debug_data]]
|
|
97
|
-
when %r{
|
|
97
|
+
when %r{/load_file_details}
|
|
98
98
|
[200, coverband_headers(content_type: "text/json"), [load_file_details]]
|
|
99
|
-
when %r{
|
|
99
|
+
when %r{/json}
|
|
100
100
|
[200, coverband_headers(content_type: "text/json"), [json]]
|
|
101
|
-
when %r{
|
|
101
|
+
when %r{/report_json}
|
|
102
102
|
[200, coverband_headers(content_type: "text/json"), [report_json]]
|
|
103
|
-
when %r{
|
|
103
|
+
when %r{/$}
|
|
104
104
|
[200, coverband_headers, [index]]
|
|
105
105
|
else
|
|
106
106
|
[404, coverband_headers, ["404 error!"]]
|
|
@@ -125,7 +125,10 @@ module Coverband
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def json
|
|
128
|
-
Coverband::Reporters::JSONReport.new(
|
|
128
|
+
Coverband::Reporters::JSONReport.new(
|
|
129
|
+
Coverband.configuration.store,
|
|
130
|
+
line_coverage: request.params["line_coverage"] == "true"
|
|
131
|
+
).report
|
|
129
132
|
end
|
|
130
133
|
|
|
131
134
|
def report_json
|
|
@@ -241,7 +244,7 @@ module Coverband
|
|
|
241
244
|
# %r{\/.*\/}.match?(request.path) ? request.path.match("\/.*\/")[0] : "/"
|
|
242
245
|
# ^^ the above is NOT valid Ruby 2.3/2.4 even though rubocop / standard think it is
|
|
243
246
|
def base_path
|
|
244
|
-
(request.path =~ %r{
|
|
247
|
+
(request.path =~ %r{/.*/}) ? request.path.match("/.*/")[0] : "/"
|
|
245
248
|
end
|
|
246
249
|
end
|
|
247
250
|
end
|
|
@@ -14,12 +14,13 @@ module Coverband
|
|
|
14
14
|
module Utils
|
|
15
15
|
class Result
|
|
16
16
|
extend Forwardable
|
|
17
|
+
|
|
17
18
|
# Returns the original Coverage.result used for this instance of Coverband::Result
|
|
18
19
|
attr_reader :original_result
|
|
19
20
|
# Returns all files that are applicable to this result (sans filters!)
|
|
20
21
|
# as instances of Coverband::SourceFile. Aliased as :source_files
|
|
21
22
|
attr_reader :files
|
|
22
|
-
|
|
23
|
+
alias_method :source_files, :files
|
|
23
24
|
# Explicitly set the Time this result has been created
|
|
24
25
|
attr_writer :created_at
|
|
25
26
|
|
|
@@ -29,9 +29,9 @@ module Coverband
|
|
|
29
29
|
attr_reader :coverage_posted
|
|
30
30
|
|
|
31
31
|
# Lets grab some fancy aliases, shall we?
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
alias_method :source, :src
|
|
33
|
+
alias_method :line, :line_number
|
|
34
|
+
alias_method :number, :line_number
|
|
35
35
|
|
|
36
36
|
def initialize(src, line_number, coverage, coverage_posted = nil)
|
|
37
37
|
raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
|
|
@@ -132,14 +132,14 @@ module Coverband
|
|
|
132
132
|
# suppress reading unused source code.
|
|
133
133
|
@src ||= File.open(filename, "rb", &:readlines)
|
|
134
134
|
end
|
|
135
|
-
|
|
135
|
+
alias_method :source, :src
|
|
136
136
|
|
|
137
137
|
# Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
|
|
138
138
|
# and thus including coverage data. Aliased as :source_lines
|
|
139
139
|
def lines
|
|
140
140
|
@lines ||= build_lines
|
|
141
141
|
end
|
|
142
|
-
|
|
142
|
+
alias_method :source_lines, :lines
|
|
143
143
|
|
|
144
144
|
def build_lines
|
|
145
145
|
coverage_exceeding_source_warn if coverage.size > src.size
|
|
@@ -126,6 +126,37 @@ namespace :coverband do
|
|
|
126
126
|
Port: ENV.fetch("COVERBAND_COVERAGE_PORT", 9022).to_i
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
desc "Start MCP server for AI assistant integration (set COVERBAND_MCP_HTTP=true for HTTP mode)"
|
|
130
|
+
task :mcp do
|
|
131
|
+
if Rake::Task.task_defined?("environment")
|
|
132
|
+
Rake.application["environment"].invoke
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
begin
|
|
136
|
+
require "coverband/mcp"
|
|
137
|
+
rescue LoadError
|
|
138
|
+
abort "The 'mcp' gem is required for MCP server support. Add `gem 'mcp'` to your Gemfile."
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
server = Coverband::MCP::Server.new
|
|
142
|
+
|
|
143
|
+
if ENV["COVERBAND_MCP_HTTP"]
|
|
144
|
+
# HTTP mode with Streamable HTTP transport (SSE)
|
|
145
|
+
begin
|
|
146
|
+
require "rackup"
|
|
147
|
+
rescue LoadError
|
|
148
|
+
abort "The 'rackup' gem is required for HTTP mode. Add `gem 'rackup'` to your Gemfile."
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
port = ENV.fetch("COVERBAND_MCP_PORT", 9023).to_i
|
|
152
|
+
host = ENV.fetch("COVERBAND_MCP_HOST", "localhost")
|
|
153
|
+
server.run_http(port: port, host: host)
|
|
154
|
+
else
|
|
155
|
+
# Default stdio mode
|
|
156
|
+
server.run_stdio
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
129
160
|
# experimental dead method detection using RubyVM::AbstractSyntaxTree
|
|
130
161
|
# combined with the coverband coverage.
|
|
131
162
|
if defined?(RubyVM::AbstractSyntaxTree)
|
data/lib/coverband/version.rb
CHANGED
data/lib/coverband.rb
CHANGED
|
@@ -51,8 +51,8 @@ module Coverband
|
|
|
51
51
|
yield(configuration)
|
|
52
52
|
elsif File.exist?(configuration_file)
|
|
53
53
|
load configuration_file
|
|
54
|
-
|
|
55
|
-
configuration.logger.debug("using default configuration")
|
|
54
|
+
elsif Coverband.configuration.verbose
|
|
55
|
+
configuration.logger.debug("using default configuration")
|
|
56
56
|
end
|
|
57
57
|
@@configured = true
|
|
58
58
|
coverage_instance.reset_instance
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require File.expand_path("../test_helper", File.dirname(__FILE__))
|
|
4
|
+
|
|
5
|
+
class FileStoreIntegrationTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
super
|
|
8
|
+
# Reset state
|
|
9
|
+
Thread.current[:coverband_instance] = nil
|
|
10
|
+
Coverband.class_variable_set(:@@configured, false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
super
|
|
15
|
+
Thread.current[:coverband_instance] = nil
|
|
16
|
+
Coverband.configure do |config|
|
|
17
|
+
# Reset to default
|
|
18
|
+
end
|
|
19
|
+
Coverband.class_variable_set(:@@configured, false)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
test "auto-start with FileStore configuration does not try Redis" do
|
|
23
|
+
# Clear any existing store to simulate fresh start
|
|
24
|
+
Coverband.configuration.instance_variable_set(:@store, nil)
|
|
25
|
+
|
|
26
|
+
# This simulates the typical user setup
|
|
27
|
+
Coverband.configure do |config|
|
|
28
|
+
config.root = Dir.pwd
|
|
29
|
+
config.background_reporting_enabled = false
|
|
30
|
+
config.store = Coverband::Adapters::FileStore.new("tmp/integration_test_coverage")
|
|
31
|
+
config.logger = Logger.new($stdout)
|
|
32
|
+
config.verbose = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Start coverband - this should work without Redis errors
|
|
36
|
+
begin
|
|
37
|
+
Coverband.start
|
|
38
|
+
Coverband.report_coverage
|
|
39
|
+
rescue Redis::CannotConnectError => e
|
|
40
|
+
flunk "Should not try to connect to Redis when FileStore is configured: #{e.message}"
|
|
41
|
+
rescue => e
|
|
42
|
+
flunk "Unexpected error: #{e.class}: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Verify we're using the correct store
|
|
46
|
+
assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
test "store fallback works even in auto-start scenario" do
|
|
50
|
+
# Backup original ENV to avoid side effects
|
|
51
|
+
original_disable = ENV["COVERBAND_DISABLE_AUTO_START"]
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
# Enable auto-start
|
|
55
|
+
ENV.delete("COVERBAND_DISABLE_AUTO_START")
|
|
56
|
+
|
|
57
|
+
# Clear configuration state
|
|
58
|
+
Coverband.configuration.instance_variable_set(:@store, nil)
|
|
59
|
+
Coverband.class_variable_set(:@@configured, false)
|
|
60
|
+
|
|
61
|
+
# Mock Redis to fail
|
|
62
|
+
Redis.expects(:new).raises(Redis::CannotConnectError.new("Connection refused")).at_least_once
|
|
63
|
+
|
|
64
|
+
# This should not raise an error even with Redis unavailable
|
|
65
|
+
store = Coverband.configuration.store
|
|
66
|
+
|
|
67
|
+
assert_instance_of Coverband::Adapters::NullStore, store
|
|
68
|
+
ensure
|
|
69
|
+
ENV["COVERBAND_DISABLE_AUTO_START"] = original_disable
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require File.expand_path("../test_helper", File.dirname(__FILE__))
|
|
4
|
+
|
|
5
|
+
class FileStoreRedisErrorTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
super
|
|
8
|
+
# Reset the singleton instance to ensure clean state
|
|
9
|
+
Coverband::Collectors::Coverage.instance.reset_instance
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def teardown
|
|
13
|
+
super
|
|
14
|
+
Thread.current[:coverband_instance] = nil
|
|
15
|
+
Coverband.configure do |config|
|
|
16
|
+
# Reset to default redis store
|
|
17
|
+
end
|
|
18
|
+
Coverband::Collectors::Coverage.instance.reset_instance
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
test "Default store gracefully handles Redis connection failure" do
|
|
22
|
+
# Clear any existing store configuration
|
|
23
|
+
Coverband.configuration.instance_variable_set(:@store, nil)
|
|
24
|
+
|
|
25
|
+
# Mock Redis to simulate connection failure
|
|
26
|
+
Redis.expects(:new).raises(Redis::CannotConnectError.new("Connection refused"))
|
|
27
|
+
|
|
28
|
+
# This should not raise an error, instead it should fall back to NullStore
|
|
29
|
+
store = Coverband.configuration.store
|
|
30
|
+
|
|
31
|
+
assert_instance_of Coverband::Adapters::NullStore, store
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
test "FileStore configuration overrides default store" do
|
|
35
|
+
# Clear any existing store
|
|
36
|
+
Coverband.configuration.instance_variable_set(:@store, nil)
|
|
37
|
+
|
|
38
|
+
file_store = Coverband::Adapters::FileStore.new("tmp/test_coverage")
|
|
39
|
+
Coverband.configure do |config|
|
|
40
|
+
config.store = file_store
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
assert_same file_store, Coverband.configuration.store
|
|
44
|
+
assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
test "FileStore is properly set after configuration" do
|
|
48
|
+
store_path = "tmp/test_coverage"
|
|
49
|
+
Coverband.configure do |config|
|
|
50
|
+
config.store = Coverband::Adapters::FileStore.new(store_path)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
|
|
54
|
+
refute Coverband.configuration.store.is_a?(Coverband::Adapters::RedisStore)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This test demonstrates the fix for GitHub issue #586
|
|
4
|
+
# https://github.com/danmayer/coverband/issues/586
|
|
5
|
+
|
|
6
|
+
require File.expand_path("../test_helper", File.dirname(__FILE__))
|
|
7
|
+
|
|
8
|
+
class GitHubIssue586Test < Minitest::Test
|
|
9
|
+
def setup
|
|
10
|
+
super
|
|
11
|
+
# Reset the singleton instance to ensure clean state
|
|
12
|
+
Coverband::Collectors::Coverage.instance.reset_instance
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def teardown
|
|
16
|
+
super
|
|
17
|
+
Thread.current[:coverband_instance] = nil
|
|
18
|
+
Coverband.configure do |config|
|
|
19
|
+
# Reset to default
|
|
20
|
+
end
|
|
21
|
+
Coverband::Collectors::Coverage.instance.reset_instance
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test "GitHub issue #586: FileStore should not cause Redis connection errors" do
|
|
25
|
+
# This reproduces the exact configuration from the GitHub issue
|
|
26
|
+
Coverband.configure do |config|
|
|
27
|
+
config.root = Dir.pwd
|
|
28
|
+
config.background_reporting_enabled = false
|
|
29
|
+
config.store = Coverband::Adapters::FileStore.new("tmp/coverband_log")
|
|
30
|
+
config.logger = Logger.new($stdout)
|
|
31
|
+
config.verbose = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# This should not raise Redis connection errors
|
|
35
|
+
begin
|
|
36
|
+
# Simulate running ruby code to analyze
|
|
37
|
+
Coverband.start
|
|
38
|
+
Coverband.report_coverage
|
|
39
|
+
|
|
40
|
+
# Verify the store is what the user configured
|
|
41
|
+
assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
|
|
42
|
+
rescue Redis::CannotConnectError => e
|
|
43
|
+
flunk "Should not get Redis connection error when using FileStore: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This test simulates the exact scenario from GitHub issue #586
|
|
4
|
+
# to verify the timing and sequence of initialization
|
|
5
|
+
|
|
6
|
+
require File.expand_path("../test_helper", File.dirname(__FILE__))
|
|
7
|
+
|
|
8
|
+
class InitializationTimingTest < Minitest::Test
|
|
9
|
+
def setup
|
|
10
|
+
super
|
|
11
|
+
# Clear all state to simulate fresh require
|
|
12
|
+
Thread.current[:coverband_instance] = nil
|
|
13
|
+
Coverband.class_variable_set(:@@configured, false)
|
|
14
|
+
Coverband.configuration.instance_variable_set(:@store, nil)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def teardown
|
|
18
|
+
super
|
|
19
|
+
Thread.current[:coverband_instance] = nil
|
|
20
|
+
Coverband.configure do |config|
|
|
21
|
+
# Reset to default
|
|
22
|
+
end
|
|
23
|
+
Coverband.class_variable_set(:@@configured, false)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test "simulate auto-start before user configuration" do
|
|
27
|
+
# Clear configuration to simulate fresh require
|
|
28
|
+
Coverband.configuration.instance_variable_set(:@store, nil)
|
|
29
|
+
Coverband.class_variable_set(:@@configured, false)
|
|
30
|
+
|
|
31
|
+
# Mock Redis to fail when auto-start tries to create default store
|
|
32
|
+
Redis.expects(:new).raises(Redis::CannotConnectError.new("Connection refused")).once
|
|
33
|
+
|
|
34
|
+
# This simulates what happens during auto-start (configure + start)
|
|
35
|
+
# before user has configured anything
|
|
36
|
+
Coverband.configure # No block, no file
|
|
37
|
+
Coverband.start
|
|
38
|
+
|
|
39
|
+
# At this point, the store should have fallen back to NullStore
|
|
40
|
+
assert_instance_of Coverband::Adapters::NullStore, Coverband.configuration.store
|
|
41
|
+
|
|
42
|
+
# Now user tries to configure with FileStore
|
|
43
|
+
Coverband.configure do |config|
|
|
44
|
+
config.store = Coverband::Adapters::FileStore.new("coverband/log")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Should now use the configured FileStore
|
|
48
|
+
assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test "check what happens if store is accessed before user configures it" do
|
|
52
|
+
# Clear configuration to simulate unconfigured state
|
|
53
|
+
Coverband.configuration.instance_variable_set(:@store, nil)
|
|
54
|
+
Coverband.class_variable_set(:@@configured, false)
|
|
55
|
+
|
|
56
|
+
# Mock Redis to fail
|
|
57
|
+
Redis.expects(:new).raises(Redis::CannotConnectError.new("Connection refused")).once
|
|
58
|
+
|
|
59
|
+
# Access store before configuration - should trigger fallback
|
|
60
|
+
store = Coverband.configuration.store
|
|
61
|
+
assert_instance_of Coverband::Adapters::NullStore, store
|
|
62
|
+
|
|
63
|
+
# Now configure with FileStore
|
|
64
|
+
Coverband.configure do |config|
|
|
65
|
+
config.store = Coverband::Adapters::FileStore.new("coverband/log")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Should now use the configured FileStore
|
|
69
|
+
assert_instance_of Coverband::Adapters::FileStore, Coverband.configuration.store
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require File.expand_path("../../test_helper", File.dirname(__FILE__))
|
|
4
|
+
require "rack/test"
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
require "coverband/mcp"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
puts "MCP gem not available, skipping MCP HTTP handler tests"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
if defined?(Coverband::MCP)
|
|
13
|
+
class MCPHttpHandlerTest < Minitest::Test
|
|
14
|
+
include Rack::Test::Methods
|
|
15
|
+
|
|
16
|
+
def setup
|
|
17
|
+
super
|
|
18
|
+
Coverband.configure do |config|
|
|
19
|
+
config.store = Coverband::Adapters::RedisStore.new(Redis.new(db: 2))
|
|
20
|
+
config.mcp_enabled = true # Enable MCP for testing
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def teardown
|
|
25
|
+
super
|
|
26
|
+
Coverband.configuration.store&.clear!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def app
|
|
30
|
+
@app ||= Coverband::MCP::HttpHandler.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def app_with_wrapped_handler
|
|
34
|
+
@wrapped_app ||= begin
|
|
35
|
+
mock_app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ["wrapped app response"]] }
|
|
36
|
+
Coverband::MCP::HttpHandler.new(mock_app)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
test "handles MCP requests at /mcp endpoint" do
|
|
41
|
+
# Mock the server to return a simple response
|
|
42
|
+
server_mock = mock("server")
|
|
43
|
+
server_mock.expects(:handle_json).returns({"result" => "success"})
|
|
44
|
+
|
|
45
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
46
|
+
handler.expects(:mcp_server).returns(server_mock)
|
|
47
|
+
|
|
48
|
+
@app = handler
|
|
49
|
+
|
|
50
|
+
json_request = {
|
|
51
|
+
"jsonrpc" => "2.0",
|
|
52
|
+
"method" => "tools/list",
|
|
53
|
+
"id" => 1
|
|
54
|
+
}.to_json
|
|
55
|
+
|
|
56
|
+
post "/mcp", json_request, {"CONTENT_TYPE" => "application/json"}
|
|
57
|
+
|
|
58
|
+
assert_equal 200, last_response.status
|
|
59
|
+
assert_equal "application/json", last_response.content_type
|
|
60
|
+
|
|
61
|
+
# Check CORS headers
|
|
62
|
+
assert_equal "*", last_response.headers["Access-Control-Allow-Origin"]
|
|
63
|
+
assert_equal "POST, OPTIONS", last_response.headers["Access-Control-Allow-Methods"]
|
|
64
|
+
assert_equal "Content-Type", last_response.headers["Access-Control-Allow-Headers"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
test "returns 404 for non-MCP requests when no wrapped app" do
|
|
68
|
+
get "/other-path"
|
|
69
|
+
|
|
70
|
+
assert_equal 404, last_response.status
|
|
71
|
+
assert_equal "text/plain", last_response.content_type
|
|
72
|
+
assert_equal "Not Found", last_response.body
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
test "delegates non-MCP requests to wrapped app" do
|
|
76
|
+
@app = app_with_wrapped_handler
|
|
77
|
+
|
|
78
|
+
get "/other-path"
|
|
79
|
+
|
|
80
|
+
assert_equal 200, last_response.status
|
|
81
|
+
assert_equal "wrapped app response", last_response.body
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
test "only responds to POST requests for MCP endpoint" do
|
|
85
|
+
get "/mcp"
|
|
86
|
+
|
|
87
|
+
assert_equal 404, last_response.status
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
test "handles invalid JSON gracefully" do
|
|
91
|
+
post "/mcp", "invalid json", {"CONTENT_TYPE" => "application/json"}
|
|
92
|
+
|
|
93
|
+
assert_equal 400, last_response.status
|
|
94
|
+
assert_equal "application/json", last_response.content_type
|
|
95
|
+
|
|
96
|
+
response = JSON.parse(last_response.body)
|
|
97
|
+
assert_includes response["error"], "Invalid JSON"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
test "handles server errors gracefully" do
|
|
101
|
+
# Mock server to raise an error
|
|
102
|
+
server_mock = mock("server")
|
|
103
|
+
server_mock.expects(:handle_json).raises(StandardError.new("Test error"))
|
|
104
|
+
|
|
105
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
106
|
+
handler.expects(:mcp_server).returns(server_mock)
|
|
107
|
+
|
|
108
|
+
@app = handler
|
|
109
|
+
|
|
110
|
+
json_request = {"test" => "request"}.to_json
|
|
111
|
+
post "/mcp", json_request, {"CONTENT_TYPE" => "application/json"}
|
|
112
|
+
|
|
113
|
+
assert_equal 500, last_response.status
|
|
114
|
+
|
|
115
|
+
response = JSON.parse(last_response.body)
|
|
116
|
+
assert_includes response["error"], "Server error: Test error"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
test "mcp_server is lazily initialized" do
|
|
120
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
121
|
+
|
|
122
|
+
# First call creates the server
|
|
123
|
+
server1 = handler.send(:mcp_server)
|
|
124
|
+
assert_instance_of Coverband::MCP::Server, server1
|
|
125
|
+
|
|
126
|
+
# Second call returns the same instance
|
|
127
|
+
server2 = handler.send(:mcp_server)
|
|
128
|
+
assert_same server1, server2
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
test "mcp_request? correctly identifies MCP requests" do
|
|
132
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
133
|
+
|
|
134
|
+
# POST request to /mcp path
|
|
135
|
+
env = Rack::MockRequest.env_for("/mcp", method: "POST")
|
|
136
|
+
request = Rack::Request.new(env)
|
|
137
|
+
assert handler.send(:mcp_request?, request)
|
|
138
|
+
|
|
139
|
+
# POST request to /some-path/mcp (ends with /mcp)
|
|
140
|
+
env = Rack::MockRequest.env_for("/some-path/mcp", method: "POST")
|
|
141
|
+
request = Rack::Request.new(env)
|
|
142
|
+
assert handler.send(:mcp_request?, request)
|
|
143
|
+
|
|
144
|
+
# GET request to /mcp
|
|
145
|
+
env = Rack::MockRequest.env_for("/mcp", method: "GET")
|
|
146
|
+
request = Rack::Request.new(env)
|
|
147
|
+
refute handler.send(:mcp_request?, request)
|
|
148
|
+
|
|
149
|
+
# POST request to /other-path
|
|
150
|
+
env = Rack::MockRequest.env_for("/other-path", method: "POST")
|
|
151
|
+
request = Rack::Request.new(env)
|
|
152
|
+
refute handler.send(:mcp_request?, request)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
test "constant MCP_PATH is defined" do
|
|
156
|
+
assert_equal "/mcp", Coverband::MCP::HttpHandler::MCP_PATH
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|