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,37 +2,47 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
+ # Array-driven test cases for comprehensive coverage
6
+ # Format: [argv, tty?, expected_result, description]
7
+ CLI_MODE_SCENARIOS = [
8
+ # Priority 1: --force-cli flag (highest priority)
9
+ [['--force-cli'], false, true, '--force-cli with piped input'],
10
+ [['--force-cli', '--format', 'json'], false, true, '--force-cli with other flags'],
11
+
12
+ # Priority 2: Valid subcommands (must be first arg)
13
+ [['list'], false, true, 'list subcommand'],
14
+ [['summary', 'lib/foo.rb'], false, true, 'summary with path'],
15
+ [['version'], false, true, 'version subcommand'],
16
+ [['total'], false, true, 'total subcommand'],
17
+ [['list', '--format', 'json'], false, true, 'subcommand with trailing flags'],
18
+
19
+ # Priority 3: Invalid subcommand attempts (must be first non-flag arg)
20
+ [['invalid-command'], false, true, 'invalid subcommand (shows error)'],
21
+ [['lib/foo.rb'], false, true, 'file path (shows error)'],
22
+
23
+ # Priority 4: TTY determines mode when no subcommand/force-cli
24
+ [[], true, true, 'empty args with TTY'],
25
+ [[], false, false, 'empty args with piped input'],
26
+ [['--format', 'json'], true, true, 'flags only with TTY'],
27
+ [['--format', 'json'], false, false, 'flags only with piped input'],
28
+ [['-r', 'foo', '--format', 'json'], false, false, 'multiple flags with piped input'],
29
+
30
+ # Edge cases: flags before subcommands should now be detected as CLI mode
31
+ [['--format', 'json', 'list'], false, true, 'flag first = CLI mode'],
32
+ [['-r', 'foo', 'summary'], false, true, 'option first = CLI mode'],
33
+ ].freeze
34
+
35
+ # Simpler test cases for the inverse method
36
+ MCP_SCENARIOS = [
37
+ [[], false, true, 'piped input, no args'],
38
+ [['--format', 'json'], false, true, 'piped input with flags'],
39
+ [[], true, false, 'TTY, no args'],
40
+ [['--force-cli'], false, false, '--force-cli flag'],
41
+ [['list'], false, false, 'subcommand'],
42
+ ].freeze
43
+
5
44
  RSpec.describe SimpleCovMcp::ModeDetector do
6
45
  describe '.cli_mode?' do
7
- # Array-driven test cases for comprehensive coverage
8
- # Format: [argv, tty?, expected_result, description]
9
- CLI_MODE_SCENARIOS = [
10
- # Priority 1: --force-cli flag (highest priority)
11
- [['--force-cli'], false, true, '--force-cli with piped input'],
12
- [['--force-cli', '--json'], false, true, '--force-cli with other flags'],
13
-
14
- # Priority 2: Valid subcommands (must be first arg)
15
- [['list'], false, true, 'list subcommand'],
16
- [['summary', 'lib/foo.rb'], false, true, 'summary with path'],
17
- [['version'], false, true, 'version subcommand'],
18
- [['list', '--json'], false, true, 'subcommand with trailing flags'],
19
-
20
- # Priority 3: Invalid subcommand attempts (must be first non-flag arg)
21
- [['invalid-command'], false, true, 'invalid subcommand (shows error)'],
22
- [['lib/foo.rb'], false, true, 'file path (shows error)'],
23
-
24
- # Priority 4: TTY determines mode when no subcommand/force-cli
25
- [[], true, true, 'empty args with TTY'],
26
- [[], false, false, 'empty args with piped input'],
27
- [['--json'], true, true, 'flags only with TTY'],
28
- [['--json'], false, false, 'flags only with piped input'],
29
- [['-r', 'foo', '--json'], false, false, 'multiple flags with piped input'],
30
-
31
- # Edge cases: flags before subcommands should now be detected as CLI mode
32
- [['--json', 'list'], false, true, 'flag first = CLI mode'],
33
- [['-r', 'foo', 'summary'], false, true, 'option first = CLI mode'],
34
- ].freeze
35
-
36
46
  CLI_MODE_SCENARIOS.each do |argv, is_tty, expected, description|
