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