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
data/lib/simplecov_mcp.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'json'
4
4
  require 'time'
5
5
  require 'pathname'
6
+ require 'set' # rubocop:disable Lint/RedundantRequireStatement -- Ruby >= 3.4 requires explicit require for set; RuboCop targets 3.2
6
7
  require 'optparse'
7
8
  require 'mcp'
8
9
  require 'mcp/server/transports/stdio_transport'
@@ -17,13 +18,16 @@ require_relative 'simplecov_mcp/path_relativizer'
17
18
  require_relative 'simplecov_mcp/resultset_loader'
18
19
  require_relative 'simplecov_mcp/mode_detector'
19
20
  require_relative 'simplecov_mcp/model'
21
+ require_relative 'simplecov_mcp/coverage_reporter'
20
22
  require_relative 'simplecov_mcp/base_tool'
21
23
  require_relative 'simplecov_mcp/tools/coverage_raw_tool'
22
24
  require_relative 'simplecov_mcp/tools/coverage_summary_tool'
23
25
  require_relative 'simplecov_mcp/tools/uncovered_lines_tool'
24
26
  require_relative 'simplecov_mcp/tools/coverage_detailed_tool'
25
27
  require_relative 'simplecov_mcp/tools/all_files_coverage_tool'
28
+ require_relative 'simplecov_mcp/tools/coverage_totals_tool'
26
29
  require_relative 'simplecov_mcp/tools/coverage_table_tool'
30
+ require_relative 'simplecov_mcp/tools/validate_tool'
27
31
  require_relative 'simplecov_mcp/tools/version_tool'
28
32
  require_relative 'simplecov_mcp/tools/help_tool'
29
33
  require_relative 'simplecov_mcp/mcp_server'
@@ -34,23 +38,26 @@ module SimpleCovMcp
34
38
  THREAD_CONTEXT_KEY = :simplecov_mcp_context
35
39
 
36
40
  def run(argv)
37
- # Parse environment options for mode detection
38
- env_opts = parse_env_opts_for_mode_detection
39
- full_argv = env_opts + argv
41
+ # Prepend environment options once at entry point
42
+ full_argv = extract_env_opts + argv
40
43
 
41
44
  if ModeDetector.cli_mode?(full_argv)
42
- CoverageCLI.new.run(argv) # CLI will re-parse env opts internally
45
+ # CLI mode: pass merged argv to CoverageCLI
46
+ CoverageCLI.new.run(full_argv)
43
47
  else
44
- log_file = parse_log_file(full_argv)
48
+ # MCP server mode: parse config once from full_argv
49
+ require_relative 'simplecov_mcp/config_parser'
50
+ config = ConfigParser.parse(full_argv)
45
51
 
46
- if log_file == 'stdout'
52
+ if config.log_file == 'stdout'
47
53
  raise ConfigurationError,
48
54
  'Logging to stdout is not permitted in MCP server mode as it interferes with ' \
49
55
  "the JSON-RPC protocol. Please use 'stderr' or a file path."
50
56
  end
51
57
 
52
- handler = ErrorHandlerFactory.for_mcp_server
53
- context = create_context(error_handler: handler, log_target: log_file, mode: :mcp_server)
58
+ handler = ErrorHandlerFactory.for_mcp_server(error_mode: config.error_mode)
59
+ context = create_context(error_handler: handler, log_target: config.log_file,
60
+ mode: :mcp)
54
61
  with_context(context) { MCPServer.new(context: context).run }
55
62
  end
56
63
  end
@@ -86,7 +93,7 @@ module SimpleCovMcp
86
93
  if active.nil? || active.log_target == previous_default.log_target
87
94
  Thread.current[THREAD_CONTEXT_KEY] = @default_context
88
95
  end
89
- value
96
+ value # rubocop:disable Lint/Void -- return assigned log target for symmetry
90
97
  end
91
98
 
92
99
  def active_log_file
@@ -95,12 +102,12 @@ module SimpleCovMcp
95
102
 
96
103
  def active_log_file=(value)