37
47
  it "#{expected ? 'CLI' : 'MCP'}: #{description}" do
38
48
  stdin = double('stdin', tty?: is_tty)
@@ -53,21 +63,12 @@ RSpec.describe SimpleCovMcp::ModeDetector do
53
63
  end
54
64
 
55
65
  it 'uses STDIN by default when no stdin parameter given' do
56
- allow(STDIN).to receive(:tty?).and_return(true)
66
+ allow($stdin).to receive(:tty?).and_return(true)
57
67
  expect(described_class.cli_mode?([])).to be true
58
68
  end
59
69
  end
60
70
 
61
71
  describe '.mcp_server_mode?' do
62
- # Simpler test cases for the inverse method
63
- MCP_SCENARIOS = [
64
- [[], false, true, 'piped input, no args'],
65
- [['--json'], false, true, 'piped input with flags'],
66
- [[], true, false, 'TTY, no args'],
67
- [['--force-cli'], false, false, '--force-cli flag'],
68
- [['list'], false, false, 'subcommand'],
69
- ].freeze
70
-
71
72
  MCP_SCENARIOS.each do |argv, is_tty, expected, description|
72
73
  it "#{expected ? 'MCP' : 'CLI'}: #{description}" do
73
74
  stdin = double('stdin', tty?: is_tty)
@@ -137,12 +138,16 @@ RSpec.describe SimpleCovMcp::ModeDetector do
137
138
  expect(described_class.cli_mode?(['--version'], stdin: stdin)).to be true
138
139
  end
139
140
 
141
+ it 'chooses CLI mode for -v' do
142
+ expect(described_class.cli_mode?(['-v'], stdin: stdin)).to be true
143
+ end
144
+
140
145
  it 'chooses CLI mode for --json list' do
141
- expect(described_class.cli_mode?(['--json', 'list'], stdin: stdin)).to be true
146
+ expect(described_class.cli_mode?(['--format', 'json', 'list'], stdin: stdin)).to be true
142
147
  end
143
148
 
144
149
  it 'chooses MCP mode for flags without a subcommand' do
145
- expect(described_class.cli_mode?(['--json'], stdin: stdin)).to be false
150
+ expect(described_class.cli_mode?(['--format', 'json'], stdin: stdin)).to be false
146
151
  end
147
152
  end
148
153
  end
@@ -4,14 +4,40 @@ require 'spec_helper'
4
4
 
5
5
  RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
6
6
  let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
+ let(:malformed_resultset) do
8
+ {
9
+ 'RSpec' => {
10
+ 'coverage' => 'not_a_hash' # Should be a hash, not a string
11
+ }
12
+ }
13
+ end
7
14
 
8
15
  describe 'initialization error handling' do
16
+ let(:valid_resultset) do
17
+ {
18
+ 'RSpec' => {
19
+ 'coverage' => {
20
+ "lib/foo\x00bar.rb" => { 'lines' => [1, 0, 1] } # Path with NULL byte
21
+ }
22
+ },
23
+ 'timestamp' => 1000
24
+ }
25
+ end
26
+ let(:malformed_resultset) do
27
+ {
28
+ 'RSpec' => {
29
+ 'coverage' => 'not_a_hash' # Should be a hash, not a string
30
+ }
31
+ }
32
+ end
33
+
9
34
  it 'raises CoverageDataError with message detail for invalid JSON format' do
10
35
  # Mock JSON.parse to raise JSON::ParserError
11
- allow(JSON).to receive(:parse).and_raise(JSON::ParserError.new('unexpected token'))
36
+ allow(JSON).to receive(:load_file).with(anything)
37
+ .and_raise(JSON::ParserError.new('unexpected token'))
12
38
 
13
39
  expect do
14
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
40
+ described_class.new(root: root, resultset: 'coverage')
15
41
  end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
16
42
  expect(error.message).to include('Invalid coverage data format')
