simplecov-mcp 1.0.1 → 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 +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 +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 +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,151 +2,74 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- RSpec.describe 'SimpleCov MCP Integration Tests' do
6
- # Timeout for MCP server operations (increased for JRuby compatibility)
7
- MCP_TIMEOUT = 5
5
+ # Timeout for MCP server operations (increased for JRuby compatibility)
6
+ MCP_TIMEOUT = 5
8
7
 
8
+ RSpec.describe 'SimpleCov MCP Integration Tests' do
9
9
  let(:project_root) { (FIXTURES_DIR / 'project1').to_s }
10
10
  let(:coverage_dir) { File.join(project_root, 'coverage') }
11
11
  let(:resultset_path) { File.join(coverage_dir, '.resultset.json') }
12
12
 
13
13
  describe 'End-to-End Coverage Model Functionality' do
14
- context 'with real coverage data and files' do
15
- it 'provides complete coverage analysis workflow' do
16
- # Initialize model with real fixture data
17
- model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
18
-
19
- # Test all_files returns real coverage data
20
- all_files = model.all_files
21
- expect(all_files).to be_an(Array)
22
- expect(all_files.length).to eq(2)
23
-
24
- # Verify file paths and coverage data structure
25
- foo_file = all_files.find { |f| f['file'].include?('foo.rb') }
26
- bar_file = all_files.find { |f| f['file'].include?('bar.rb') }
27
-
28
- expect(foo_file).to include('covered', 'total', 'percentage', 'stale')
29
- expect(bar_file).to include('covered', 'total', 'percentage', 'stale')
30
-
31
- # Verify actual coverage calculations match fixture data
32
- # foo.rb has coverage: [1, 0, nil, 2] -> 2 covered out of 3 executable = 66.67%
33
- expect(foo_file['total']).to eq(3)
34
- expect(foo_file['covered']).to eq(2)
35
- expect(foo_file['percentage']).to be_within(0.01).of(66.67)
36
-
37
- # bar.rb has coverage: [0, 0, 1] -> 1 covered out of 3 executable = 33.33%
38
- expect(bar_file['total']).to eq(3)
39
- expect(bar_file['covered']).to eq(1)
40
- expect(bar_file['percentage']).to be_within(0.01).of(33.33)
41
- end
42
-
43
- it 'provides detailed per-file analysis' do
44
- model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
45
-
46
- # Test raw coverage data
47
- raw = model.raw_for('lib/foo.rb')
48
- expect(raw['file']).to end_with('lib/foo.rb')
49
- expect(raw['lines']).to eq([1, 0, nil, 2])
50
-
51
- # Test summary calculation
52
- summary = model.summary_for('lib/foo.rb')
53
- expect(summary['file']).to end_with('lib/foo.rb')
54
- expect(summary['summary']).to include('covered' => 2, 'total' => 3)
55
- expect(summary['summary']['pct']).to be_within(0.01).of(66.67)
56
-
57
- # Test uncovered lines detection
58
- uncovered = model.uncovered_for('lib/foo.rb')
59
- expect(uncovered['file']).to end_with('lib/foo.rb')
60
- expect(uncovered['uncovered']).to eq([2]) # Line 2 has 0 hits
61
- expect(uncovered['summary']).to include('covered' => 2, 'total' => 3)
62
-
63
- # Test detailed line-by-line analysis
64
- detailed = model.detailed_for('lib/foo.rb')
65
- expect(detailed['file']).to end_with('lib/foo.rb')
66
- expect(detailed['lines']).to eq([
67
- { 'line' => 1, 'hits' => 1, 'covered' => true },
68
- { 'line' => 2, 'hits' => 0, 'covered' => false },
69
- { 'line' => 4, 'hits' => 2, 'covered' => true }
70
- ])
71
- end
72
-
73
- it 'generates properly formatted coverage tables' do
74
- model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
75
-
76
- # Test default table generation
77
- table = model.format_table
78
-
79
- # Verify table structure (Unicode box drawing)
80
- expect(table).to include('┌', '┬', '┐', '│', '├', '┼', '┤', '└', '┴', '┘')
81
-
82
- # Verify headers
83
- expect(table).to include('File', '%', 'Covered', 'Total', 'Stale')
14
+ it 'loads fixture coverage and surfaces core stats across APIs' do
15
+ model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
84
16
 
