simplecov-mcp 0.3.0 → 1.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +173 -356
  4. data/docs/ADVANCED_USAGE.md +967 -0
  5. data/docs/ARCHITECTURE.md +79 -0
  6. data/docs/BRANCH_ONLY_COVERAGE.md +81 -0
  7. data/docs/CLI_USAGE.md +637 -0
  8. data/docs/DEVELOPMENT.md +82 -0
  9. data/docs/ERROR_HANDLING.md +93 -0
  10. data/docs/EXAMPLES.md +430 -0
  11. data/docs/INSTALLATION.md +352 -0
  12. data/docs/LIBRARY_API.md +635 -0
  13. data/docs/MCP_INTEGRATION.md +488 -0
  14. data/docs/TROUBLESHOOTING.md +276 -0
  15. data/docs/arch-decisions/001-x-arch-decision.md +93 -0
  16. data/docs/arch-decisions/002-x-arch-decision.md +157 -0
  17. data/docs/arch-decisions/003-x-arch-decision.md +163 -0
  18. data/docs/arch-decisions/004-x-arch-decision.md +199 -0
  19. data/docs/arch-decisions/005-x-arch-decision.md +187 -0
  20. data/docs/arch-decisions/README.md +60 -0
  21. data/docs/presentations/simplecov-mcp-presentation.md +249 -0
  22. data/exe/simplecov-mcp +4 -4
  23. data/lib/simplecov_mcp/app_context.rb +26 -0
  24. data/lib/simplecov_mcp/base_tool.rb +74 -0
  25. data/lib/simplecov_mcp/cli.rb +234 -0
  26. data/lib/simplecov_mcp/cli_config.rb +56 -0
  27. data/lib/simplecov_mcp/commands/base_command.rb +78 -0
  28. data/lib/simplecov_mcp/commands/command_factory.rb +39 -0
  29. data/lib/simplecov_mcp/commands/detailed_command.rb +24 -0
  30. data/lib/simplecov_mcp/commands/list_command.rb +13 -0
  31. data/lib/simplecov_mcp/commands/raw_command.rb +22 -0
  32. data/lib/simplecov_mcp/commands/summary_command.rb +24 -0
  33. data/lib/simplecov_mcp/commands/uncovered_command.rb +26 -0
  34. data/lib/simplecov_mcp/commands/version_command.rb +18 -0
  35. data/lib/simplecov_mcp/constants.rb +22 -0
  36. data/lib/simplecov_mcp/error_handler.rb +124 -0
  37. data/lib/simplecov_mcp/error_handler_factory.rb +31 -0
  38. data/lib/simplecov_mcp/errors.rb +179 -0
  39. data/lib/simplecov_mcp/formatters/source_formatter.rb +148 -0
  40. data/lib/simplecov_mcp/mcp_server.rb +40 -0
  41. data/lib/simplecov_mcp/mode_detector.rb +55 -0
  42. data/lib/simplecov_mcp/model.rb +300 -0
  43. data/lib/simplecov_mcp/option_normalizers.rb +92 -0
  44. data/lib/simplecov_mcp/option_parser_builder.rb +134 -0
  45. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +50 -0
  46. data/lib/simplecov_mcp/option_parsers/error_helper.rb +109 -0
  47. data/lib/simplecov_mcp/path_relativizer.rb +61 -0
  48. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +44 -0
  49. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +16 -0
  50. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +16 -0
  51. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +16 -0
  52. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +16 -0
  53. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +52 -0
  54. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +126 -0
  55. data/lib/simplecov_mcp/resolvers/resolver_factory.rb +28 -0
  56. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +78 -0
  57. data/lib/simplecov_mcp/resultset_loader.rb +136 -0
  58. data/lib/simplecov_mcp/staleness_checker.rb +243 -0
  59. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/all_files_coverage_tool.rb +31 -13
  60. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_detailed_tool.rb +7 -7
  61. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_raw_tool.rb +7 -7
  62. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_summary_tool.rb +7 -7
  63. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +90 -0
  64. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/help_tool.rb +13 -4
  65. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/uncovered_lines_tool.rb +7 -7
  66. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/version_tool.rb +11 -3
  67. data/lib/simplecov_mcp/util.rb +82 -0
  68. data/lib/{simple_cov_mcp → simplecov_mcp}/version.rb +1 -1
  69. data/lib/simplecov_mcp.rb +144 -2
  70. data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
  71. data/spec/TIMESTAMPS.md +48 -0
  72. data/spec/all_files_coverage_tool_spec.rb +29 -25
  73. data/spec/base_tool_spec.rb +11 -10
  74. data/spec/cli/show_default_report_spec.rb +33 -0
  75. data/spec/cli_config_spec.rb +137 -0
  76. data/spec/cli_enumerated_options_spec.rb +68 -0
  77. data/spec/cli_error_spec.rb +105 -47
  78. data/spec/cli_source_spec.rb +82 -23
  79. data/spec/cli_spec.rb +140 -5
  80. data/spec/cli_success_predicate_spec.rb +141 -0
  81. data/spec/cli_table_spec.rb +1 -1
  82. data/spec/cli_usage_spec.rb +10 -26
  83. data/spec/commands/base_command_spec.rb +187 -0
  84. data/spec/commands/command_factory_spec.rb +72 -0
  85. data/spec/commands/detailed_command_spec.rb +48 -0
  86. data/spec/commands/raw_command_spec.rb +46 -0
  87. data/spec/commands/summary_command_spec.rb +47 -0
  88. data/spec/commands/uncovered_command_spec.rb +49 -0
  89. data/spec/constants_spec.rb +61 -0
  90. data/spec/coverage_table_tool_spec.rb +17 -33
  91. data/spec/error_handler_spec.rb +22 -13
  92. data/spec/error_mode_spec.rb +143 -0
  93. data/spec/errors_edge_cases_spec.rb +239 -0
  94. data/spec/errors_stale_spec.rb +2 -2
  95. data/spec/file_based_mcp_tools_spec.rb +99 -0
  96. data/spec/fixtures/project1/lib/bar.rb +0 -1
  97. data/spec/fixtures/project1/lib/foo.rb +0 -1
  98. data/spec/help_tool_spec.rb +11 -17
  99. data/spec/integration_spec.rb +845 -0
  100. data/spec/logging_fallback_spec.rb +128 -0
  101. data/spec/mcp_logging_spec.rb +44 -0
  102. data/spec/mcp_server_integration_spec.rb +23 -0
  103. data/spec/mcp_server_spec.rb +15 -4
  104. data/spec/mode_detector_spec.rb +148 -0
  105. data/spec/model_error_handling_spec.rb +210 -0
  106. data/spec/model_staleness_spec.rb +40 -10
  107. data/spec/option_normalizers_spec.rb +204 -0
  108. data/spec/option_parsers/env_options_parser_spec.rb +233 -0
  109. data/spec/option_parsers/error_helper_spec.rb +222 -0
  110. data/spec/path_relativizer_spec.rb +83 -0
  111. data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
  112. data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
  113. data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
  114. data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
  115. data/spec/presenters/project_coverage_presenter_spec.rb +86 -0
  116. data/spec/resolvers/coverage_line_resolver_spec.rb +57 -0
  117. data/spec/resolvers/resolver_factory_spec.rb +61 -0
  118. data/spec/resolvers/resultset_path_resolver_spec.rb +55 -0
  119. data/spec/resultset_loader_spec.rb +167 -0
  120. data/spec/shared_examples/README.md +115 -0
  121. data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
  122. data/spec/shared_examples/file_based_mcp_tools.rb +174 -0
  123. data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
  124. data/spec/simple_cov_mcp_module_spec.rb +16 -0
  125. data/spec/simplecov_mcp_model_spec.rb +340 -9
  126. data/spec/simplecov_mcp_opts_spec.rb +182 -0
  127. data/spec/spec_helper.rb +147 -4
  128. data/spec/staleness_checker_spec.rb +373 -0
  129. data/spec/staleness_more_spec.rb +16 -13
  130. data/spec/support/mcp_runner.rb +64 -0
  131. data/spec/tools_error_handling_spec.rb +144 -0
  132. data/spec/util_spec.rb +109 -34
  133. data/spec/version_spec.rb +117 -9
  134. data/spec/version_tool_spec.rb +131 -10
  135. metadata +120 -63
  136. data/lib/simple_cov/mcp.rb +0 -9
  137. data/lib/simple_cov_mcp/base_tool.rb +0 -70
  138. data/lib/simple_cov_mcp/cli.rb +0 -390
  139. data/lib/simple_cov_mcp/error_handler.rb +0 -131
  140. data/lib/simple_cov_mcp/error_handler_factory.rb +0 -38
  141. data/lib/simple_cov_mcp/errors.rb +0 -176
  142. data/lib/simple_cov_mcp/mcp_server.rb +0 -30
  143. data/lib/simple_cov_mcp/model.rb +0 -104
  144. data/lib/simple_cov_mcp/staleness_checker.rb +0 -125
  145. data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +0 -61
  146. data/lib/simple_cov_mcp/util.rb +0 -122
  147. data/lib/simple_cov_mcp.rb +0 -102
  148. data/spec/coverage_detailed_tool_spec.rb +0 -36
  149. data/spec/coverage_raw_tool_spec.rb +0 -32
  150. data/spec/coverage_summary_tool_spec.rb +0 -39
  151. data/spec/legacy_shim_spec.rb +0 -13
  152. data/spec/uncovered_lines_tool_spec.rb +0 -33
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SimpleCovMcp::Constants do
6
+ describe 'OPTIONS_EXPECTING_ARGUMENT' do
7
+ subject(:options) { described_class::OPTIONS_EXPECTING_ARGUMENT }
8
+
9
+ it 'exists' do
10
+ expect(options).not_to be_nil
11
+ end
12
+
13
+ it 'is frozen' do
14
+ expect(options).to be_frozen
15
+ end
16
+
17
+ it 'contains expected CLI options' do
18
+ expected_options = %w[
19
+ -r --resultset
20
+ -R --root
21
+ -o --sort-order
22
+ -s --source
23
+ -c --source-context
24
+ -S --stale
25
+ -g --tracked-globs
26
+ -l --log-file
27
+ --error-mode
28
+ --success-predicate
29
+ ]
30
+
31
+ expect(options).to eq(expected_options)
32
+ end
33
+
34
+ it 'contains only strings' do
35
+ expect(options).to all(be_a(String))
36
+ end
37
+
38
+ it 'contains options that start with dashes' do
39
+ expect(options).to all(start_with('-'))
40
+ end
41
+ end
42
+
43
+ describe 'usage by other classes' do
44
+ it 'is used by ModeDetector' do
45
+ expect(SimpleCovMcp::ModeDetector::OPTIONS_EXPECTING_ARGUMENT)
46
+ .to equal(SimpleCovMcp::Constants::OPTIONS_EXPECTING_ARGUMENT)
47
+ end
48
+
49
+ it 'is used by CoverageCLI' do
50
+ expect(SimpleCovMcp::CoverageCLI::OPTIONS_EXPECTING_ARGUMENT)
51
+ .to equal(SimpleCovMcp::Constants::OPTIONS_EXPECTING_ARGUMENT)
52
+ end
53
+
54
+ it 'ensures both classes reference the same object' do
55
+ cli_options = SimpleCovMcp::CoverageCLI::OPTIONS_EXPECTING_ARGUMENT
56
+ detector_options = SimpleCovMcp::ModeDetector::OPTIONS_EXPECTING_ARGUMENT
57
+
58
+ expect(cli_options).to equal(detector_options)
59
+ end
60
+ end
61
+ end
@@ -1,63 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
- require 'simple_cov_mcp/tools/coverage_table_tool'
4
+ require 'simplecov_mcp/tools/coverage_table_tool'
5
5
 