97
104
  current = Thread.current[THREAD_CONTEXT_KEY]
98
- if current
99
- Thread.current[THREAD_CONTEXT_KEY] = current.with_log_target(value)
105
+ Thread.current[THREAD_CONTEXT_KEY] = if current
106
+ current.with_log_target(value)
100
107
  else
101
- Thread.current[THREAD_CONTEXT_KEY] = default_context.with_log_target(value)
108
+ default_context.with_log_target(value)
102
109
  end
103
- value
110
+ value # rubocop:disable Lint/Void -- return assigned log target for symmetry
104
111
  end
105
112
 
106
113
  def error_handler
@@ -111,27 +118,14 @@ module SimpleCovMcp
111
118
  @default_context = default_context.with_error_handler(handler)
112
119
  end
113
120
 
114
- private
115
-
116
- def default_context
121
+ private def default_context
117
122
  @default_context ||= AppContext.new(
118
123
  error_handler: ErrorHandlerFactory.for_cli,
119
124
  log_target: nil
120
125
  )
121
126
  end
122
127
 
123
- def parse_log_file(argv)
124
- log_file = nil
125
- parser = OptionParser.new do |o|
126
- # Define the option we're looking for
127
- o.on('-l', '--log-file PATH') { |v| log_file = v }
128
- end
129
- # Parse arguments, but ignore errors and stop at the first non-option
130
- parser.order!(argv.dup) {} rescue nil
131
- log_file
132
- end
133
-
134
- def parse_env_opts_for_mode_detection
128
+ private def extract_env_opts
135
129
  require 'shellwords'
136
130
  opts_string = ENV['SIMPLECOV_MCP_OPTS']
137
131
  return [] unless opts_string && !opts_string.empty?
@@ -139,7 +133,7 @@ module SimpleCovMcp
139
133
  begin
140
134
  Shellwords.split(opts_string)
141
135
  rescue ArgumentError
142
- [] # Ignore parsing errors for mode detection
136
+ [] # Ignore parsing errors
143
137
  end
144
138
  end
145
139
  end
@@ -4,6 +4,8 @@ require 'spec_helper'
4
4
  require 'simplecov_mcp/tools/all_files_coverage_tool'
5
5
 
6
6
  RSpec.describe SimpleCovMcp::Tools::AllFilesCoverageTool do
7
+ subject(:call_tool) { described_class.call(root: root, server_context: server_context) }
8
+
7
9
  let(:root) { (FIXTURES_DIR / 'project1').to_s }
8
10
  let(:server_context) { instance_double('ServerContext').as_null_object }
9
11
 
@@ -27,13 +29,12 @@ RSpec.describe SimpleCovMcp::Tools::AllFilesCoverageTool do
27
29
  allow(presenter).to receive(:relativized_payload).and_return(payload)
28
30
  end
29
31
 
30
- subject { described_class.call(root: root, server_context: server_context) }
31
32
 
32
33
  it_behaves_like 'an MCP tool that returns text JSON'
33
34
 
34
35
  it 'returns all files coverage data with counts' do
35
- response = subject
36
- data, item = expect_mcp_text_json(response, expected_keys: ['files', 'counts'])
36
+ response = call_tool
37
+ data, _item = expect_mcp_text_json(response, expected_keys: ['files', 'counts'])
37
38
 
38
39
  files = data['files']
39
40
  counts = data['counts']
@@ -2,42 +2,41 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- RSpec.describe SimpleCovMcp::CLIConfig do
5
+ RSpec.describe SimpleCovMcp::AppConfig do
6
6
  describe '#initialize' do
7
7
  it 'creates a config with default values' do
8
8
  config = described_class.new
9
9
  expect(config.root).to eq('.')
10
- expect(config.json).to be false
11
- expect(config.sort_order).to eq(:ascending)
10
+ expect(config.format).to eq(:table)
11
+ expect(config.sort_order).to eq(:descending)
12
12
  expect(config.source_context).to eq(2)