85
- # Verify file data appears
86
- expect(table).to include('lib/foo.rb', 'lib/bar.rb')
17
+ files = model.all_files
18
+ expect(files.length).to eq(2)
19
+ files_by_name = files.to_h { |f| [File.basename(f['file']), f] }
87
20
 
88
- # Verify percentages are formatted correctly
89
- expect(table).to include('66.67', '33.33')
21
+ foo = files_by_name.fetch('foo.rb')
22
+ bar = files_by_name.fetch('bar.rb')
23
+ expect(foo['percentage']).to be_within(0.01).of(66.67)
24
+ expect(bar['percentage']).to be_within(0.01).of(33.33)
90
25
 
91
- # Verify counts summary
92
- expect(table).to include('Files: total 2')
26
+ raw = model.raw_for('lib/foo.rb')
27
+ expect(raw['lines']).to eq([1, 0, nil, 2])
93
28
 
94
- # Test sorting (ascending by default - bar.rb should be first with lower coverage)
95
- lines = table.split("\n")
96
- data_lines = lines.select { |line| line.include?('lib/') }
97
- expect(data_lines.first).to include('lib/bar.rb') # Lower coverage first
98
- expect(data_lines.last).to include('lib/foo.rb') # Higher coverage last
99
- end
29
+ summary = model.summary_for('lib/foo.rb')
30
+ expect(summary['summary']).to include('covered' => 2, 'total' => 3)
100
31
 
101
- it 'supports different sorting options' do
102
- model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
32
+ uncovered = model.uncovered_for('lib/foo.rb')
33
+ expect(uncovered['uncovered']).to eq([2])
103
34
 
104
- # Test ascending sort
105
- asc_files = model.all_files(sort_order: :ascending)
106
- expect(asc_files.first['file']).to end_with('lib/bar.rb') # Lower coverage first
107
- expect(asc_files.last['file']).to end_with('lib/foo.rb') # Higher coverage last
35
+ detailed = model.detailed_for('lib/foo.rb')
36
+ expect(detailed['lines']).to include({ 'line' => 2, 'hits' => 0, 'covered' => false })
108
37
 
109
- # Test descending sort
110
- desc_files = model.all_files(sort_order: :descending)
111
- expect(desc_files.first['file']).to end_with('lib/foo.rb') # Higher coverage first
112
- expect(desc_files.last['file']).to end_with('lib/bar.rb') # Lower coverage last
113
- end
38
+ table = model.format_table
39
+ expect(table).to include('lib/foo.rb', 'lib/bar.rb', '66.67', '33.33')
40
+ data_lines = table.split("\n").select { |line| line.include?('lib/') }
41
+ expect(data_lines.first).to include('lib/foo.rb') # Highest coverage first (descending default)
42
+ expect(data_lines.last).to include('lib/bar.rb')
114
43
  end
115
44
  end
116
45
 
117
46
  describe 'CLI Integration with Real Coverage Data' do
118
47
  it 'executes all major CLI commands without errors' do
119
48
  # Test list command
120
- list_output = nil
121
- silence_output do |out, _err|
122
- cli = SimpleCovMcp::CoverageCLI.new
123
- cli.run(['list', '--root', project_root, '--resultset', coverage_dir])
124
- list_output = out.string
125
- end
126
-
49
+ list_output, _err, status = run_cli_with_status('--root', project_root, '--resultset',
50
+ coverage_dir, 'list')
51
+ expect(status).to eq(0)
127
52
  expect(list_output).to include('lib/foo.rb', 'lib/bar.rb')
