simplecov-mcp 1.0.1 → 2.0.1

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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -50
  3. data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
  4. data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
  5. data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
  6. data/docs/dev/README.md +10 -0
  7. data/docs/dev/RELEASING.md +146 -0
  8. data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
  9. data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
  10. data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
  11. data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
  12. data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
  13. data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
  14. data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
  15. data/docs/fixtures/demo_project/README.md +9 -0
  16. data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
  17. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
  18. data/docs/user/CLI_USAGE.md +750 -0
  19. data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
  20. data/docs/user/EXAMPLES.md +588 -0
  21. data/docs/user/INSTALLATION.md +130 -0
  22. data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
  23. data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
  24. data/docs/user/README.md +14 -0
  25. data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
  26. data/docs/user/V2-BREAKING-CHANGES.md +472 -0
  27. data/exe/simplecov-mcp +1 -1
  28. data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
  29. data/lib/simplecov_mcp/app_context.rb +1 -1
  30. data/lib/simplecov_mcp/base_tool.rb +66 -38
  31. data/lib/simplecov_mcp/cli.rb +67 -123
  32. data/lib/simplecov_mcp/commands/base_command.rb +16 -27
  33. data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
  34. data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
  35. data/lib/simplecov_mcp/commands/list_command.rb +1 -1
  36. data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
  37. data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
  38. data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
  39. data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
  40. data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
  41. data/lib/simplecov_mcp/commands/version_command.rb +19 -4
  42. data/lib/simplecov_mcp/config_parser.rb +32 -0
  43. data/lib/simplecov_mcp/constants.rb +3 -3
  44. data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
  45. data/lib/simplecov_mcp/error_handler.rb +81 -40
  46. data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
  47. data/lib/simplecov_mcp/errors.rb +12 -19
  48. data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
  49. data/lib/simplecov_mcp/formatters.rb +51 -0
  50. data/lib/simplecov_mcp/mcp_server.rb +9 -7
  51. data/lib/simplecov_mcp/mode_detector.rb +6 -5
  52. data/lib/simplecov_mcp/model.rb +122 -88
  53. data/lib/simplecov_mcp/option_normalizers.rb +39 -18
  54. data/lib/simplecov_mcp/option_parser_builder.rb +85 -72
  55. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
  56. data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
  57. data/lib/simplecov_mcp/path_relativizer.rb +17 -14
  58. data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
  59. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
  60. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
  61. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
  62. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
  63. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
  64. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
  65. data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
  66. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
  67. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
  68. data/lib/simplecov_mcp/resultset_loader.rb +20 -25
  69. data/lib/simplecov_mcp/staleness_checker.rb +50 -46
  70. data/lib/simplecov_mcp/table_formatter.rb +64 -0
  71. data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
  72. data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
  73. data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
  74. data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
  75. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
  76. data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
  77. data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
  78. data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
  79. data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
  80. data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
  81. data/lib/simplecov_mcp/util.rb +18 -12
  82. data/lib/simplecov_mcp/version.rb +1 -1
  83. data/lib/simplecov_mcp.rb +23 -29
  84. data/spec/all_files_coverage_tool_spec.rb +4 -3
  85. data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
  86. data/spec/base_tool_spec.rb +17 -14
  87. data/spec/cli/show_default_report_spec.rb +2 -2
  88. data/spec/cli_enumerated_options_spec.rb +31 -9
  89. data/spec/cli_error_spec.rb +46 -23
  90. data/spec/cli_format_spec.rb +123 -0
  91. data/spec/cli_json_options_spec.rb +50 -0
  92. data/spec/cli_source_spec.rb +11 -63
  93. data/spec/cli_spec.rb +82 -97
  94. data/spec/cli_usage_spec.rb +15 -15
  95. data/spec/commands/base_command_spec.rb +12 -92
  96. data/spec/commands/command_factory_spec.rb +7 -3
  97. data/spec/commands/detailed_command_spec.rb +10 -24
  98. data/spec/commands/list_command_spec.rb +28 -0
  99. data/spec/commands/raw_command_spec.rb +43 -20
  100. data/spec/commands/summary_command_spec.rb +10 -23
  101. data/spec/commands/totals_command_spec.rb +34 -0
  102. data/spec/commands/uncovered_command_spec.rb +29 -23
  103. data/spec/commands/validate_command_spec.rb +213 -0
  104. data/spec/commands/version_command_spec.rb +38 -0
  105. data/spec/constants_spec.rb +3 -3
  106. data/spec/coverage_reporter_spec.rb +102 -0
  107. data/spec/coverage_table_tool_spec.rb +21 -10
  108. data/spec/coverage_totals_tool_spec.rb +37 -0
  109. data/spec/error_handler_spec.rb +120 -4
  110. data/spec/error_mode_spec.rb +18 -22
  111. data/spec/errors_edge_cases_spec.rb +101 -28
  112. data/spec/errors_stale_spec.rb +34 -0
  113. data/spec/file_based_mcp_tools_spec.rb +6 -6
  114. data/spec/fixtures/project1/lib/bar.rb +2 -0
  115. data/spec/fixtures/project1/lib/foo.rb +2 -0
  116. data/spec/help_tool_spec.rb +2 -18
  117. data/spec/integration_spec.rb +103 -161
  118. data/spec/logging_fallback_spec.rb +3 -3
  119. data/spec/mcp_server_integration_spec.rb +1 -1
  120. data/spec/mcp_server_spec.rb +70 -53
  121. data/spec/mode_detector_spec.rb +46 -41
  122. data/spec/model_error_handling_spec.rb +139 -78
  123. data/spec/model_staleness_spec.rb +13 -13
  124. data/spec/option_normalizers_spec.rb +111 -112
  125. data/spec/option_parsers/env_options_parser_spec.rb +25 -37
  126. data/spec/option_parsers/error_helper_spec.rb +56 -56
  127. data/spec/path_relativizer_spec.rb +15 -0
  128. data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
  129. data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
  130. data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
  131. data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
  132. data/spec/presenters/project_totals_presenter_spec.rb +144 -0
  133. data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
  134. data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
  135. data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
  136. data/spec/shared_examples/formatted_command_examples.rb +64 -0
  137. data/spec/simple_cov_mcp_module_spec.rb +24 -3
  138. data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
  139. data/spec/simplecov_mcp/formatters_spec.rb +76 -0
  140. data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
  141. data/spec/simplecov_mcp_model_spec.rb +97 -47
  142. data/spec/simplecov_mcp_opts_spec.rb +42 -39
  143. data/spec/spec_helper.rb +27 -92
  144. data/spec/staleness_checker_spec.rb +10 -9
  145. data/spec/staleness_more_spec.rb +4 -4
  146. data/spec/support/cli_helpers.rb +22 -0
  147. data/spec/support/control_flow_helpers.rb +20 -0
  148. data/spec/support/fake_mcp.rb +40 -0
  149. data/spec/support/io_helpers.rb +29 -0
  150. data/spec/support/mcp_helpers.rb +35 -0
  151. data/spec/support/mcp_runner.rb +10 -8
  152. data/spec/support/mocking_helpers.rb +30 -0
  153. data/spec/table_format_spec.rb +70 -0
  154. data/spec/tools/validate_tool_spec.rb +132 -0
  155. data/spec/tools_error_handling_spec.rb +34 -48
  156. data/spec/util_spec.rb +5 -4
  157. data/spec/version_spec.rb +7 -7
  158. data/spec/version_tool_spec.rb +20 -22
  159. metadata +90 -23
  160. data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
  161. data/docs/CLI_USAGE.md +0 -637
  162. data/docs/EXAMPLES.md +0 -430
  163. data/docs/INSTALLATION.md +0 -352
  164. data/spec/cli_success_predicate_spec.rb +0 -141
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- RSpec.describe 'Additional staleness cases' do
5
+ RSpec.describe SimpleCovMcp do
6
6
  let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