6
6
  RSpec.describe SimpleCovMcp::Tools::CoverageTableTool do
7
- let(:root) { (FIXTURES / 'project1').to_s }
7
+ let(:root) { (FIXTURES_DIR / 'project1').to_s }
8
8
  let(:server_context) { instance_double('ServerContext').as_null_object }
9
9
 
10
10
  before do
11
- stub_const('MCP::Tool::Response', Struct.new(:payload))
11
+ setup_mcp_response_stub
12
12
  end
13
13
 
14
14
  def run_tool(stale: 'off')
15
- # Stub the CoverageModel to avoid file system access
16
- model = instance_double(SimpleCovMcp::CoverageModel)
17
- allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
18
- allow(model).to receive(:all_files).and_return([
19
- { 'file' => "#{root}/lib/foo.rb", 'percentage' => 100.0, 'covered' => 10, 'total' => 10, 'stale' => false },
20
- { 'file' => "#{root}/lib/bar.rb", 'percentage' => 50.0, 'covered' => 5, 'total' => 10, 'stale' => true }
21
- ])
22
-
23
- response = described_class.call(root: root, stale: stale, server_context: server_context)
24
- response.payload.first[:text]
15
+ # Let real CoverageModel work to test actual format_table behavior
16
+ described_class.call(root: root, stale: stale,
17
+ server_context: server_context).payload.first[:text]
25
18
  end