128
53
  expect(list_output).to include('66.67', '33.33')
54
+ data_lines = list_output.lines.select { |line| line.include?('lib/') }
55
+ expect(data_lines.first).to include('lib/foo.rb') # Highest coverage first (descending default)
56
+ expect(data_lines.last).to include('lib/bar.rb')
129
57
 
130
58
  # Test summary command
131
- summary_output = nil
132
- silence_output do |out, _err|
133
- cli = SimpleCovMcp::CoverageCLI.new
134
- cli.run(['summary', 'lib/foo.rb', '--root', project_root, '--resultset', coverage_dir])
135
- summary_output = out.string
136
- end
137
-
138
- expect(summary_output).to include('66.67%', '2/3')
59
+ summary_output, _err, status = run_cli_with_status('--root', project_root, '--resultset',
60
+ coverage_dir, 'summary', 'lib/foo.rb')
61
+ expect(status).to eq(0)
62
+ expect(summary_output).to include('') # Table format
63
+ expect(summary_output).to include('66.67%')
64
+ expect(summary_output).to include('2')
65
+ expect(summary_output).to include('3')
139
66
 
140
67
  # Test JSON output
141
- json_output = nil
142
- silence_output do |out, _err|
143
- cli = SimpleCovMcp::CoverageCLI.new
144
- cli.run([
145
- 'summary', 'lib/foo.rb', '--json', '--root', project_root, '--resultset', coverage_dir
146
- ])
147
- json_output = out.string
148
- end
149
-
68
+ json_output, _err, status = run_cli_with_status(
69
+ '--format', 'json', '--root', project_root, '--resultset', coverage_dir,
70
+ 'summary', 'lib/foo.rb'
71
+ )
72
+ expect(status).to eq(0)
150
73
  json_data = JSON.parse(json_output)
151
74
  expect(json_data).to include('file', 'summary')
152
75
  expect(json_data['summary']).to include('covered' => 2, 'total' => 3)
@@ -154,23 +77,17 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
154
77
 
155
78
  it 'handles different output formats correctly' do
156
79
  # Test uncovered command with different outputs
157
- uncovered_output = nil
158
- silence_output do |out, _err|
159
- cli = SimpleCovMcp::CoverageCLI.new
160
- cli.run(['uncovered', 'lib/foo.rb', '--root', project_root, '--resultset', coverage_dir])
161
- uncovered_output = out.string
162
- end
163
-
164
- expect(uncovered_output).to match(/Uncovered lines:\s*2\b/)
80
+ uncovered_output, _err, status = run_cli_with_status(
81
+ '--root', project_root, '--resultset', coverage_dir, 'uncovered', 'lib/foo.rb'
82
+ )
83
+ expect(status).to eq(0)
84
+ expect(uncovered_output).to include('│') # Table format
165
85
 
166
86
  # Test detailed command
167
- detailed_output = nil
168
- silence_output do |out, _err|
169
- cli = SimpleCovMcp::CoverageCLI.new
170
- cli.run(['detailed', 'lib/foo.rb', '--root', project_root, '--resultset', coverage_dir])
171
- detailed_output = out.string
172
- end
173
-
87
+ detailed_output, _err, status = run_cli_with_status(
88
+ '--root', project_root, '--resultset', coverage_dir, 'detailed', 'lib/foo.rb'
89
+ )
90
+ expect(status).to eq(0)
174
91
  expect(detailed_output).to include('Line', 'Hits', 'Covered')
175
92
  end
176
93
  end
@@ -191,7 +108,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
191
108
  server_context: server_context
192
109
  )
193
110
 
194
- data, item = expect_mcp_text_json(summary_response, expected_keys: ['file', 'summary'])
111
+ data, _item = expect_mcp_text_json(summary_response, expected_keys: ['file', 'summary'])
195
112
  expect(data['summary']).to include('covered' => 2, 'total' => 3)
