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,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
RSpec.describe SimpleCovMcp::ResultsetLoader do
|
7
|
+
describe '.load' do
|
8
|
+
it 'parses a single suite and returns coverage map and timestamp' do
|
9
|
+
Dir.mktmpdir do |dir|
|
10
|
+
resultset_path = File.join(dir, '.resultset.json')
|
11
|
+
coverage = {
|
12
|
+
File.join(dir, 'lib', 'foo.rb') => { 'lines' => [1, 0, nil, 2] }
|
13
|
+
}
|
14
|
+
data = {
|
15
|
+
'SuiteA' => {
|
16
|
+
'timestamp' => 123,
|
17
|
+
'coverage' => coverage
|
18
|
+
}
|
19
|
+
}
|
20
|
+
File.write(resultset_path, JSON.generate(data))
|
21
|
+
|
22
|
+
result = described_class.load(resultset_path: resultset_path)
|
23
|
+
|
24
|
+
expect(result.coverage_map).to eq(coverage)
|
25
|
+
expect(result.timestamp).to eq(123)
|
26
|
+
expect(result.suite_names).to eq(['SuiteA'])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'merges multiple suites and combines coverage' do
|
31
|
+
Dir.mktmpdir do |dir|
|
32
|
+
resultset_path = File.join(dir, '.resultset.json')
|
33
|
+
foo_path = File.join(dir, 'lib', 'foo.rb')
|
34
|
+
bar_path = File.join(dir, 'lib', 'bar.rb')
|
35
|
+
|
36
|
+
data = {
|
37
|
+
'RSpec' => {
|
38
|
+
'timestamp' => 100,
|
39
|
+
'coverage' => {
|
40
|
+
foo_path => { 'lines' => [1, 0, nil, 0] }
|
41
|
+
}
|
42
|
+
},
|
43
|
+
'Cucumber' => {
|
44
|
+
'timestamp' => 200,
|
45
|
+
'coverage' => {
|
46
|
+
foo_path => { 'lines' => [0, 3, nil, 1] },
|
47
|
+
bar_path => { 'lines' => [0, 1, 1] }
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
File.write(resultset_path, JSON.generate(data))
|
52
|
+
|
53
|
+
result = described_class.load(resultset_path: resultset_path)
|
54
|
+
expect(result.coverage_map[foo_path]['lines']).to eq([1, 3, nil, 1])
|
55
|
+
expect(result.coverage_map[bar_path]['lines']).to eq([0, 1, 1])
|
56
|
+
expect(result.timestamp).to eq(200)
|
57
|
+
expect(result.suite_names).to contain_exactly('RSpec', 'Cucumber')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'adapts legacy array coverage entries' do
|
62
|
+
Dir.mktmpdir do |dir|
|
63
|
+
resultset_path = File.join(dir, '.resultset.json')
|
64
|
+
foo_path = File.join(dir, 'lib', 'foo.rb')
|
65
|
+
data = {
|
66
|
+
'SuiteA' => {
|
67
|
+
'timestamp' => 50,
|
68
|
+
'coverage' => {
|
69
|
+
foo_path => [1, 0, nil, 2]
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
File.write(resultset_path, JSON.generate(data))
|
74
|
+
|
75
|
+
result = described_class.load(resultset_path: resultset_path)
|
76
|
+
expect(result.coverage_map[foo_path]).to eq('lines' => [1, 0, nil, 2])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'raises CoverageDataError when no suites are present' do
|
81
|
+
Dir.mktmpdir do |dir|
|
82
|
+
resultset_path = File.join(dir, '.resultset.json')
|
83
|
+
File.write(resultset_path, '{}')
|
84
|
+
|
85
|
+
expect do
|
86
|
+
described_class.load(resultset_path: resultset_path)
|
87
|
+
end.to raise_error(SimpleCovMcp::CoverageDataError, /No test suite/)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'SimpleCov loading and logging' do
|
93
|
+
it 'raises CoverageDataError when SimpleCov cannot be required' do
|
94
|
+
singleton = class << described_class; self; end
|
95
|
+
singleton.send(:define_method, :require) do |name|
|
96
|
+
raise LoadError if name == 'simplecov'
|
97
|
+
|
98
|
+
Kernel.require(name)
|
99
|
+
end
|
100
|
+
|
101
|
+
expect do
|
102
|
+
described_class.send(:require_simplecov_for_merge!, '/tmp/resultset.json')
|
103
|
+
end.to raise_error(SimpleCovMcp::CoverageDataError, /Install simplecov/)
|
104
|
+
ensure
|
105
|
+
if singleton.method_defined?(:require)
|
106
|
+
singleton.send(:remove_method, :require)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'logs duplicate suite names when merging coverage' do
|
111
|
+
suites = [
|
112
|
+
described_class::SuiteEntry.new(name: 'RSpec', coverage: {}, timestamp: 0),
|
113
|
+
described_class::SuiteEntry.new(name: 'RSpec', coverage: {}, timestamp: 0),
|
114
|
+
described_class::SuiteEntry.new(name: 'Cucumber', coverage: {}, timestamp: 0)
|
115
|
+
]
|
116
|
+
|
117
|
+
expect(SimpleCovMcp::CovUtil).to receive(:log)
|
118
|
+
.with(include('Merging duplicate coverage suites for RSpec'))
|
119
|
+
described_class.send(:log_duplicate_suite_names, suites)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe 'timestamp normalization' do
|
124
|
+
it 'handles float timestamps' do
|
125
|
+
value = described_class.send(:normalize_coverage_timestamp, 123.9, nil)
|
126
|
+
expect(value).to eq(123)
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'handles Time objects' do
|
130
|
+
time = Time.at(456)
|
131
|
+
value = described_class.send(:normalize_coverage_timestamp, time, nil)
|
132
|
+
expect(value).to eq(456)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'parses numeric string timestamps' do
|
136
|
+
value = described_class.send(:normalize_coverage_timestamp, '789.42', nil)
|
137
|
+
expect(value).to eq(789)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'falls back to created_at when timestamp missing' do
|
141
|
+
value = described_class.send(:normalize_coverage_timestamp, nil, 321)
|
142
|
+
expect(value).to eq(321)
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'logs warning and returns zero for invalid timestamp strings' do
|
146
|
+
messages = []
|
147
|
+
allow(SimpleCovMcp::CovUtil).to receive(:log) { |msg| messages << msg }
|
148
|
+
|
149
|
+
value = described_class.send(:normalize_coverage_timestamp, 'not-a-timestamp', nil)
|
150
|
+
|
151
|
+
expect(value).to eq(0)
|
152
|
+
expect(messages.join).to include('Coverage resultset timestamp could not be parsed')
|
153
|
+
expect(messages.join).to include('not-a-timestamp')
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'logs warning and returns zero for unsupported types' do
|
157
|
+
messages = []
|
158
|
+
allow(SimpleCovMcp::CovUtil).to receive(:log) { |msg| messages << msg }
|
159
|
+
|
160
|
+
value = described_class.send(:normalize_coverage_timestamp, [:invalid], nil)
|
161
|
+
|
162
|
+
expect(value).to eq(0)
|
163
|
+
expect(messages.join).to include('Coverage resultset timestamp could not be parsed')
|
164
|
+
expect(messages.join).to include('[:invalid]')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# Shared Examples for MCP Tools
|
2
|
+
|
3
|
+
This directory contains reusable test patterns for SimpleCov MCP tools.
|
4
|
+
|
5
|
+
## File-Based MCP Tools
|
6
|
+
|
7
|
+
The `file_based_mcp_tools.rb` shared example provides parameterized testing for MCP tools that follow the same pattern:
|
8
|
+
|
9
|
+
- Take a `path` parameter (file to analyze)
|
10
|
+
- Call a specific method on `CoverageModel`
|
11
|
+
- Return JSON resource with predictable structure
|
12
|
+
- Have consistent output filenames
|
13
|
+
|
14
|
+
### Usage
|
15
|
+
|
16
|
+
Instead of creating separate spec files for each similar tool, add your tool to the `FILE_BASED_TOOL_CONFIGS` hash:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# In spec/shared_examples/file_based_mcp_tools.rb
|
20
|
+
your_tool: {
|
21
|
+
tool_class: SimpleCovMcp::Tools::YourTool,
|
22
|
+
model_method: :your_method,
|
23
|
+
expected_keys: ['file', 'your_data'],
|
24
|
+
output_filename: 'your_tool.json',
|
25
|
+
description: 'your tool data',
|
26
|
+
mock_data: {
|
27
|
+
'file' => '/abs/path/lib/foo.rb',
|
28
|
+
'your_data' => { 'key' => 'value' }
|
29
|
+
},
|
30
|
+
additional_validations: ->(data, item) {
|
31
|
+
expect(data['your_data']).to include('key')
|
32
|
+
}
|
33
|
+
}
|
34
|
+
```
|
35
|
+
|
36
|
+
The parameterized test will automatically:
|
37
|
+
- ✅ Test basic MCP resource structure
|
38
|
+
- ✅ Verify expected JSON keys are present
|
39
|
+
- ✅ Check correct output filename
|
40
|
+
- ✅ Run tool-specific validations
|
41
|
+
- ✅ Test parameter consistency across tools
|
42
|
+
- ✅ Validate JSON structure consistency
|
43
|
+
|
44
|
+
### Benefits vs Individual Spec Files
|
45
|
+
|
46
|
+
#### Before (Individual Files)
|
47
|
+
```ruby
|
48
|
+
# spec/your_tool_spec.rb - 25+ lines
|
49
|
+
RSpec.describe SimpleCovMcp::Tools::YourTool do
|
50
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
51
|
+
|
52
|
+
before do
|
53
|
+
setup_mcp_response_stub
|
54
|
+
model = instance_double(SimpleCovMcp::CoverageModel)
|
55
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
56
|
+
allow(model).to receive(:your_method).with('lib/foo.rb').and_return({
|
57
|
+
'file' => '/abs/path/lib/foo.rb',
|
58
|
+
'your_data' => { 'key' => 'value' }
|
59
|
+
})
|
60
|
+
end
|
61
|
+
|
62
|
+
subject { described_class.call(path: 'lib/foo.rb', server_context: server_context) }
|
63
|
+
|
64
|
+
it_behaves_like 'an MCP tool that returns JSON resource'
|
65
|
+
|
66
|
+
it 'returns your tool data with expected structure' do
|
67
|
+
response = subject
|
68
|
+
data, item = expect_mcp_json_resource(response, expected_keys: ['file', 'your_data'])
|
69
|
+
|
70
|
+
expect(item['resource']['name']).to eq('your_tool.json')
|
71
|
+
expect(data['your_data']).to include('key')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
#### After (Parameterized)
|
77
|
+
```ruby
|
78
|
+
# Just add to FILE_BASED_TOOL_CONFIGS - 8 lines
|
79
|
+
your_tool: {
|
80
|
+
tool_class: SimpleCovMcp::Tools::YourTool,
|
81
|
+
model_method: :your_method,
|
82
|
+
expected_keys: ['file', 'your_data'],
|
83
|
+
output_filename: 'your_tool.json',
|
84
|
+
description: 'your tool data',
|
85
|
+
mock_data: { 'file' => '/abs/path/lib/foo.rb', 'your_data' => { 'key' => 'value' } },
|
86
|
+
additional_validations: ->(data, item) { expect(data['your_data']).to include('key') }
|
87
|
+
}
|
88
|
+
```
|
89
|
+
|
90
|
+
### Additional Benefits
|
91
|
+
|
92
|
+
1. **Cross-tool consistency testing**: Automatically tests that all tools handle parameters consistently
|
93
|
+
2. **Structural validation**: Ensures all tools return properly formed MCP resources
|
94
|
+
3. **Reduced maintenance**: Bug fixes and improvements benefit all tools at once
|
95
|
+
4. **Better coverage**: Gets consistency tests you wouldn't write individually
|
96
|
+
5. **Enforces patterns**: Encourages consistent tool design
|
97
|
+
|
98
|
+
### When NOT to Use This
|
99
|
+
|
100
|
+
Don't use the parameterized approach for tools that:
|
101
|
+
- Don't follow the file-based pattern (e.g., `AllFilesCoverageTool`, `VersionTool`)
|
102
|
+
- Have significantly different parameter signatures
|
103
|
+
- Need extensive tool-specific testing that doesn't fit the pattern
|
104
|
+
- Are prototypes or experimental tools
|
105
|
+
|
106
|
+
For these cases, create individual spec files as needed.
|
107
|
+
|
108
|
+
### Current Tools Using This Pattern
|
109
|
+
|
110
|
+
- ✅ `CoverageSummaryTool` - File summary data
|
111
|
+
- ✅ `CoverageRawTool` - Raw coverage arrays
|
112
|
+
- ✅ `UncoveredLinesTool` - Uncovered line numbers
|
113
|
+
- ✅ `CoverageDetailedTool` - Line-by-line coverage details
|
114
|
+
|
115
|
+
All tested with 13 shared tests plus 6 tool-specific tests = 19 total tests for 4 tools.
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples 'a coverage presenter' do |config|
|
4
|
+
subject(:presenter) { described_class.new(model: model, path: config.fetch(:path, 'lib/foo.rb')) }
|
5
|
+
|
6
|
+
let(:model) { instance_double(SimpleCovMcp::CoverageModel) }
|
7
|
+
let(:raw_payload) { config.fetch(:payload) }
|
8
|
+
let(:stale_value) { config.fetch(:stale) }
|
9
|
+
let(:relative_path) { config.fetch(:relative_path, 'lib/foo.rb') }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow(model).to receive(config.fetch(:model_method)).with(config.fetch(:path,
|
13
|
+
'lib/foo.rb')).and_return(raw_payload)
|
14
|
+
allow(model).to receive(:staleness_for).with(config.fetch(:path,
|
15
|
+
'lib/foo.rb')).and_return(stale_value)
|
16
|
+
allow(model).to receive(:relativize) do |payload|
|
17
|
+
payload.merge('file' => relative_path)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#absolute_payload' do
|
22
|
+
it 'returns data with stale metadata' do
|
23
|
+
result = presenter.absolute_payload
|
24
|
+
|
25
|
+
expect(result).to include('file' => raw_payload['file'])
|
26
|
+
Array(config.fetch(:expected_keys)).each do |key|
|
27
|
+
expect(result).to include(key => raw_payload[key])
|
28
|
+
end
|
29
|
+
expect(result['stale']).to eq(stale_value)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'does not mutate the underlying model data' do
|
33
|
+
presenter.absolute_payload
|
34
|
+
expect(raw_payload).not_to have_key('stale')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#relativized_payload' do
|
39
|
+
it 'relativizes the payload once data is loaded' do
|
40
|
+
result = presenter.relativized_payload
|
41
|
+
expect(result['file']).to eq(relative_path)
|
42
|
+
expect(result['stale']).to eq(stale_value)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'only fetches model data once across calls' do
|
46
|
+
presenter.absolute_payload
|
47
|
+
presenter.relativized_payload
|
48
|
+
expect(model).to have_received(config.fetch(:model_method)).once
|
49
|
+
expect(model).to have_received(:staleness_for).once
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#relative_path' do
|
54
|
+
it 'returns the relativized path' do
|
55
|
+
expect(presenter.relative_path).to eq(relative_path)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#stale' do
|
60
|
+
it 'returns the cached staleness flag' do
|
61
|
+
expect(presenter.stale).to eq(stale_value)
|
62
|
+
presenter.relativized_payload
|
63
|
+
expect(presenter.stale).to eq(stale_value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
# Shared examples for file-based MCP tools that follow the same pattern:
|
6
|
+
# - Take a path parameter
|
7
|
+
# - Call a specific method on CoverageModel
|
8
|
+
# - Return JSON resource with consistent structure
|
9
|
+
# - Have predictable output filename
|
10
|
+
|
11
|
+
RSpec.shared_examples 'a file-based MCP tool' do |config|
|
12
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
13
|
+
let(:tool_class) { config[:tool_class] }
|
14
|
+
let(:model_method) { config[:model_method] }
|
15
|
+
let(:expected_keys) { config[:expected_keys] }
|
16
|
+
let(:output_filename) { config[:output_filename] }
|
17
|
+
let(:mock_data) { config[:mock_data] }
|
18
|
+
let(:additional_validations) { config[:additional_validations] }
|
19
|
+
|
20
|
+
before do
|
21
|
+
setup_mcp_response_stub
|
22
|
+
model = instance_double(SimpleCovMcp::CoverageModel)
|
23
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
24
|
+
allow(model).to receive(model_method).with('lib/foo.rb').and_return(mock_data)
|
25
|
+
relativizer = SimpleCovMcp::PathRelativizer.new(
|
26
|
+
root: '/abs/path',
|
27
|
+
scalar_keys: %w[file file_path],
|
28
|
+
array_keys: %w[newer_files missing_files deleted_files]
|
29
|
+
)
|
30
|
+
allow(model).to receive(:relativize) { |payload| relativizer.relativize(payload) }
|
31
|
+
allow(model).to receive(:staleness_for).with('lib/foo.rb').and_return(false)
|
32
|
+
end
|
33
|
+
|
34
|
+
subject { tool_class.call(path: 'lib/foo.rb', server_context: server_context) }
|
35
|
+
|
36
|
+
it_behaves_like 'an MCP tool that returns text JSON'
|
37
|
+
|
38
|
+
it "returns #{config[:description]} with expected structure" do
|
39
|
+
response = subject
|
40
|
+
data, item = expect_mcp_text_json(response, expected_keys: expected_keys)
|
41
|
+
|
42
|
+
if data.is_a?(Hash) && data.key?('file')
|
43
|
+
expect(data['file']).to eq('lib/foo.rb')
|
44
|
+
end
|
45
|
+
|
46
|
+
expect(data).to have_key('stale')
|
47
|
+
expect(data['stale']).to eq(false)
|
48
|
+
|
49
|
+
# Run tool-specific validations if provided
|
50
|
+
if additional_validations
|
51
|
+
instance_exec(data, item, &additional_validations)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Generate tool-specific examples dynamically
|
56
|
+
tool_specific_examples = config[:tool_specific_examples] || {}
|
57
|
+
tool_specific_examples.each do |example_name, example_block|
|
58
|
+
it example_name do
|
59
|
+
instance_exec(config, &example_block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Configuration data for each file-based MCP tool
|
65
|
+
FILE_BASED_TOOL_CONFIGS = {
|
66
|
+
summary: {
|
67
|
+
tool_class: SimpleCovMcp::Tools::CoverageSummaryTool,
|
68
|
+
model_method: :summary_for,
|
69
|
+
expected_keys: ['file', 'summary', 'stale'],
|
70
|
+
output_filename: 'coverage_summary.json',
|
71
|
+
description: 'coverage summary data',
|
72
|
+
mock_data: {
|
73
|
+
'file' => '/abs/path/lib/foo.rb',
|
74
|
+
'summary' => { 'covered' => 10, 'total' => 12, 'pct' => 83.33 }
|
75
|
+
},
|
76
|
+
additional_validations: ->(data, item) {
|
77
|
+
expect(data['summary']).to include('covered', 'total', 'pct')
|
78
|
+
},
|
79
|
+
tool_specific_examples: {
|
80
|
+
'includes percentage in summary data' => ->(config) {
|
81
|
+
model = instance_double(SimpleCovMcp::CoverageModel)
|
82
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
83
|
+
allow(model).to receive(:summary_for).and_return(config[:mock_data])
|
84
|
+
allow(model).to receive(:staleness_for).and_return(false)
|
85
|
+
relativizer = SimpleCovMcp::PathRelativizer.new(
|
86
|
+
root: '/abs/path',
|
87
|
+
scalar_keys: %w[file file_path],
|
88
|
+
array_keys: %w[newer_files missing_files deleted_files]
|
89
|
+
)
|
90
|
+
allow(model).to receive(:relativize) { |payload| relativizer.relativize(payload) }
|
91
|
+
setup_mcp_response_stub
|
92
|
+
|
93
|
+
response = config[:tool_class].call(path: 'lib/foo.rb',
|
94
|
+
server_context: instance_double('ServerContext').as_null_object)
|
95
|
+
data, _ = expect_mcp_text_json(response)
|
96
|
+
|
97
|
+
expect(data['summary']['pct']).to be_a(Float)
|
98
|
+
}
|
99
|
+
}
|
100
|
+
},
|
101
|
+
|
102
|
+
raw: {
|
103
|
+
tool_class: SimpleCovMcp::Tools::CoverageRawTool,
|
104
|
+
model_method: :raw_for,
|
105
|
+
expected_keys: ['file', 'lines', 'stale'],
|
106
|
+
output_filename: 'coverage_raw.json',
|
107
|
+
description: 'raw coverage data',
|
108
|
+
mock_data: {
|
109
|
+
'file' => '/abs/path/lib/foo.rb',
|
110
|
+
'lines' => [nil, 1, 0]
|
111
|
+
},
|
112
|
+
additional_validations: ->(data, _item) {
|
113
|
+
expect(data['lines']).to be_an(Array)
|
114
|
+
}
|
115
|
+
},
|
116
|
+
|
117
|
+
uncovered: {
|
118
|
+
tool_class: SimpleCovMcp::Tools::UncoveredLinesTool,
|
119
|
+
model_method: :uncovered_for,
|
120
|
+
expected_keys: ['file', 'uncovered', 'summary', 'stale'],
|
121
|
+
output_filename: 'uncovered_lines.json',
|
122
|
+
description: 'uncovered lines data',
|
123
|
+
mock_data: {
|
124
|
+
'file' => '/abs/path/lib/foo.rb',
|
125
|
+
'uncovered' => [5, 9, 12],
|
126
|
+
'summary' => { 'covered' => 10, 'total' => 12, 'pct' => 83.33 }
|
127
|
+
},
|
128
|
+
additional_validations: ->(data, item) {
|
129
|
+
expect(data['uncovered']).to eq([5, 9, 12])
|
130
|
+
},
|
131
|
+
tool_specific_examples: {
|
132
|
+
'includes both uncovered lines and summary' => ->(config) {
|
133
|
+
model = instance_double(SimpleCovMcp::CoverageModel)
|
134
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
135
|
+
allow(model).to receive(:uncovered_for).and_return(config[:mock_data])
|
136
|
+
allow(model).to receive(:staleness_for).and_return(false)
|
137
|
+
relativizer = SimpleCovMcp::PathRelativizer.new(
|
138
|
+
root: '/abs/path',
|
139
|
+
scalar_keys: %w[file file_path],
|
140
|
+
array_keys: %w[newer_files missing_files deleted_files]
|
141
|
+
)
|
142
|
+
allow(model).to receive(:relativize) { |payload| relativizer.relativize(payload) }
|
143
|
+
setup_mcp_response_stub
|
144
|
+
|
145
|
+
response = config[:tool_class].call(path: 'lib/foo.rb',
|
146
|
+
server_context: instance_double('ServerContext').as_null_object)
|
147
|
+
data, _ = expect_mcp_text_json(response)
|
148
|
+
|
149
|
+
expect(data['uncovered']).to be_an(Array)
|
150
|
+
expect(data['summary']).to include('covered', 'total', 'pct')
|
151
|
+
}
|
152
|
+
}
|
153
|
+
},
|
154
|
+
|
155
|
+
detailed: {
|
156
|
+
tool_class: SimpleCovMcp::Tools::CoverageDetailedTool,
|
157
|
+
model_method: :detailed_for,
|
158
|
+
expected_keys: ['file', 'lines', 'summary', 'stale'],
|
159
|
+
output_filename: 'coverage_detailed.json',
|
160
|
+
description: 'detailed coverage data',
|
161
|
+
mock_data: {
|
162
|
+
'file' => '/abs/path/lib/foo.rb',
|
163
|
+
'lines' => [
|
164
|
+
{ 'line' => 1, 'hits' => 1, 'covered' => true },
|
165
|
+
{ 'line' => 2, 'hits' => 0, 'covered' => false }
|
166
|
+
],
|
167
|
+
'summary' => { 'covered' => 1, 'total' => 2, 'pct' => 50.0 }
|
168
|
+
},
|
169
|
+
additional_validations: ->(data, item) {
|
170
|
+
expect(data['lines']).to be_an(Array)
|
171
|
+
expect(data['lines'].first).to include('line', 'hits', 'covered')
|
172
|
+
}
|
173
|
+
}
|
174
|
+
}.freeze
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.shared_examples 'an MCP tool that returns text JSON' do
|
6
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
7
|
+
|
8
|
+
before do
|
9
|
+
setup_mcp_response_stub
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns a properly structured MCP text JSON response' do
|
13
|
+
response = subject
|
14
|
+
expect_mcp_text_json(response)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp do
|
6
|
+
# Mode detection tests moved to mode_detector_spec.rb
|
7
|
+
# These tests verify the integration with ModeDetector
|
8
|
+
describe 'mode detection integration' do
|
9
|
+
it 'uses ModeDetector for CLI mode detection' do
|
10
|
+
expect(SimpleCovMcp::ModeDetector).to receive(:cli_mode?).with(['--force-cli'])
|
11
|
+
.and_return(true)
|
12
|
+
expect(SimpleCovMcp::CoverageCLI).to receive_message_chain(:new, :run)
|
13
|
+
SimpleCovMcp.run(['--force-cli'])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|