26
19
 
27
20
  it 'returns a formatted table as a string' do
28
21
  output = run_tool
29
22
 
30
- # Contains a header row and at least one data row with expected columns
31
- expect(output).to include('File')
32
- expect(output).to include('Covered')
33
- expect(output).to include('Total')
34
- # Staleness column header reads 'Stale'
35
- expect(output).to include(' Stale ')
36
-
37
- # Should list fixture files from the demo project
38
- expect(output).to include('lib/foo.rb')
39
- expect(output).to include('lib/bar.rb')
40
-
41
- # Check for table borders
42
- expect(output).to include('┌')
43
- expect(output).to include('│')
44
- expect(output).to include('└')
45
-
46
- # Summary counts line appears after the table
47
- expect(output).to include('Files: total 2, ok 1, stale 1')
23
+ # Contains table structure, headers, and file data
24
+ expect(output).to include(
25
+ '┌', '', '┐', '│', '├', '┼', '┤', '└', '┴', '┘',
26
+ 'File', 'Covered', 'Total', ' │ Stale │',
27
+ 'lib/foo.rb', 'lib/bar.rb',
28
+ 'Files: total 2, ok 0, stale 2'
29
+ )
48
30
  end
49
31
 
50
- it 'configures the CLI to enforce stale checking when requested' do
32
+ it 'configures CLI to enforce stale checking when requested' do
51
33
  model = instance_double(SimpleCovMcp::CoverageModel)
