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,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
|
|
@@ -0,0 +1,162 @@
|
|
|
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 GetDeadMethodsTest < 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::GetDeadMethods.description, "methods that have never been executed"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test "input schema has optional file_pattern parameter" do
|
|
31
|
+
schema = Coverband::MCP::Tools::GetDeadMethods.input_schema
|
|
32
|
+
assert_instance_of ::MCP::Tool::InputSchema, schema
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if defined?(RubyVM::AbstractSyntaxTree)
|
|
36
|
+
test "call returns dead methods when AST support available" do
|
|
37
|
+
mock_dead_methods = [
|
|
38
|
+
{
|
|
39
|
+
file_path: "/app/models/user.rb",
|
|
40
|
+
class_name: "User",
|
|
41
|
+
method_name: "unused_method",
|
|
42
|
+
line_number: 10
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
file_path: "/app/models/user.rb",
|
|
46
|
+
class_name: "User",
|
|
47
|
+
method_name: "another_unused",
|
|
48
|
+
line_number: 15
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
file_path: "/app/models/order.rb",
|
|
52
|
+
class_name: "Order",
|
|
53
|
+
method_name: "dead_method",
|
|
54
|
+
line_number: 20
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
Coverband::Utils::DeadMethods.expects(:scan_all).returns(mock_dead_methods)
|
|
59
|
+
|
|
60
|
+
response = Coverband::MCP::Tools::GetDeadMethods.call(server_context: {})
|
|
61
|
+
|
|
62
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
63
|
+
|
|
64
|
+
result = JSON.parse(response.content.first[:text])
|
|
65
|
+
|
|
66
|
+
assert_equal 3, result["total_dead_methods"]
|
|
67
|
+
assert_equal 2, result["files_with_dead_methods"]
|
|
68
|
+
assert_nil result["file_pattern"]
|
|
69
|
+
|
|
70
|
+
# Check grouped results
|
|
71
|
+
user_file = result["results"].find { |f| f["file"] == "/app/models/user.rb" }
|
|
72
|
+
assert_equal 2, user_file["dead_methods"].length
|
|
73
|
+
|
|
74
|
+
order_file = result["results"].find { |f| f["file"] == "/app/models/order.rb" }
|
|
75
|
+
assert_equal 1, order_file["dead_methods"].length
|
|
76
|
+
|
|
77
|
+
# Check method details
|
|
78
|
+
user_method = user_file["dead_methods"].first
|
|
79
|
+
assert_equal "User", user_method["class_name"]
|
|
80
|
+
assert_equal "unused_method", user_method["method_name"]
|
|
81
|
+
assert_equal 10, user_method["line_number"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
test "call filters by file_pattern when provided" do
|
|
85
|
+
mock_dead_methods = [
|
|
86
|
+
{
|
|
87
|
+
file_path: "/app/models/user.rb",
|
|
88
|
+
class_name: "User",
|
|
89
|
+
method_name: "unused_method",
|
|
90
|
+
line_number: 10
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
file_path: "/app/helpers/user_helper.rb",
|
|
94
|
+
class_name: "UserHelper",
|
|
95
|
+
method_name: "dead_helper",
|
|
96
|
+
line_number: 5
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
Coverband::Utils::DeadMethods.expects(:scan_all).returns(mock_dead_methods)
|
|
101
|
+
|
|
102
|
+
response = Coverband::MCP::Tools::GetDeadMethods.call(
|
|
103
|
+
file_pattern: "/app/models/**/*.rb",
|
|
104
|
+
server_context: {}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
result = JSON.parse(response.content.first[:text])
|
|
108
|
+
|
|
109
|
+
# Should only include the models file
|
|
110
|
+
assert_equal 1, result["total_dead_methods"]
|
|
111
|
+
assert_equal 1, result["files_with_dead_methods"]
|
|
112
|
+
assert_equal "/app/models/**/*.rb", result["file_pattern"]
|
|
113
|
+
|
|
114
|
+
assert_equal 1, result["results"].length
|
|
115
|
+
assert_equal "/app/models/user.rb", result["results"].first["file"]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
test "call handles no dead methods found" do
|
|
119
|
+
Coverband::Utils::DeadMethods.expects(:scan_all).returns([])
|
|
120
|
+
|
|
121
|
+
response = Coverband::MCP::Tools::GetDeadMethods.call(server_context: {})
|
|
122
|
+
|
|
123
|
+
result = JSON.parse(response.content.first[:text])
|
|
124
|
+
|
|
125
|
+
assert_equal 0, result["total_dead_methods"]
|
|
126
|
+
assert_equal 0, result["files_with_dead_methods"]
|
|
127
|
+
assert_empty result["results"]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
test "call returns error when AST support not available" do
|
|
132
|
+
# Temporarily hide the constant
|
|
133
|
+
if defined?(RubyVM::AbstractSyntaxTree)
|
|
134
|
+
original_ast = RubyVM::AbstractSyntaxTree
|
|
135
|
+
RubyVM.send(:remove_const, :AbstractSyntaxTree)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
response = Coverband::MCP::Tools::GetDeadMethods.call(server_context: {})
|
|
140
|
+
|
|
141
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
142
|
+
assert_includes response.content.first[:text], "requires Ruby 2.6+ with RubyVM::AbstractSyntaxTree"
|
|
143
|
+
ensure
|
|
144
|
+
# Restore the constant if it was defined
|
|
145
|
+
if defined?(original_ast)
|
|
146
|
+
RubyVM.const_set(:AbstractSyntaxTree, original_ast)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
test "call handles errors gracefully" do
|
|
152
|
+
if defined?(RubyVM::AbstractSyntaxTree)
|
|
153
|
+
Coverband::Utils::DeadMethods.expects(:scan_all).raises(StandardError.new("Test error"))
|
|
154
|
+
|
|
155
|
+
response = Coverband::MCP::Tools::GetDeadMethods.call(server_context: {})
|
|
156
|
+
|
|
157
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
158
|
+
assert_includes response.content.first[:text], "Error analyzing dead methods: Test error"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
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 GetFileCoverageTest < 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::GetFileCoverage.description, "line-by-line coverage data"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test "input schema requires filename parameter" do
|
|
31
|
+
schema = Coverband::MCP::Tools::GetFileCoverage.input_schema
|
|
32
|
+
assert_instance_of ::MCP::Tool::InputSchema, schema
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
test "call returns file coverage data when file exists" do
|
|
36
|
+
filename = "app/models/user.rb"
|
|
37
|
+
full_path = "/app/app/models/user.rb"
|
|
38
|
+
|
|
39
|
+
mock_file_data = {
|
|
40
|
+
"filename" => full_path,
|
|
41
|
+
"covered_percent" => 85.0,
|
|
42
|
+
"lines_of_code" => 100,
|
|
43
|
+
"lines_covered" => 85,
|
|
44
|
+
"lines_missed" => 15,
|
|
45
|
+
"runtime_percentage" => 90.0,
|
|
46
|
+
"never_loaded" => false,
|
|
47
|
+
"coverage" => [1, 1, 0, 1, 1, nil, 1]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mock_data = {
|
|
51
|
+
"files" => {
|
|
52
|
+
full_path => mock_file_data
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
report_mock = mock("json_report")
|
|
57
|
+
report_mock.expects(:report).returns(mock_data.to_json)
|
|
58
|
+
Coverband::Reporters::JSONReport.expects(:new).with(
|
|
59
|
+
Coverband.configuration.store,
|
|
60
|
+
{filename: filename, line_coverage: true}
|
|
61
|
+
).returns(report_mock)
|
|
62
|
+
|
|
63
|
+
response = Coverband::MCP::Tools::GetFileCoverage.call(
|
|
64
|
+
filename: filename,
|
|
65
|
+
server_context: {}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
69
|
+
|
|
70
|
+
result = JSON.parse(response.content.first[:text])
|
|
71
|
+
assert_includes result, full_path
|
|
72
|
+
assert_equal 85.0, result[full_path]["covered_percent"]
|
|
73
|
+
assert_equal 100, result[full_path]["lines_of_code"]
|
|
74
|
+
assert_equal [1, 1, 0, 1, 1, nil, 1], result[full_path]["coverage"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
test "call returns message when no files found" do
|
|
78
|
+
filename = "nonexistent.rb"
|
|
79
|
+
|
|
80
|
+
mock_data = {"files" => nil}
|
|
81
|
+
|
|
82
|
+
report_mock = mock("json_report")
|
|
83
|
+
report_mock.expects(:report).returns(mock_data.to_json)
|
|
84
|
+
Coverband::Reporters::JSONReport.expects(:new).returns(report_mock)
|
|
85
|
+
|
|
86
|
+
response = Coverband::MCP::Tools::GetFileCoverage.call(
|
|
87
|
+
filename: filename,
|
|
88
|
+
server_context: {}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
92
|
+
assert_includes response.content.first[:text], "No coverage data found for file: nonexistent.rb"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
test "call returns message when no matching files" do
|
|
96
|
+
filename = "nonexistent.rb"
|
|
97
|
+
|
|
98
|
+
mock_data = {
|
|
99
|
+
"files" => {
|
|
100
|
+
"/app/other/file.rb" => {"filename" => "/app/other/file.rb"}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
report_mock = mock("json_report")
|
|
105
|
+
report_mock.expects(:report).returns(mock_data.to_json)
|
|
106
|
+
Coverband::Reporters::JSONReport.expects(:new).returns(report_mock)
|
|
107
|
+
|
|
108
|
+
response = Coverband::MCP::Tools::GetFileCoverage.call(
|
|
109
|
+
filename: filename,
|
|
110
|
+
server_context: {}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
114
|
+
assert_includes response.content.first[:text], "No coverage data found for file matching: nonexistent.rb"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
test "call handles partial filename matches" do
|
|
118
|
+
filename = "user"
|
|
119
|
+
|
|
120
|
+
matching_files = {
|
|
121
|
+
"/app/models/user.rb" => {"filename" => "/app/models/user.rb", "covered_percent" => 85.0},
|
|
122
|
+
"/app/helpers/user_helper.rb" => {"filename" => "/app/helpers/user_helper.rb", "covered_percent" => 90.0}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
mock_data = {
|
|
126
|
+
"files" => matching_files.merge({
|
|
127
|
+
"/app/models/order.rb" => {"filename" => "/app/models/order.rb", "covered_percent" => 75.0}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
report_mock = mock("json_report")
|
|
132
|
+
report_mock.expects(:report).returns(mock_data.to_json)
|
|
133
|
+
Coverband::Reporters::JSONReport.expects(:new).returns(report_mock)
|
|
134
|
+
|
|
135
|
+
response = Coverband::MCP::Tools::GetFileCoverage.call(
|
|
136
|
+
filename: filename,
|
|
137
|
+
server_context: {}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
result = JSON.parse(response.content.first[:text])
|
|
141
|
+
assert_equal 2, result.keys.length
|
|
142
|
+
assert_includes result, "/app/models/user.rb"
|
|
143
|
+
assert_includes result, "/app/helpers/user_helper.rb"
|
|
144
|
+
refute_includes result, "/app/models/order.rb"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
test "call handles errors gracefully" do
|
|
148
|
+
Coverband::Reporters::JSONReport.expects(:new).raises(StandardError.new("Test error"))
|
|
149
|
+
|
|
150
|
+
response = Coverband::MCP::Tools::GetFileCoverage.call(
|
|
151
|
+
filename: "test.rb",
|
|
152
|
+
server_context: {}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
assert_instance_of ::MCP::Tool::Response, response
|
|
156
|
+
assert_includes response.content.first[:text], "Error getting file coverage: Test error"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|