13
- expect(config.error_mode).to eq(:on)
14
- expect(config.stale_mode).to eq(:off)
13
+ expect(config.error_mode).to eq(:log)
14
+ expect(config.staleness).to eq(:off)
15
15
  expect(config.resultset).to be_nil
16
16
  expect(config.source_mode).to be_nil
17
17
  expect(config.tracked_globs).to be_nil
18
18
  expect(config.log_file).to be_nil
19
- expect(config.success_predicate).to be_nil
20
19
  end
21
20
 
22
21
  it 'allows overriding defaults via keyword arguments' do
23
22
  config = described_class.new(
24
23
  root: '/custom',
25
- json: true,
24
+ format: :json,
26
25
  sort_order: :descending,
27
- stale_mode: :error
26
+ staleness: :error
28
27
  )
29
28
  expect(config.root).to eq('/custom')
30
- expect(config.json).to be true
29
+ expect(config.format).to eq(:json)
31
30
  expect(config.sort_order).to eq(:descending)
32
- expect(config.stale_mode).to eq(:error)
31
+ expect(config.staleness).to eq(:error)
33
32
  end
34
33
 
35
34
  it 'is mutable (struct fields can be changed)' do
36
35
  config = described_class.new
37
36
  config.root = '/new/root'
38
- config.json = true
37
+ config.format = :json
39
38
  expect(config.root).to eq('/new/root')
40
- expect(config.json).to be true
39
+ expect(config.format).to eq(:json)
41
40
  end
42
41
  end
43
42
 
@@ -46,7 +45,7 @@ RSpec.describe SimpleCovMcp::CLIConfig do
46
45
  config = described_class.new(
47
46
  root: '/custom/root',
48
47
  resultset: '/custom/.resultset.json',
49
- stale_mode: :error,
48
+ staleness: :error,
50
49
  tracked_globs: ['lib/**/*.rb']
51
50
  )
52
51
 
@@ -85,46 +84,52 @@ RSpec.describe SimpleCovMcp::CLIConfig do
85
84
 
86
85
  describe 'struct behavior' do
87
86
  it 'supports equality comparison' do
88
- config1 = described_class.new(root: '/foo', json: true)
89
- config2 = described_class.new(root: '/foo', json: true)
90
- config3 = described_class.new(root: '/bar', json: true)
87
+ config1 = described_class.new(root: '/foo', format: :json)
88
+ config2 = described_class.new(root: '/foo', format: :json)
89
+ config3 = described_class.new(root: '/bar', format: :json)
91
90
 
92
91
  expect(config1).to eq(config2)
93
92
  expect(config1).not_to eq(config3)
94
93
  end
95
94
 
96
95
  it 'provides readable inspect output' do
97
- config = described_class.new(root: '/test', json: true)
96
+ config = described_class.new(root: '/test', format: :json)
98
97
  output = config.inspect
99
98
  expect(output).to include('root="/test"')
100
- expect(output).to include('json=true')
99
+ expect(output).to include('format=:json')
101
100
  end
102
101
 
103
102
  it 'converts to hash' do
104
- config = described_class.new(root: '/test', json: true)
103
+ config = described_class.new(root: '/test', format: :json)
105
104
  hash = config.to_h
106
105
  expect(hash).to be_a(Hash)
107
106
  expect(hash[:root]).to eq('/test')
108
- expect(hash[:json]).to be true
107
+ expect(hash[:format]).to eq(:json)
109
108
  end
110
109
  end
111
110
 
112
111
  describe 'symbol enumerated values' do
112
+ it 'uses symbols for format' do
113
+ config = described_class.new(format: :json)
114
+ expect(config.format).to eq(:json)
115
+ expect(config.format).to be_a(Symbol)
116
+ end
117
+
113
118
  it 'uses symbols for sort_order' do
114
119
  config = described_class.new(sort_order: :descending)
115
120
  expect(config.sort_order).to eq(:descending)
116
121
  expect(config.sort_order).to be_a(Symbol)
117
122
  end
118
123
 
