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,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
RSpec.describe SimpleCovMcp::CoverageCLI, 'success predicate' do
|
7
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
8
|
+
|
9
|
+
def with_temp_predicate(content)
|
10
|
+
Tempfile.create(['predicate', '.rb']) do |file|
|
11
|
+
file.write(content)
|
12
|
+
file.flush
|
13
|
+
yield file.path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '--success-predicate' do
|
18
|
+
it 'exits 0 when predicate returns truthy value' do
|
19
|
+
with_temp_predicate("->(model) { true }\n") do |path|
|
20
|
+
_out, _err, status = run_cli_with_status(
|
21
|
+
'--success-predicate', path,
|
22
|
+
'--root', root,
|
23
|
+
'--resultset', 'coverage'
|
24
|
+
)
|
25
|
+
expect(status).to eq(0)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'exits 1 when predicate returns falsy value' do
|
30
|
+
with_temp_predicate("->(model) { false }\n") do |path|
|
31
|
+
_out, _err, status = run_cli_with_status(
|
32
|
+
'--success-predicate', path,
|
33
|
+
'--root', root,
|
34
|
+
'--resultset', 'coverage'
|
35
|
+
)
|
36
|
+
expect(status).to eq(1)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'exits 2 when predicate raises an error' do
|
41
|
+
with_temp_predicate("->(model) { raise 'Boom!' }\n") do |path|
|
42
|
+
_out, err, status = run_cli_with_status(
|
43
|
+
'--success-predicate', path,
|
44
|
+
'--root', root,
|
45
|
+
'--resultset', 'coverage'
|
46
|
+
)
|
47
|
+
expect(status).to eq(2)
|
48
|
+
expect(err).to include('Success predicate error: Boom!')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'shows backtrace when predicate errors with --error-mode trace' do
|
53
|
+
with_temp_predicate("->(model) { raise 'Boom!' }\n") do |path|
|
54
|
+
_out, err, status = run_cli_with_status(
|
55
|
+
'--success-predicate', path,
|
56
|
+
'--root', root,
|
57
|
+
'--resultset', 'coverage',
|
58
|
+
'--error-mode', 'trace'
|
59
|
+
)
|
60
|
+
expect(status).to eq(2)
|
61
|
+
expect(err).to include('Success predicate error: Boom!')
|
62
|
+
# With trace mode, should show backtrace
|
63
|
+
expect(err).to match(/predicate.*\.rb:\d+/)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'exits 2 when predicate file is not found' do
|
68
|
+
_out, err, status = run_cli_with_status(
|
69
|
+
'--success-predicate', '/nonexistent/predicate.rb',
|
70
|
+
'--root', root,
|
71
|
+
'--resultset', 'coverage'
|
72
|
+
)
|
73
|
+
expect(status).to eq(2)
|
74
|
+
expect(err).to include('Success predicate file not found')
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'exits 2 when predicate has syntax error' do
|
78
|
+
with_temp_predicate("-> { this is invalid syntax\n") do |path|
|
79
|
+
_out, err, status = run_cli_with_status(
|
80
|
+
'--success-predicate', path,
|
81
|
+
'--root', root,
|
82
|
+
'--resultset', 'coverage'
|
83
|
+
)
|
84
|
+
expect(status).to eq(2)
|
85
|
+
expect(err).to include('Syntax error in success predicate file')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'exits 2 when predicate is not callable' do
|
90
|
+
with_temp_predicate("42\n") do |path|
|
91
|
+
_out, err, status = run_cli_with_status(
|
92
|
+
'--success-predicate', path,
|
93
|
+
'--root', root,
|
94
|
+
'--resultset', 'coverage'
|
95
|
+
)
|
96
|
+
expect(status).to eq(2)
|
97
|
+
expect(err).to include('Success predicate must be callable')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'provides model to predicate that can query coverage' do
|
102
|
+
# Test that the predicate receives a working CoverageModel
|
103
|
+
with_temp_predicate(<<~RUBY) do |path|
|
104
|
+
->(model) do
|
105
|
+
# Access coverage data via the model
|
106
|
+
summary = model.summary_for('lib/foo.rb')
|
107
|
+
summary['summary']['pct'] > 50 # Should be true for foo.rb
|
108
|
+
end
|
109
|
+
RUBY
|
110
|
+
_out, _err, status = run_cli_with_status(
|
111
|
+
'--success-predicate', path,
|
112
|
+
'--root', root,
|
113
|
+
'--resultset', 'coverage'
|
114
|
+
)
|
115
|
+
expect(status).to eq(0)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe 'run_subcommand error handling' do
|
121
|
+
it 'handles generic errors in subcommands' do
|
122
|
+
# Force a generic error in the command execution
|
123
|
+
fake_command_class = Class.new do
|
124
|
+
def initialize(_cli); end
|
125
|
+
|
126
|
+
def execute(_args)
|
127
|
+
raise StandardError, 'Generic error in command'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
allow(SimpleCovMcp::Commands::CommandFactory).to receive(:create)
|
132
|
+
.and_return(fake_command_class.new(nil))
|
133
|
+
|
134
|
+
_out, err, status = run_cli_with_status('summary', 'lib/foo.rb', '--root', root,
|
135
|
+
'--resultset', 'coverage')
|
136
|
+
|
137
|
+
expect(status).to eq(1)
|
138
|
+
expect(err).to include('An unexpected error occurred')
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/spec/cli_table_spec.rb
CHANGED
data/spec/cli_usage_spec.rb
CHANGED
@@ -3,24 +3,7 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
RSpec.describe SimpleCovMcp::CoverageCLI do
|
6
|
-
let(:root) { (
|
7
|
-
|
8
|
-
def run_cli_with_status(*argv)
|
9
|
-
cli = described_class.new
|
10
|
-
status = nil
|
11
|
-
out_str = err_str = nil
|
12
|
-
silence_output do |out, err|
|
13
|
-
begin
|
14
|
-
cli.run(argv.flatten)
|
15
|
-
status = 0
|
16
|
-
rescue SystemExit => e
|
17
|
-
status = e.status
|
18
|
-
end
|
19
|
-
out_str = out.string
|
20
|
-
err_str = err.string
|
21
|
-
end
|
22
|
-
[out_str, err_str, status]
|
23
|
-
end
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
24
7
|
|
25
8
|
it 'errors with usage when summary path is missing' do
|
26
9
|
_out, err, status = run_cli_with_status('summary', '--root', root, '--resultset', 'coverage')
|
@@ -28,19 +11,19 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
28
11
|
expect(err).to include('Usage: simplecov-mcp summary <path>')
|
29
12
|
end
|
30
13
|
|
31
|
-
it '
|
14
|
+
it 'errors with meaningful message for unknown subcommand' do
|
32
15
|
out, err, status = run_cli_with_status('bogus', '--root', root, '--resultset', 'coverage')
|
33
|
-
expect(status).to eq(
|
34
|
-
expect(err).to
|
35
|
-
expect(
|
36
|
-
expect(out).to include('lib/foo.rb')
|
16
|
+
expect(status).to eq(1)
|
17
|
+
expect(err).to include("Unknown subcommand: 'bogus'")
|
18
|
+
expect(err).to include('Valid subcommands:')
|
37
19
|
end
|
38
20
|
|
39
21
|
it 'list honors stale=error and tracked_globs by exiting 1 when project is stale' do
|
40
22
|
tmp = File.join(root, 'lib', 'brand_new_file_for_cli_usage_spec.rb')
|
41
23
|
begin
|
42
24
|
File.write(tmp, "# new file\n")
|
43
|
-
_out, err, status = run_cli_with_status('list', '--root', root, '--resultset', 'coverage',
|
25
|
+
_out, err, status = run_cli_with_status('list', '--root', root, '--resultset', 'coverage',
|
26
|
+
'--stale', 'error', '--tracked-globs', 'lib/**/*.rb')
|
44
27
|
expect(status).to eq(1)
|
45
28
|
expect(err).to include('Coverage data stale (project)')
|
46
29
|
ensure
|
@@ -49,9 +32,10 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
49
32
|
end
|
50
33
|
|
51
34
|
it 'list with stale=off prints table and exits 0' do
|
52
|
-
out, err, status = run_cli_with_status('list', '--root', root, '--resultset', 'coverage',
|
35
|
+
out, err, status = run_cli_with_status('list', '--root', root, '--resultset', 'coverage',
|
36
|
+
'--stale', 'off')
|
53
37
|
expect(status).to eq(0)
|
54
|
-
expect(err).to eq(
|
38
|
+
expect(err).to eq('')
|
55
39
|
expect(out).to include('File')
|
56
40
|
expect(out).to include('lib/foo.rb')
|
57
41
|
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::Commands::BaseCommand do
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
|
+
let(:cli_context) { SimpleCovMcp::CoverageCLI.new }
|
8
|
+
|
9
|
+
# Create a test command class that exposes protected methods for testing
|
10
|
+
let(:test_command_class) do
|
11
|
+
Class.new(SimpleCovMcp::Commands::BaseCommand) do
|
12
|
+
def execute(args)
|
13
|
+
# Not needed for these tests
|
14
|
+
end
|
15
|
+
|
16
|
+
# Expose protected methods for testing
|
17
|
+
def public_handle_with_path(args, name, &block)
|
18
|
+
handle_with_path(args, name, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def public_fetch_raw(model, path)
|
22
|
+
fetch_raw(model, path)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:test_command) { test_command_class.new(cli_context) }
|
28
|
+
|
29
|
+
describe '#handle_with_path' do
|
30
|
+
context 'when Errno::ENOENT is raised' do
|
31
|
+
it 'converts to FileNotFoundError with correct message' do
|
32
|
+
args = ['lib/missing.rb']
|
33
|
+
|
34
|
+
# Stub the block to raise Errno::ENOENT
|
35
|
+
expect do
|
36
|
+
test_command.public_handle_with_path(args, 'test') do |path|
|
37
|
+
raise Errno::ENOENT.new('No such file or directory')
|
38
|
+
end
|
39
|
+
end.to raise_error(SimpleCovMcp::FileNotFoundError, 'File not found: lib/missing.rb')
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'includes the path from the args in the error message' do
|
43
|
+
args = ['some/other/path.rb']
|
44
|
+
|
45
|
+
expect do
|
46
|
+
test_command.public_handle_with_path(args, 'test') do |path|
|
47
|
+
raise Errno::ENOENT.new('No such file or directory')
|
48
|
+
end
|
49
|
+
end.to raise_error(SimpleCovMcp::FileNotFoundError, /some\/other\/path\.rb/)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when Errno::EACCES is raised' do
|
54
|
+
it 'converts to FilePermissionError with correct message' do
|
55
|
+
args = ['lib/secret.rb']
|
56
|
+
|
57
|
+
# Stub the block to raise Errno::EACCES
|
58
|
+
expect do
|
59
|
+
test_command.public_handle_with_path(args, 'test') do |path|
|
60
|
+
raise Errno::EACCES.new('Permission denied')
|
61
|
+
end
|
62
|
+
end.to raise_error(SimpleCovMcp::FilePermissionError, 'Permission denied: lib/secret.rb')
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'includes the path from the args in the error message' do
|
66
|
+
args = ['/root/protected.rb']
|
67
|
+
|
68
|
+
expect do
|
69
|
+
test_command.public_handle_with_path(args, 'test') do |path|
|
70
|
+
raise Errno::EACCES.new('Permission denied')
|
71
|
+
end
|
72
|
+
end.to raise_error(SimpleCovMcp::FilePermissionError, /\/root\/protected\.rb/)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'when no path is provided' do
|
77
|
+
it 'raises UsageError' do
|
78
|
+
args = []
|
79
|
+
|
80
|
+
expect do
|
81
|
+
test_command.public_handle_with_path(args, 'summary') do |path|
|
82
|
+
# Should not reach here
|
83
|
+
end
|
84
|
+
end.to raise_error(SimpleCovMcp::UsageError, /summary <path>/)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'when successful' do
|
89
|
+
it 'yields the path to the block' do
|
90
|
+
args = ['lib/foo.rb']
|
91
|
+
yielded_path = nil
|
92
|
+
|
93
|
+
test_command.public_handle_with_path(args, 'test') do |path|
|
94
|
+
yielded_path = path
|
95
|
+
end
|
96
|
+
|
97
|
+
expect(yielded_path).to eq('lib/foo.rb')
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'shifts the path from args' do
|
101
|
+
args = ['lib/foo.rb', 'extra', 'args']
|
102
|
+
|
103
|
+
test_command.public_handle_with_path(args, 'test') do |path|
|
104
|
+
# Block execution
|
105
|
+
end
|
106
|
+
|
107
|
+
expect(args).to eq(['extra', 'args'])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe '#fetch_raw' do
|
113
|
+
let(:model) { SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage') }
|
114
|
+
|
115
|
+
context 'when model.raw_for raises an exception' do
|
116
|
+
it 'returns nil instead of propagating the error' do
|
117
|
+
# Stub model.raw_for to raise an exception
|
118
|
+
allow(model).to receive(:raw_for).and_raise(StandardError, 'Something went wrong')
|
119
|
+
|
120
|
+
result = test_command.public_fetch_raw(model, 'lib/nonexistent.rb')
|
121
|
+
|
122
|
+
expect(result).to be_nil
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'handles RuntimeError' do
|
126
|
+
allow(model).to receive(:raw_for).and_raise(RuntimeError, 'Runtime error')
|
127
|
+
|
128
|
+
result = test_command.public_fetch_raw(model, 'lib/foo.rb')
|
129
|
+
|
130
|
+
expect(result).to be_nil
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'handles ArgumentError' do
|
134
|
+
allow(model).to receive(:raw_for).and_raise(ArgumentError, 'Invalid argument')
|
135
|
+
|
136
|
+
result = test_command.public_fetch_raw(model, 'lib/foo.rb')
|
137
|
+
|
138
|
+
expect(result).to be_nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'when successful' do
|
143
|
+
it 'returns the raw coverage data' do
|
144
|
+
result = test_command.public_fetch_raw(model, 'lib/foo.rb')
|
145
|
+
|
146
|
+
expect(result).to be_a(Hash)
|
147
|
+
expect(result).to have_key('lines')
|
148
|
+
expect(result['lines']).to be_an(Array)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'caches the result for subsequent calls' do
|
152
|
+
# First call should hit the model
|
153
|
+
expect(model).to receive(:raw_for).with('lib/foo.rb').once.and_call_original
|
154
|
+
|
155
|
+
result1 = test_command.public_fetch_raw(model, 'lib/foo.rb')
|
156
|
+
result2 = test_command.public_fetch_raw(model, 'lib/foo.rb')
|
157
|
+
|
158
|
+
expect(result1).to eq(result2)
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'caches different paths separately' do
|
162
|
+
result1 = test_command.public_fetch_raw(model, 'lib/foo.rb')
|
163
|
+
result2 = test_command.public_fetch_raw(model, 'lib/bar.rb')
|
164
|
+
|
165
|
+
expect(result1).not_to eq(result2)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'does not cache nil results from exceptions' do
|
169
|
+
# Set up the stub to raise an error
|
170
|
+
call_count = 0
|
171
|
+
allow(model).to receive(:raw_for).with('lib/missing.rb') do
|
172
|
+
call_count += 1
|
173
|
+
raise StandardError, 'File not found'
|
174
|
+
end
|
175
|
+
|
176
|
+
result1 = test_command.public_fetch_raw(model, 'lib/missing.rb')
|
177
|
+
result2 = test_command.public_fetch_raw(model, 'lib/missing.rb')
|
178
|
+
|
179
|
+
expect(result1).to be_nil
|
180
|
+
expect(result2).to be_nil
|
181
|
+
# Note: Due to current implementation, nil results are NOT cached,
|
182
|
+
# so raw_for is called each time an exception occurs
|
183
|
+
expect(call_count).to eq(2)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::Commands::CommandFactory do
|
6
|
+
let(:cli_context) { SimpleCovMcp::CoverageCLI.new }
|
7
|
+
|
8
|
+
describe '.create' do
|
9
|
+
context 'with valid command names' do
|
10
|
+
[
|
11
|
+
['list', SimpleCovMcp::Commands::ListCommand],
|
12
|
+
['version', SimpleCovMcp::Commands::VersionCommand],
|
13
|
+
['summary', SimpleCovMcp::Commands::SummaryCommand],
|
14
|
+
['raw', SimpleCovMcp::Commands::RawCommand],
|
15
|
+
['uncovered', SimpleCovMcp::Commands::UncoveredCommand],
|
16
|
+
['detailed', SimpleCovMcp::Commands::DetailedCommand]
|
17
|
+
].each do |command_name, command_class|
|
18
|
+
it "creates a #{command_class.name.split('::').last} for \"#{command_name}\"" do
|
19
|
+
command = described_class.create(command_name, cli_context)
|
20
|
+
expect(command).to be_a(command_class)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with unknown command name' do
|
26
|
+
[
|
27
|
+
[
|
28
|
+
'invalid_cmd',
|
29
|
+
'invalid command',
|
30
|
+
/list | summary <path> | raw <path> | uncovered <path> | detailed <path> | version/
|
31
|
+
],
|
32
|
+
[nil, 'nil command', nil],
|
33
|
+
['', 'empty string command', nil],
|
34
|
+
['sumary', 'misspelled command', nil]
|
35
|
+
].each do |command_name, description, pattern|
|
36
|
+
it "raises UsageError for #{description}" do
|
37
|
+
expect do
|
38
|
+
described_class.create(command_name, cli_context)
|
39
|
+
end.to raise_error(SimpleCovMcp::UsageError, pattern)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '.available_commands' do
|
46
|
+
it 'returns an array of available command names' do
|
47
|
+
commands = described_class.available_commands
|
48
|
+
expect(commands).to be_an(Array)
|
49
|
+
expect(commands).to contain_exactly('list', 'version', 'summary', 'raw', 'uncovered',
|
50
|
+
'detailed')
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'returns the keys from COMMAND_MAP' do
|
54
|
+
expect(described_class.available_commands).to eq(described_class::COMMAND_MAP.keys)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'COMMAND_MAP' do
|
59
|
+
it 'is frozen to prevent modifications' do
|
60
|
+
expect(described_class::COMMAND_MAP).to be_frozen
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'maps command names to command classes' do
|
64
|
+
expect(described_class::COMMAND_MAP['list']).to eq(SimpleCovMcp::Commands::ListCommand)
|
65
|
+
expect(described_class::COMMAND_MAP['version']).to eq(SimpleCovMcp::Commands::VersionCommand)
|
66
|
+
expect(described_class::COMMAND_MAP['summary']).to eq(SimpleCovMcp::Commands::SummaryCommand)
|
67
|
+
expect(described_class::COMMAND_MAP['raw']).to eq(SimpleCovMcp::Commands::RawCommand)
|
68
|
+
expect(described_class::COMMAND_MAP['uncovered']).to eq(SimpleCovMcp::Commands::UncoveredCommand)
|
69
|
+
expect(described_class::COMMAND_MAP['detailed']).to eq(SimpleCovMcp::Commands::DetailedCommand)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::Commands::DetailedCommand do
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
|
+
let(:cli_context) { SimpleCovMcp::CoverageCLI.new }
|
8
|
+
let(:command) { described_class.new(cli_context) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
cli_context.config.root = root
|
12
|
+
cli_context.config.resultset = 'coverage'
|
13
|
+
cli_context.config.json = false
|
14
|
+
cli_context.config.source_mode = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#execute' do
|
18
|
+
it 'prints the detailed coverage table' do
|
19
|
+
output = nil
|
20
|
+
|
21
|
+
silence_output do |stdout, _stderr|
|
22
|
+
command.execute(['lib/foo.rb'])
|
23
|
+
output = stdout.string
|
24
|
+
end
|
25
|
+
|
26
|
+
expect(output).to include('File: lib/foo.rb')
|
27
|
+
expect(output).to include('Line')
|
28
|
+
expect(output).to include('Covered')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'emits JSON when requested, including stale metadata' do
|
32
|
+
cli_context.config.json = true
|
33
|
+
stub_staleness_check('L')
|
34
|
+
|
35
|
+
json_output = nil
|
36
|
+
silence_output do |stdout, _stderr|
|
37
|
+
command.execute(['lib/foo.rb'])
|
38
|
+
json_output = stdout.string
|
39
|
+
end
|
40
|
+
|
41
|
+
payload = JSON.parse(json_output)
|
42
|
+
expect(payload['file']).to eq('lib/foo.rb')
|
43
|
+
expect(payload['lines']).to be_an(Array)
|
44
|
+
expect(payload['summary']).to include('covered' => 2, 'total' => 3, 'pct' => 66.67)
|
45
|
+
expect(payload['stale']).to eq('L')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::Commands::RawCommand do
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
|
+
let(:cli_context) { SimpleCovMcp::CoverageCLI.new }
|
8
|
+
let(:command) { described_class.new(cli_context) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
cli_context.config.root = root
|
12
|
+
cli_context.config.resultset = 'coverage'
|
13
|
+
cli_context.config.json = false
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#execute' do
|
17
|
+
it 'prints the raw coverage lines for the requested file' do
|
18
|
+
output = nil
|
19
|
+
|
20
|
+
silence_output do |stdout, _stderr|
|
21
|
+
command.execute(['lib/foo.rb'])
|
22
|
+
output = stdout.string
|
23
|
+
end
|
24
|
+
|
25
|
+
expect(output).to include('File: lib/foo.rb')
|
26
|
+
# Example match: "[1, 0, nil, 2]"
|
27
|
+
expect(output).to match(/\[1,\s0,\snil,\s2\]/)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'emits JSON when requested, including stale metadata' do
|
31
|
+
cli_context.config.json = true
|
32
|
+
stub_staleness_check('L')
|
33
|
+
|
34
|
+
json_output = nil
|
35
|
+
silence_output do |stdout, _stderr|
|
36
|
+
command.execute(['lib/foo.rb'])
|
37
|
+
json_output = stdout.string
|
38
|
+
end
|
39
|
+
|
40
|
+
payload = JSON.parse(json_output)
|
41
|
+
expect(payload['file']).to eq('lib/foo.rb')
|
42
|
+
expect(payload['lines']).to eq([1, 0, nil, 2])
|
43
|
+
expect(payload['stale']).to eq('L')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::Commands::SummaryCommand do
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
|
+
let(:cli_context) { SimpleCovMcp::CoverageCLI.new }
|
8
|
+
let(:command) { described_class.new(cli_context) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
cli_context.config.root = root
|
12
|
+
cli_context.config.resultset = 'coverage'
|
13
|
+
cli_context.config.json = false
|
14
|
+
cli_context.config.source_mode = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#execute' do
|
18
|
+
it 'prints a coverage summary line with a relative path' do
|
19
|
+
output = nil
|
20
|
+
|
21
|
+
silence_output do |stdout, _stderr|
|
22
|
+
command.execute(['lib/foo.rb'])
|
23
|
+
output = stdout.string
|
24
|
+
end
|
25
|
+
|
26
|
+
# Example match: " 66.67% 2/3 lib/foo.rb"
|
27
|
+
expect(output).to match(/66\.67%.*2\/3.*lib\/foo\.rb/)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'emits JSON when requested, including stale metadata' do
|
31
|
+
cli_context.config.json = true
|
32
|
+
stub_staleness_check('L')
|
33
|
+
|
34
|
+
json_output = nil
|
35
|
+
silence_output do |stdout, _stderr|
|
36
|
+
command.execute(['lib/foo.rb'])
|
37
|
+
json_output = stdout.string
|
38
|
+
end
|
39
|
+
|
40
|
+
payload = JSON.parse(json_output)
|
41
|
+
expect(payload['file']).to eq('lib/foo.rb')
|
42
|
+
expect(payload['summary']).to include('covered' => 2, 'total' => 3, 'pct' => 66.67)
|
43
|
+
expect(payload).to have_key('stale')
|
44
|
+
expect(payload['stale']).to eq('L')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::Commands::UncoveredCommand do
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
|
+
let(:cli_context) { SimpleCovMcp::CoverageCLI.new }
|
8
|
+
let(:command) { described_class.new(cli_context) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
cli_context.config.root = root
|
12
|
+
cli_context.config.resultset = 'coverage'
|
13
|
+
cli_context.config.json = false
|
14
|
+
cli_context.config.source_mode = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#execute' do
|
18
|
+
it 'prints uncovered line numbers with the summary' do
|
19
|
+
output = nil
|
20
|
+
|
21
|
+
silence_output do |stdout, _stderr|
|
22
|
+
command.execute(['lib/foo.rb'])
|
23
|
+
output = stdout.string
|
24
|
+
end
|
25
|
+
|
26
|
+
expect(output).to include('File: lib/foo.rb')
|
27
|
+
expect(output).to include('Uncovered lines: 2')
|
28
|
+
# Example match: "Summary: 66.67% 2/3"
|
29
|
+
expect(output).to match(/Summary:\s+66\.67%.*2\/3/)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'emits JSON when requested, including stale metadata' do
|
33
|
+
cli_context.config.json = true
|
34
|
+
stub_staleness_check('L')
|
35
|
+
|
36
|
+
json_output = nil
|
37
|
+
silence_output do |stdout, _stderr|
|
38
|
+
command.execute(['lib/foo.rb'])
|
39
|
+
json_output = stdout.string
|
40
|
+
end
|
41
|
+
|
42
|
+
payload = JSON.parse(json_output)
|
43
|
+
expect(payload['file']).to eq('lib/foo.rb')
|
44
|
+
expect(payload['uncovered']).to eq([2])
|
45
|
+
expect(payload['summary']).to include('covered' => 2, 'total' => 3, 'pct' => 66.67)
|
46
|
+
expect(payload['stale']).to eq('L')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|