17
43
  expect(error.message).to include('unexpected token')
@@ -20,33 +46,24 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
20
46
 
21
47
  it 'raises FilePermissionError when coverage file is not readable' do
22
48
  # Mock File.read to raise Errno::EACCES
23
- allow(File).to receive(:read).and_call_original
24
- allow(File).to receive(:read).with(end_with('.resultset.json')).and_raise(
49
+ allow(JSON).to receive(:load_file).with(anything).and_raise(
25
50
  Errno::EACCES.new('Permission denied')
26
51
  )
27
52
 
28
53
  expect do
29
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
54
+ described_class.new(root: root, resultset: 'coverage')
30
55
  end.to raise_error(SimpleCovMcp::FilePermissionError) do |error|
31
56
  expect(error.message).to include('Permission denied reading coverage data')
32
57
  expect(error.message).to include('Permission denied')
33
58
  end
34
59
  end
35
60
 
36
- it 'raises CoverageDataError when resultset structure is invalid (TypeError)' do
37
- # Create a malformed resultset that will cause TypeError
38
- malformed_resultset = {
39
- 'RSpec' => {
40
- 'coverage' => 'not_a_hash' # Should be a hash, not a string
41
- }
42
- }
43
61
 
44
- allow(File).to receive(:read).and_call_original
45
- allow(File).to receive(:read).with(end_with('.resultset.json'))
46
- .and_return(malformed_resultset.to_json)
62
+ it 'raises CoverageDataError when resultset structure is invalid (TypeError)' do
63
+ allow(JSON).to receive(:load_file).with(anything).and_return(malformed_resultset)
47
64
 
48
65
  expect do
49
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
66
+ described_class.new(root: root, resultset: 'coverage')
50
67
  end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
51
68
  expect(error.message).to include('Invalid coverage data structure')
52
69
  end
@@ -62,36 +79,31 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
62
79
  }
63
80
  }
64
81
 
65
- allow(File).to receive(:read).and_call_original
66
- allow(File).to receive(:read).with(end_with('.resultset.json'))
67
- .and_return(malformed_resultset.to_json)
82
+ allow(File).to receive(:open).and_call_original
83
+ allow(File).to receive(:open).with(end_with('.resultset.json'), 'r')
84
+ .and_return(StringIO.new(malformed_resultset.to_json))
68
85
 
69
- # This might succeed or fail depending on how the code handles it
70
- # Let's make it fail by mocking transform_keys to raise NoMethodError
71
- allow_any_instance_of(Hash).to receive(:transform_keys)
86
+ broken_map = instance_double('CoverageMap')
87
+ allow(broken_map).to receive(:transform_keys)
72
88
  .and_raise(NoMethodError.new("undefined method `upcase' for nil:NilClass"))
89
+ allow(SimpleCovMcp::ResultsetLoader).to receive(:load).and_return(
90
+ SimpleCovMcp::ResultsetLoader::Result.new(coverage_map: broken_map,
91
+ timestamp: 0, suite_names: ['RSpec'])
92
+ )
73
93
 
74
94
  expect do
75
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
95
+ described_class.new(root: root, resultset: 'coverage')
76
96
  end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
77
97
  expect(error.message).to include('Invalid coverage data structure')
78
98
  end
79
99
  end
80
100
 
81
- it 'raises CoverageDataError when path operations raise ArgumentError' do
82
- # Create a valid resultset structure with a problematic path
83
- valid_resultset = {
84
- 'RSpec' => {
85
- 'coverage' => {
86
- "lib/foo\x00bar.rb" => { 'lines' => [1, 0, 1] } # Path with NULL byte
87
- },
88
- 'timestamp' => 1000
89
- }
90
- }
91
101
 
92
- allow(File).to receive(:read).and_call_original
93
- allow(File).to receive(:read).with(end_with('.resultset.json'))
94
- .and_return(valid_resultset.to_json)
102
+
103
+
104
+ it 'raises CoverageDataError when path operations raise ArgumentError' do
105
+ allow(JSON).to receive(:load_file).with(end_with('.resultset.json'))
106
+ .and_return(valid_resultset)
95
107
 