119
- it 'uses symbols for stale_mode' do
120
- config = described_class.new(stale_mode: :error)
121
- expect(config.stale_mode).to eq(:error)
122
- expect(config.stale_mode).to be_a(Symbol)
124
+ it 'uses symbols for staleness' do
125
+ config = described_class.new(staleness: :error)
126
+ expect(config.staleness).to eq(:error)
127
+ expect(config.staleness).to be_a(Symbol)
123
128
  end
124
129
 
125
130
  it 'uses symbols for error_mode' do
126
- config = described_class.new(error_mode: :trace)
127
- expect(config.error_mode).to eq(:trace)
131
+ config = described_class.new(error_mode: :debug)
132
+ expect(config.error_mode).to eq(:debug)
128
133
  expect(config.error_mode).to be_a(Symbol)
129
134
  end
130
135
 
@@ -3,41 +3,42 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  RSpec.describe SimpleCovMcp::BaseTool do
6
- let(:handler) { SimpleCovMcp::ErrorHandler.new(error_mode: :on, logger: test_logger) }
6
+ let(:handler) { SimpleCovMcp::ErrorHandler.new(error_mode: :log, logger: test_logger) }
7
7
  let(:test_logger) do
8
8
  Class.new do
9
9
  attr_reader :messages
10
10
 
11
- def initialize; @messages = []; end
12
- def error(msg); @messages << msg; end
11
+ def initialize = @messages = []
12
+ def error(msg) = @messages << msg
13
13
  end.new
14
14
  end
15
15
 
16
+ let(:orig_handler) do
17
+ SimpleCovMcp.error_handler
18
+ rescue
19
+ nil
20
+ end
21
+
16
22
  before do
17
- @orig_handler = begin
18
- SimpleCovMcp.error_handler
19
- rescue StandardError
20
- nil
21
- end
22
23
  SimpleCovMcp.error_handler = handler
23
24
  setup_mcp_response_stub
24
25
  end
25
26
 
26
27
  after do
27
- SimpleCovMcp.error_handler = @orig_handler if @orig_handler
28
+ SimpleCovMcp.error_handler = orig_handler if orig_handler
28
29
  end
29
30
 
30
31
  shared_examples 'friendly response and logged' do
31
32
  it 'returns friendly text' do
32
- resp = described_class.handle_mcp_error(error, tool, error_mode: :on)
33
+ resp = described_class.handle_mcp_error(error, tool, error_mode: :log)
33
34
  expect(resp).to be_a(MCP::Tool::Response)
34
- expect(resp.payload.first[:text]).to match(expected_pattern)
35
+ expect(resp.payload.first['text']).to match(expected_pattern)
35
36
  end
36
37
 
37
38
  it 'respects error_mode :off' do
38
39
  resp = described_class.handle_mcp_error(error, tool, error_mode: :off)
39
40
  expect(resp).to be_a(MCP::Tool::Response)
40
- expect(resp.payload.first[:text]).to match(expected_pattern)
41
+ expect(resp.payload.first['text']).to match(expected_pattern)
41
42
  end
42
43
  end
43
44
 
@@ -46,7 +47,8 @@ RSpec.describe SimpleCovMcp::BaseTool do
46
47
  let(:tool) { 'coverage_summary' }
47
48
  let(:expected_pattern) { /Error: invalid args/ }
48
49
  let(:log_fragment) { 'invalid args' }
49
- include_examples 'friendly response and logged'
50
+
51
+ it_behaves_like 'friendly response and logged'
50
52
  end
51
53
 
52
54
  context 'with standard error' do
@@ -54,6 +56,7 @@ RSpec.describe SimpleCovMcp::BaseTool do
54
56
  let(:tool) { 'coverage_raw' }
55
57
  let(:expected_pattern) { /Error: .*File not found: missing.rb/ }
56
58
  let(:log_fragment) { 'File not found' }
57
- include_examples 'friendly response and logged'
59
+
60
+ it_behaves_like 'friendly response and logged'
58
61
  end
59
62
  end
@@ -9,13 +9,13 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
9
9
  before do
10
10
  cli.config.root = root
