simplecov-mcp 1.0.1 → 2.0.1
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/README.md +98 -50
- data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
- data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
- data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
- data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
- data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
- data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
- data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
- data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
- data/docs/user/README.md +14 -0
- data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/simplecov-mcp +1 -1
- data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
- data/lib/simplecov_mcp/app_context.rb +1 -1
- data/lib/simplecov_mcp/base_tool.rb +66 -38
- data/lib/simplecov_mcp/cli.rb +67 -123
- data/lib/simplecov_mcp/commands/base_command.rb +16 -27
- data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
- data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
- data/lib/simplecov_mcp/commands/list_command.rb +1 -1
- data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
- data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
- data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
- data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
- data/lib/simplecov_mcp/commands/version_command.rb +19 -4
- data/lib/simplecov_mcp/config_parser.rb +32 -0
- data/lib/simplecov_mcp/constants.rb +3 -3
- data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
- data/lib/simplecov_mcp/error_handler.rb +81 -40
- data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
- data/lib/simplecov_mcp/errors.rb +12 -19
- data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
- data/lib/simplecov_mcp/formatters.rb +51 -0
- data/lib/simplecov_mcp/mcp_server.rb +9 -7
- data/lib/simplecov_mcp/mode_detector.rb +6 -5
- data/lib/simplecov_mcp/model.rb +122 -88
- data/lib/simplecov_mcp/option_normalizers.rb +39 -18
- data/lib/simplecov_mcp/option_parser_builder.rb +85 -72
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
- data/lib/simplecov_mcp/path_relativizer.rb +17 -14
- data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
- data/lib/simplecov_mcp/resultset_loader.rb +20 -25
- data/lib/simplecov_mcp/staleness_checker.rb +50 -46
- data/lib/simplecov_mcp/table_formatter.rb +64 -0
- data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
- data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
- data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
- data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
- data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
- data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
- data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
- data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
- data/lib/simplecov_mcp/util.rb +18 -12
- data/lib/simplecov_mcp/version.rb +1 -1
- data/lib/simplecov_mcp.rb +23 -29
- data/spec/all_files_coverage_tool_spec.rb +4 -3
- data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
- data/spec/base_tool_spec.rb +17 -14
- data/spec/cli/show_default_report_spec.rb +2 -2
- data/spec/cli_enumerated_options_spec.rb +31 -9
- data/spec/cli_error_spec.rb +46 -23
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +11 -63
- data/spec/cli_spec.rb +82 -97
- data/spec/cli_usage_spec.rb +15 -15
- data/spec/commands/base_command_spec.rb +12 -92
- data/spec/commands/command_factory_spec.rb +7 -3
- data/spec/commands/detailed_command_spec.rb +10 -24
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +43 -20
- data/spec/commands/summary_command_spec.rb +10 -23
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +29 -23
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +3 -3
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +21 -10
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +120 -4
- data/spec/error_mode_spec.rb +18 -22
- data/spec/errors_edge_cases_spec.rb +101 -28
- data/spec/errors_stale_spec.rb +34 -0
- data/spec/file_based_mcp_tools_spec.rb +6 -6
- data/spec/fixtures/project1/lib/bar.rb +2 -0
- data/spec/fixtures/project1/lib/foo.rb +2 -0
- data/spec/help_tool_spec.rb +2 -18
- data/spec/integration_spec.rb +103 -161
- data/spec/logging_fallback_spec.rb +3 -3
- data/spec/mcp_server_integration_spec.rb +1 -1
- data/spec/mcp_server_spec.rb +70 -53
- data/spec/mode_detector_spec.rb +46 -41
- data/spec/model_error_handling_spec.rb +139 -78
- data/spec/model_staleness_spec.rb +13 -13
- data/spec/option_normalizers_spec.rb +111 -112
- data/spec/option_parsers/env_options_parser_spec.rb +25 -37
- data/spec/option_parsers/error_helper_spec.rb +56 -56
- data/spec/path_relativizer_spec.rb +15 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
- data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
- data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
- data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/simple_cov_mcp_module_spec.rb +24 -3
- data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
- data/spec/simplecov_mcp/formatters_spec.rb +76 -0
- data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/simplecov_mcp_model_spec.rb +97 -47
- data/spec/simplecov_mcp_opts_spec.rb +42 -39
- data/spec/spec_helper.rb +27 -92
- data/spec/staleness_checker_spec.rb +10 -9
- data/spec/staleness_more_spec.rb +4 -4
- data/spec/support/cli_helpers.rb +22 -0
- data/spec/support/control_flow_helpers.rb +20 -0
- data/spec/support/fake_mcp.rb +40 -0
- data/spec/support/io_helpers.rb +29 -0
- data/spec/support/mcp_helpers.rb +35 -0
- data/spec/support/mcp_runner.rb +10 -8
- data/spec/support/mocking_helpers.rb +30 -0
- data/spec/table_format_spec.rb +70 -0
- data/spec/tools/validate_tool_spec.rb +132 -0
- data/spec/tools_error_handling_spec.rb +34 -48
- data/spec/util_spec.rb +5 -4
- data/spec/version_spec.rb +7 -7
- data/spec/version_tool_spec.rb +20 -22
- metadata +90 -23
- data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
- data/docs/CLI_USAGE.md +0 -637
- data/docs/EXAMPLES.md +0 -430
- data/docs/INSTALLATION.md +0 -352
- data/spec/cli_success_predicate_spec.rb +0 -141
data/spec/integration_spec.rb
CHANGED
|
@@ -2,151 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
MCP_TIMEOUT = 5
|
|
5
|
+
# Timeout for MCP server operations (increased for JRuby compatibility)
|
|
6
|
+
MCP_TIMEOUT = 5
|
|
8
7
|
|
|
8
|
+
RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
9
9
|
let(:project_root) { (FIXTURES_DIR / 'project1').to_s }
|
|
10
10
|
let(:coverage_dir) { File.join(project_root, 'coverage') }
|
|
11
11
|
let(:resultset_path) { File.join(coverage_dir, '.resultset.json') }
|
|
12
12
|
|
|
13
13
|
describe 'End-to-End Coverage Model Functionality' do
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Initialize model with real fixture data
|
|
17
|
-
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
|
18
|
-
|
|
19
|
-
# Test all_files returns real coverage data
|
|
20
|
-
all_files = model.all_files
|
|
21
|
-
expect(all_files).to be_an(Array)
|
|
22
|
-
expect(all_files.length).to eq(2)
|
|
23
|
-
|
|
24
|
-
# Verify file paths and coverage data structure
|
|
25
|
-
foo_file = all_files.find { |f| f['file'].include?('foo.rb') }
|
|
26
|
-
bar_file = all_files.find { |f| f['file'].include?('bar.rb') }
|
|
27
|
-
|
|
28
|
-
expect(foo_file).to include('covered', 'total', 'percentage', 'stale')
|
|
29
|
-
expect(bar_file).to include('covered', 'total', 'percentage', 'stale')
|
|
30
|
-
|
|
31
|
-
# Verify actual coverage calculations match fixture data
|
|
32
|
-
# foo.rb has coverage: [1, 0, nil, 2] -> 2 covered out of 3 executable = 66.67%
|
|
33
|
-
expect(foo_file['total']).to eq(3)
|
|
34
|
-
expect(foo_file['covered']).to eq(2)
|
|
35
|
-
expect(foo_file['percentage']).to be_within(0.01).of(66.67)
|
|
36
|
-
|
|
37
|
-
# bar.rb has coverage: [0, 0, 1] -> 1 covered out of 3 executable = 33.33%
|
|
38
|
-
expect(bar_file['total']).to eq(3)
|
|
39
|
-
expect(bar_file['covered']).to eq(1)
|
|
40
|
-
expect(bar_file['percentage']).to be_within(0.01).of(33.33)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
it 'provides detailed per-file analysis' do
|
|
44
|
-
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
|
45
|
-
|
|
46
|
-
# Test raw coverage data
|
|
47
|
-
raw = model.raw_for('lib/foo.rb')
|
|
48
|
-
expect(raw['file']).to end_with('lib/foo.rb')
|
|
49
|
-
expect(raw['lines']).to eq([1, 0, nil, 2])
|
|
50
|
-
|
|
51
|
-
# Test summary calculation
|
|
52
|
-
summary = model.summary_for('lib/foo.rb')
|
|
53
|
-
expect(summary['file']).to end_with('lib/foo.rb')
|
|
54
|
-
expect(summary['summary']).to include('covered' => 2, 'total' => 3)
|
|
55
|
-
expect(summary['summary']['pct']).to be_within(0.01).of(66.67)
|
|
56
|
-
|
|
57
|
-
# Test uncovered lines detection
|
|
58
|
-
uncovered = model.uncovered_for('lib/foo.rb')
|
|
59
|
-
expect(uncovered['file']).to end_with('lib/foo.rb')
|
|
60
|
-
expect(uncovered['uncovered']).to eq([2]) # Line 2 has 0 hits
|
|
61
|
-
expect(uncovered['summary']).to include('covered' => 2, 'total' => 3)
|
|
62
|
-
|
|
63
|
-
# Test detailed line-by-line analysis
|
|
64
|
-
detailed = model.detailed_for('lib/foo.rb')
|
|
65
|
-
expect(detailed['file']).to end_with('lib/foo.rb')
|
|
66
|
-
expect(detailed['lines']).to eq([
|
|
67
|
-
{ 'line' => 1, 'hits' => 1, 'covered' => true },
|
|
68
|
-
{ 'line' => 2, 'hits' => 0, 'covered' => false },
|
|
69
|
-
{ 'line' => 4, 'hits' => 2, 'covered' => true }
|
|
70
|
-
])
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
it 'generates properly formatted coverage tables' do
|
|
74
|
-
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
|
75
|
-
|
|
76
|
-
# Test default table generation
|
|
77
|
-
table = model.format_table
|
|
78
|
-
|
|
79
|
-
# Verify table structure (Unicode box drawing)
|
|
80
|
-
expect(table).to include('┌', '┬', '┐', '│', '├', '┼', '┤', '└', '┴', '┘')
|
|
81
|
-
|
|
82
|
-
# Verify headers
|
|
83
|
-
expect(table).to include('File', '%', 'Covered', 'Total', 'Stale')
|
|
14
|
+
it 'loads fixture coverage and surfaces core stats across APIs' do
|
|
15
|
+
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
|
84
16
|
|
|
85
|
-
|
|
86
|
-
|
|
17
|
+
files = model.all_files
|
|
18
|
+
expect(files.length).to eq(2)
|
|
19
|
+
files_by_name = files.to_h { |f| [File.basename(f['file']), f] }
|
|
87
20
|
|
|
88
|
-
|
|
89
|
-
|
|
21
|
+
foo = files_by_name.fetch('foo.rb')
|
|
22
|
+
bar = files_by_name.fetch('bar.rb')
|
|
23
|
+
expect(foo['percentage']).to be_within(0.01).of(66.67)
|
|
24
|
+
expect(bar['percentage']).to be_within(0.01).of(33.33)
|
|
90
25
|
|
|
91
|
-
|
|
92
|
-
|
|
26
|
+
raw = model.raw_for('lib/foo.rb')
|
|
27
|
+
expect(raw['lines']).to eq([1, 0, nil, 2])
|
|
93
28
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
data_lines = lines.select { |line| line.include?('lib/') }
|
|
97
|
-
expect(data_lines.first).to include('lib/bar.rb') # Lower coverage first
|
|
98
|
-
expect(data_lines.last).to include('lib/foo.rb') # Higher coverage last
|
|
99
|
-
end
|
|
29
|
+
summary = model.summary_for('lib/foo.rb')
|
|
30
|
+
expect(summary['summary']).to include('covered' => 2, 'total' => 3)
|
|
100
31
|
|
|
101
|
-
|
|
102
|
-
|
|
32
|
+
uncovered = model.uncovered_for('lib/foo.rb')
|
|
33
|
+
expect(uncovered['uncovered']).to eq([2])
|
|
103
34
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect(asc_files.first['file']).to end_with('lib/bar.rb') # Lower coverage first
|
|
107
|
-
expect(asc_files.last['file']).to end_with('lib/foo.rb') # Higher coverage last
|
|
35
|
+
detailed = model.detailed_for('lib/foo.rb')
|
|
36
|
+
expect(detailed['lines']).to include({ 'line' => 2, 'hits' => 0, 'covered' => false })
|
|
108
37
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
38
|
+
table = model.format_table
|
|
39
|
+
expect(table).to include('lib/foo.rb', 'lib/bar.rb', '66.67', '33.33')
|
|
40
|
+
data_lines = table.split("\n").select { |line| line.include?('lib/') }
|
|
41
|
+
expect(data_lines.first).to include('lib/foo.rb') # Highest coverage first (descending default)
|
|
42
|
+
expect(data_lines.last).to include('lib/bar.rb')
|
|
114
43
|
end
|
|
115
44
|
end
|
|
116
45
|
|
|
117
46
|
describe 'CLI Integration with Real Coverage Data' do
|
|
118
47
|
it 'executes all major CLI commands without errors' do
|
|
119
48
|
# Test list command
|
|
120
|
-
list_output =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
cli.run(['list', '--root', project_root, '--resultset', coverage_dir])
|
|
124
|
-
list_output = out.string
|
|
125
|
-
end
|
|
126
|
-
|
|
49
|
+
list_output, _err, status = run_cli_with_status('--root', project_root, '--resultset',
|
|
50
|
+
coverage_dir, 'list')
|
|
51
|
+
expect(status).to eq(0)
|
|
127
52
|
expect(list_output).to include('lib/foo.rb', 'lib/bar.rb')
|
|
128
53
|
expect(list_output).to include('66.67', '33.33')
|
|
54
|
+
data_lines = list_output.lines.select { |line| line.include?('lib/') }
|
|
55
|
+
expect(data_lines.first).to include('lib/foo.rb') # Highest coverage first (descending default)
|
|
56
|
+
expect(data_lines.last).to include('lib/bar.rb')
|
|
129
57
|
|
|
130
58
|
# Test summary command
|
|
131
|
-
summary_output =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
expect(summary_output).to include('66.67%', '2/3')
|
|
59
|
+
summary_output, _err, status = run_cli_with_status('--root', project_root, '--resultset',
|
|
60
|
+
coverage_dir, 'summary', 'lib/foo.rb')
|
|
61
|
+
expect(status).to eq(0)
|
|
62
|
+
expect(summary_output).to include('│') # Table format
|
|
63
|
+
expect(summary_output).to include('66.67%')
|
|
64
|
+
expect(summary_output).to include('2')
|
|
65
|
+
expect(summary_output).to include('3')
|
|
139
66
|
|
|
140
67
|
# Test JSON output
|
|
141
|
-
json_output =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
])
|
|
147
|
-
json_output = out.string
|
|
148
|
-
end
|
|
149
|
-
|
|
68
|
+
json_output, _err, status = run_cli_with_status(
|
|
69
|
+
'--format', 'json', '--root', project_root, '--resultset', coverage_dir,
|
|
70
|
+
'summary', 'lib/foo.rb'
|
|
71
|
+
)
|
|
72
|
+
expect(status).to eq(0)
|
|
150
73
|
json_data = JSON.parse(json_output)
|
|
151
74
|
expect(json_data).to include('file', 'summary')
|
|
152
75
|
expect(json_data['summary']).to include('covered' => 2, 'total' => 3)
|
|
@@ -154,23 +77,17 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
154
77
|
|
|
155
78
|
it 'handles different output formats correctly' do
|
|
156
79
|
# Test uncovered command with different outputs
|
|
157
|
-
uncovered_output =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
expect(uncovered_output).to match(/Uncovered lines:\s*2\b/)
|
|
80
|
+
uncovered_output, _err, status = run_cli_with_status(
|
|
81
|
+
'--root', project_root, '--resultset', coverage_dir, 'uncovered', 'lib/foo.rb'
|
|
82
|
+
)
|
|
83
|
+
expect(status).to eq(0)
|
|
84
|
+
expect(uncovered_output).to include('│') # Table format
|
|
165
85
|
|
|
166
86
|
# Test detailed command
|
|
167
|
-
detailed_output =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
detailed_output = out.string
|
|
172
|
-
end
|
|
173
|
-
|
|
87
|
+
detailed_output, _err, status = run_cli_with_status(
|
|
88
|
+
'--root', project_root, '--resultset', coverage_dir, 'detailed', 'lib/foo.rb'
|
|
89
|
+
)
|
|
90
|
+
expect(status).to eq(0)
|
|
174
91
|
expect(detailed_output).to include('Line', 'Hits', 'Covered')
|
|
175
92
|
end
|
|
176
93
|
end
|
|
@@ -191,7 +108,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
191
108
|
server_context: server_context
|
|
192
109
|
)
|
|
193
110
|
|
|
194
|
-
data,
|
|
111
|
+
data, _item = expect_mcp_text_json(summary_response, expected_keys: ['file', 'summary'])
|
|
195
112
|
expect(data['summary']).to include('covered' => 2, 'total' => 3)
|
|
196
113
|
|
|
197
114
|
# Test raw coverage tool
|
|
@@ -202,7 +119,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
202
119
|
server_context: server_context
|
|
203
120
|
)
|
|
204
121
|
|
|
205
|
-
raw_data,
|
|
122
|
+
raw_data, _raw_item = expect_mcp_text_json(raw_response, expected_keys: ['file', 'lines'])
|
|
206
123
|
expect(raw_data['lines']).to eq([1, 0, nil, 2])
|
|
207
124
|
|
|
208
125
|
# Test all files tool
|
|
@@ -212,7 +129,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
212
129
|
server_context: server_context
|
|
213
130
|
)
|
|
214
131
|
|
|
215
|
-
all_data,
|
|
132
|
+
all_data, = expect_mcp_text_json(all_files_response, expected_keys: ['files', 'counts'])
|
|
216
133
|
expect(all_data['files'].length).to eq(2)
|
|
217
134
|
expect(all_data['counts']['total']).to eq(2)
|
|
218
135
|
end
|
|
@@ -225,7 +142,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
225
142
|
resultset: coverage_dir,
|
|
226
143
|
server_context: server_context
|
|
227
144
|
)
|
|
228
|
-
summary_data,
|
|
145
|
+
summary_data, = expect_mcp_text_json(summary_response)
|
|
229
146
|
|
|
230
147
|
# Get data from detailed tool
|
|
231
148
|
detailed_response = SimpleCovMcp::Tools::CoverageDetailedTool.call(
|
|
@@ -234,7 +151,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
234
151
|
resultset: coverage_dir,
|
|
235
152
|
server_context: server_context
|
|
236
153
|
)
|
|
237
|
-
detailed_data,
|
|
154
|
+
detailed_data, = expect_mcp_text_json(detailed_response)
|
|
238
155
|
|
|
239
156
|
# Verify consistency between tools
|
|
240
157
|
expect(summary_data['summary']['covered']).to eq(2)
|
|
@@ -264,20 +181,9 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
264
181
|
end
|
|
265
182
|
|
|
266
183
|
it 'provides helpful CLI error messages' do
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
cli = SimpleCovMcp::CoverageCLI.new
|
|
271
|
-
cli.run([
|
|
272
|
-
'summary', 'lib/nonexistent.rb', '--root', project_root, '--resultset', coverage_dir
|
|
273
|
-
])
|
|
274
|
-
status = 0
|
|
275
|
-
rescue SystemExit => e
|
|
276
|
-
status = e.status
|
|
277
|
-
end
|
|
278
|
-
output = out.string
|
|
279
|
-
error = err.string
|
|
280
|
-
end
|
|
184
|
+
_output, error, status = run_cli_with_status(
|
|
185
|
+
'--root', project_root, '--resultset', coverage_dir, 'summary', 'lib/nonexistent.rb'
|
|
186
|
+
)
|
|
281
187
|
|
|
282
188
|
expect(status).to eq(1)
|
|
283
189
|
expect(error).to include('File error:', 'No coverage entry found')
|
|
@@ -321,7 +227,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
321
227
|
let(:default_env) do
|
|
322
228
|
{
|
|
323
229
|
'RUBY_LIB' => lib_path,
|
|
324
|
-
'SIMPLECOV_MCP_OPTS' => "--root #{project_root} --resultset #{coverage_dir}"
|
|
230
|
+
'SIMPLECOV_MCP_OPTS' => "--root #{project_root} --resultset #{coverage_dir} --log-file /dev/null"
|
|
325
231
|
}
|
|
326
232
|
end
|
|
327
233
|
|
|
@@ -360,7 +266,9 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
360
266
|
|
|
361
267
|
def parse_jsonrpc_response(output)
|
|
362
268
|
# MCP server should only write JSON-RPC responses to stdout.
|
|
363
|
-
output.
|
|
269
|
+
# Force UTF-8 encoding to handle any binary data in the output stream.
|
|
270
|
+
safe_output = output.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
|
|
271
|
+
safe_output.lines.each do |line|
|
|
364
272
|
stripped = line.strip
|
|
365
273
|
next if stripped.empty?
|
|
366
274
|
|
|
@@ -562,6 +470,37 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
562
470
|
expect(version_text).to match(/SimpleCovMcp version: \d+\.\d+/)
|
|
563
471
|
end
|
|
564
472
|
|
|
473
|
+
it 'executes validate_tool via JSON-RPC' do
|
|
474
|
+
request = {
|
|
475
|
+
jsonrpc: '2.0',
|
|
476
|
+
id: 80,
|
|
477
|
+
method: 'tools/call',
|
|
478
|
+
params: {
|
|
479
|
+
name: 'validate_tool',
|
|
480
|
+
arguments: {
|
|
481
|
+
code: '->(m) { true }',
|
|
482
|
+
root: project_root,
|
|
483
|
+
resultset: coverage_dir
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
stdout = run_mcp_json(request)[:stdout]
|
|
489
|
+
response = parse_jsonrpc_response(stdout)
|
|
490
|
+
|
|
491
|
+
expect(response['id']).to eq(80)
|
|
492
|
+
content = response['result']['content']
|
|
493
|
+
expect(content.first['type']).to eq('text')
|
|
494
|
+
|
|
495
|
+
begin
|
|
496
|
+
result_json = JSON.parse(content.first['text'])
|
|
497
|
+
rescue JSON::ParserError
|
|
498
|
+
puts "DEBUG: Failed to parse JSON. Content was: #{content.first['text']}"
|
|
499
|
+
raise
|
|
500
|
+
end
|
|
501
|
+
expect(result_json).to include('result' => true)
|
|
502
|
+
end
|
|
503
|
+
|
|
565
504
|
it 'handles error responses for invalid tool calls' do
|
|
566
505
|
request = {
|
|
567
506
|
jsonrpc: '2.0',
|
|
@@ -649,7 +588,10 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
649
588
|
|
|
650
589
|
result = run_mcp_json_stream(requests)
|
|
651
590
|
|
|
652
|
-
|
|
591
|
+
# Force UTF-8 encoding to handle any binary data in the output stream
|
|
592
|
+
safe_stdout = result[:stdout].to_s
|
|
593
|
+
.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
|
|
594
|
+
responses = safe_stdout.lines.map do |line|
|
|
653
595
|
next if line.strip.empty?
|
|
654
596
|
|
|
655
597
|
begin
|
|
@@ -665,7 +607,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
665
607
|
expect(response_ids).to include(100).or include(101)
|
|
666
608
|
end
|
|
667
609
|
|
|
668
|
-
context 'MCP protocol
|
|
610
|
+
context 'when handling MCP protocol errors' do
|
|
669
611
|
it 'returns error for unknown tool name' do
|
|
670
612
|
request = {
|
|
671
613
|
jsonrpc: '2.0',
|
|
@@ -697,7 +639,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
697
639
|
text = content.first['text']
|
|
698
640
|
expect(text.downcase).to include('error').or include('not found')
|
|
699
641
|
else
|
|
700
|
-
|
|
642
|
+
raise 'Expected either error or result field in response'
|
|
701
643
|
end
|
|
702
644
|
end
|
|
703
645
|
|
|
@@ -726,7 +668,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
726
668
|
text = content.first['text']
|
|
727
669
|
expect(text.downcase).to include('error').or include('required').or include('path')
|
|
728
670
|
else
|
|
729
|
-
|
|
671
|
+
raise 'Expected either error or result field in response'
|
|
730
672
|
end
|
|
731
673
|
end
|
|
732
674
|
|
|
@@ -738,7 +680,7 @@ RSpec.describe 'SimpleCov MCP Integration Tests' do
|
|
|
738
680
|
params: {
|
|
739
681
|
name: 'coverage_summary_tool',
|
|
740
682
|
arguments: {
|
|
741
|
-
path:
|
|
683
|
+
path: 12_345, # Should be string, not number
|
|
742
684
|
root: project_root,
|
|
743
685
|
resultset: coverage_dir
|
|
744
686
|
}
|
|
@@ -54,7 +54,7 @@ RSpec.describe 'Logging Fallback Behavior' do
|
|
|
54
54
|
context = SimpleCovMcp.create_context(
|
|
55
55
|
error_handler: SimpleCovMcp::ErrorHandlerFactory.for_mcp_server,
|
|
56
56
|
log_target: '/invalid/path/that/does/not/exist.log',
|
|
57
|
-
mode: :
|
|
57
|
+
mode: :mcp
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
stderr_output = nil
|
|
@@ -115,10 +115,10 @@ RSpec.describe 'Logging Fallback Behavior' do
|
|
|
115
115
|
expect(context.mcp_mode?).to be false
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
it 'correctly identifies MCP
|
|
118
|
+
it 'correctly identifies MCP mode' do
|
|
119
119
|
context = SimpleCovMcp.create_context(
|
|
120
120
|
error_handler: SimpleCovMcp::ErrorHandlerFactory.for_mcp_server,
|
|
121
|
-
mode: :
|
|
121
|
+
mode: :mcp
|
|
122
122
|
)
|
|
123
123
|
expect(context.library_mode?).to be false
|
|
124
124
|
expect(context.cli_mode?).to be false
|
|
@@ -5,7 +5,7 @@ require 'spec_helper'
|
|
|
5
5
|
RSpec.describe 'MCP Server Bootstrap' do
|
|
6
6
|
it 'does not crash on startup in non-TTY environments' do
|
|
7
7
|
# Simulate a non-TTY environment, which should trigger MCP mode
|
|
8
|
-
allow(
|
|
8
|
+
allow($stdin).to receive(:tty?).and_return(false)
|
|
9
9
|
|
|
10
10
|
# The server will try to run, but we only need to ensure it gets past
|
|
11
11
|
# the point where the NameError would have occurred. We can mock the
|
data/spec/mcp_server_spec.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'support/fake_mcp'
|
|
4
5
|
|
|
5
6
|
RSpec.describe SimpleCovMcp::MCPServer do
|
|
6
7
|
# This spec verifies the MCP server boot path without requiring the real
|
|
@@ -10,55 +11,8 @@ RSpec.describe SimpleCovMcp::MCPServer do
|
|
|
10
11
|
# Prepare fakes for MCP server and transport
|
|
11
12
|
module ::MCP; end unless defined?(::MCP)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# `last_instance` accessor is a class-level handle to the most recently
|
|
16
|
-
# instantiated fake. Because the production code constructs the server
|
|
17
|
-
# internally, we can't grab the instance directly; recording the most
|
|
18
|
-
# recent instance lets the test fetch it after `run` completes.
|
|
19
|
-
fake_server_class = Class.new do
|
|
20
|
-
class << self
|
|
21
|
-
# Holds the most recently created fake server instance so tests can
|
|
22
|
-
# inspect it after the code under test performs internal construction.
|
|
23
|
-
attr_accessor :last_instance
|
|
24
|
-
end
|
|
25
|
-
attr_reader :params
|
|
26
|
-
|
|
27
|
-
def initialize(name:, version:, tools:)
|
|
28
|
-
@params = { name: name, version: version, tools: tools }
|
|
29
|
-
self.class.last_instance = self
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Fake stdio transport records whether `open` was called and the server
|
|
34
|
-
# it was initialized with, to confirm that the server was started. It also
|
|
35
|
-
# exposes a `last_instance` class accessor for the same reason as above:
|
|
36
|
-
# to retrieve the instance created during `run` so we can assert on it.
|
|
37
|
-
fake_transport_class = Class.new do
|
|
38
|
-
class << self
|
|
39
|
-
# Holds the most recently created fake transport instance for later
|
|
40
|
-
# assertions (e.g., that `open` was invoked).
|
|
41
|
-
attr_accessor :last_instance
|
|
42
|
-
end
|
|
43
|
-
attr_reader :server, :opened
|
|
44
|
-
|
|
45
|
-
def initialize(server)
|
|
46
|
-
@server = server
|
|
47
|
-
@opened = false
|
|
48
|
-
self.class.last_instance = self
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def open
|
|
52
|
-
@opened = true
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def opened?
|
|
56
|
-
@opened
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
stub_const('MCP::Server', fake_server_class)
|
|
61
|
-
stub_const('MCP::Server::Transports::StdioTransport', fake_transport_class)
|
|
14
|
+
stub_const('MCP::Server', FakeMCP::Server)
|
|
15
|
+
stub_const('MCP::Server::Transports::StdioTransport', FakeMCP::StdioTransport)
|
|
62
16
|
|
|
63
17
|
server_context = SimpleCovMcp.create_context(
|
|
64
18
|
error_handler: SimpleCovMcp::ErrorHandlerFactory.for_mcp_server,
|
|
@@ -73,8 +27,8 @@ RSpec.describe SimpleCovMcp::MCPServer do
|
|
|
73
27
|
expect(SimpleCovMcp.context).to eq(baseline_context)
|
|
74
28
|
|
|
75
29
|
# Fetch the instances created during `run` via the class-level hooks.
|
|
76
|
-
fake_server =
|
|
77
|
-
fake_transport =
|
|
30
|
+
fake_server = FakeMCP::Server.last_instance
|
|
31
|
+
fake_transport = FakeMCP::StdioTransport.last_instance
|
|
78
32
|
|
|
79
33
|
expect(fake_transport).not_to be_nil
|
|
80
34
|
expect(fake_transport).to be_opened
|
|
@@ -83,7 +37,70 @@ RSpec.describe SimpleCovMcp::MCPServer do
|
|
|
83
37
|
expect(fake_server.params[:name]).to eq('simplecov-mcp')
|
|
84
38
|
# Ensure expected tools are registered
|
|
85
39
|
tool_names = fake_server.params[:tools].map { |t| t.name.split('::').last }
|
|
86
|
-
expect(tool_names).to include(
|
|
87
|
-
'
|
|
40
|
+
expect(tool_names).to include(
|
|
41
|
+
'AllFilesCoverageTool',
|
|
42
|
+
'CoverageDetailedTool',
|
|
43
|
+
'CoverageRawTool',
|
|
44
|
+
'CoverageSummaryTool',
|
|
45
|
+
'CoverageTotalsTool',
|
|
46
|
+
'UncoveredLinesTool',
|
|
47
|
+
'CoverageTableTool',
|
|
48
|
+
'HelpTool',
|
|
49
|
+
'VersionTool'
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe 'TOOLSET and TOOL_GUIDE consistency' do
|
|
54
|
+
it 'includes all tools documented in HelpTool TOOL_GUIDE' do
|
|
55
|
+
# Get tool classes from TOOLSET
|
|
56
|
+
toolset_classes = described_class::TOOLSET
|
|
57
|
+
|
|
58
|
+
# Get tool classes from TOOL_GUIDE
|
|
59
|
+
tool_guide_classes = SimpleCovMcp::Tools::HelpTool::TOOL_GUIDE.map { |guide| guide[:tool] }
|
|
60
|
+
|
|
61
|
+
# Every tool in TOOL_GUIDE should be in TOOLSET
|
|
62
|
+
tool_guide_classes.each do |tool_class|
|
|
63
|
+
expect(toolset_classes).to include(tool_class),
|
|
64
|
+
"Expected TOOLSET to include #{tool_class.name}, but it was missing. " \
|
|
65
|
+
'Add it to MCPServer::TOOLSET or remove from HelpTool::TOOL_GUIDE.'
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'has corresponding TOOL_GUIDE entry for all tools (except HelpTool itself)' do
|
|
70
|
+
toolset_classes = described_class::TOOLSET
|
|
71
|
+
tool_guide_classes = SimpleCovMcp::Tools::HelpTool::TOOL_GUIDE.map { |guide| guide[:tool] }
|
|
72
|
+
|
|
73
|
+
# Every tool in TOOLSET should be in TOOL_GUIDE (except HelpTool which documents itself)
|
|
74
|
+
toolset_classes.each do |tool_class|
|
|
75
|
+
# HelpTool doesn't need an entry about itself
|
|
76
|
+
next if tool_class == SimpleCovMcp::Tools::HelpTool
|
|
77
|
+
|
|
78
|
+
expect(tool_guide_classes).to include(tool_class),
|
|
79
|
+
"Expected TOOL_GUIDE to document #{tool_class.name}, but it was missing. " \
|
|
80
|
+
'Add documentation for this tool to HelpTool::TOOL_GUIDE.'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'registers the expected number of tools' do
|
|
85
|
+
expect(described_class::TOOLSET.length).to eq(10)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'registers all tool classes defined in SimpleCovMcp::Tools module' do
|
|
89
|
+
# This test catches the bug where a tool file is created, required in
|
|
90
|
+
# simplecov_mcp.rb, but not added to MCPServer::TOOLSET.
|
|
91
|
+
#
|
|
92
|
+
# Get all classes in the Tools module that inherit from BaseTool
|
|
93
|
+
tool_classes = SimpleCovMcp::Tools.constants
|
|
94
|
+
.map { |const_name| SimpleCovMcp::Tools.const_get(const_name) }
|
|
95
|
+
.select { |const| const.is_a?(Class) && const < SimpleCovMcp::BaseTool }
|
|
96
|
+
|
|
97
|
+
toolset_classes = described_class::TOOLSET
|
|
98
|
+
|
|
99
|
+
tool_classes.each do |tool_class|
|
|
100
|
+
expect(toolset_classes).to include(tool_class),
|
|
101
|
+
"Expected TOOLSET to include #{tool_class.name}, but it was missing. " \
|
|
102
|
+
'The tool class exists in SimpleCovMcp::Tools but is not registered in MCPServer::TOOLSET.'
|
|
103
|
+
end
|
|
104
|
+
end
|
|
88
105
|
end
|
|
89
106
|
end
|