52
34
  allow(model).to receive(:all_files).and_return([
53
35
  { 'file' => "#{root}/lib/foo.rb", 'percentage' => 100.0, 'covered' => 10, 'total' => 10, 'stale' => false }
54
36
  ])
37
+ allow(model).to receive(:relativize) { |payload| payload }
55
38
  expect(SimpleCovMcp::CoverageModel).to receive(:new).with(
56
39
  root: root,
57
40
  resultset: nil,
58
- staleness: 'error',
41
+ staleness: :error,
59
42
  tracked_globs: nil
60
43
  ).and_return(model)
44
+ allow(model).to receive(:format_table).and_return('Mock table output')
61
45
 
62
46
  described_class.call(root: root, stale: 'error', server_context: server_context)
63
47
  end
@@ -6,18 +6,21 @@ RSpec.describe SimpleCovMcp::ErrorHandler do
6
6
  let(:logger) do
7
7
  Class.new do
8
8
  attr_reader :messages
9
+
9
10
  def initialize; @messages = []; end
10
11
  def error(msg); @messages << msg; end
11
12
  end.new
12
13
  end
13
14
 
14
- subject(:handler) { described_class.new(log_errors: true, show_stack_traces: false, logger: logger) }
15
+ subject(:handler) { described_class.new(error_mode: :on, logger: logger) }
15
16
 
16
17
  it 'maps filesystem errors to friendly custom errors' do
17
18
  e = handler.convert_standard_error(Errno::EISDIR.new('Is a directory @ rb_sysopen - a_dir'))
18
19
  expect(e).to be_a(SimpleCovMcp::NotAFileError)
19
20
 
