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,845 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'SimpleCov MCP Integration Tests' do
|
6
|
+
let(:project_root) { (FIXTURES_DIR / 'project1').to_s }
|
7
|
+
let(:coverage_dir) { File.join(project_root, 'coverage') }
|
8
|
+
let(:resultset_path) { File.join(coverage_dir, '.resultset.json') }
|
9
|
+
|
10
|
+
describe 'End-to-End Coverage Model Functionality' do
|
11
|
+
context 'with real coverage data and files' do
|
12
|
+
it 'provides complete coverage analysis workflow' do
|
13
|
+
# Initialize model with real fixture data
|
14
|
+
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
15
|
+
|
16
|
+
# Test all_files returns real coverage data
|
17
|
+
all_files = model.all_files
|
18
|
+
expect(all_files).to be_an(Array)
|
19
|
+
expect(all_files.length).to eq(2)
|
20
|
+
|
21
|
+
# Verify file paths and coverage data structure
|
22
|
+
foo_file = all_files.find { |f| f['file'].include?('foo.rb') }
|
23
|
+
bar_file = all_files.find { |f| f['file'].include?('bar.rb') }
|
24
|
+
|
25
|
+
expect(foo_file).to include('covered', 'total', 'percentage', 'stale')
|
26
|
+
expect(bar_file).to include('covered', 'total', 'percentage', 'stale')
|
27
|
+
|
28
|
+
# Verify actual coverage calculations match fixture data
|
29
|
+
# foo.rb has coverage: [1, 0, nil, 2] -> 2 covered out of 3 executable = 66.67%
|
30
|
+
expect(foo_file['total']).to eq(3)
|
31
|
+
expect(foo_file['covered']).to eq(2)
|
32
|
+
expect(foo_file['percentage']).to be_within(0.01).of(66.67)
|
33
|
+
|
34
|
+
# bar.rb has coverage: [0, 0, 1] -> 1 covered out of 3 executable = 33.33%
|
35
|
+
expect(bar_file['total']).to eq(3)
|
36
|
+
expect(bar_file['covered']).to eq(1)
|
37
|
+
expect(bar_file['percentage']).to be_within(0.01).of(33.33)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'provides detailed per-file analysis' do
|
41
|
+
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
42
|
+
|
43
|
+
# Test raw coverage data
|
44
|
+
raw = model.raw_for('lib/foo.rb')
|
45
|
+
expect(raw['file']).to end_with('lib/foo.rb')
|
46
|
+
expect(raw['lines']).to eq([1, 0, nil, 2])
|
47
|
+
|
48
|
+
# Test summary calculation
|
49
|
+
summary = model.summary_for('lib/foo.rb')
|
50
|
+
expect(summary['file']).to end_with('lib/foo.rb')
|
51
|
+
expect(summary['summary']).to include('covered' => 2, 'total' => 3)
|
52
|
+
expect(summary['summary']['pct']).to be_within(0.01).of(66.67)
|
53
|
+
|
54
|
+
# Test uncovered lines detection
|
55
|
+
uncovered = model.uncovered_for('lib/foo.rb')
|
56
|
+
expect(uncovered['file']).to end_with('lib/foo.rb')
|
57
|
+
expect(uncovered['uncovered']).to eq([2]) # Line 2 has 0 hits
|
58
|
+
expect(uncovered['summary']).to include('covered' => 2, 'total' => 3)
|
59
|
+
|
60
|
+
# Test detailed line-by-line analysis
|
61
|
+
detailed = model.detailed_for('lib/foo.rb')
|
62
|
+
expect(detailed['file']).to end_with('lib/foo.rb')
|
63
|
+
expect(detailed['lines']).to eq([
|
64
|
+
{ 'line' => 1, 'hits' => 1, 'covered' => true },
|
65
|
+
{ 'line' => 2, 'hits' => 0, 'covered' => false },
|
66
|
+
{ 'line' => 4, 'hits' => 2, 'covered' => true }
|
67
|
+
])
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'generates properly formatted coverage tables' do
|
71
|
+
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
72
|
+
|
73
|
+
# Test default table generation
|
74
|
+
table = model.format_table
|
75
|
+
|
76
|
+
# Verify table structure (Unicode box drawing)
|
77
|
+
expect(table).to include('┌', '┬', '┐', '│', '├', '┼', '┤', '└', '┴', '┘')
|
78
|
+
|
79
|
+
# Verify headers
|
80
|
+
expect(table).to include('File', '%', 'Covered', 'Total', 'Stale')
|
81
|
+
|
82
|
+
# Verify file data appears
|
83
|
+
expect(table).to include('lib/foo.rb', 'lib/bar.rb')
|
84
|
+
|
85
|
+
# Verify percentages are formatted correctly
|
86
|
+
expect(table).to include('66.67', '33.33')
|
87
|
+
|
88
|
+
# Verify counts summary
|
89
|
+
expect(table).to include('Files: total 2')
|
90
|
+
|
91
|
+
# Test sorting (ascending by default - bar.rb should be first with lower coverage)
|
92
|
+
lines = table.split("\n")
|
93
|
+
data_lines = lines.select { |line| line.include?('lib/') }
|
94
|
+
expect(data_lines.first).to include('lib/bar.rb') # Lower coverage first
|
95
|
+
expect(data_lines.last).to include('lib/foo.rb') # Higher coverage last
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'supports different sorting options' do
|
99
|
+
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
100
|
+
|
101
|
+
# Test ascending sort
|
102
|
+
asc_files = model.all_files(sort_order: :ascending)
|
103
|
+
expect(asc_files.first['file']).to end_with('lib/bar.rb') # Lower coverage first
|
104
|
+
expect(asc_files.last['file']).to end_with('lib/foo.rb') # Higher coverage last
|
105
|
+
|
106
|
+
# Test descending sort
|
107
|
+
desc_files = model.all_files(sort_order: :descending)
|
108
|
+
expect(desc_files.first['file']).to end_with('lib/foo.rb') # Higher coverage first
|
109
|
+
expect(desc_files.last['file']).to end_with('lib/bar.rb') # Lower coverage last
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'CLI Integration with Real Coverage Data' do
|
115
|
+
it 'executes all major CLI commands without errors' do
|
116
|
+
# Test list command
|
117
|
+
list_output = nil
|
118
|
+
silence_output do |out, _err|
|
119
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
120
|
+
cli.run(['list', '--root', project_root, '--resultset', coverage_dir])
|
121
|
+
list_output = out.string
|
122
|
+
end
|
123
|
+
|
124
|
+
expect(list_output).to include('lib/foo.rb', 'lib/bar.rb')
|
125
|
+
expect(list_output).to include('66.67', '33.33')
|
126
|
+
|
127
|
+
# Test summary command
|
128
|
+
summary_output = nil
|
129
|
+
silence_output do |out, _err|
|
130
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
131
|
+
cli.run(['summary', 'lib/foo.rb', '--root', project_root, '--resultset', coverage_dir])
|
132
|
+
summary_output = out.string
|
133
|
+
end
|
134
|
+
|
135
|
+
expect(summary_output).to include('66.67%', '2/3')
|
136
|
+
|
137
|
+
# Test JSON output
|
138
|
+
json_output = nil
|
139
|
+
silence_output do |out, _err|
|
140
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
141
|
+
cli.run([
|
142
|
+
'summary', 'lib/foo.rb', '--json', '--root', project_root, '--resultset', coverage_dir
|
143
|
+
])
|
144
|
+
json_output = out.string
|
145
|
+
end
|
146
|
+
|
147
|
+
json_data = JSON.parse(json_output)
|
148
|
+
expect(json_data).to include('file', 'summary')
|
149
|
+
expect(json_data['summary']).to include('covered' => 2, 'total' => 3)
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'handles different output formats correctly' do
|
153
|
+
# Test uncovered command with different outputs
|
154
|
+
uncovered_output = nil
|
155
|
+
silence_output do |out, _err|
|
156
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
157
|
+
cli.run(['uncovered', 'lib/foo.rb', '--root', project_root, '--resultset', coverage_dir])
|
158
|
+
uncovered_output = out.string
|
159
|
+
end
|
160
|
+
|
161
|
+
expect(uncovered_output).to match(/Uncovered lines:\s*2\b/)
|
162
|
+
|
163
|
+
# Test detailed command
|
164
|
+
detailed_output = nil
|
165
|
+
silence_output do |out, _err|
|
166
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
167
|
+
cli.run(['detailed', 'lib/foo.rb', '--root', project_root, '--resultset', coverage_dir])
|
168
|
+
detailed_output = out.string
|
169
|
+
end
|
170
|
+
|
171
|
+
expect(detailed_output).to include('Line', 'Hits', 'Covered')
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe 'MCP Tool Integration with Real Data' do
|
176
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
177
|
+
|
178
|
+
before do
|
179
|
+
setup_mcp_response_stub
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'executes all MCP tools with real coverage data' do
|
183
|
+
# Test coverage summary tool
|
184
|
+
summary_response = SimpleCovMcp::Tools::CoverageSummaryTool.call(
|
185
|
+
path: 'lib/foo.rb',
|
186
|
+
root: project_root,
|
187
|
+
resultset: coverage_dir,
|
188
|
+
server_context: server_context
|
189
|
+
)
|
190
|
+
|
191
|
+
data, item = expect_mcp_text_json(summary_response, expected_keys: ['file', 'summary'])
|
192
|
+
expect(data['summary']).to include('covered' => 2, 'total' => 3)
|
193
|
+
|
194
|
+
# Test raw coverage tool
|
195
|
+
raw_response = SimpleCovMcp::Tools::CoverageRawTool.call(
|
196
|
+
path: 'lib/foo.rb',
|
197
|
+
root: project_root,
|
198
|
+
resultset: coverage_dir,
|
199
|
+
server_context: server_context
|
200
|
+
)
|
201
|
+
|
202
|
+
raw_data, raw_item = expect_mcp_text_json(raw_response, expected_keys: ['file', 'lines'])
|
203
|
+
expect(raw_data['lines']).to eq([1, 0, nil, 2])
|
204
|
+
|
205
|
+
# Test all files tool
|
206
|
+
all_files_response = SimpleCovMcp::Tools::AllFilesCoverageTool.call(
|
207
|
+
root: project_root,
|
208
|
+
resultset: coverage_dir,
|
209
|
+
server_context: server_context
|
210
|
+
)
|
211
|
+
|
212
|
+
all_data, _ = expect_mcp_text_json(all_files_response, expected_keys: ['files', 'counts'])
|
213
|
+
expect(all_data['files'].length).to eq(2)
|
214
|
+
expect(all_data['counts']['total']).to eq(2)
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'provides consistent data across different tools' do
|
218
|
+
# Get data from summary tool
|
219
|
+
summary_response = SimpleCovMcp::Tools::CoverageSummaryTool.call(
|
220
|
+
path: 'lib/foo.rb',
|
221
|
+
root: project_root,
|
222
|
+
resultset: coverage_dir,
|
223
|
+
server_context: server_context
|
224
|
+
)
|
225
|
+
summary_data, _ = expect_mcp_text_json(summary_response)
|
226
|
+
|
227
|
+
# Get data from detailed tool
|
228
|
+
detailed_response = SimpleCovMcp::Tools::CoverageDetailedTool.call(
|
229
|
+
path: 'lib/foo.rb',
|
230
|
+
root: project_root,
|
231
|
+
resultset: coverage_dir,
|
232
|
+
server_context: server_context
|
233
|
+
)
|
234
|
+
detailed_data, _ = expect_mcp_text_json(detailed_response)
|
235
|
+
|
236
|
+
# Verify consistency between tools
|
237
|
+
expect(summary_data['summary']['covered']).to eq(2)
|
238
|
+
expect(summary_data['summary']['total']).to eq(3)
|
239
|
+
expect(detailed_data['summary']['covered']).to eq(2)
|
240
|
+
expect(detailed_data['summary']['total']).to eq(3)
|
241
|
+
|
242
|
+
# Count covered lines in detailed data
|
243
|
+
covered_lines = detailed_data['lines'].count { |line| line['covered'] }
|
244
|
+
expect(covered_lines).to eq(2)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
describe 'Error Handling Integration' do
|
249
|
+
it 'handles missing files gracefully' do
|
250
|
+
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
251
|
+
|
252
|
+
expect do
|
253
|
+
model.summary_for('lib/nonexistent.rb')
|
254
|
+
end.to raise_error(SimpleCovMcp::FileError, /No coverage entry found/)
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'handles invalid resultset paths gracefully' do
|
258
|
+
expect do
|
259
|
+
SimpleCovMcp::CoverageModel.new(root: project_root, resultset: '/nonexistent/path')
|
260
|
+
end.to raise_error(SimpleCovMcp::CoverageDataError, /Failed to load coverage data/)
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'provides helpful CLI error messages' do
|
264
|
+
output, error, status = nil, nil, nil
|
265
|
+
silence_output do |out, err|
|
266
|
+
begin
|
267
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
268
|
+
cli.run([
|
269
|
+
'summary', 'lib/nonexistent.rb', '--root', project_root, '--resultset', coverage_dir
|
270
|
+
])
|
271
|
+
status = 0
|
272
|
+
rescue SystemExit => e
|
273
|
+
status = e.status
|
274
|
+
end
|
275
|
+
output = out.string
|
276
|
+
error = err.string
|
277
|
+
end
|
278
|
+
|
279
|
+
expect(status).to eq(1)
|
280
|
+
expect(error).to include('File error:', 'No coverage entry found')
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
describe 'Multi-File Scenarios' do
|
285
|
+
it 'handles projects with mixed coverage levels' do
|
286
|
+
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
287
|
+
|
288
|
+
# Get all files and verify range of coverage
|
289
|
+
files = model.all_files
|
290
|
+
coverages = files.map { |f| f['percentage'] }
|
291
|
+
|
292
|
+
expect(coverages.min).to be < 50 # bar.rb at ~33%
|
293
|
+
expect(coverages.max).to be > 50 # foo.rb at ~67%
|
294
|
+
expect(coverages).to include(a_value_within(0.1).of(33.33))
|
295
|
+
expect(coverages).to include(a_value_within(0.1).of(66.67))
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'generates comprehensive project reports' do
|
299
|
+
model = SimpleCovMcp::CoverageModel.new(root: project_root, resultset: coverage_dir)
|
300
|
+
|
301
|
+
table = model.format_table
|
302
|
+
|
303
|
+
# Should show both files with different coverage levels
|
304
|
+
expect(table).to match(/lib\/bar\.rb.*33\.33/)
|
305
|
+
expect(table).to match(/lib\/foo\.rb.*66\.67/)
|
306
|
+
|
307
|
+
# Should show project totals
|
308
|
+
expect(table).to include('Files: total 2')
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
describe 'MCP Server Protocol Integration', :slow do
|
313
|
+
# spec/ is one level deep, so ../.. goes up to repo root
|
314
|
+
let(:repo_root) { File.expand_path('..', __dir__) }
|
315
|
+
let(:exe_path) { File.join(repo_root, 'exe', 'simplecov-mcp') }
|
316
|
+
let(:lib_path) { File.join(repo_root, 'lib') }
|
317
|
+
|
318
|
+
let(:default_env) do
|
319
|
+
{
|
320
|
+
'RUBY_LIB' => lib_path,
|
321
|
+
'SIMPLECOV_MCP_OPTS' => "--root #{project_root} --resultset #{coverage_dir}"
|
322
|
+
}
|
323
|
+
end
|
324
|
+
|
325
|
+
def runner_args(env: default_env, timeout: 5)
|
326
|
+
{
|
327
|
+
env: env,
|
328
|
+
lib_path: lib_path,
|
329
|
+
exe_path: exe_path,
|
330
|
+
timeout: timeout
|
331
|
+
}
|
332
|
+
end
|
333
|
+
|
334
|
+
# Run the MCP executable with a single JSON-RPC request hash and return the captured streams.
|
335
|
+
def run_mcp_json(request_hash, env: default_env, timeout: 5)
|
336
|
+
Spec::Support::McpRunner.call_json(
|
337
|
+
request_hash,
|
338
|
+
**runner_args(env: env, timeout: timeout)
|
339
|
+
)
|
340
|
+
end
|
341
|
+
|
342
|
+
# Run the MCP executable with a sequence of JSON-RPC requests (one per line).
|
343
|
+
def run_mcp_json_stream(request_hashes, env: default_env, timeout: 5)
|
344
|
+
Spec::Support::McpRunner.call_json_stream(
|
345
|
+
request_hashes,
|
346
|
+
**runner_args(env: env, timeout: timeout)
|
347
|
+
)
|
348
|
+
end
|
349
|
+
|
350
|
+
# Run the MCP executable with a raw string payload (already encoded as needed).
|
351
|
+
def run_mcp_input(input, env: default_env, timeout: 5)
|
352
|
+
Spec::Support::McpRunner.call(
|
353
|
+
input: input,
|
354
|
+
**runner_args(env: env, timeout: timeout)
|
355
|
+
)
|
356
|
+
end
|
357
|
+
|
358
|
+
def parse_jsonrpc_response(output)
|
359
|
+
# MCP server should only write JSON-RPC responses to stdout.
|
360
|
+
output.lines.each do |line|
|
361
|
+
stripped = line.strip
|
362
|
+
next if stripped.empty?
|
363
|
+
|
364
|
+
begin
|
365
|
+
parsed = JSON.parse(stripped)
|
366
|
+
rescue JSON::ParserError => e
|
367
|
+
raise "Unexpected non-JSON output from MCP server stdout: #{stripped.inspect} (#{e.message})"
|
368
|
+
end
|
369
|
+
|
370
|
+
return parsed if parsed['jsonrpc'] == '2.0'
|
371
|
+
|
372
|
+
raise "Unexpected JSON-RPC payload on stdout: #{stripped.inspect}"
|
373
|
+
end
|
374
|
+
|
375
|
+
raise "No JSON-RPC response found on stdout. Raw output: #{output.inspect}"
|
376
|
+
end
|
377
|
+
|
378
|
+
it 'starts MCP server without errors' do
|
379
|
+
request = {
|
380
|
+
jsonrpc: '2.0',
|
381
|
+
id: 1,
|
382
|
+
method: 'tools/list'
|
383
|
+
}
|
384
|
+
|
385
|
+
result = run_mcp_json(request)
|
386
|
+
stdout = result[:stdout]
|
387
|
+
stderr = result[:stderr]
|
388
|
+
|
389
|
+
# Should not crash with NameError about OptionParser
|
390
|
+
expect(stderr).not_to include('NameError')
|
391
|
+
expect(stderr).not_to include('uninitialized constant')
|
392
|
+
expect(stderr).not_to include('OptionParser')
|
393
|
+
|
394
|
+
# Should produce valid JSON-RPC output
|
395
|
+
response = parse_jsonrpc_response(stdout)
|
396
|
+
expect(response).not_to be_nil
|
397
|
+
expect(response['jsonrpc']).to eq('2.0')
|
398
|
+
expect(response['id']).to eq(1)
|
399
|
+
end
|
400
|
+
|
401
|
+
it 'handles tools/list request' do
|
402
|
+
request = {
|
403
|
+
jsonrpc: '2.0',
|
404
|
+
id: 2,
|
405
|
+
method: 'tools/list'
|
406
|
+
}
|
407
|
+
|
408
|
+
stdout = run_mcp_json(request)[:stdout]
|
409
|
+
response = parse_jsonrpc_response(stdout)
|
410
|
+
|
411
|
+
expect(response).to include('result')
|
412
|
+
tools = response['result']['tools']
|
413
|
+
expect(tools).to be_an(Array)
|
414
|
+
|
415
|
+
# Verify expected tools are registered
|
416
|
+
tool_names = tools.map { |t| t['name'] }
|
417
|
+
expect(tool_names).to include(
|
418
|
+
'all_files_coverage_tool',
|
419
|
+
'coverage_summary_tool',
|
420
|
+
'coverage_raw_tool',
|
421
|
+
'uncovered_lines_tool',
|
422
|
+
'coverage_detailed_tool',
|
423
|
+
'coverage_table_tool',
|
424
|
+
'help_tool',
|
425
|
+
'version_tool'
|
426
|
+
)
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'executes coverage_summary_tool via JSON-RPC' do
|
430
|
+
request = {
|
431
|
+
jsonrpc: '2.0',
|
432
|
+
id: 3,
|
433
|
+
method: 'tools/call',
|
434
|
+
params: {
|
435
|
+
name: 'coverage_summary_tool',
|
436
|
+
arguments: {
|
437
|
+
path: 'lib/foo.rb',
|
438
|
+
root: project_root,
|
439
|
+
resultset: coverage_dir
|
440
|
+
}
|
441
|
+
}
|
442
|
+
}
|
443
|
+
|
444
|
+
stdout = run_mcp_json(request)[:stdout]
|
445
|
+
response = parse_jsonrpc_response(stdout)
|
446
|
+
|
447
|
+
expect(response['id']).to eq(3)
|
448
|
+
expect(response).to have_key('result')
|
449
|
+
|
450
|
+
content = response['result']['content']
|
451
|
+
expect(content).to be_an(Array)
|
452
|
+
expect(content.first['type']).to eq('text')
|
453
|
+
|
454
|
+
# Parse the JSON coverage data from the text response
|
455
|
+
coverage_data = JSON.parse(content.first['text'])
|
456
|
+
expect(coverage_data).to include('file', 'summary')
|
457
|
+
expect(coverage_data['summary']).to include('covered' => 2, 'total' => 3)
|
458
|
+
end
|
459
|
+
|
460
|
+
it 'executes all_files_coverage_tool via JSON-RPC' do
|
461
|
+
request = {
|
462
|
+
jsonrpc: '2.0',
|
463
|
+
id: 4,
|
464
|
+
method: 'tools/call',
|
465
|
+
params: {
|
466
|
+
name: 'all_files_coverage_tool',
|
467
|
+
arguments: {
|
468
|
+
root: project_root,
|
469
|
+
resultset: coverage_dir
|
470
|
+
}
|
471
|
+
}
|
472
|
+
}
|
473
|
+
|
474
|
+
stdout = run_mcp_json(request)[:stdout]
|
475
|
+
response = parse_jsonrpc_response(stdout)
|
476
|
+
|
477
|
+
expect(response['id']).to eq(4)
|
478
|
+
content = response['result']['content']
|
479
|
+
coverage_data = JSON.parse(content.first['text'])
|
480
|
+
|
481
|
+
expect(coverage_data).to include('files', 'counts')
|
482
|
+
expect(coverage_data['files']).to be_an(Array)
|
483
|
+
expect(coverage_data['files'].length).to eq(2)
|
484
|
+
expect(coverage_data['counts']['total']).to eq(2)
|
485
|
+
end
|
486
|
+
|
487
|
+
it 'executes uncovered_lines_tool via JSON-RPC' do
|
488
|
+
request = {
|
489
|
+
jsonrpc: '2.0',
|
490
|
+
id: 5,
|
491
|
+
method: 'tools/call',
|
492
|
+
params: {
|
493
|
+
name: 'uncovered_lines_tool',
|
494
|
+
arguments: {
|
495
|
+
path: 'lib/foo.rb',
|
496
|
+
root: project_root,
|
497
|
+
resultset: coverage_dir
|
498
|
+
}
|
499
|
+
}
|
500
|
+
}
|
501
|
+
|
502
|
+
stdout = run_mcp_json(request)[:stdout]
|
503
|
+
response = parse_jsonrpc_response(stdout)
|
504
|
+
|
505
|
+
expect(response['id']).to eq(5)
|
506
|
+
content = response['result']['content']
|
507
|
+
coverage_data = JSON.parse(content.first['text'])
|
508
|
+
|
509
|
+
expect(coverage_data).to include('file', 'uncovered', 'summary')
|
510
|
+
expect(coverage_data['uncovered']).to eq([2]) # Line 2 is uncovered
|
511
|
+
end
|
512
|
+
|
513
|
+
it 'executes help_tool via JSON-RPC' do
|
514
|
+
request = {
|
515
|
+
jsonrpc: '2.0',
|
516
|
+
id: 6,
|
517
|
+
method: 'tools/call',
|
518
|
+
params: {
|
519
|
+
name: 'help_tool',
|
520
|
+
arguments: {}
|
521
|
+
}
|
522
|
+
}
|
523
|
+
|
524
|
+
stdout = run_mcp_json(request)[:stdout]
|
525
|
+
response = parse_jsonrpc_response(stdout)
|
526
|
+
|
527
|
+
expect(response['id']).to eq(6)
|
528
|
+
content = response['result']['content']
|
529
|
+
expect(content.first['type']).to eq('text')
|
530
|
+
|
531
|
+
# Help tool returns JSON with tool list
|
532
|
+
help_data = JSON.parse(content.first['text'])
|
533
|
+
expect(help_data).to have_key('tools')
|
534
|
+
expect(help_data['tools']).to be_an(Array)
|
535
|
+
tool_names = help_data['tools'].map { |t| t['tool'] }
|
536
|
+
expect(tool_names).to include('coverage_summary_tool')
|
537
|
+
end
|
538
|
+
|
539
|
+
it 'executes version_tool via JSON-RPC' do
|
540
|
+
request = {
|
541
|
+
jsonrpc: '2.0',
|
542
|
+
id: 7,
|
543
|
+
method: 'tools/call',
|
544
|
+
params: {
|
545
|
+
name: 'version_tool',
|
546
|
+
arguments: {}
|
547
|
+
}
|
548
|
+
}
|
549
|
+
|
550
|
+
stdout = run_mcp_json(request)[:stdout]
|
551
|
+
response = parse_jsonrpc_response(stdout)
|
552
|
+
|
553
|
+
expect(response['id']).to eq(7)
|
554
|
+
content = response['result']['content']
|
555
|
+
expect(content.first['type']).to eq('text')
|
556
|
+
|
557
|
+
version_text = content.first['text']
|
558
|
+
# Version format is "SimpleCovMcp version: X.Y.Z"
|
559
|
+
expect(version_text).to match(/SimpleCovMcp version: \d+\.\d+/)
|
560
|
+
end
|
561
|
+
|
562
|
+
it 'handles error responses for invalid tool calls' do
|
563
|
+
request = {
|
564
|
+
jsonrpc: '2.0',
|
565
|
+
id: 8,
|
566
|
+
method: 'tools/call',
|
567
|
+
params: {
|
568
|
+
name: 'coverage_summary_tool',
|
569
|
+
arguments: {
|
570
|
+
path: 'nonexistent_file.rb',
|
571
|
+
root: project_root,
|
572
|
+
resultset: coverage_dir
|
573
|
+
}
|
574
|
+
}
|
575
|
+
}
|
576
|
+
|
577
|
+
result = run_mcp_json(request)
|
578
|
+
response = parse_jsonrpc_response(result[:stdout])
|
579
|
+
|
580
|
+
# MCP should return a response (not crash)
|
581
|
+
expect(response['id']).to eq(8)
|
582
|
+
|
583
|
+
# Should include error information in content or error field
|
584
|
+
if response['error']
|
585
|
+
expect(response['error']).to have_key('message')
|
586
|
+
elsif response['result']
|
587
|
+
content = response['result']['content']
|
588
|
+
text = content.first['text']
|
589
|
+
expect(text.downcase).to include('error').or include('not found')
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
it 'handles malformed JSON-RPC requests' do
|
594
|
+
malformed_request = "{'jsonrpc': '2.0', 'id': 999, 'method': 'invalid'}"
|
595
|
+
|
596
|
+
env = { 'RUBY_LIB' => lib_path }
|
597
|
+
result = run_mcp_input(malformed_request, env: env, timeout: 3)
|
598
|
+
|
599
|
+
# Should handle gracefully without crashing
|
600
|
+
# May return error response or empty output
|
601
|
+
expect(result[:stderr]).not_to include('NameError')
|
602
|
+
expect(result[:stderr]).not_to include('uninitialized constant')
|
603
|
+
end
|
604
|
+
|
605
|
+
it 'respects --log-file configuration in MCP mode' do
|
606
|
+
request = {
|
607
|
+
jsonrpc: '2.0',
|
608
|
+
id: 10,
|
609
|
+
method: 'tools/call',
|
610
|
+
params: {
|
611
|
+
name: 'version_tool',
|
612
|
+
arguments: {}
|
613
|
+
}
|
614
|
+
}
|
615
|
+
|
616
|
+
result = run_mcp_json(
|
617
|
+
request,
|
618
|
+
env: default_env.merge('SIMPLECOV_MCP_OPTS' => '--log-file stderr'),
|
619
|
+
timeout: 3
|
620
|
+
)
|
621
|
+
|
622
|
+
response = parse_jsonrpc_response(result[:stdout])
|
623
|
+
expect(response).not_to be_nil
|
624
|
+
expect(response['id']).to eq(10)
|
625
|
+
end
|
626
|
+
|
627
|
+
it 'prohibits stdout logging in MCP mode' do
|
628
|
+
# Attempt to start MCP server with --log-file stdout should fail
|
629
|
+
env = {
|
630
|
+
'RUBY_LIB' => lib_path,
|
631
|
+
'SIMPLECOV_MCP_OPTS' => '--log-file stdout'
|
632
|
+
}
|
633
|
+
|
634
|
+
result = run_mcp_input(nil, env: env, timeout: 3)
|
635
|
+
|
636
|
+
combined_output = result[:stdout] + result[:stderr]
|
637
|
+
expect(combined_output).to include('stdout').and include('not permitted')
|
638
|
+
expect(result[:status].exitstatus).not_to eq(0)
|
639
|
+
end
|
640
|
+
|
641
|
+
it 'handles multiple sequential requests' do
|
642
|
+
requests = [
|
643
|
+
{ jsonrpc: '2.0', id: 100, method: 'tools/list' },
|
644
|
+
{ jsonrpc: '2.0', id: 101, method: 'tools/call',
|
645
|
+
params: { name: 'version_tool', arguments: {} } }
|
646
|
+
]
|
647
|
+
|
648
|
+
result = run_mcp_json_stream(requests, timeout: 5)
|
649
|
+
|
650
|
+
responses = result[:stdout].lines.map do |line|
|
651
|
+
next if line.strip.empty?
|
652
|
+
|
653
|
+
begin
|
654
|
+
parsed = JSON.parse(line)
|
655
|
+
parsed if parsed['jsonrpc'] == '2.0'
|
656
|
+
rescue JSON::ParserError
|
657
|
+
nil
|
658
|
+
end
|
659
|
+
end.compact
|
660
|
+
|
661
|
+
expect(responses.length).to be >= 1
|
662
|
+
response_ids = responses.map { |r| r['id'] }
|
663
|
+
expect(response_ids).to include(100).or include(101)
|
664
|
+
end
|
665
|
+
|
666
|
+
context 'MCP protocol error handling' do
|
667
|
+
it 'returns error for unknown tool name' do
|
668
|
+
request = {
|
669
|
+
jsonrpc: '2.0',
|
670
|
+
id: 200,
|
671
|
+
method: 'tools/call',
|
672
|
+
params: {
|
673
|
+
name: 'nonexistent_tool_that_does_not_exist',
|
674
|
+
arguments: {}
|
675
|
+
}
|
676
|
+
}
|
677
|
+
|
678
|
+
result = run_mcp_json(request)
|
679
|
+
response = parse_jsonrpc_response(result[:stdout])
|
680
|
+
|
681
|
+
expect(response['id']).to eq(200)
|
682
|
+
expect(response['jsonrpc']).to eq('2.0')
|
683
|
+
|
684
|
+
# MCP server should return error in result content or error field
|
685
|
+
if response['error']
|
686
|
+
# Standard JSON-RPC error format
|
687
|
+
expect(response['error']).to have_key('message')
|
688
|
+
# MCP returns "Internal error" for unknown tools
|
689
|
+
expect(response['error']['message']).to be_a(String)
|
690
|
+
expect(response['error']['message'].length).to be > 0
|
691
|
+
elsif response['result']
|
692
|
+
# MCP may wrap errors in content
|
693
|
+
content = response['result']['content']
|
694
|
+
expect(content).to be_an(Array)
|
695
|
+
text = content.first['text']
|
696
|
+
expect(text.downcase).to include('error').or include('not found')
|
697
|
+
else
|
698
|
+
fail 'Expected either error or result field in response'
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
it 'returns error for missing required arguments' do
|
703
|
+
request = {
|
704
|
+
jsonrpc: '2.0',
|
705
|
+
id: 201,
|
706
|
+
method: 'tools/call',
|
707
|
+
params: {
|
708
|
+
name: 'coverage_summary_tool',
|
709
|
+
arguments: {} # Missing required 'path' argument
|
710
|
+
}
|
711
|
+
}
|
712
|
+
|
713
|
+
result = run_mcp_json(request)
|
714
|
+
response = parse_jsonrpc_response(result[:stdout])
|
715
|
+
|
716
|
+
expect(response['id']).to eq(201)
|
717
|
+
expect(response['jsonrpc']).to eq('2.0')
|
718
|
+
|
719
|
+
# Should return an error about missing path
|
720
|
+
if response['error']
|
721
|
+
expect(response['error']).to have_key('message')
|
722
|
+
elsif response['result']
|
723
|
+
content = response['result']['content']
|
724
|
+
text = content.first['text']
|
725
|
+
expect(text.downcase).to include('error').or include('required').or include('path')
|
726
|
+
else
|
727
|
+
fail 'Expected either error or result field in response'
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
it 'handles invalid argument types gracefully' do
|
732
|
+
request = {
|
733
|
+
jsonrpc: '2.0',
|
734
|
+
id: 202,
|
735
|
+
method: 'tools/call',
|
736
|
+
params: {
|
737
|
+
name: 'coverage_summary_tool',
|
738
|
+
arguments: {
|
739
|
+
path: 12345, # Should be string, not number
|
740
|
+
root: project_root,
|
741
|
+
resultset: coverage_dir
|
742
|
+
}
|
743
|
+
}
|
744
|
+
}
|
745
|
+
|
746
|
+
result = run_mcp_json(request)
|
747
|
+
response = parse_jsonrpc_response(result[:stdout])
|
748
|
+
|
749
|
+
expect(response['id']).to eq(202)
|
750
|
+
expect(response['jsonrpc']).to eq('2.0')
|
751
|
+
|
752
|
+
# Should handle gracefully (may coerce to string or return error)
|
753
|
+
expect(response).to have_key('result').or have_key('error')
|
754
|
+
end
|
755
|
+
|
756
|
+
it 'returns properly formatted JSON-RPC error responses' do
|
757
|
+
request = {
|
758
|
+
jsonrpc: '2.0',
|
759
|
+
id: 203,
|
760
|
+
method: 'tools/call',
|
761
|
+
params: {
|
762
|
+
name: 'coverage_summary_tool',
|
763
|
+
arguments: {
|
764
|
+
path: 'definitely_does_not_exist.rb',
|
765
|
+
root: project_root,
|
766
|
+
resultset: coverage_dir
|
767
|
+
}
|
768
|
+
}
|
769
|
+
}
|
770
|
+
|
771
|
+
result = run_mcp_json(request)
|
772
|
+
response = parse_jsonrpc_response(result[:stdout])
|
773
|
+
|
774
|
+
# Verify JSON-RPC 2.0 compliance
|
775
|
+
expect(response).to include('jsonrpc' => '2.0', 'id' => 203)
|
776
|
+
|
777
|
+
# Must have either 'result' or 'error', but not both
|
778
|
+
has_result = response.key?('result')
|
779
|
+
has_error = response.key?('error')
|
780
|
+
expect(has_result ^ has_error).to be true
|
781
|
+
|
782
|
+
# If error field exists, verify structure
|
783
|
+
if has_error
|
784
|
+
expect(response['error']).to have_key('message')
|
785
|
+
expect(response['error']['message']).to be_a(String)
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
it 'handles requests with missing params field' do
|
790
|
+
request = {
|
791
|
+
jsonrpc: '2.0',
|
792
|
+
id: 204,
|
793
|
+
method: 'tools/call'
|
794
|
+
# Missing params field entirely
|
795
|
+
}
|
796
|
+
|
797
|
+
result = run_mcp_json(request)
|
798
|
+
|
799
|
+
# Should not crash - either returns error or handles gracefully
|
800
|
+
expect(result[:stderr]).not_to include('NameError')
|
801
|
+
expect(result[:stderr]).not_to include('NoMethodError')
|
802
|
+
|
803
|
+
# Parse response if available
|
804
|
+
if result[:stdout] && !result[:stdout].strip.empty?
|
805
|
+
response = parse_jsonrpc_response(result[:stdout])
|
806
|
+
expect(response['jsonrpc']).to eq('2.0')
|
807
|
+
expect(response['id']).to eq(204)
|
808
|
+
end
|
809
|
+
end
|
810
|
+
|
811
|
+
it 'handles completely invalid JSON input' do
|
812
|
+
invalid_json = 'this is not JSON at all'
|
813
|
+
|
814
|
+
result = run_mcp_input(invalid_json, env: default_env, timeout: 3)
|
815
|
+
|
816
|
+
# Should not crash with unhandled exception
|
817
|
+
combined = result[:stdout] + result[:stderr]
|
818
|
+
expect(combined).not_to include('uninitialized constant')
|
819
|
+
|
820
|
+
# May log error to stderr, but shouldn't crash
|
821
|
+
if result[:status]
|
822
|
+
# Exit code may be non-zero but shouldn't be a crash (e.g., signal)
|
823
|
+
expect(result[:status].exitstatus).to be_a(Integer)
|
824
|
+
end
|
825
|
+
end
|
826
|
+
|
827
|
+
it 'handles empty input gracefully' do
|
828
|
+
result = run_mcp_input('', env: default_env, timeout: 2)
|
829
|
+
|
830
|
+
# Empty input should be handled without crash
|
831
|
+
expect(result[:stderr]).not_to include('NameError')
|
832
|
+
expect(result[:stderr]).not_to include('NoMethodError')
|
833
|
+
end
|
834
|
+
|
835
|
+
it 'handles partial JSON input' do
|
836
|
+
partial_json = '{"jsonrpc": "2.0", "id": 300, "method":'
|
837
|
+
|
838
|
+
result = run_mcp_input(partial_json, env: default_env, timeout: 2)
|
839
|
+
|
840
|
+
# Should handle gracefully without crashing
|
841
|
+
expect(result[:stderr]).not_to include('uninitialized constant')
|
842
|
+
end
|
843
|
+
end
|
844
|
+
end
|
845
|
+
end
|