196
113
 
197
114
  # Test raw coverage tool
@@ -202,7 +119,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
202
119
  server_context: server_context
203
120
  )
204
121
 
205
- raw_data, raw_item = expect_mcp_text_json(raw_response, expected_keys: ['file', 'lines'])
122
+ raw_data, _raw_item = expect_mcp_text_json(raw_response, expected_keys: ['file', 'lines'])
206
123
  expect(raw_data['lines']).to eq([1, 0, nil, 2])
207
124
 
208
125
  # Test all files tool
@@ -212,7 +129,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
212
129
  server_context: server_context
213
130
  )
214
131
 
215
- all_data, _ = expect_mcp_text_json(all_files_response, expected_keys: ['files', 'counts'])
132
+ all_data, = expect_mcp_text_json(all_files_response, expected_keys: ['files', 'counts'])
216
133
  expect(all_data['files'].length).to eq(2)
217
134
  expect(all_data['counts']['total']).to eq(2)
218
135
  end
@@ -225,7 +142,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
225
142
  resultset: coverage_dir,
226
143
  server_context: server_context
227
144
  )
228
- summary_data, _ = expect_mcp_text_json(summary_response)
145
+ summary_data, = expect_mcp_text_json(summary_response)
229
146
 
230
147
  # Get data from detailed tool
231
148
  detailed_response = SimpleCovMcp::Tools::CoverageDetailedTool.call(
@@ -234,7 +151,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
234
151
  resultset: coverage_dir,
235
152
  server_context: server_context
236
153
  )
237
- detailed_data, _ = expect_mcp_text_json(detailed_response)
154
+ detailed_data, = expect_mcp_text_json(detailed_response)
238
155
 
239
156
  # Verify consistency between tools
240
157
  expect(summary_data['summary']['covered']).to eq(2)
@@ -264,20 +181,9 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
264
181
  end
265
182
 
266
183
  it 'provides helpful CLI error messages' do
267
- output, error, status = nil, nil, nil
268
- silence_output do |out, err|
269
- begin
270
- cli = SimpleCovMcp::CoverageCLI.new
271
- cli.run([
272
- 'summary', 'lib/nonexistent.rb', '--root', project_root, '--resultset', coverage_dir
273
- ])
274
- status = 0
275
- rescue SystemExit => e
276
- status = e.status
277
- end
278
- output = out.string
279
- error = err.string
280
- end
184
+ _output, error, status = run_cli_with_status(
185
+ '--root', project_root, '--resultset', coverage_dir, 'summary', 'lib/nonexistent.rb'
186
+ )
281
187
 
282
188
  expect(status).to eq(1)
283
189
  expect(error).to include('File error:', 'No coverage entry found')
@@ -321,7 +227,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
321
227
  let(:default_env) do
322
228
  {
323
229
  'RUBY_LIB' => lib_path,
324
- 'SIMPLECOV_MCP_OPTS' => "--root #{project_root} --resultset #{coverage_dir}"
230
+ 'SIMPLECOV_MCP_OPTS' => "--root #{project_root} --resultset #{coverage_dir} --log-file /dev/null"
325
231
  }
326
232
  end
327
233
 
@@ -360,7 +266,9 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
360
266
 
361
267
  def parse_jsonrpc_response(output)
362
268
  # MCP server should only write JSON-RPC responses to stdout.
363
- output.lines.each do |line|
269
+ # Force UTF-8 encoding to handle any binary data in the output stream.
270
+ safe_output = output.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
271
+ safe_output.lines.each do |line|
364
272
  stripped = line.strip
365
273
  next if stripped.empty?
366
274
 
@@ -562,6 +470,37 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
562
470
  expect(version_text).to match(/SimpleCovMcp version: \d+\.\d+/)
563
471
  end
564
472
 
