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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +4 -4
  3. data/README.md +123 -0
  4. data/agents.md +217 -0
  5. data/bin/coverband-mcp +42 -0
  6. data/changes.md +18 -0
  7. data/coverband/log.272267 +1 -0
  8. data/coverband.gemspec +3 -1
  9. data/lib/coverband/collectors/route_tracker.rb +1 -1
  10. data/lib/coverband/collectors/view_tracker.rb +21 -13
  11. data/lib/coverband/configuration.rb +57 -18
  12. data/lib/coverband/integrations/resque.rb +2 -2
  13. data/lib/coverband/mcp/http_handler.rb +118 -0
  14. data/lib/coverband/mcp/server.rb +116 -0
  15. data/lib/coverband/mcp/tools/get_coverage_summary.rb +41 -0
  16. data/lib/coverband/mcp/tools/get_dead_methods.rb +69 -0
  17. data/lib/coverband/mcp/tools/get_file_coverage.rb +72 -0
  18. data/lib/coverband/mcp/tools/get_route_tracker_data.rb +60 -0
  19. data/lib/coverband/mcp/tools/get_translation_tracker_data.rb +60 -0
  20. data/lib/coverband/mcp/tools/get_uncovered_files.rb +73 -0
  21. data/lib/coverband/mcp/tools/get_view_tracker_data.rb +60 -0
  22. data/lib/coverband/mcp.rb +27 -0
  23. data/lib/coverband/reporters/base.rb +2 -4
  24. data/lib/coverband/reporters/web.rb +17 -14
  25. data/lib/coverband/utils/lines_classifier.rb +1 -1
  26. data/lib/coverband/utils/result.rb +2 -1
  27. data/lib/coverband/utils/source_file.rb +5 -5
  28. data/lib/coverband/utils/tasks.rb +31 -0
  29. data/lib/coverband/version.rb +1 -1
  30. data/lib/coverband.rb +2 -2
  31. data/test/coverband/file_store_integration_test.rb +72 -0
  32. data/test/coverband/file_store_redis_error_test.rb +56 -0
  33. data/test/coverband/github_issue_586_test.rb +46 -0
  34. data/test/coverband/initialization_timing_test.rb +71 -0
  35. data/test/coverband/mcp/http_handler_test.rb +159 -0
  36. data/test/coverband/mcp/security_test.rb +145 -0
  37. data/test/coverband/mcp/server_test.rb +125 -0
  38. data/test/coverband/mcp/tools/get_coverage_summary_test.rb +75 -0
  39. data/test/coverband/mcp/tools/get_dead_methods_test.rb +162 -0
  40. data/test/coverband/mcp/tools/get_file_coverage_test.rb +159 -0
  41. data/test/coverband/mcp/tools/get_route_tracker_data_test.rb +122 -0
  42. data/test/coverband/mcp/tools/get_translation_tracker_data_test.rb +122 -0
  43. data/test/coverband/mcp/tools/get_uncovered_files_test.rb +177 -0
  44. data/test/coverband/mcp/tools/get_view_tracker_data_test.rb +122 -0
  45. data/test/coverband/reporters/web_test.rb +5 -0
  46. data/test/coverband/tracker_initialization_test.rb +75 -0
  47. data/test/coverband/user_environment_simulation_test.rb +75 -0
  48. data/test/coverband/utils/lines_classifier_test.rb +1 -1
  49. data/test/integration/mcp_integration_test.rb +175 -0
  50. data/test/test_helper.rb +4 -5
  51. 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