simplecov-mcp 1.0.0 → 2.0.0

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 +32 -20
  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 -83
  53. data/lib/simplecov_mcp/option_normalizers.rb +39 -18
  54. data/lib/simplecov_mcp/option_parser_builder.rb +82 -65
  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 +114 -170
  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 +141 -82
  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 +99 -49
  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
@@ -5,6 +5,7 @@ require 'fileutils'
5
5
 
6
6
  RSpec.describe SimpleCovMcp::StalenessChecker do
7
7
  let(:tmpdir) { Dir.mktmpdir('scmcp-stale') }
8
+
8
9
  after { FileUtils.remove_entry(tmpdir) if tmpdir && File.directory?(tmpdir) }
9
10
 
10
11
  def write_file(path, lines)
@@ -57,8 +58,8 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
57
58
  end
58
59
  end
59
60
 
60
- context 'compute_file_staleness_details' do
61
- include_examples 'a staleness check',
61
+ context 'when computing file staleness details' do
62
+ it_behaves_like 'a staleness check',
62
63
  description: 'detects newer file vs coverage timestamp',
63
64
  file_lines: ['a', 'b'],
64
65
  coverage_lines: [1, 1],
@@ -75,7 +76,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
75
76
  expected_stale_char: 'T',
76
77
  expected_error: SimpleCovMcp::CoverageDataStaleError
77
78
 
78
- include_examples 'a staleness check',
79
+ it_behaves_like 'a staleness check',
79
80
  description: 'detects length mismatch between source and coverage',
80
81
  file_lines: ['a', 'b', 'c', 'd'],
81
82
  coverage_lines: [1, 1],
@@ -92,7 +93,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
92
93
  expected_stale_char: 'L',
93
94
  expected_error: SimpleCovMcp::CoverageDataStaleError
94
95
 
95
- include_examples 'a staleness check',
96
+ it_behaves_like 'a staleness check',
96
97
  description: 'treats missing file as stale',
97
98
  file_lines: nil,
98
99
  coverage_lines: [1, 1, 1],
@@ -107,7 +108,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
107
108
  expected_stale_char: 'M',
108
109
  expected_error: SimpleCovMcp::CoverageDataStaleError
109
110
 
110
- include_examples 'a staleness check',
111
+ it_behaves_like 'a staleness check',
111
112
  description: 'is not stale when timestamps and lengths match',
112
113
  file_lines: ['a', 'b', 'c'],
113
114
  coverage_lines: [1, 0, nil],
@@ -123,7 +124,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
123
124
  expected_error: nil
124
125
  end
125
126
 
126
- context 'missing_trailing_newline? edge cases' do
127
+ context 'when handling missing_trailing_newline? edge cases' do
127
128
  let(:checker) do
128
129
  described_class.new(root: tmpdir, resultset: nil, mode: 'off', timestamp: Time.now)
129
130
  end
@@ -192,7 +193,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
192
193
  end
193
194
  end
194
195
 
195
- context 'line count adjustment with missing trailing newline' do
196
+ context 'when adjusting line count with missing trailing newline' do
196
197
  let(:checker) do
197
198
  described_class.new(root: tmpdir, resultset: nil, mode: 'off', timestamp: Time.now)
198
199
  end
@@ -246,7 +247,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
246
247
  end
247
248
  end
248
249
 
249
- context 'safe_count_lines edge cases' do
250
+ context 'when handling safe_count_lines edge cases' do
250
251
  let(:checker) do
251
252
  described_class.new(root: tmpdir, resultset: nil, mode: 'off', timestamp: Time.now)
252
253
  end
@@ -287,7 +288,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
287
288
  end
288
289
  end
289
290
 
290
- context 'rel method with path prefix mismatches' do
291
+ context 'when rel has path prefix mismatches' do
291
292
  let(:checker) do
292
293
  described_class.new(root: tmpdir, resultset: nil, mode: 'off', timestamp: Time.now)
293
294
  end
@@ -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