473
+ it 'executes validate_tool via JSON-RPC' do
474
+ request = {
475
+ jsonrpc: '2.0',
476
+ id: 80,
477
+ method: 'tools/call',
478
+ params: {
479
+ name: 'validate_tool',
480
+ arguments: {
481
+ code: '->(m) { true }',
482
+ root: project_root,
483
+ resultset: coverage_dir
484
+ }
485
+ }
486
+ }
487
+
488
+ stdout = run_mcp_json(request)[:stdout]
489
+ response = parse_jsonrpc_response(stdout)
490
+
491
+ expect(response['id']).to eq(80)
492
+ content = response['result']['content']
493
+ expect(content.first['type']).to eq('text')
494
+
495
+ begin
496
+ result_json = JSON.parse(content.first['text'])
497
+ rescue JSON::ParserError
498
+ puts "DEBUG: Failed to parse JSON. Content was: #{content.first['text']}"
499
+ raise
500
+ end
501
+ expect(result_json).to include('result' => true)
502
+ end
503
+
565
504
  it 'handles error responses for invalid tool calls' do
566
505
  request = {
567
506
  jsonrpc: '2.0',
@@ -649,7 +588,10 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
649
588
 
650
589
  result = run_mcp_json_stream(requests)
651
590
 
652
- responses = result[:stdout].lines.map do |line|
591
+ # Force UTF-8 encoding to handle any binary data in the output stream
592
+ safe_stdout = result[:stdout].to_s
593
+ .encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
594
+ responses = safe_stdout.lines.map do |line|
653
595
  next if line.strip.empty?
654
596
 
655
597
  begin
@@ -665,7 +607,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
665
607
  expect(response_ids).to include(100).or include(101)
666
608
  end
667
609
 
668
- context 'MCP protocol error handling' do
610
+ context 'when handling MCP protocol errors' do
669
611
  it 'returns error for unknown tool name' do
670
612
  request = {
671
613
  jsonrpc: '2.0',
@@ -697,7 +639,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
697
639
  text = content.first['text']
698
640
  expect(text.downcase).to include('error').or include('not found')
699
641
  else
700
- fail 'Expected either error or result field in response'
642
+ raise 'Expected either error or result field in response'
701
643
  end
702
644
  end
703
645
 
@@ -726,7 +668,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
726
668
  text = content.first['text']
727
669
  expect(text.downcase).to include('error').or include('required').or include('path')
728
670
  else
729
- fail 'Expected either error or result field in response'
671
+ raise 'Expected either error or result field in response'
730
672
  end
731
673
  end
732
674
 
@@ -738,7 +680,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
738
680
  params: {
739
681
  name: 'coverage_summary_tool',
740
682
  arguments: {
741
- path: 12345, # Should be string, not number
683
+ path: 12_345, # Should be string, not number
742
684
  root: project_root,
743
685
  resultset: coverage_dir
744
686
  }
@@ -54,7 +54,7 @@ RSpec.describe 'Logging Fallback Behavior' do
54
54
  context = SimpleCovMcp.create_context(
55
55
  error_handler: SimpleCovMcp::ErrorHandlerFactory.for_mcp_server,
56
56
  log_target: '/invalid/path/that/does/not/exist.log',
57
- mode: :mcp_server
57
+ mode: :mcp
58
58
  )
59
59
 
60
60
  stderr_output = nil
@@ -115,10 +115,10 @@ RSpec.describe 'Logging Fallback Behavior' do
115
115
  expect(context.mcp_mode?).to be false
116
116
  end
117
117
 
118
- it 'correctly identifies MCP server mode' do
118
+ it 'correctly identifies MCP mode' do
119
119
  context = SimpleCovMcp.create_context(
120
120
  error_handler: SimpleCovMcp::ErrorHandlerFactory.for_mcp_server,
121
- mode: :mcp_server
121
+ mode: :mcp
122
122
  )
123
123
  expect(context.library_mode?).to be false
124
124
  expect(context.cli_mode?).to be false
@@ -5,7 +5,7 @@ require 'spec_helper'
5
5
  RSpec.describe 'MCP Server Bootstrap' do
6
6
  it 'does not crash on startup in non-TTY environments' do
7
7
  # Simulate a non-TTY environment, which should trigger MCP mode
8
- allow(STDIN).to receive(:tty?).and_return(false)
8
+ allow($stdin).to receive(:tty?).and_return(false)
9
9
 
10
10
  # The server will try to run, but we only need to ensure it gets past
11
11
  # the point where the NameError would have occurred. We can mock the
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'support/fake_mcp'
4
5
 
5
6
  RSpec.describe SimpleCovMcp::MCPServer do
6
7
  # This spec verifies the MCP server boot path without requiring the real
@@ -10,55 +11,8 @@ RSpec.describe SimpleCovMcp::MCPServer do
10
11
  # Prepare fakes for MCP server and transport
11
12
  module ::MCP; end unless defined?(::MCP)
12
13
 
13
- # Fake server captures the last created instance so we can assert on the
14
- # name/version/tools passed in by SimpleCovMcp::MCPServer. The
15
- # `last_instance` accessor is a class-level handle to the most recently
16
- # instantiated fake. Because the production code constructs the server
17
- # internally, we can't grab the instance directly; recording the most
18
- # recent instance lets the test fetch it after `run` completes.
19
- fake_server_class = Class.new do
20
- class << self
21
- # Holds the most recently created fake server instance so tests can
22
- # inspect it after the code under test performs internal construction.
23
- attr_accessor :last_instance
24
- end
25
- attr_reader :params
26
-
27
- def initialize(name:, version:, tools:)
28
- @params = { name: name, version: version, tools: tools }
29
- self.class.last_instance = self
30
- end
31
- end
32
-
33
- # Fake stdio transport records whether `open` was called and the server
34
- # it was initialized with, to confirm that the server was started. It also
35
- # exposes a `last_instance` class accessor for the same reason as above:
36
- # to retrieve the instance created during `run` so we can assert on it.
37
- fake_transport_class = Class.new do
38
- class << self
39
- # Holds the most recently created fake transport instance for later
40
- # assertions (e.g., that `open` was invoked).
41
- attr_accessor :last_instance
42
- end
43
- attr_reader :server, :opened
44
-
45
- def initialize(server)
46
- @server = server
47
- @opened = false
48
- self.class.last_instance = self
49
- end
50
-
51
- def open
52
- @opened = true
53
- end
54
-
55
- def opened?
56
- @opened
57
- end
58
- end
59
-
60
- stub_const('MCP::Server', fake_server_class)
61
- stub_const('MCP::Server::Transports::StdioTransport', fake_transport_class)
14
+ stub_const('MCP::Server', FakeMCP::Server)
15
+ stub_const('MCP::Server::Transports::StdioTransport', FakeMCP::StdioTransport)
62
16
 
63
17
  server_context = SimpleCovMcp.create_context(
64
18
  error_handler: SimpleCovMcp::ErrorHandlerFactory.for_mcp_server,
@@ -73,8 +27,8 @@ RSpec.describe SimpleCovMcp::MCPServer do
73
27
  expect(SimpleCovMcp.context).to eq(baseline_context)
74
28
 
75
29
  # Fetch the instances created during `run` via the class-level hooks.
76
- fake_server = fake_server_class.last_instance
77
- fake_transport = fake_transport_class.last_instance
30
+ fake_server = FakeMCP::Server.last_instance
31
+ fake_transport = FakeMCP::StdioTransport.last_instance
78
32
 
79
33
  expect(fake_transport).not_to be_nil
80
34
  expect(fake_transport).to be_opened
@@ -83,7 +37,70 @@ RSpec.describe SimpleCovMcp::MCPServer do
83
37
  expect(fake_server.params[:name]).to eq('simplecov-mcp')
84
38
  # Ensure expected tools are registered
85
39
  tool_names = fake_server.params[:tools].map { |t| t.name.split('::').last }
86
- expect(tool_names).to include('AllFilesCoverageTool', 'CoverageDetailedTool',
87
- 'CoverageRawTool', 'CoverageSummaryTool', 'UncoveredLinesTool', 'HelpTool')
40
+ expect(tool_names).to include(
41
+ 'AllFilesCoverageTool',
42
+ 'CoverageDetailedTool',
43
+ 'CoverageRawTool',
44
+ 'CoverageSummaryTool',
45
+ 'CoverageTotalsTool',
46
+ 'UncoveredLinesTool',
47
+ 'CoverageTableTool',
48
+ 'HelpTool',
49
+ 'VersionTool'
50
+ )
51
+ end
52
+
53
+ describe 'TOOLSET and TOOL_GUIDE consistency' do
54
+ it 'includes all tools documented in HelpTool TOOL_GUIDE' do
55
+ # Get tool classes from TOOLSET
56
+ toolset_classes = described_class::TOOLSET
57
+
58
+ # Get tool classes from TOOL_GUIDE
59
+ tool_guide_classes = SimpleCovMcp::Tools::HelpTool::TOOL_GUIDE.map { |guide| guide[:tool] }
60
+
61
+ # Every tool in TOOL_GUIDE should be in TOOLSET
62
+ tool_guide_classes.each do |tool_class|
63
+ expect(toolset_classes).to include(tool_class),
64
+ "Expected TOOLSET to include #{tool_class.name}, but it was missing. " \
65
+ 'Add it to MCPServer::TOOLSET or remove from HelpTool::TOOL_GUIDE.'
66
+ end
67
+ end
68
+
69
+ it 'has corresponding TOOL_GUIDE entry for all tools (except HelpTool itself)' do
70
+ toolset_classes = described_class::TOOLSET
71
+ tool_guide_classes = SimpleCovMcp::Tools::HelpTool::TOOL_GUIDE.map { |guide| guide[:tool] }
72
+
73
+ # Every tool in TOOLSET should be in TOOL_GUIDE (except HelpTool which documents itself)
74
+ toolset_classes.each do |tool_class|
75
+ # HelpTool doesn't need an entry about itself
76
+ next if tool_class == SimpleCovMcp::Tools::HelpTool
77
+
78
+ expect(tool_guide_classes).to include(tool_class),
79
+ "Expected TOOL_GUIDE to document #{tool_class.name}, but it was missing. " \
80
+ 'Add documentation for this tool to HelpTool::TOOL_GUIDE.'
81
+ end
82
+ end
83
+
84
+ it 'registers the expected number of tools' do
85
+ expect(described_class::TOOLSET.length).to eq(10)
86
+ end
87
+
88
+ it 'registers all tool classes defined in SimpleCovMcp::Tools module' do
89
+ # This test catches the bug where a tool file is created, required in
90
+ # simplecov_mcp.rb, but not added to MCPServer::TOOLSET.
91
+ #
92
+ # Get all classes in the Tools module that inherit from BaseTool
93
+ tool_classes = SimpleCovMcp::Tools.constants
94
+ .map { |const_name| SimpleCovMcp::Tools.const_get(const_name) }
95
+ .select { |const| const.is_a?(Class) && const < SimpleCovMcp::BaseTool }
96
+
97
+ toolset_classes = described_class::TOOLSET
98
+
99
+ tool_classes.each do |tool_class|
100
+ expect(toolset_classes).to include(tool_class),
101
+ "Expected TOOLSET to include #{tool_class.name}, but it was missing. " \
102
+ 'The tool class exists in SimpleCovMcp::Tools but is not registered in MCPServer::TOOLSET.'
103
+ end
104
+ end
88
105
  end
89
106
  end