20
- e = handler.convert_standard_error(Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.txt'))
21
+ e = handler.convert_standard_error(
22
+ Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.txt')
23
+ )
21
24
  expect(e).to be_a(SimpleCovMcp::FileNotFoundError)
22
25
 
23
26
  e = handler.convert_standard_error(Errno::EACCES.new('Permission denied @ rb_sysopen - secret'))
@@ -31,7 +34,9 @@ RSpec.describe SimpleCovMcp::ErrorHandler do
31
34
  end
32
35
 
33
36
  it 'maps ArgumentError by message' do
34
- e = handler.convert_standard_error(ArgumentError.new('wrong number of arguments (given 1, expected 2)'))
37
+ e = handler.convert_standard_error(
38
+ ArgumentError.new('wrong number of arguments (given 1, expected 2)')
39
+ )
35
40
  expect(e).to be_a(SimpleCovMcp::UsageError)
36
41
 
37
42
  e = handler.convert_standard_error(ArgumentError.new('invalid option'))
@@ -39,34 +44,38 @@ RSpec.describe SimpleCovMcp::ErrorHandler do
39
44
  end
40
45
 
41
46
  it 'maps NoMethodError to CoverageDataError with helpful info' do
42
- e = handler.convert_standard_error(NoMethodError.new("undefined method `fetch' for #<Hash:0x123>"))
47
+ e = handler.convert_standard_error(
48
+ NoMethodError.new("undefined method `fetch' for #<Hash:0x123>")
49
+ )
43
50
  expect(e).to be_a(SimpleCovMcp::CoverageDataError)
44
51
  expect(e.user_friendly_message).to include('Invalid coverage data structure')
45
52
  end
46
53
 
47
54
  it 'maps runtime strings from util to friendly errors' do
48
- e = handler.convert_standard_error(RuntimeError.new('No coverage entry found for /tmp/foo.rb'))
49
- expect(e).to be_a(SimpleCovMcp::FileError)
50
- expect(e.user_friendly_message).to include('No coverage data found for file')
51
-
52
- e = handler.convert_standard_error(RuntimeError.new('Could not find .resultset.json under /path; run tests'))
55
+ e = handler.convert_standard_error(
56
+ RuntimeError.new('Could not find .resultset.json under /path; run tests')
57
+ )
53
58
  expect(e).to be_a(SimpleCovMcp::CoverageDataError)
54
59
  expect(e.user_friendly_message).to include('run your tests first')
55
60
 
56
- e = handler.convert_standard_error(RuntimeError.new('No .resultset.json found in directory: /path'))
61
+ e = handler.convert_standard_error(
62
+ RuntimeError.new('No .resultset.json found in directory: /path')
63
+ )
57
64
  expect(e).to be_a(SimpleCovMcp::CoverageDataError)
58
65
 
59
- e = handler.convert_standard_error(RuntimeError.new('Specified resultset not found: /nowhere/file.json'))
66
+ e = handler.convert_standard_error(
67
+ RuntimeError.new('Specified resultset not found: /nowhere/file.json')
68
+ )
60
69
  expect(e).to be_a(SimpleCovMcp::ResultsetNotFoundError)
61
70
  end
62
71
 
63
72
  it 'logs via provided logger' do
64
73
  begin
65
- handler.handle_error(Errno::ENOENT.new('No such file or directory @ rb_sysopen - x'), context: 'test', reraise: false)
74
+ handler.handle_error(Errno::ENOENT.new('No such file or directory @ rb_sysopen - x'),
75
+ context: 'test', reraise: false)
66
76
  rescue StandardError
67
77
  # reraise disabled
68
78
  end
69
79
  expect(logger.messages.join).to include('Error in test')
70
80
  end
71
81
  end
72
-
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Error Mode System' do
6
+ let(:test_logger) do
7
+ Class.new do
8
+ attr_reader :messages
9
+
10
+ def initialize; @messages = []; end
11
+ def error(msg); @messages << msg; end
12
+ end.new
13
+ end
14
+
15
+ let(:test_error) { StandardError.new('Test error message') }
16
+
17
+ describe 'ErrorHandler error modes' do
18
+ context 'with error_mode: :off' do
19
+ subject(:handler) { SimpleCovMcp::ErrorHandler.new(error_mode: :off, logger: test_logger) }
20
+
21
+ it 'does not log errors' do
22
+ expect(handler.log_errors?).to be false
23
+ expect(handler.show_stack_traces?).to be false
24
+
25
+ handler.handle_error(test_error, context: 'test', reraise: false)
26
+ expect(test_logger.messages).to be_empty
27
+ end
28
+ end
29
+
30
+ context 'with error_mode: :on' do
31
+ subject(:handler) { SimpleCovMcp::ErrorHandler.new(error_mode: :on, logger: test_logger) }
32
+
33
+ it 'logs errors but not stack traces' do
34
+ expect(handler.log_errors?).to be true
35
+ expect(handler.show_stack_traces?).to be false
36
+
37
+ handler.handle_error(test_error, context: 'test', reraise: false)
38
+ logged_message = test_logger.messages.join
39
+ expect(logged_message).to include('Error in test: StandardError: Test error message')
40
+ expect(logged_message).not_to include('spec/error_mode_spec.rb') # No stack trace
41
+ end
42
+ end
43
+
44
+ context 'with error_mode: :trace' do
45
+ subject(:handler) { SimpleCovMcp::ErrorHandler.new(error_mode: :trace, logger: test_logger) }
46
+
47
+ it 'logs errors with stack traces' do
48
+ expect(handler.log_errors?).to be true
49
+ expect(handler.show_stack_traces?).to be true
50
+
51
+ # Create an error with a proper backtrace
52
+ begin
53
+ raise StandardError, 'Test error message'
54
+ rescue StandardError => e
55
+ handler.handle_error(e, context: 'test', reraise: false)
56
+ end
57
+
58
+ logged_message = test_logger.messages.join
59
+ expect(logged_message).to include('Error in test: StandardError: Test error message')
60
+ expect(logged_message).to include('spec/error_mode_spec.rb') # Stack trace included
61
+ end
62
+ end
63
+ end
64
+
65
+ describe 'ErrorHandlerFactory' do
66
+ it 'creates handlers with correct modes' do
67
+ cli_handler = SimpleCovMcp::ErrorHandlerFactory.for_cli(error_mode: :trace)
68
+ expect(cli_handler.error_mode).to eq(:trace)
69
+
70
+ lib_handler = SimpleCovMcp::ErrorHandlerFactory.for_library(error_mode: :off)
71
+ expect(lib_handler.error_mode).to eq(:off)
72
+
73
+ mcp_handler = SimpleCovMcp::ErrorHandlerFactory.for_mcp_server(error_mode: :on)
74
+ expect(mcp_handler.error_mode).to eq(:on)
75
+ end
76
+ end
77
+
78
+ describe 'MCP Tools error mode support' do
79
+ before { setup_mcp_response_stub }
80
+
81
+ it 'BaseTool.handle_mcp_error respects error modes' do
82
+ test_error = StandardError.new('Test MCP error')
83
+
84
+ # Test different error modes
85
+ [:off, :on, :trace].each do |mode|
86
+ expect(SimpleCovMcp::ErrorHandlerFactory)
87
+ .to receive(:for_mcp_server).with(error_mode: mode).and_call_original
88
+
89
+ response = SimpleCovMcp::BaseTool.handle_mcp_error(test_error, 'TestTool', error_mode: mode)
90
+ expect(response).to be_a(MCP::Tool::Response)
91
+ expect(response.payload.first[:text]).to include('Error:')
92
+ end
93
+ end
94
+ end
95
+
96
+ describe 'CLI error mode support' do
97
+ let(:project_dir) { File.join(__dir__, 'fixtures', 'project1') }
98
+
99
+ it 'accepts --error-mode flag' do
100
+ cli = SimpleCovMcp::CoverageCLI.new
101
+
102
+ # Test that the option parser accepts the flag
103
+ expect do
104
+ cli.send(:parse_options!, ['--error-mode', 'trace', 'summary', 'lib/foo.rb'])
105
+ end.not_to raise_error
106
+
107
+ expect(cli.config.error_mode).to eq(:trace)
108
+ end
109
+
110
+ it 'creates error handler with specified mode' do
111
+ cli = SimpleCovMcp::CoverageCLI.new
112
+ cli.send(:parse_options!, ['--error-mode', 'off', 'summary', 'lib/foo.rb'])
113
+
114
+ # Trigger error handler creation
115
+ cli.send(:ensure_error_handler)
116
+
117
+ error_handler = cli.instance_variable_get(:@error_handler)
118
+ expect(error_handler.error_mode).to eq(:off)
119
+ end
120
+
121
+ it 'validates error mode values' do
122
+ cli = SimpleCovMcp::CoverageCLI.new
123
+
124
+ expect do
125
+ cli.send(:parse_options!, ['--error-mode', 'invalid', 'summary', 'lib/foo.rb'])
126
+ end.to raise_error(OptionParser::InvalidArgument)
127
+ end
128
+ end
129
+
130
+ describe 'Error mode validation' do
131
+ it 'raises ArgumentError for invalid error modes' do
132
+ expect do
133
+ SimpleCovMcp::ErrorHandler.new(error_mode: :invalid)
134
+ end.to raise_error(ArgumentError, /Invalid error_mode: :invalid/)
135
+ end
136
+
137
+ it 'accepts all valid error modes' do
138
+ expect { SimpleCovMcp::ErrorHandler.new(error_mode: :off) }.not_to raise_error
139
+ expect { SimpleCovMcp::ErrorHandler.new(error_mode: :on) }.not_to raise_error
140
+ expect { SimpleCovMcp::ErrorHandler.new(error_mode: :trace) }.not_to raise_error
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'SimpleCovMcp error edge cases' do
6
+ describe SimpleCovMcp::CoverageDataStaleError do
7
+ describe 'time formatting edge cases' do
8
+ it 'handles invalid epoch seconds gracefully in rescue path' do
9
+ # Create an object that responds to to_i but breaks Time.at
10
+ bad_timestamp = Object.new
11
+ def bad_timestamp.to_i
12
+ raise ArgumentError, "Can't convert"
13
+ end
14
+
15
+ error = SimpleCovMcp::CoverageDataStaleError.new(
16
+ 'Test error',
17
+ nil,
18
+ file_path: 'test.rb',
19
+ file_mtime: Time.at(1000),
20
+ cov_timestamp: bad_timestamp,
21
+ src_len: 10,
22
+ cov_len: 8
23
+ )
24
+
25
+ message = error.user_friendly_message
26
+ expect(message).to include('Coverage data stale')
27
+ expect(message).to include('Test error')
28
+ end
29
+
30
+ it 'handles time that breaks Time.parse but has valid to_s' do
31
+ # Create an object that can't be parsed but has valid to_s
32
+ bad_time = Object.new
33
+ def bad_time.to_s
34
+ 'unparseable_time_string'
35
+ end
36
+
37
+ error = SimpleCovMcp::CoverageDataStaleError.new(
38
+ 'Test error',
39
+ nil,
40
+ file_path: 'test.rb',
41
+ file_mtime: bad_time,
42
+ cov_timestamp: 1000,
43
+ src_len: 10,
44
+ cov_len: 8
45
+ )
46
+
47
+ message = error.user_friendly_message
48
+ expect(message).to include('Coverage data stale')
49
+ expect(message).to include('Test error')
50
+ # Should fallback to string representation
51
+ expect(message).to include('unparseable_time_string')
52
+ end
53
+
54
+ it 'handles delta calculation with invalid values in rescue path' do
55
+ # Create objects that break arithmetic
56
+ bad_time = Object.new
57
+ def bad_time.to_i
58
+ raise ArgumentError, "Can't convert"
59
+ end
60
+
61
+ bad_timestamp = Object.new
62
+ def bad_timestamp.to_i
63
+ raise ArgumentError, "Can't convert"
64
+ end
65
+
66
+ error = SimpleCovMcp::CoverageDataStaleError.new(
67
+ 'Test error',
68
+ nil,
69
+ file_path: 'test.rb',
70
+ file_mtime: bad_time,
71
+ cov_timestamp: bad_timestamp,
72
+ src_len: 10,
73
+ cov_len: 8
74
+ )
75
+
76
+ message = error.user_friendly_message
77
+ expect(message).to include('Coverage data stale')
78
+ # Delta line should not appear when calculation fails
79
+ expect(message).not_to match(/Delta\s+- file is/)
80
+ end
81
+ end
82
+
83
+ describe 'default message generation' do
84
+ it 'uses default message when message is nil' do
85
+ error = SimpleCovMcp::CoverageDataStaleError.new(
86
+ nil, # No message provided
87
+ nil,
88
+ file_path: 'test.rb',
89
+ file_mtime: Time.at(2000),
90
+ cov_timestamp: 1000
91
+ )
92
+
93
+ message = error.user_friendly_message
94
+ # When message is nil, the error class name is used by StandardError
95
+ # which then triggers default_message to be called
96
+ expect(message).to include('Coverage data')
97
+ expect(message).to include('stale')
98
+ # File path should appear in the details section
99
+ expect(message).to match(/File\s+-/)
100
+ end
101
+
102
+ it 'uses generic default message when file_path is nil' do
103
+ error = SimpleCovMcp::CoverageDataStaleError.new(
104
+ nil, # No message
105
+ nil,
106
+ file_path: nil, # No file path
107
+ file_mtime: Time.at(2000),
108
+ cov_timestamp: 1000
109
+ )
110
+
111
+ message = error.user_friendly_message
112
+ # When file_path is nil, should use 'file' as fallback
113
+ expect(message).to include('Coverage data')
114
+ expect(message).to include('file')
115
+ end
116
+ end
117
+ end
118
+
119
+ describe SimpleCovMcp::CoverageDataProjectStaleError do
120
+ describe 'default message generation' do
121
+ it 'uses default message when message is nil' do
122
+ error = SimpleCovMcp::CoverageDataProjectStaleError.new(
123
+ nil, # No message provided
124
+ nil,
125
+ cov_timestamp: 1000,
126
+ newer_files: ['file1.rb', 'file2.rb']
127
+ )
128
+
129
+ message = error.user_friendly_message
130
+ # When message is nil, StandardError uses class name, then default_message is called
131
+ expect(message).to include('Coverage data')
132
+ expect(message).to include('project')
133
+ end
134
+ end
135
+
136
+ describe 'large file list truncation' do
137
+ it 'shows all files when there are 10 or fewer deleted files' do
138
+ deleted_files = (1..10).map { |i| "deleted_file_#{i}.rb" }
139
+ error = SimpleCovMcp::CoverageDataProjectStaleError.new(
140
+ 'Test error',
141
+ nil,
142
+ cov_timestamp: 1000,
143
+ deleted_files: deleted_files
144
+ )
145
+
146
+ message = error.user_friendly_message
147
+ expect(message).to include('Coverage-only files (deleted or moved in project, 10):')
148
+ deleted_files.each do |file|
149
+ expect(message).to include(" - #{file}")
150
+ end
151
+ expect(message).not_to include('...')
152
+ end
153
+
154
+ it 'truncates and shows ellipsis when there are more than 10 deleted files' do
155
+ deleted_files = (1..15).map { |i| "deleted_file_#{i}.rb" }
156
+ error = SimpleCovMcp::CoverageDataProjectStaleError.new(
157
+ 'Test error',
158
+ nil,
159
+ cov_timestamp: 1000,
160
+ deleted_files: deleted_files
161
+ )
162
+
163
+ message = error.user_friendly_message
164
+ expect(message).to include('Coverage-only files (deleted or moved in project, 15):')
165
+ # Should show first 10 files
166
+ deleted_files[0..9].each do |file|
167
+ expect(message).to include(" - #{file}")
168
+ end
169
+ # Should not show files beyond 10
170
+ deleted_files[10..14].each do |file|
171
+ expect(message).not_to include(" - #{file}")
172
+ end
173
+ # Should show ellipsis
174
+ expect(message).to include('...')
175
+ end
176
+
177
+ it 'shows all files when there are 10 or fewer missing files' do
178
+ missing_files = (1..10).map { |i| "missing_file_#{i}.rb" }
179
+ error = SimpleCovMcp::CoverageDataProjectStaleError.new(
180
+ 'Test error',
181
+ nil,
182
+ cov_timestamp: 1000,
183
+ missing_files: missing_files
184
+ )
185
+
186
+ message = error.user_friendly_message
187
+ expect(message).to include('Missing files (new in project, not in coverage, 10):')
188
+ missing_files.each do |file|
189
+ expect(message).to include(" - #{file}")
190
+ end
191
+ expect(message).not_to include('...')
192
+ end
193
+
194
+ it 'truncates and shows ellipsis when there are more than 10 missing files' do
195
+ missing_files = (1..12).map { |i| "missing_file_#{i}.rb" }
196
+ error = SimpleCovMcp::CoverageDataProjectStaleError.new(
197
+ 'Test error',
198
+ nil,
199
+ cov_timestamp: 1000,
200
+ missing_files: missing_files
201
+ )
202
+
203
+ message = error.user_friendly_message
204
+ expect(message).to include('Missing files (new in project, not in coverage, 12):')
205
+ # Should show first 10 files
206
+ missing_files[0..9].each do |file|
207
+ expect(message).to include(" - #{file}")
208
+ end
209
+ # Should not show files beyond 10
210
+ expect(message).not_to include(" - #{missing_files[11]}")
211
+ # Should show ellipsis
212
+ expect(message).to include('...')
213
+ end
214
+
215
+ it 'truncates and shows ellipsis when there are more than 10 newer files' do
216
+ newer_files = (1..20).map { |i| "newer_file_#{i}.rb" }
217
+ error = SimpleCovMcp::CoverageDataProjectStaleError.new(
218
+ 'Test error',
219
+ nil,
220
+ cov_timestamp: 1000,
221
+ newer_files: newer_files
222
+ )
223
+
224
+ message = error.user_friendly_message
225
+ expect(message).to include('Newer files (20):')
226
+ # Should show first 10 files
227
+ newer_files[0..9].each do |file|
228
+ expect(message).to include(" - #{file}")
229
+ end
230
+ # Should not show files beyond 10
231
+ newer_files[10..19].each do |file|
232
+ expect(message).not_to include(" - #{file}")
233
+ end
234
+ # Should show ellipsis
235
+ expect(message).to include('...')
236
+ end
237
+ end
238
+ end
239
+ end
@@ -4,8 +4,8 @@ require 'spec_helper'
4
4
 
5
5
  RSpec.describe SimpleCovMcp::CoverageDataStaleError do
6
6
  it 'formats a detailed, user-friendly message with UTC/local, delta, and resultset' do
7
- file_time = Time.at(1_000) # 1970-01-01T00:16:40Z
8
- cov_epoch = 0 # 1970-01-01T00:00:00Z
7
+ file_time = Time.at(TEST_FILE_TIMESTAMP) # 1970-01-01T00:16:40Z
8
+ cov_epoch = VERY_OLD_TIMESTAMP # 1970-01-01T00:00:00Z
9
9
  err = described_class.new(
10
10
  'Coverage data appears stale for foo.rb',
11
11
  nil,