96
108
  # Mock File.absolute_path to raise ArgumentError when called with the problematic path
97
109
  # But allow it to work for the root initialization
@@ -101,7 +113,7 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
101
113
  )
102
114
 
103
115
  expect do
104
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
116
+ described_class.new(root: root, resultset: 'coverage')
105
117
  end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
106
118
  expect(error.message).to include('Invalid path in coverage data')
107
119
  expect(error.message).to include('null byte')
@@ -109,13 +121,12 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
109
121
  end
110
122
 
111
123
  it 'preserves error context in JSON::ParserError messages' do
112
- # Mock JSON.parse to raise JSON::ParserError with specific message
113
- allow(JSON).to receive(:parse).and_raise(
124
+ allow(JSON).to receive(:load_file).with(anything).and_raise(
114
125
  JSON::ParserError.new('765: unexpected token at line 3, column 5')
115
126
  )
116
127
 
117
128
  expect do
118
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
129
+ described_class.new(root: root, resultset: 'coverage')
119
130
  end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
120
131
  # Verify the original error message details are preserved
121
132
  expect(error.message).to include('765')
@@ -126,13 +137,12 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
126
137
  it 'provides helpful error for permission issues with file path' do
127
138
  # Mock to raise permission error with actual file path
128
139
  resultset_path = File.join(root, 'coverage', '.resultset.json')
129
- allow(File).to receive(:read).and_call_original
130
- allow(File).to receive(:read).with(resultset_path).and_raise(
140
+ allow(JSON).to receive(:load_file).with(resultset_path).and_raise(
131
141
  Errno::EACCES.new(resultset_path)
132
142
  )
133
143
 
134
144
  expect do
135
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
145
+ described_class.new(root: root, resultset: 'coverage')
136
146
  end.to raise_error(SimpleCovMcp::FilePermissionError) do |error|
137
147
  expect(error.message).to include('Permission denied')
138
148
  expect(error.message).to match(/\.resultset\.json/)
@@ -141,39 +151,37 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
141
151
  end
142
152
 
143
153
  describe 'error context preservation' do
144
- it 'includes original exception message in all specific error types' do
145
- test_cases = [
146
- {
147
- error_class: JSON::ParserError,
148
- message: 'unexpected character at byte 42',
149
- expected_type: SimpleCovMcp::CoverageDataError,
150
- expected_content: 'unexpected character at byte 42'
151
- },
152
- {
153
- error_class: Errno::EACCES,
154
- message: '/path/to/coverage/.resultset.json',
155
- expected_type: SimpleCovMcp::FilePermissionError,
156
- expected_content: '/path/to/coverage/.resultset.json'
157
- },
158
- {
159
- error_class: TypeError,
160
- message: 'no implicit conversion of String into Integer',
161
- expected_type: SimpleCovMcp::CoverageDataError,
162
- expected_content: 'no implicit conversion'
163
- }
164
- ]
165
-
166
- test_cases.each do |test_case|
167
- allow(File).to receive(:read).and_call_original
168
- allow(File).to receive(:read).with(end_with('.resultset.json')).and_raise(
169
- test_case[:error_class].new(test_case[:message])
170
- )
171
-
172
- expect do
173
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
174
- end.to raise_error(test_case[:expected_type]) do |error|
175
- expect(error.message).to include(test_case[:expected_content])
176
- end
154
+ it 'includes original exception message for JSON::ParserError' do
155
+ allow(JSON).to receive(:load_file).with(anything)
156
+ .and_raise(JSON::ParserError.new('unexpected character at byte 42'))
157
+
158
+ expect do
159
+ described_class.new(root: root, resultset: 'coverage')
160
+ end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
161
+ expect(error.message).to include('unexpected character at byte 42')
162
+ end
163
+ end
164
+
165
+ it 'includes original exception message for Errno::EACCES' do
166
+ resultset_path = File.join(root, 'coverage', '.resultset.json')
167
+ allow(JSON).to receive(:load_file).with(resultset_path).and_raise(Errno::EACCES.new(resultset_path))
168
+
169
+ expect do
170
+ described_class.new(root: root, resultset: 'coverage')
171
+ end.to raise_error(SimpleCovMcp::FilePermissionError) do |error|
172
+ expect(error.message).to include(resultset_path)
173
+ end
174
+ end
175
+
176
+ it 'includes original exception message for TypeError' do
177
+ # Mock to cause TypeError within ResultsetLoader's processing
178
+ allow(JSON).to receive(:load_file).with(anything).and_return(malformed_resultset)
179
+
180
+ expect do
181
+ described_class.new(root: root, resultset: 'coverage')
182
+ end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
183
+ expect(error.message).to include('Invalid coverage data structure')
184
+ expect(error.message).to include('suite "RSpec"')
177
185
  end
178
186
  end
179
187
  end
@@ -186,7 +194,7 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
186
194
  )
187
195
 
188
196
  expect do
189
- SimpleCovMcp::CoverageModel.new(root: root, resultset: '/nonexistent/path')
197
+ described_class.new(root: root, resultset: '/nonexistent/path')
190
198
  end.to raise_error(SimpleCovMcp::ResultsetNotFoundError) do |error|
191
199
  expect(error.message).to include('Specified resultset not found')
192
200
  end
@@ -199,10 +207,63 @@ RSpec.describe SimpleCovMcp::CoverageModel, 'error handling' do
199
207
  )