11
11
  cli.config.resultset = 'coverage'
12
- cli.config.stale_mode = :off
12
+ cli.config.staleness = :off
13
13
  cli.config.tracked_globs = nil
14
14
  end
15
15
 
16
16
  describe '#show_default_report' do
17
17
  it 'prints JSON summary using relativized payload when json mode is enabled' do
18
- cli.config.json = true
18
+ cli.config.format = :json
19
19
 
20
20
  output = nil
21
21
  silence_output do |stdout, _stderr|
@@ -18,18 +18,22 @@ RSpec.describe 'CLI enumerated option parsing' do
18
18
  expected: :descending },
19
19
 
20
20
  { argv: ['--source', 'f', 'summary', 'lib/foo.rb'], accessor: :source_mode, expected: :full },
21
- { argv: ['--source=u', 'summary', 'lib/foo.rb'], accessor: :source_mode,
21
+ { argv: ['--source', 'u', 'summary', 'lib/foo.rb'], accessor: :source_mode,
22
22
  expected: :uncovered },
23
- { argv: ['--source=full', 'summary', 'lib/foo.rb'], accessor: :source_mode, expected: :full },
24
- { argv: ['--source=uncovered', 'summary', 'lib/foo.rb'], accessor: :source_mode,
23
+ { argv: ['--source', 'full', 'summary', 'lib/foo.rb'], accessor: :source_mode,
24
+ expected: :full },
25
+ { argv: ['--source', 'uncovered', 'summary', 'lib/foo.rb'], accessor: :source_mode,
25
26
  expected: :uncovered },
26
27
 
27
- { argv: ['-S', 'e', 'list'], accessor: :stale_mode, expected: :error },
28
- { argv: ['-S', 'o', 'list'], accessor: :stale_mode, expected: :off },
28
+ { argv: ['-S', 'e', 'list'], accessor: :staleness, expected: :error },
29
+ { argv: ['-S', 'o', 'list'], accessor: :staleness, expected: :off },
30
+ { argv: ['--staleness', 'e', 'list'], accessor: :staleness, expected: :error },
31
+ { argv: ['--staleness', 'o', 'list'], accessor: :staleness, expected: :off },
29
32
 
30
33
  { argv: ['--error-mode', 'off', 'list'], accessor: :error_mode, expected: :off },
31
- { argv: ['--error-mode', 'on', 'list'], accessor: :error_mode, expected: :on },
32
- { argv: ['--error-mode', 't', 'list'], accessor: :error_mode, expected: :trace }
34
+ { argv: ['--error-mode', 'o', 'list'], accessor: :error_mode, expected: :off },
35
+ { argv: ['--error-mode', 'log', 'list'], accessor: :error_mode, expected: :log },
36
+ { argv: ['--error-mode', 'debug', 'list'], accessor: :error_mode, expected: :debug }
33
37
  ]
34
38
 
35
39
  cases.each do |c|
@@ -43,9 +47,12 @@ RSpec.describe 'CLI enumerated option parsing' do
43
47
  describe 'rejects invalid values' do
44
48
  invalid_cases = [
45
49
  { argv: ['--sort-order', 'asc', 'list'] },
46
- { argv: ['--source=x', 'summary', 'lib/foo.rb'] },
50
+ { argv: ['--source', 'x', 'summary', 'lib/foo.rb'] },
47
51
  { argv: ['-S', 'x', 'list'] },
48
- { argv: ['--error-mode', 'bad', 'list'] }
52
+ { argv: ['--staleness', 'x', 'list'] },
53
+ { argv: ['--error-mode', 'bad', 'list'] },
54
+ { argv: ['--error-mode', 'on', 'list'] },
55
+ { argv: ['--error-mode', 'trace', 'list'] }
49
56
  ]
50
57
 
51
58
  invalid_cases.each do |c|
@@ -64,5 +71,20 @@ RSpec.describe 'CLI enumerated option parsing' do
64
71
  expect(status).to eq(1)
65
72
  expect(err).to include('invalid argument')
66
73
  end
74
+
75
+ it 'exits 1 when --staleness is provided without a value' do
76
+ _out, err, status = run_cli_with_status('--staleness', 'list')
77
+ expect(status).to eq(1)
78
+ expect(err).to include('invalid argument')
79
+ end
80
+
81
+ it 'exits 1 when --source is provided without a value' do
82
+ _out, err, status = run_cli_with_status('--source', 'summary', 'lib/foo.rb')
83
+ expect(status).to eq(1)
84
+ # Depending on OptParse implementation for required argument, it might say "missing argument"
85
+ # But usually it consumes next arg. If 'summary' is consumed as argument for source:
86
+ # normalize_source_mode('summary') -> raises InvalidArgument.
87
+ expect(err).to include('invalid argument')
88
+ end
67
89
  end
68
90
  end
@@ -9,9 +9,11 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
9
9
  out, err, status = run_cli_with_status('--help')
10
10
  expect(status).to eq(0)
11
11
  expect(out).to match(/Usage:.*simplecov-mcp/)
12
- expect(out).to include('Repository: https://github.com/keithrbennett/simplecov-mcp')
12
+ expect(out).to include(
13
+ 'Repository: https://github.com/keithrbennett/simplecov-mcp',
14
+ 'Subcommands:'
15
+ )
13
16
  expect(out).to match(/Version:.*#{SimpleCovMcp::VERSION}/)
14
- expect(out).to include('Subcommands:')
15
17
  expect(err).to eq('')
16
18
  end
17
19
 
@@ -19,7 +21,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
19
21
  before do
20
22
  # Build a fake model that raises the specified error from the specified method
21
23
  fake_model = Class.new do
22
- def initialize(*) end
24
+ def initialize(*)
25
+ end
23
26
  end
24
27
  error_to_raise = raised_error
25
28
  fake_model.define_method(model_method) { |*| raise error_to_raise }
@@ -33,20 +36,22 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
33
36
  end
34
37
  end
35
38
 
36
- context 'ENOENT mapping' do
39
+ context 'when mapping ENOENT' do
37
40
  let(:model_method) { :summary_for }
38
41
  let(:raised_error) { Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb') }
39
- let(:invoke_args) { ['summary', 'lib/missing.rb', '--root', root, '--resultset', 'coverage'] }
42
+ let(:invoke_args) { ['--root', root, '--resultset', 'coverage', 'summary', 'lib/missing.rb'] }
40
43
  let(:expected_message) { 'File error: File not found: lib/missing.rb' }
41
- include_examples 'maps error to exit 1 with message'
44
+
45
+ it_behaves_like 'maps error to exit 1 with message'
42
46
  end
43
47
 
44
- context 'EACCES mapping' do
48
+ context 'when mapping EACCES' do
45
49
  let(:model_method) { :raw_for }
46
50
  let(:raised_error) { Errno::EACCES.new('Permission denied @ rb_sysopen - secret.rb') }
47
- let(:invoke_args) { ['raw', 'lib/secret.rb', '--root', root, '--resultset', 'coverage'] }
51
+ let(:invoke_args) { ['--root', root, '--resultset', 'coverage', 'raw', 'lib/secret.rb'] }
48
52
  let(:expected_message) { 'Permission denied: lib/secret.rb' }
49
- include_examples 'maps error to exit 1 with message'
53
+
54
+ it_behaves_like 'maps error to exit 1 with message'
50
55
  end
51
56
 
52
57
  it 'emits detailed stale coverage info and exits 1' do
@@ -54,8 +59,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
54
59
  File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, 1] }
55
60
  })
