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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +173 -356
- data/docs/ADVANCED_USAGE.md +967 -0
- data/docs/ARCHITECTURE.md +79 -0
- data/docs/BRANCH_ONLY_COVERAGE.md +81 -0
- data/docs/CLI_USAGE.md +637 -0
- data/docs/DEVELOPMENT.md +82 -0
- data/docs/ERROR_HANDLING.md +93 -0
- data/docs/EXAMPLES.md +430 -0
- data/docs/INSTALLATION.md +352 -0
- data/docs/LIBRARY_API.md +635 -0
- data/docs/MCP_INTEGRATION.md +488 -0
- data/docs/TROUBLESHOOTING.md +276 -0
- data/docs/arch-decisions/001-x-arch-decision.md +93 -0
- data/docs/arch-decisions/002-x-arch-decision.md +157 -0
- data/docs/arch-decisions/003-x-arch-decision.md +163 -0
- data/docs/arch-decisions/004-x-arch-decision.md +199 -0
- data/docs/arch-decisions/005-x-arch-decision.md +187 -0
- data/docs/arch-decisions/README.md +60 -0
- data/docs/presentations/simplecov-mcp-presentation.md +249 -0
- data/exe/simplecov-mcp +4 -4
- data/lib/simplecov_mcp/app_context.rb +26 -0
- data/lib/simplecov_mcp/base_tool.rb +74 -0
- data/lib/simplecov_mcp/cli.rb +234 -0
- data/lib/simplecov_mcp/cli_config.rb +56 -0
- data/lib/simplecov_mcp/commands/base_command.rb +78 -0
- data/lib/simplecov_mcp/commands/command_factory.rb +39 -0
- data/lib/simplecov_mcp/commands/detailed_command.rb +24 -0
- data/lib/simplecov_mcp/commands/list_command.rb +13 -0
- data/lib/simplecov_mcp/commands/raw_command.rb +22 -0
- data/lib/simplecov_mcp/commands/summary_command.rb +24 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +26 -0
- data/lib/simplecov_mcp/commands/version_command.rb +18 -0
- data/lib/simplecov_mcp/constants.rb +22 -0
- data/lib/simplecov_mcp/error_handler.rb +124 -0
- data/lib/simplecov_mcp/error_handler_factory.rb +31 -0
- data/lib/simplecov_mcp/errors.rb +179 -0
- data/lib/simplecov_mcp/formatters/source_formatter.rb +148 -0
- data/lib/simplecov_mcp/mcp_server.rb +40 -0
- data/lib/simplecov_mcp/mode_detector.rb +55 -0
- data/lib/simplecov_mcp/model.rb +300 -0
- data/lib/simplecov_mcp/option_normalizers.rb +92 -0
- data/lib/simplecov_mcp/option_parser_builder.rb +134 -0
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +50 -0
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +109 -0
- data/lib/simplecov_mcp/path_relativizer.rb +61 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +44 -0
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +52 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +126 -0
- data/lib/simplecov_mcp/resolvers/resolver_factory.rb +28 -0
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +78 -0
- data/lib/simplecov_mcp/resultset_loader.rb +136 -0
- data/lib/simplecov_mcp/staleness_checker.rb +243 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/all_files_coverage_tool.rb +31 -13
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_detailed_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_raw_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_summary_tool.rb +7 -7
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +90 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/help_tool.rb +13 -4
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/uncovered_lines_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/version_tool.rb +11 -3
- data/lib/simplecov_mcp/util.rb +82 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/version.rb +1 -1
- data/lib/simplecov_mcp.rb +144 -2
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +29 -25
- data/spec/base_tool_spec.rb +11 -10
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_config_spec.rb +137 -0
- data/spec/cli_enumerated_options_spec.rb +68 -0
- data/spec/cli_error_spec.rb +105 -47
- data/spec/cli_source_spec.rb +82 -23
- data/spec/cli_spec.rb +140 -5
- data/spec/cli_success_predicate_spec.rb +141 -0
- data/spec/cli_table_spec.rb +1 -1
- data/spec/cli_usage_spec.rb +10 -26
- data/spec/commands/base_command_spec.rb +187 -0
- data/spec/commands/command_factory_spec.rb +72 -0
- data/spec/commands/detailed_command_spec.rb +48 -0
- data/spec/commands/raw_command_spec.rb +46 -0
- data/spec/commands/summary_command_spec.rb +47 -0
- data/spec/commands/uncovered_command_spec.rb +49 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/coverage_table_tool_spec.rb +17 -33
- data/spec/error_handler_spec.rb +22 -13
- data/spec/error_mode_spec.rb +143 -0
- data/spec/errors_edge_cases_spec.rb +239 -0
- data/spec/errors_stale_spec.rb +2 -2
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +0 -1
- data/spec/fixtures/project1/lib/foo.rb +0 -1
- data/spec/help_tool_spec.rb +11 -17
- data/spec/integration_spec.rb +845 -0
- data/spec/logging_fallback_spec.rb +128 -0
- data/spec/mcp_logging_spec.rb +44 -0
- data/spec/mcp_server_integration_spec.rb +23 -0
- data/spec/mcp_server_spec.rb +15 -4
- data/spec/mode_detector_spec.rb +148 -0
- data/spec/model_error_handling_spec.rb +210 -0
- data/spec/model_staleness_spec.rb +40 -10
- data/spec/option_normalizers_spec.rb +204 -0
- data/spec/option_parsers/env_options_parser_spec.rb +233 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +83 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
- data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
- data/spec/presenters/project_coverage_presenter_spec.rb +86 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +57 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +55 -0
- data/spec/resultset_loader_spec.rb +167 -0
- data/spec/shared_examples/README.md +115 -0
- data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
- data/spec/shared_examples/file_based_mcp_tools.rb +174 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/simple_cov_mcp_module_spec.rb +16 -0
- data/spec/simplecov_mcp_model_spec.rb +340 -9
- data/spec/simplecov_mcp_opts_spec.rb +182 -0
- data/spec/spec_helper.rb +147 -4
- data/spec/staleness_checker_spec.rb +373 -0
- data/spec/staleness_more_spec.rb +16 -13
- data/spec/support/mcp_runner.rb +64 -0
- data/spec/tools_error_handling_spec.rb +144 -0
- data/spec/util_spec.rb +109 -34
- data/spec/version_spec.rb +117 -9
- data/spec/version_tool_spec.rb +131 -10
- metadata +120 -63
- data/lib/simple_cov/mcp.rb +0 -9
- data/lib/simple_cov_mcp/base_tool.rb +0 -70
- data/lib/simple_cov_mcp/cli.rb +0 -390
- data/lib/simple_cov_mcp/error_handler.rb +0 -131
- data/lib/simple_cov_mcp/error_handler_factory.rb +0 -38
- data/lib/simple_cov_mcp/errors.rb +0 -176
- data/lib/simple_cov_mcp/mcp_server.rb +0 -30
- data/lib/simple_cov_mcp/model.rb +0 -104
- data/lib/simple_cov_mcp/staleness_checker.rb +0 -125
- data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +0 -61
- data/lib/simple_cov_mcp/util.rb +0 -122
- data/lib/simple_cov_mcp.rb +0 -102
- data/spec/coverage_detailed_tool_spec.rb +0 -36
- data/spec/coverage_raw_tool_spec.rb +0 -32
- data/spec/coverage_summary_tool_spec.rb +0 -39
- data/spec/legacy_shim_spec.rb +0 -13
- 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 '
|
4
|
+
require 'simplecov_mcp/tools/coverage_table_tool'
|
5
5
|
|
6
6
|
RSpec.describe SimpleCovMcp::Tools::CoverageTableTool do
|
7
|
-
let(:root) { (
|
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
|
-
|
11
|
+
setup_mcp_response_stub
|
12
12
|
end
|
13
13
|
|
14
14
|
def run_tool(stale: 'off')
|
15
|
-
#
|
16
|
-
|
17
|
-
|
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
|
31
|
-
expect(output).to include(
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
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:
|
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
|
data/spec/error_handler_spec.rb
CHANGED
@@ -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(
|
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(
|
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(
|
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(
|
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(
|
49
|
-
|
50
|
-
|
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(
|
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(
|
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'),
|
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
|
data/spec/errors_stale_spec.rb
CHANGED
@@ -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(
|
8
|
-
cov_epoch =
|
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,
|