200
208
 
201
209
  expect do
202
- SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage')
210
+ described_class.new(root: root, resultset: 'coverage')
203
211
  end.to raise_error(SimpleCovMcp::ResultsetNotFoundError) do |error|
204
212
  expect(error.message).to include('Something went wrong during resultset lookup')
205
213
  end
206
214
  end
215
+
216
+ it 'converts RuntimeError without "resultset" in message to CoverageDataError' do
217
+ # Test RuntimeError that does NOT contain 'resultset' in its message
218
+ # This exercises the else branch in the RuntimeError rescue clause
219
+ allow(SimpleCovMcp::CovUtil).to receive(:find_resultset).and_raise(
220
+ RuntimeError.new('Some completely unrelated runtime error')
221
+ )
222
+
223
+ expect do
224
+ described_class.new(root: root, resultset: 'coverage')
225
+ end.to raise_error(SimpleCovMcp::CoverageDataError) do |error|
226
+ expect(error.message).to include('Failed to load coverage data')
227
+ expect(error.message).to include('Some completely unrelated runtime error')
228
+ end
229
+ end
230
+ end
231
+
232
+ describe 'all_files error handling' do
233
+ it 'skips files that raise FileError during coverage lookup' do
234
+ # This exercises the `next` statement in the all_files loop when FileError is raised
235
+ model = described_class.new(root: root, resultset: 'coverage')
236
+
237
+ # Mock lookup_lines to raise FileError for one specific file
238
+ allow(SimpleCovMcp::CovUtil).to receive(:lookup_lines).and_call_original
239
+ allow(SimpleCovMcp::CovUtil).to receive(:lookup_lines)
240
+ .with(anything, include('/lib/foo.rb'))
241
+ .and_raise(SimpleCovMcp::FileError.new('Corrupted coverage entry'))
242
+
243
+ # Should not raise, just skip the problematic file
244
+ result = model.all_files(check_stale: false)
245
+
246
+ # The result should contain bar.rb but not foo.rb
247
+ file_names = result.map { |r| File.basename(r['file']) }
248
+ expect(file_names).to include('bar.rb')
249
+ expect(file_names).not_to include('foo.rb')
250
+ end
251
+ end
252
+
253
+ describe 'resolve method error handling' do
254
+ it 'converts RuntimeError from lookup_lines to FileError' do
255
+ # This exercises the RuntimeError rescue clause in the resolve method
256
+ model = described_class.new(root: root, resultset: 'coverage')
257
+
258
+ # Mock lookup_lines to raise RuntimeError for a specific file
259
+ allow(SimpleCovMcp::CovUtil).to receive(:lookup_lines)
260
+ .and_raise(RuntimeError.new('Unexpected runtime error during lookup'))
261
+
262
+ expect do
263
+ model.summary_for('nonexistent_file.rb')
264
+ end.to raise_error(SimpleCovMcp::FileError) do |error|
265
+ expect(error.message).to include('No coverage data found for file')
266
+ end
267
+ end
207
268
  end