56
61
 
57
- _out, err, status = run_cli_with_status('summary', 'lib/foo.rb', '--root', root, '--resultset',
58
- 'coverage', '--stale', 'error')
62
+ _out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage',
63
+ '--staleness', 'error', 'summary', 'lib/foo.rb')
59
64
  expect(status).to eq(1)
60
65
  expect(err).to include('Coverage data stale:')
61
66
  expect(err).to match(/File\s+- time:/)
@@ -69,8 +74,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
69
74
  File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, 1] }
70
75
  })
71
76
 
72
- _out, err, status = run_cli_with_status('summary', 'lib/foo.rb', '--root', root, '--resultset',
73
- 'coverage', '--stale', 'off')
77
+ _out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage',
78
+ '--staleness', 'off', 'summary', 'lib/foo.rb')
74
79
  expect(status).to eq(0)
75
80
  expect(err).to eq('')
76
81
  end
@@ -80,22 +85,22 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
80
85
  # This is a regression test for the "can't convert nil into Integer" crash
81
86
  # that was previously mentioned in comments
82
87
  out, err, status = run_cli_with_status(
83
- 'uncovered', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
84
- '--source=uncovered', '--source-context', '2', '--no-color'
88
+ '--root', root, '--resultset', 'coverage', '--source', 'uncovered', '--context-lines', '2',
89
+ '--no-color', 'uncovered', 'lib/foo.rb'
85
90
  )