7
 
8
8
  describe SimpleCovMcp::CoverageModel do
@@ -11,7 +11,7 @@ RSpec.describe 'Additional staleness cases' do
11
11
  mock_resultset_with_timestamp(root, Time.now.to_i, coverage: {
12
12
  File.join(root, 'lib', 'bar.rb') => { 'lines' => [1, 1] } # 2 entries vs 3 lines in source
13
13
  })
14
- model = SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage', staleness: 'error')
14
+ model = described_class.new(root: root, resultset: 'coverage', staleness: :error)
15
15
  # bar.rb has 2 coverage entries but 3 source lines in fixtures
16
16
  expect do
17
17
  model.summary_for('lib/bar.rb')
@@ -21,7 +21,7 @@ RSpec.describe 'Additional staleness cases' do
21
21
 
22
22
  describe SimpleCovMcp::StalenessChecker do
23
23
  it 'flags deleted files present only in coverage' do
24
- checker = described_class.new(root: root, resultset: 'coverage', mode: 'error',
24
+ checker = described_class.new(root: root, resultset: 'coverage', mode: :error,
25
25
  timestamp: Time.now.to_i)
26
26
  coverage_map = {
27
27
  File.join(root, 'lib', 'does_not_exist_anymore.rb') => { 'lines' => [1] }
@@ -32,7 +32,7 @@ RSpec.describe 'Additional staleness cases' do
32
32
  end
33
33
 
34
34
  it 'does not raise for empty tracked_globs when nothing else is stale' do
35
- checker = described_class.new(root: root, resultset: 'coverage', mode: 'error',
35
+ checker = described_class.new(root: root, resultset: 'coverage', mode: :error,
36
36
  tracked_globs: [], timestamp: Time.now.to_i)
37
37
  expect do
38
38
  checker.check_project!({})
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CLI test helpers
4
+ module CLITestHelpers
5
+ # Run CLI with the given arguments and return [stdout, stderr, exit_status]
6
+ def run_cli_with_status(*argv)
7
+ cli = SimpleCovMcp::CoverageCLI.new
8
+ status = nil
9
+ out_str = err_str = nil
10
+ silence_output do |out, err|
11
+ begin
12
+ cli.run(argv.flatten)
13
+ status = 0
14
+ rescue SystemExit => e
15
+ status = e.status
16
+ end
17
+ out_str = out.string
18
+ err_str = err.string
19
+ end
20
+ [out_str, err_str, status]
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helpers for managing control flow in RSpec tests.
4
+ module ControlFlowHelpers
5
+ # Execute a block that's expected to call exit() without terminating the test.
6
+ # Useful for testing CLI commands that normally exit.
7
+ # Returns the exit status code if exit was called, otherwise returns the block's value.
8
+ #
9
+ # Examples:
10
+ # status = swallow_system_exit { cli.run(['--help']) }
11
+ # expect(status).to eq(0) # --help calls exit(0)
12
+ #
13
+ # result = swallow_system_exit { some_computation }
14
+ # expect(result).to eq(expected_value) # no exit, returns block value
15
+ def swallow_system_exit
16
+ yield
17
+ rescue SystemExit => e
18
+ e.status
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FakeMCP
4
+ # Fake server captures the last created instance so we can assert on the
5
+ # name/version/tools passed in by SimpleCovMcp::MCPServer.
6
+ class Server
7
+ class << self
8
+ attr_accessor :last_instance
9
+ end
10
+ attr_reader :params
11
+
12
+ def initialize(name:, version:, tools:)
13
+ @params = { name: name, version: version, tools: tools }
14
+ self.class.last_instance = self
15
+ end
16
+ end
17
+
18
+ # Fake stdio transport records whether `open` was called and the server
19
+ # it was initialized with.
20
+ class StdioTransport
21
+ class << self
22
+ attr_accessor :last_instance
23
+ end
24
+ attr_reader :server, :opened
25
+
26
+ def initialize(server)
27
+ @server = server
28
+ @opened = false
29
+ self.class.last_instance = self
30
+ end
31
+
32
+ def open
33
+ @opened = true
34
+ end
35
+
36
+ def opened?
37
+ @opened
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared test helpers for I/O operations (e.g., capturing stdout/stderr).
4
+ module TestIOHelpers
5
+ # Suppress stdout/stderr within the given block, yielding the StringIOs
6
+ def silence_output
7
+ original_stdout = $stdout
8
+ original_stderr = $stderr
9
+ $stdout = StringIO.new
10
+ $stderr = StringIO.new
11
+ yield $stdout, $stderr
12
+ ensure
13
+ $stdout = original_stdout
14
+ $stderr = original_stderr
15
+ end
16
+
17
+ # Capture the output of a command execution
18
+ # @param command [SimpleCovMcp::Commands::BaseCommand] The command instance to execute
19
+ # @param args [Array] The arguments to pass to execute
20
+ # @return [String] The captured output
21
+ def capture_command_output(command, args)
22
+ output = nil
23
+ silence_output do |stdout, _stderr|
24
+ command.execute(args.dup)
25
+ output = stdout.string
26
+ end
27
+ output
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # MCP Tool shared examples and helpers
4
+ module MCPToolTestHelpers
5
+ def setup_mcp_response_stub
6
+ # Standardized MCP::Tool::Response stub that works for all tools
7
+ response_class = Class.new do
8
+ attr_reader :payload, :meta
9
+
10
+ def initialize(payload, meta: nil)
11
+ @payload = payload
12
+ @meta = meta
13
+ end
14
+ end
15
+ stub_const('MCP::Tool::Response', response_class)
16
+ end
17
+
18
+ def expect_mcp_text_json(response, expected_keys: [])
19
+ item = response.payload.first
20
+
21
+ # Check for a 'text' part
22
+ expect(item['type']).to eq('text')
23
+ expect(item).to have_key('text')
24
+
25
+ # Parse and validate JSON content
26
+ data = JSON.parse(item['text'])
27
+
28
+ # Check for expected keys
29
+ expected_keys.each do |key|
30
+ expect(data).to have_key(key)
31
+ end
32
+
33
+ [data, item] # Return for additional custom assertions
34
+ end
35
+ end
@@ -15,9 +15,7 @@ module Spec
15
15
  # always returns a hash containing those streams plus the `Process::Status`
16
16
  # so callers can make assertions without duplicating the boilerplate.
17
17
 
18
- module_function
19
-
20
- def call(requests: nil, input: nil, env: {}, lib_path:, exe_path:, timeout: 5,
18
+ module_function def call(requests: nil, input: nil, env: {}, lib_path:, exe_path:, timeout: 5,
21
19
  close_stdin: true)
22
20
  payload = build_payload(requests, input)
23
21
 
@@ -44,15 +42,19 @@ module Spec
44
42
  raise "MCP server timed out after #{timeout} seconds"
45
43
  end
46
44
 
47
- def call_json(request_hash, **kwargs)
48
- call(requests: request_hash, **kwargs)
45
+ module_function def call_json(request_hash, input: nil, env: {}, lib_path:, exe_path:,
46
+ timeout: 5, close_stdin: true)
47
+ call(requests: request_hash, input: input, env: env, lib_path: lib_path,
48
+ exe_path: exe_path, timeout: timeout, close_stdin: close_stdin)
49
49
  end
50
50
 
51
- def call_json_stream(request_hashes, **kwargs)
52
- call(requests: Array(request_hashes), **kwargs)
51
+ module_function def call_json_stream(request_hashes, input: nil, env: {}, lib_path:,
52
+ exe_path:, timeout: 5, close_stdin: true)
53
+ call(requests: Array(request_hashes), input: input, env: env, lib_path: lib_path,
54
+ exe_path: exe_path, timeout: timeout, close_stdin: close_stdin)
53
55
  end
54
56
 
55
- def build_payload(requests, input)
57
+ module_function def build_payload(requests, input)
56
58
  return input unless requests
57
59
 
58
60
  normalized = requests.is_a?(Array) ? requests : [requests]
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helpers for mocking and stubbing objects in RSpec tests.
4
+ module MockingHelpers
5
+ # Stub staleness checking to return a specific value
6
+ # @param value [String, false] The staleness value to return ('L', 'T', 'M', or false)
7
+ def stub_staleness_check(value)
8
+ checker_double = instance_double(SimpleCovMcp::StalenessChecker)
9
+ allow(checker_double).to receive_messages(
10
+ stale_for_file?: value,
11
+ off?: false
12
+ )
13
+ allow(checker_double).to receive(:check_file!)
14
+ allow(SimpleCovMcp::StalenessChecker).to receive(:new).and_return(checker_double)
15
+ end
16
+
17
+ # Stub a presenter with specific payload data
18
+ # @param presenter_class [Class] The presenter class to stub (e.g., SimpleCovMcp::Presenters::CoverageRawPresenter)
19
+ # @param absolute_payload [Hash] The data hash to return from #absolute_payload
20
+ # @param relative_path [String] The path to return from #relative_path
21
+ def mock_presenter(presenter_class, absolute_payload:, relative_path:)
22
+ presenter_double = instance_double(presenter_class)
23
+ allow(presenter_double).to receive_messages(
24
+ absolute_payload: absolute_payload,
25
+ relative_path: relative_path
26
+ )
27
+ allow(presenter_class).to receive(:new).and_return(presenter_double)
28
+ presenter_double
29
+ end
30
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SimpleCovMcp::CoverageCLI, 'table format for all commands' do
6
+ let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
+
8
+ def run_cli(*argv)
9
+ cli = SimpleCovMcp::CoverageCLI.new
10
+ output = nil
11
+ silence_output do |stdout, _stderr|
12
+ cli.send(:run, argv)
13
+ output = stdout.string
14
+ end
15
+ output
16
+ end
17
+
18
+ describe 'table format consistency' do
19
+ it 'list command produces formatted table' do
20
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table', 'list')
21
+ expect(output).to include('│') # Box drawing character
22
+ expect(output).to include('File')
23
+ expect(output).to include('%')
24
+ end
25
+
26
+ it 'summary command produces formatted table' do
27
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table',
28
+ 'summary', 'lib/foo.rb')
29
+ expect(output).to include('│') # Box drawing character
30
+ expect(output).to include('File')
31
+ expect(output).to include('%')
32
+ end
33
+
34
+ it 'totals command produces formatted table' do
35
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table', 'totals')
36
+ expect(output).to include('│') # Box drawing character
37
+ expect(output).to include('Lines')
38
+ expect(output).to include('%')
39
+ end
40
+
41
+ it 'detailed command produces formatted table' do
42
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table',
43
+ 'detailed', 'lib/foo.rb')
44
+ expect(output).to include('│') # Box drawing character
45
+ expect(output).to include('Line')
46
+ expect(output).to include('Hits')
47
+ end
48
+
49
+ it 'uncovered command produces formatted table' do
50
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table',
51
+ 'uncovered', 'lib/bar.rb') # bar.rb has uncovered lines
52
+ expect(output).to include('│') # Box drawing character
53
+ expect(output).to include('Line')
54
+ end
55
+
56
+ it 'raw command produces formatted table' do
57
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table',
58
+ 'raw', 'lib/foo.rb')
59
+ expect(output).to include('│') # Box drawing character
60
+ expect(output).to include('Line')
61
+ expect(output).to include('Coverage')
62
+ end
63
+
64
+ it 'version command produces formatted table' do
65
+ output = run_cli('--format', 'table', 'version')
66
+ expect(output).to include('│') # Box drawing character
67
+ expect(output).to include('Version')
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'pathname'
5
+ require 'tempfile'
6
+ require 'simplecov_mcp/tools/validate_tool'
7
+
8
+ RSpec.describe SimpleCovMcp::Tools::ValidateTool do
9
+ let(:root) { (FIXTURES_DIR / 'project1').to_s }
10
+ let(:resultset) { 'coverage' }
11
+ let(:server_context) { instance_double('ServerContext').as_null_object }
12
+
13
+ before do
14
+ setup_mcp_response_stub
15
+ end
16
+
17
+ def call_tool(**params)
18
+ described_class.call(**params, root: root, resultset: resultset, server_context: server_context)
19
+ end
20
+
21
+ def response_text(response)
22
+ item = response.payload.first
23
+ item['text']
24
+ end
25
+
26
+ def with_predicate_file(content, dir: nil)
27
+ Tempfile.create(['predicate', '.rb'], dir) do |file|
28
+ file.write(content)
29
+ file.flush
30
+ yield file
31
+ end
32
+ end
33
+
34
+ shared_examples 'syntax error handling' do |_source, error_message_fragment|
35
+ it 'returns an error for syntax errors' do
36
+ response = call_with_predicate('->(_m) { 1 + }')
37
+
38
+ expect(response_text(response)).to include(error_message_fragment)
39
+ end
40
+ end
41
+
42
+ shared_examples 'non-callable handling' do |_source, content|
43
+ it 'returns an error when the predicate is not callable' do
44
+ response = call_with_predicate(content)
45
+
46
+ expect(response_text(response)).to include('Predicate must be callable')
47
+ end
48
+ end
49
+
50
+ shared_examples 'false result' do
51
+ it 'returns false when the predicate evaluates to false' do
52
+ response = call_with_predicate('->(_m) { false }')
53
+
54
+ data, = expect_mcp_text_json(response, expected_keys: ['result'])
55
+ expect(data['result']).to be(false)
56
+ end
57
+ end
58
+
59
+ describe '.call' do
60
+ context 'with inline code' do
61
+ def call_with_predicate(code)
62
+ call_tool(code: code)
63
+ end
64
+
65
+ it 'evaluates the predicate against the coverage model' do
66
+ expect(SimpleCovMcp::CoverageModel).to receive(:new).and_call_original
67
+
68
+ # Realistic coverage policy: foo.rb must have at least 50% coverage
69
+ response = call_with_predicate(
70
+ '->(m) { m.all_files.detect { |f| f["file"].include?("foo.rb") }["percentage"] >= 50.0 }'
71
+ )
72
+
73
+ data, = expect_mcp_text_json(response, expected_keys: ['result'])
74
+ expect(data['result']).to be(true)
75
+ end
76
+
77
+ it_behaves_like 'false result'
78
+ it_behaves_like 'syntax error handling', :code, 'Syntax error in predicate code'
79
+ it_behaves_like 'non-callable handling', :code, '123'
80
+
81
+ it 'returns an error when the predicate raises during execution' do
82
+ response = call_with_predicate("->(_m) { raise 'Boom' }")
83
+
84
+ text = response_text(response)
85
+ expect(text).to include('Error:', 'Boom')
86
+ # Verify it's an error response, not a JSON result
87
+ expect(text).not_to match(/\{"result"/)
88
+ end
89
+ end
90
+
91
+ context 'with a predicate file' do
92
+ def call_with_predicate(content)
93
+ with_predicate_file(content) do |file|
94
+ call_tool(file: file.path)
95
+ end
96
+ end
97
+
98
+ it_behaves_like 'false result'
99
+ it_behaves_like 'syntax error handling', :file, 'Syntax error in predicate file'
100
+ it_behaves_like 'non-callable handling', :file, 'true'
101
+
102
+ it 'expands relative paths from the provided root before evaluation' do
103
+ with_predicate_file('->(_m) { true }', dir: root) do |file|
104
+ relative_path = Pathname.new(file.path).relative_path_from(Pathname.new(root)).to_s
105
+ allow(SimpleCovMcp::PredicateEvaluator)
106
+ .to receive(:evaluate_file)
107
+ .and_return(true)
108
+
109
+ response = call_tool(file: relative_path)
110
+
111
+ expect(SimpleCovMcp::PredicateEvaluator)
112
+ .to have_received(:evaluate_file)
113
+ .with(file.path, kind_of(SimpleCovMcp::CoverageModel))
114
+ data, = expect_mcp_text_json(response, expected_keys: ['result'])
115
+ expect(data['result']).to be(true)
116
+ end
117
+ end
118
+
119
+ it 'returns an error when the predicate file is missing' do
120
+ response = call_tool(file: 'missing_predicate.rb')
121
+
122
+ expect(response_text(response)).to include('Predicate file not found')
123
+ end
124
+ end
125
+
126
+ it 'returns an error when neither code nor file is provided' do
127
+ response = call_tool
128
+
129
+ expect(response_text(response)).to include("Either 'code' or 'file' must be provided")
130
+ end
131
+ end
132
+ end
@@ -7,60 +7,30 @@ require 'simplecov_mcp/tools/coverage_summary_tool'
7
7
  require 'simplecov_mcp/tools/coverage_raw_tool'
8
8
  require 'simplecov_mcp/tools/uncovered_lines_tool'
9
9
  require 'simplecov_mcp/tools/coverage_detailed_tool'
10
+ require 'simplecov_mcp/tools/coverage_totals_tool'
10
11
 
11
- RSpec.describe 'MCP Tool error handling' do
12
+ RSpec.describe SimpleCovMcp::Tools do
12
13
  let(:server_context) { instance_double('ServerContext').as_null_object }
13
14
 
14
15
  before do
15
16
  setup_mcp_response_stub
16
17
  end
17
18
 
18
- # Note: VersionTool error handling is difficult to test because the tool is so simple
19
+ # NOTE: VersionTool error handling is difficult to test because the tool is so simple
19
20
  # and doesn't have any complex logic that could fail. The rescue clause in the tool
20
21
  # exists for consistency with other tools but is unlikely to be triggered in practice.
21
22
 
22
23
  describe SimpleCovMcp::Tools::HelpTool do
23
- it 'handles errors during query processing' do
24
- # Simulate an error during filter_entries
25
- allow(described_class).to receive(:filter_entries).and_raise(StandardError, 'Filter error')
24
+ it 'returns tool information without errors' do
25
+ response = described_class.call(error_mode: 'log', server_context: server_context)
26
26
 
27
- response = described_class.call(query: 'test', error_mode: 'on',
28
- server_context: server_context)
29
-
30
- # Should return error response
31
27
  expect(response).to be_a(MCP::Tool::Response)
32
28
  item = response.payload.first
33
29
  expect(item[:type] || item['type']).to eq('text')
34
- expect(item[:text] || item['text']).to include('Error')
35
- end
36
-
37
- it 'returns empty array when tokens are empty after filtering' do
38
- # Test the edge case where query contains only non-word characters
39
- response = described_class.call(query: '!!!', server_context: server_context)
40
-
41
- data = JSON.parse(response.payload.first['text'])
42
- # With empty tokens, should return all entries (no filtering applied)
43
- expect(data['tools']).not_to be_empty
44
- end
45
30
 
46
- it 'handles non-string, non-array values in filter' do
47
- # Test value_matches? with values that are neither strings nor arrays
48
- # This exercises the 'else false' branch
49
- allow(described_class).to receive(:format_entry).and_return({
50
- 'tool' => 'test_tool',
51
- 'label' => nil, # Neither string nor array
52
- 'use_when' => 123, # Neither string nor array
53
- 'avoid_when' => true, # Neither string nor array
54
- 'inputs' => {}, # Neither string nor array
55
- 'example' => 'example'
56
- })
57
-
58
- response = described_class.call(query: 'test', server_context: server_context)
59
-
60
- # Should not crash, should return response
61
- expect(response).to be_a(MCP::Tool::Response)
62
- data = JSON.parse(response.payload.first['text'])
31
+ data = JSON.parse(item['text'])
63
32
  expect(data).to have_key('tools')
33
+ expect(data['tools']).not_to be_empty
64
34
  end
65
35
  end
66
36
 
@@ -70,7 +40,7 @@ RSpec.describe 'MCP Tool error handling' do
70
40
 
71
41
  response = described_class.call(
72
42
  path: 'lib/foo.rb',
73
- error_mode: 'on',
43
+ error_mode: 'log',
74
44
  server_context: server_context
75
45
  )
76
46
 
@@ -78,7 +48,7 @@ RSpec.describe 'MCP Tool error handling' do
78
48
  expect(response).to be_a(MCP::Tool::Response)
79
49
  item = response.payload.first
80
50
  expect(item[:type] || item['type']).to eq('text')
81
- expect(item[:text] || item['text']).to include('Error')
51
+ expect(item['text']).to include('Error')
82
52
  end
83
53
  end
84
54
 
@@ -88,9 +58,9 @@ RSpec.describe 'MCP Tool error handling' do
88
58
  allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
89
59
  allow(model).to receive(:raw_for).and_raise(StandardError, 'Raw data error')
90
60
 
91
- response = SimpleCovMcp::Tools::CoverageRawTool.call(
61
+ response = described_class.call(
92
62
  path: 'lib/foo.rb',
93
- error_mode: 'on',
63
+ error_mode: 'log',
94
64
  server_context: server_context
95
65
  )
96
66
 
@@ -98,7 +68,7 @@ RSpec.describe 'MCP Tool error handling' do
98
68
  expect(response).to be_a(MCP::Tool::Response)
99
69
  item = response.payload.first
100
70
  expect(item[:type] || item['type']).to eq('text')
101
- expect(item[:text] || item['text']).to include('Error')
71
+ expect(item['text']).to include('Error')
102
72
  end
103
73
  end
104
74
 
@@ -108,9 +78,9 @@ RSpec.describe 'MCP Tool error handling' do
108
78
  allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
109
79
  allow(model).to receive(:uncovered_for).and_raise(StandardError, 'Uncovered error')
110
80
 
111
- response = SimpleCovMcp::Tools::UncoveredLinesTool.call(
81
+ response = described_class.call(
112
82
  path: 'lib/foo.rb',
113
- error_mode: 'on',
83
+ error_mode: 'log',
114
84
  server_context: server_context
115
85
  )
116
86
 
@@ -118,7 +88,7 @@ RSpec.describe 'MCP Tool error handling' do
118
88
  expect(response).to be_a(MCP::Tool::Response)
119
89
  item = response.payload.first
120
90
  expect(item[:type] || item['type']).to eq('text')
121
- expect(item[:text] || item['text']).to include('Error')
91
+ expect(item['text']).to include('Error')
122
92
  end
123
93
  end
124
94
 
@@ -128,9 +98,9 @@ RSpec.describe 'MCP Tool error handling' do
128
98
  allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
129
99
  allow(model).to receive(:detailed_for).and_raise(StandardError, 'Detailed error')
130
100
 
131
- response = SimpleCovMcp::Tools::CoverageDetailedTool.call(
101
+ response = described_class.call(
132
102
  path: 'lib/foo.rb',
133
- error_mode: 'on',
103
+ error_mode: 'log',
134
104
  server_context: server_context
135
105
  )
136
106
 
@@ -138,7 +108,23 @@ RSpec.describe 'MCP Tool error handling' do
138
108
  expect(response).to be_a(MCP::Tool::Response)
139
109
  item = response.payload.first
140
110
  expect(item[:type] || item['type']).to eq('text')
141
- expect(item[:text] || item['text']).to include('Error')
111
+ expect(item['text']).to include('Error')
112
+ end
113
+ end
114
+
115
+ describe SimpleCovMcp::Tools::CoverageTotalsTool do
116
+ it 'handles errors during totals calculation' do
117
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_raise(StandardError, 'Model error')
118
+
119
+ response = described_class.call(
120
+ error_mode: 'log',
121
+ server_context: server_context
122
+ )
123
+
124
+ expect(response).to be_a(MCP::Tool::Response)
125
+ item = response.payload.first
126
+ expect(item[:type] || item['type']).to eq('text')
127
+ expect(item['text']).to include('Error')
142
128
  end
143
129
  end
144
130
  end
data/spec/util_spec.rb CHANGED
@@ -35,11 +35,12 @@ RSpec.describe SimpleCovMcp::CovUtil do
35
35
  end
36
36
 
37
37
  it 'summary handles edge cases and coercion' do
38
- expect(described_class.summary([])).to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
38
+ expect(described_class.summary([]))
39
+ .to include('percentage' => 100.0, 'total' => 0, 'covered' => 0)
39
40
  expect(described_class.summary([nil, nil]))
40
- .to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
41
+ .to include('percentage' => 100.0, 'total' => 0, 'covered' => 0)
41
42
  expect(described_class.summary(['1', '0', nil]))
42
- .to include('pct' => 50.0, 'total' => 2, 'covered' => 1)
43
+ .to include('percentage' => 50.0, 'total' => 2, 'covered' => 1)
43
44
  end
44
45
 
45
46
  it 'uncovered and detailed ignore nils' do
@@ -65,7 +66,7 @@ RSpec.describe SimpleCovMcp::CovUtil do
65
66
  describe 'logging configuration' do
66
67
  let(:test_message) { 'test log message' }
67
68
 
68
- around(:each) do |example|
69
+ around do |example|
69
70
  # Reset logging settings so each example starts clean.
70
71
  old_default = SimpleCovMcp.default_log_file
71
72
  old_active = SimpleCovMcp.active_log_file