coverband 6.1.5 → 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 +23 -0
- data/coverband/log.272267 +1 -0
- data/coverband.gemspec +3 -1
- data/lib/coverband/adapters/hash_redis_store.rb +1 -3
- 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/railtie.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 +7 -7
- data/test/benchmarks/benchmark.rake +7 -15
- 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/track_key_test.rb +9 -9
- 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 +67 -11
|
@@ -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
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require File.expand_path("../../test_helper", File.dirname(__FILE__))
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "coverband/mcp"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
puts "MCP gem not available, skipping MCP security tests"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
if defined?(Coverband::MCP)
|
|
12
|
+
class MCPSecurityTest < Minitest::Test
|
|
13
|
+
def setup
|
|
14
|
+
super
|
|
15
|
+
# Don't enable MCP by default - we want to test security
|
|
16
|
+
Coverband.configure do |config|
|
|
17
|
+
config.store = Coverband::Adapters::RedisStore.new(Redis.new(db: 2))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def teardown
|
|
22
|
+
super
|
|
23
|
+
Coverband.configuration.store&.clear!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test "MCP is disabled by default" do
|
|
27
|
+
refute Coverband.configuration.mcp_enabled?, "MCP should be disabled by default"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test "cannot create MCP server when disabled" do
|
|
31
|
+
error = assert_raises(SecurityError) do
|
|
32
|
+
Coverband::MCP::Server.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
assert_includes error.message, "MCP is not enabled"
|
|
36
|
+
assert_includes error.message, "config.mcp_enabled = true"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
test "MCP can be enabled for allowed environments" do
|
|
40
|
+
# Test environment should be allowed by default
|
|
41
|
+
Coverband.configuration.mcp_enabled = true
|
|
42
|
+
|
|
43
|
+
assert Coverband.configuration.mcp_enabled?, "MCP should be enabled when explicitly set"
|
|
44
|
+
|
|
45
|
+
# Should be able to create server now
|
|
46
|
+
server = Coverband::MCP::Server.new
|
|
47
|
+
refute_nil server
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
test "environment restrictions work correctly" do
|
|
51
|
+
Coverband.configuration.mcp_enabled = true
|
|
52
|
+
|
|
53
|
+
# Temporarily override environment detection
|
|
54
|
+
original_env_var = ENV["RAILS_ENV"]
|
|
55
|
+
ENV["RAILS_ENV"] = "production"
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
refute Coverband.configuration.mcp_enabled?,
|
|
59
|
+
"MCP should be disabled in production environment"
|
|
60
|
+
ensure
|
|
61
|
+
if original_env_var
|
|
62
|
+
ENV["RAILS_ENV"] = original_env_var
|
|
63
|
+
else
|
|
64
|
+
ENV.delete("RAILS_ENV")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
test "authentication works with valid password" do
|
|
70
|
+
Coverband.configuration.mcp_enabled = true
|
|
71
|
+
Coverband.configuration.mcp_password = "test-password"
|
|
72
|
+
|
|
73
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
74
|
+
request = create_mock_request_with_auth("Bearer test-password")
|
|
75
|
+
|
|
76
|
+
# Should pass authentication
|
|
77
|
+
handler.call(request.env)
|
|
78
|
+
# Note: We're not testing the full response, just that it doesn't error with auth
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
test "authentication fails with invalid password" do
|
|
82
|
+
Coverband.configuration.mcp_enabled = true
|
|
83
|
+
Coverband.configuration.mcp_password = "test-password"
|
|
84
|
+
|
|
85
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
86
|
+
request = create_mock_request_with_auth("Bearer wrong-password")
|
|
87
|
+
|
|
88
|
+
response = handler.call(request.env)
|
|
89
|
+
|
|
90
|
+
assert_equal 401, response[0], "Should return 401 Unauthorized"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
test "authentication fails without password" do
|
|
94
|
+
Coverband.configuration.mcp_enabled = true
|
|
95
|
+
Coverband.configuration.mcp_password = "test-password"
|
|
96
|
+
|
|
97
|
+
handler = Coverband::MCP::HttpHandler.new
|
|
98
|
+
request = create_mock_request_without_auth
|
|
99
|
+
|
|
100
|
+
response = handler.call(request.env)
|
|
101
|
+
|
|
102
|
+
assert_equal 401, response[0], "Should return 401 Unauthorized without auth"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
test "allowed environments can be customized" do
|
|
106
|
+
original_envs = Coverband.configuration.mcp_allowed_environments
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
Coverband.configuration.mcp_allowed_environments = ["custom"]
|
|
110
|
+
Coverband.configuration.mcp_enabled = true
|
|
111
|
+
|
|
112
|
+
# Should be disabled because "test" is not in custom allowed environments
|
|
113
|
+
refute Coverband.configuration.mcp_enabled?,
|
|
114
|
+
"MCP should respect custom allowed environments"
|
|
115
|
+
ensure
|
|
116
|
+
Coverband.configuration.mcp_allowed_environments = original_envs
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def create_mock_request_with_auth(auth_header)
|
|
123
|
+
request = mock("request")
|
|
124
|
+
request.stubs(:env).returns({
|
|
125
|
+
"REQUEST_METHOD" => "POST",
|
|
126
|
+
"PATH_INFO" => "/mcp",
|
|
127
|
+
"HTTP_AUTHORIZATION" => auth_header,
|
|
128
|
+
"rack.input" => StringIO.new("{}"),
|
|
129
|
+
"CONTENT_TYPE" => "application/json"
|
|
130
|
+
})
|
|
131
|
+
request
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def create_mock_request_without_auth
|
|
135
|
+
request = mock("request")
|
|
136
|
+
request.stubs(:env).returns({
|
|
137
|
+
"REQUEST_METHOD" => "POST",
|
|
138
|
+
"PATH_INFO" => "/mcp",
|
|
139
|
+
"rack.input" => StringIO.new("{}"),
|
|
140
|
+
"CONTENT_TYPE" => "application/json"
|
|
141
|
+
})
|
|
142
|
+
request
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require File.expand_path("../../test_helper", File.dirname(__FILE__))
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "coverband/mcp"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
puts "MCP gem not available, skipping MCP tests"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
if defined?(Coverband::MCP)
|
|
12
|
+
class MCPServerTest < Minitest::Test
|
|
13
|
+
def setup
|
|
14
|
+
super
|
|
15
|
+
Coverband.configure do |config|
|
|
16
|
+
config.store = Coverband::Adapters::RedisStore.new(Redis.new(db: 2))
|
|
17
|
+
config.mcp_enabled = true # Enable MCP for testing
|
|
18
|
+
end
|
|
19
|
+
@server = Coverband::MCP::Server.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def teardown
|
|
23
|
+
super
|
|
24
|
+
Coverband.configuration.store&.clear!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
test "server initializes with correct attributes" do
|
|
28
|
+
assert_equal "coverband", @server.mcp_server.name
|
|
29
|
+
assert_equal Coverband::VERSION, @server.mcp_server.version
|
|
30
|
+
assert_includes @server.mcp_server.instructions, "Coverband production code coverage"
|
|
31
|
+
refute_empty @server.mcp_server.tools
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
test "server has all expected tools registered" do
|
|
35
|
+
tool_names = @server.mcp_server.tools.keys
|
|
36
|
+
expected_tools = [
|
|
37
|
+
"get_coverage_summary",
|
|
38
|
+
"get_file_coverage",
|
|
39
|
+
"get_uncovered_files",
|
|
40
|
+
"get_dead_methods",
|
|
41
|
+
"get_view_tracker_data",
|
|
42
|
+
"get_route_tracker_data",
|
|
43
|
+
"get_translation_tracker_data"
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
expected_tools.each do |tool_name|
|
|
47
|
+
assert_includes tool_names, tool_name, "Expected tool #{tool_name} to be registered"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test "server configures Coverband if not already configured" do
|
|
52
|
+
# Reset configuration
|
|
53
|
+
Coverband.instance_variable_set(:@configuration, nil)
|
|
54
|
+
|
|
55
|
+
# Enable MCP for the new configuration
|
|
56
|
+
Coverband.configure do |config|
|
|
57
|
+
config.mcp_enabled = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Creating server should auto-configure
|
|
61
|
+
Coverband::MCP::Server.new
|
|
62
|
+
|
|
63
|
+
assert Coverband.configured?, "Coverband should be auto-configured"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test "run_stdio creates and opens stdio transport" do
|
|
67
|
+
transport_mock = mock("stdio_transport")
|
|
68
|
+
transport_mock.expects(:open).once
|
|
69
|
+
|
|
70
|
+
::MCP::Server::Transports::StdioTransport.expects(:new).with(@server.mcp_server).returns(transport_mock)
|
|
71
|
+
|
|
72
|
+
@server.run_stdio
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
test "run_http starts server with correct configuration" do
|
|
76
|
+
# Mock the handler - stub the handler lookup method that exists
|
|
77
|
+
handler_mock = mock("handler")
|
|
78
|
+
handler_mock.expects(:run).once
|
|
79
|
+
|
|
80
|
+
# Just stub the method on the server itself to avoid version dependencies
|
|
81
|
+
@server.expects(:puts).at_least_once # For the info output
|
|
82
|
+
|
|
83
|
+
# Mock Rack handler differently to avoid version issues
|
|
84
|
+
require "rack"
|
|
85
|
+
if defined?(Rackup) && Rackup.respond_to?(:server)
|
|
86
|
+
Rackup.expects(:server).with("puma").returns(handler_mock)
|
|
87
|
+
else
|
|
88
|
+
# Skip this test if we can't properly mock the handler
|
|
89
|
+
skip "Unable to mock Rack handler in this environment"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@server.run_http(port: 9999, host: "test.local")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
test "handle_json delegates to mcp_server" do
|
|
96
|
+
json_request = {"method" => "test"}
|
|
97
|
+
expected_response = {"result" => "success"}
|
|
98
|
+
|
|
99
|
+
@server.mcp_server.expects(:handle_json).with(json_request).returns(expected_response)
|
|
100
|
+
|
|
101
|
+
result = @server.handle_json(json_request)
|
|
102
|
+
assert_equal expected_response, result
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
test "create_rack_app returns functioning rack application" do
|
|
106
|
+
transport = mock("transport")
|
|
107
|
+
app = @server.send(:create_rack_app, transport)
|
|
108
|
+
|
|
109
|
+
assert_respond_to app, :call
|
|
110
|
+
|
|
111
|
+
# Test request handling - just verify the transport is called
|
|
112
|
+
env = {"REQUEST_METHOD" => "POST", "PATH_INFO" => "/"}
|
|
113
|
+
transport.expects(:handle_request).with(kind_of(Rack::Request)).returns([200, {}, ["response"]])
|
|
114
|
+
|
|
115
|
+
response = app.call(env)
|
|
116
|
+
|
|
117
|
+
# Just check status and that we got a response (middleware may wrap body)
|
|
118
|
+
assert_equal 200, response[0]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
test "default http port is 9023" do
|
|
122
|
+
assert_equal 9023, Coverband::MCP::Server::DEFAULT_HTTP_PORT
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require File.expand_path("../../../test_helper", File.dirname(__FILE__))
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "coverband/mcp"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
puts "MCP gem not available, skipping MCP tools tests"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
if defined?(Coverband::MCP)
|
|
12
|
+
class GetCoverageSummaryTest < Minitest::Test
|
|
13
|
+
def setup
|
|
14
|
+
super
|
|
15
|
+
Coverband.configure do |config|
|
|
16
|
+
config.store = Coverband::Adapters::RedisStore.new(Redis.new(db: 2))
|
|
17
|
+
config.mcp_enabled = true # Enable MCP for testing
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def teardown
|
|
22
|
+
super
|
|
23
|
+
Coverband.configuration.store&.clear!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test "tool has correct metadata" do
|
|
27
|
+
assert_includes Coverband::MCP::Tools::GetCoverageSummary.description, "overall production code coverage"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test "input schema has no required parameters" do
|
|
31
|
+
schema = Coverband::MCP::Tools::GetCoverageSummary.input_schema
|
|
32
|
+
# Schema should be an InputSchema object
|
|
33
|
+
assert_instance_of ::MCP::Tool::InputSchema, schema
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
test "call returns coverage summary" do
|
|
37
|
+
# Mock the JSON report
|
|
38
|
+
mock_data = {
|
|
39
|
+
"total_files" => 50,
|
|
40
|
+
"lines_of_code" => 1000,
|
|
41
|
+
"lines_covered" => 800,
|
|
42
|
+
"lines_missed" => 200,
|
|
43
|
+
"covered_percent" => 80.0,
|
|
44
|
+
"covered_strength" => 85.5
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
report_mock = mock("json_report")
|
|
48
|
+
report_mock.expects(:report).returns(mock_data.to_json)
|
|
49
|
+
Coverband::Reporters::JSONReport.expects(:new).returns(report_mock)
|
|
50
|
+
|
|
51
|
+
response = Coverband::MCP::Tools::GetCoverageSummary.call(server_context: {})
|
|
52
|
+
|
|
53
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
54
|
+
assert_equal 1, response.content.length
|
|
55
|
+
assert_equal "text", response.content.first[:type]
|
|
56
|
+
|
|
57
|
+
result = JSON.parse(response.content.first[:text])
|
|
58
|
+
assert_equal 50, result["total_files"]
|
|
59
|
+
assert_equal 1000, result["lines_of_code"]
|
|
60
|
+
assert_equal 800, result["lines_covered"]
|
|
61
|
+
assert_equal 200, result["lines_missed"]
|
|
62
|
+
assert_equal 80.0, result["covered_percent"]
|
|
63
|
+
assert_equal 85.5, result["covered_strength"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test "call handles errors gracefully" do
|
|
67
|
+
Coverband::Reporters::JSONReport.expects(:new).raises(StandardError.new("Test error"))
|
|
68
|
+
|
|
69
|
+
response = Coverband::MCP::Tools::GetCoverageSummary.call(server_context: {})
|
|
70
|
+
|
|
71
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
72
|
+
assert_includes response.content.first[:text], "Error getting coverage summary: Test error"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|