86
91
 
87
92
  expect(status).to eq(0)
88
93
  expect(err).to eq('')
89
94
  expect(out).to match(/File:\s+lib\/foo\.rb/)
90
- expect(out).to include('Uncovered lines: 2')
95
+ expect(out).to include('│') # Table format
91
96
  expect(out).to show_source_table_or_fallback
92
97
  end
93
98
 
94
99
  it 'renders source with full mode without crashing' do
95
100
  # Additional regression test for source rendering with full mode
96
101
  out, err, status = run_cli_with_status(
97
- 'summary', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
98
- '--source=full', '--no-color'
102
+ '--root', root, '--resultset', 'coverage', '--source', 'full', '--no-color',
103
+ 'summary', 'lib/foo.rb'
99
104
  )
100
105
 
101
106
  expect(status).to eq(0)
@@ -115,8 +120,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
115
120
  File.rename(foo_path, temp_path) if File.exist?(foo_path)
116
121
 
117
122
  out, err, status = run_cli_with_status(
118
- 'summary', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
119
- '--source=full', '--no-color'
123
+ '--root', root, '--resultset', 'coverage', '--source', 'full', '--no-color',
124
+ 'summary', 'lib/foo.rb'
120
125
  )
121
126
 
122
127
  expect(status).to eq(0)
@@ -141,13 +146,13 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
141
146
  end
142
147
 
143
148
  it 'reports invalid enum value for --opt=value' do
144
- _out, err, status = run_cli_with_status('list', '--stale=bogus')
149
+ _out, err, status = run_cli_with_status('--staleness=bogus', 'list')
145
150
  expect(status).to eq(1)
146
- expect(err).to include('invalid argument: --stale=bogus')
151
+ expect(err).to include('invalid argument: --staleness=bogus')
147
152
  end
148
153
 
149
154
  it 'reports invalid enum value for --opt value' do
150
- _out, err, status = run_cli_with_status('list', '--stale', 'bogus')
155
+ _out, err, status = run_cli_with_status('--staleness', 'bogus', 'list')
151
156
  expect(status).to eq(1)
152
157
  expect(err).to include('invalid argument: bogus')
153
158
  end
@@ -158,4 +163,22 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
158
163
  expect(err).to include('Error: invalid option: --no-such-option')
159
164
  end
160
165
  end
166
+
167
+ describe 'subcommand error handling' do
168
+ it 'handles generic exceptions from subcommands' do
169
+ # Stub the CommandFactory to return a command that raises a StandardError
170
+ fake_command = Class.new do
171
+ def initialize(_cli) = nil
172
+ def execute(_args) = raise(StandardError, 'Unexpected error in subcommand')
173
+ end
174
+
175
+ allow(SimpleCovMcp::Commands::CommandFactory).to receive(:create)
176
+ .and_return(fake_command.new(nil))
177
+
178
+ _out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage', 'summary',
179
+ 'lib/foo.rb')
180
+ expect(status).to eq(1)
181
+ expect(err).to include('Unexpected error in subcommand')
182
+ end
183
+ end
161
184
  end