208
269
  end
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'tempfile'
4
5
 
5
6
  RSpec.describe SimpleCovMcp::CoverageModel do
6
7
  let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
8
 
8
- def with_stubbed_coverage_timestamp(ts) = begin
9
- mock_resultset_with_timestamp(root, ts)
9
+ def with_stubbed_coverage_timestamp(timestamp)
10
+ mock_resultset_with_timestamp(root, timestamp)
10
11
  yield
11
12
  end
12
13
 
13
14
  it "raises stale error when staleness mode is 'error' and file is newer" do
14
15
  with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
15
- model = described_class.new(root: root, staleness: 'error')
16
+ model = described_class.new(root: root, staleness: :error)
16
17
  expect do
17
18
  model.summary_for('lib/foo.rb')
18
19
  end.to raise_error(SimpleCovMcp::CoverageDataStaleError, /stale/i)
@@ -21,28 +22,27 @@ RSpec.describe SimpleCovMcp::CoverageModel do
21
22
 
22
23
  it "does not check staleness when mode is 'off'" do
23
24
  with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
24
- model = described_class.new(root: root, staleness: 'off')
25
+ model = described_class.new(root: root, staleness: :off)
25
26
  expect { model.summary_for('lib/foo.rb') }.not_to raise_error
26
27
  end
27
28
  end
29
+
28
30
  it 'all_files raises project-level stale when any source file is newer than coverage' do
29
31
  with_stubbed_coverage_timestamp(VERY_OLD_TIMESTAMP) do
30
- model = described_class.new(root: root, staleness: 'error')
32
+ model = described_class.new(root: root, staleness: :error)
31
33
  expect { model.all_files }.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
32
34
  end
33
35
  end
34
36
 
35
37
  it 'all_files detects new files via tracked_globs' do
36
38
  with_stubbed_coverage_timestamp(Time.now.to_i) do
37
- tmp = File.join(root, 'lib', 'brand_new_file.rb')
38
- begin
39
- File.write(tmp, "# new file\n")
40
- model = described_class.new(root: root, staleness: 'error')
39
+ Tempfile.create(['brand_new_file', '.rb'], File.join(root, 'lib')) do |f|
40
+ f.write("# new file\n")
41
+ f.flush
42
+ model = described_class.new(root: root, staleness: :error)
41
43
  expect do
42
44
  model.all_files(tracked_globs: ['lib/**/*.rb'])
43
45
  end.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
44
- ensure
45
- File.delete(tmp) if File.exist?(tmp)
46
46
  end
47
47
  end
48
48
  end
@@ -52,7 +52,7 @@ RSpec.describe SimpleCovMcp::CoverageModel do
52
52
  created_at = Time.new(2024, 7, 3, 16, 26, 40, '-07:00')
53
53
  mock_resultset_with_created_at(root, created_at.strftime('%Y-%m-%d %H:%M:%S %z'))
54
54
 
55
- model = described_class.new(root: root, staleness: 'off')
55
+ model = described_class.new(root: root, staleness: :off)
56
56
 
57
57
  expect(model.instance_variable_get(:@cov_timestamp)).to eq(created_at.to_i)
58
58
  end
@@ -66,7 +66,7 @@ RSpec.describe SimpleCovMcp::CoverageModel do
66
66
  }
67
67
  mock_resultset_with_created_at(root, created_at_time.iso8601, coverage: mismatched_coverage)
68
68
 
69
- model = described_class.new(root: root, staleness: 'error')
69
+ model = described_class.new(root: root, staleness: :error)
70
70
 
71
71
  expect(model.instance_variable_get(:@cov_timestamp)).to eq(created_at_time.to_i)
72
72
  expect do