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,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'simplecov_mcp/tools/help_tool'
|
5
|
+
require 'simplecov_mcp/tools/version_tool'
|
6
|
+
require 'simplecov_mcp/tools/coverage_summary_tool'
|
7
|
+
require 'simplecov_mcp/tools/coverage_raw_tool'
|
8
|
+
require 'simplecov_mcp/tools/uncovered_lines_tool'
|
9
|
+
require 'simplecov_mcp/tools/coverage_detailed_tool'
|
10
|
+
|
11
|
+
RSpec.describe 'MCP Tool error handling' do
|
12
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
13
|
+
|
14
|
+
before do
|
15
|
+
setup_mcp_response_stub
|
16
|
+
end
|
17
|
+
|
18
|
+
# Note: VersionTool error handling is difficult to test because the tool is so simple
|
19
|
+
# and doesn't have any complex logic that could fail. The rescue clause in the tool
|
20
|
+
# exists for consistency with other tools but is unlikely to be triggered in practice.
|
21
|
+
|
22
|
+
describe SimpleCovMcp::Tools::HelpTool do
|
23
|
+
it 'handles errors during query processing' do
|
24
|
+
# Simulate an error during filter_entries
|
25
|
+
allow(described_class).to receive(:filter_entries).and_raise(StandardError, 'Filter error')
|
26
|
+
|
27
|
+
response = described_class.call(query: 'test', error_mode: 'on',
|
28
|
+
server_context: server_context)
|
29
|
+
|
30
|
+
# Should return error response
|
31
|
+
expect(response).to be_a(MCP::Tool::Response)
|
32
|
+
item = response.payload.first
|
33
|
+
expect(item[:type] || item['type']).to eq('text')
|
34
|
+
expect(item[:text] || item['text']).to include('Error')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'returns empty array when tokens are empty after filtering' do
|
38
|
+
# Test the edge case where query contains only non-word characters
|
39
|
+
response = described_class.call(query: '!!!', server_context: server_context)
|
40
|
+
|
41
|
+
data = JSON.parse(response.payload.first['text'])
|
42
|
+
# With empty tokens, should return all entries (no filtering applied)
|
43
|
+
expect(data['tools']).not_to be_empty
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'handles non-string, non-array values in filter' do
|
47
|
+
# Test value_matches? with values that are neither strings nor arrays
|
48
|
+
# This exercises the 'else false' branch
|
49
|
+
allow(described_class).to receive(:format_entry).and_return({
|
50
|
+
'tool' => 'test_tool',
|
51
|
+
'label' => nil, # Neither string nor array
|
52
|
+
'use_when' => 123, # Neither string nor array
|
53
|
+
'avoid_when' => true, # Neither string nor array
|
54
|
+
'inputs' => {}, # Neither string nor array
|
55
|
+
'example' => 'example'
|
56
|
+
})
|
57
|
+
|
58
|
+
response = described_class.call(query: 'test', server_context: server_context)
|
59
|
+
|
60
|
+
# Should not crash, should return response
|
61
|
+
expect(response).to be_a(MCP::Tool::Response)
|
62
|
+
data = JSON.parse(response.payload.first['text'])
|
63
|
+
expect(data).to have_key('tools')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe SimpleCovMcp::Tools::CoverageSummaryTool do
|
68
|
+
it 'handles errors during model creation' do
|
69
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_raise(StandardError, 'Model error')
|
70
|
+
|
71
|
+
response = described_class.call(
|
72
|
+
path: 'lib/foo.rb',
|
73
|
+
error_mode: 'on',
|
74
|
+
server_context: server_context
|
75
|
+
)
|
76
|
+
|
77
|
+
# Should return error response
|
78
|
+
expect(response).to be_a(MCP::Tool::Response)
|
79
|
+
item = response.payload.first
|
80
|
+
expect(item[:type] || item['type']).to eq('text')
|
81
|
+
expect(item[:text] || item['text']).to include('Error')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe SimpleCovMcp::Tools::CoverageRawTool do
|
86
|
+
it 'handles errors during raw data retrieval' do
|
87
|
+
model = instance_double(SimpleCovMcp::CoverageModel)
|
88
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
89
|
+
allow(model).to receive(:raw_for).and_raise(StandardError, 'Raw data error')
|
90
|
+
|
91
|
+
response = SimpleCovMcp::Tools::CoverageRawTool.call(
|
92
|
+
path: 'lib/foo.rb',
|
93
|
+
error_mode: 'on',
|
94
|
+
server_context: server_context
|
95
|
+
)
|
96
|
+
|
97
|
+
# Should return error response
|
98
|
+
expect(response).to be_a(MCP::Tool::Response)
|
99
|
+
item = response.payload.first
|
100
|
+
expect(item[:type] || item['type']).to eq('text')
|
101
|
+
expect(item[:text] || item['text']).to include('Error')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe SimpleCovMcp::Tools::UncoveredLinesTool do
|
106
|
+
it 'handles errors during uncovered lines retrieval' do
|
107
|
+
model = instance_double(SimpleCovMcp::CoverageModel)
|
108
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
109
|
+
allow(model).to receive(:uncovered_for).and_raise(StandardError, 'Uncovered error')
|
110
|
+
|
111
|
+
response = SimpleCovMcp::Tools::UncoveredLinesTool.call(
|
112
|
+
path: 'lib/foo.rb',
|
113
|
+
error_mode: 'on',
|
114
|
+
server_context: server_context
|
115
|
+
)
|
116
|
+
|
117
|
+
# Should return error response
|
118
|
+
expect(response).to be_a(MCP::Tool::Response)
|
119
|
+
item = response.payload.first
|
120
|
+
expect(item[:type] || item['type']).to eq('text')
|
121
|
+
expect(item[:text] || item['text']).to include('Error')
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe SimpleCovMcp::Tools::CoverageDetailedTool do
|
126
|
+
it 'handles errors during detailed data retrieval' do
|
127
|
+
model = instance_double(SimpleCovMcp::CoverageModel)
|
128
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
129
|
+
allow(model).to receive(:detailed_for).and_raise(StandardError, 'Detailed error')
|
130
|
+
|
131
|
+
response = SimpleCovMcp::Tools::CoverageDetailedTool.call(
|
132
|
+
path: 'lib/foo.rb',
|
133
|
+
error_mode: 'on',
|
134
|
+
server_context: server_context
|
135
|
+
)
|
136
|
+
|
137
|
+
# Should return error response
|
138
|
+
expect(response).to be_a(MCP::Tool::Response)
|
139
|
+
item = response.payload.first
|
140
|
+
expect(item[:type] || item['type']).to eq('text')
|
141
|
+
expect(item[:text] || item['text']).to include('Error')
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/spec/util_spec.rb
CHANGED
@@ -1,28 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'spec_helper'
|
4
|
+
require 'tempfile'
|
4
5
|
|
5
6
|
RSpec.describe SimpleCovMcp::CovUtil do
|
6
|
-
let(:root) { (
|
7
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
8
|
let(:resultset_file) { File.join(root, 'coverage', '.resultset.json') }
|
8
9
|
|
9
|
-
it 'latest_timestamp returns integer from fixture' do
|
10
|
-
ts = described_class.latest_timestamp(root, resultset: 'coverage')
|
11
|
-
expect(ts).to be_a(Integer)
|
12
|
-
expect(ts).to eq(1_720_000_000)
|
13
|
-
end
|
14
10
|
|
15
|
-
it 'find_resultset honors SIMPLECOV_RESULTSET file path' do
|
16
|
-
begin
|
17
|
-
ENV['SIMPLECOV_RESULTSET'] = resultset_file
|
18
|
-
path = described_class.find_resultset(root)
|
19
|
-
expect(path).to eq(File.absolute_path(resultset_file, root))
|
20
|
-
ensure
|
21
|
-
ENV.delete('SIMPLECOV_RESULTSET')
|
22
|
-
end
|
23
|
-
end
|
24
11
|
|
25
|
-
it 'lookup_lines supports cwd-stripping
|
12
|
+
it 'lookup_lines supports cwd-stripping' do
|
26
13
|
lines = [1, 0]
|
27
14
|
|
28
15
|
# Exact key
|
@@ -30,29 +17,29 @@ RSpec.describe SimpleCovMcp::CovUtil do
|
|
30
17
|
expect(described_class.lookup_lines(cov, '/abs/path/foo.rb')).to eq(lines)
|
31
18
|
|
32
19
|
# CWD strip fallback
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
expect(described_class.lookup_lines(cov, '/cwd/sub/foo.rb')).to eq(lines)
|
37
|
-
ensure
|
38
|
-
# no-op
|
39
|
-
end
|
20
|
+
allow(Dir).to receive(:pwd).and_return('/cwd')
|
21
|
+
cov = { 'sub/foo.rb' => { 'lines' => lines } }
|
22
|
+
expect(described_class.lookup_lines(cov, '/cwd/sub/foo.rb')).to eq(lines)
|
40
23
|
|
41
|
-
#
|
24
|
+
# Different paths with same basename should not match
|
42
25
|
cov = { '/some/where/else/foo.rb' => { 'lines' => lines } }
|
43
|
-
expect
|
26
|
+
expect do
|
27
|
+
described_class.lookup_lines(cov, '/another/place/foo.rb')
|
28
|
+
end.to raise_error(SimpleCovMcp::FileError, /No coverage entry found/)
|
44
29
|
|
45
|
-
# Missing raises a
|
30
|
+
# Missing raises a FileError
|
46
31
|
cov = {}
|
47
|
-
expect
|
32
|
+
expect do
|
48
33
|
described_class.lookup_lines(cov, '/nowhere/foo.rb')
|
49
|
-
|
34
|
+
end.to raise_error(SimpleCovMcp::FileError, /No coverage entry found/)
|
50
35
|
end
|
51
36
|
|
52
37
|
it 'summary handles edge cases and coercion' do
|
53
38
|
expect(described_class.summary([])).to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
|
54
|
-
expect(described_class.summary([nil, nil]))
|
55
|
-
|
39
|
+
expect(described_class.summary([nil, nil]))
|
40
|
+
.to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
|
41
|
+
expect(described_class.summary(['1', '0', nil]))
|
42
|
+
.to include('pct' => 50.0, 'total' => 2, 'covered' => 1)
|
56
43
|
end
|
57
44
|
|
58
45
|
it 'uncovered and detailed ignore nils' do
|
@@ -65,14 +52,102 @@ RSpec.describe SimpleCovMcp::CovUtil do
|
|
65
52
|
])
|
66
53
|
end
|
67
54
|
|
68
|
-
it '
|
55
|
+
it 'load_coverage raises CoverageDataError on invalid JSON via model' do
|
69
56
|
Dir.mktmpdir do |dir|
|
70
57
|
bad = File.join(dir, '.resultset.json')
|
71
58
|
File.write(bad, '{not-json')
|
72
|
-
expect
|
59
|
+
expect do
|
73
60
|
SimpleCovMcp::CoverageModel.new(root: root, resultset: dir)
|
74
|
-
|
61
|
+
end.to raise_error(SimpleCovMcp::CoverageDataError, /Invalid coverage data format/)
|
75
62
|
end
|
76
63
|
end
|
77
|
-
end
|
78
64
|
|
65
|
+
describe 'logging configuration' do
|
66
|
+
let(:test_message) { 'test log message' }
|
67
|
+
|
68
|
+
around(:each) do |example|
|
69
|
+
# Reset logging settings so each example starts clean.
|
70
|
+
old_default = SimpleCovMcp.default_log_file
|
71
|
+
old_active = SimpleCovMcp.active_log_file
|
72
|
+
SimpleCovMcp.default_log_file = nil
|
73
|
+
SimpleCovMcp.active_log_file = nil
|
74
|
+
|
75
|
+
example.run
|
76
|
+
|
77
|
+
# Restore state
|
78
|
+
SimpleCovMcp.default_log_file = old_default
|
79
|
+
SimpleCovMcp.active_log_file = old_active
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
it "logs to stdout when active_log_file is 'stdout'" do
|
85
|
+
SimpleCovMcp.active_log_file = 'stdout'
|
86
|
+
expect(File).not_to receive(:open)
|
87
|
+
expect { described_class.log(test_message) }
|
88
|
+
.to output(/#{Regexp.escape(test_message)}/).to_stdout
|
89
|
+
end
|
90
|
+
|
91
|
+
it "logs to stderr when active_log_file is 'stderr'" do
|
92
|
+
SimpleCovMcp.active_log_file = 'stderr'
|
93
|
+
expect(File).not_to receive(:open)
|
94
|
+
expect { described_class.log(test_message) }
|
95
|
+
.to output(/#{Regexp.escape(test_message)}/).to_stderr
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'log writes to file when path is configured' do
|
99
|
+
tmp = Tempfile.new('simplecov_mcp-log')
|
100
|
+
log_path = tmp.path
|
101
|
+
tmp.close
|
102
|
+
|
103
|
+
SimpleCovMcp.active_log_file = log_path
|
104
|
+
|
105
|
+
described_class.log(test_message)
|
106
|
+
|
107
|
+
expect(File.exist?(log_path)).to be true
|
108
|
+
content = File.read(log_path)
|
109
|
+
expect(content).to include(test_message)
|
110
|
+
expect(content).to match(TIMESTAMP_REGEX)
|
111
|
+
ensure
|
112
|
+
tmp&.unlink
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'log respects runtime changes disabling logging mid-run' do
|
116
|
+
tmp = Tempfile.new('simplecov_mcp-log')
|
117
|
+
log_path = tmp.path
|
118
|
+
tmp.close
|
119
|
+
|
120
|
+
SimpleCovMcp.active_log_file = log_path
|
121
|
+
|
122
|
+
described_class.log('first entry')
|
123
|
+
expect(File.exist?(log_path)).to be true
|
124
|
+
first_content = File.read(log_path)
|
125
|
+
expect(first_content).to include('first entry')
|
126
|
+
|
127
|
+
SimpleCovMcp.active_log_file = 'stderr'
|
128
|
+
|
129
|
+
expect { described_class.log('second entry') }
|
130
|
+
.to output(/second entry/).to_stderr
|
131
|
+
expect(File.exist?(log_path)).to be true
|
132
|
+
expect(File.read(log_path)).to eq(first_content)
|
133
|
+
ensure
|
134
|
+
tmp&.unlink
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'exposes default log file configuration separately' do
|
138
|
+
original_default = SimpleCovMcp.default_log_file
|
139
|
+
SimpleCovMcp.default_log_file = 'stderr'
|
140
|
+
expect(SimpleCovMcp.default_log_file).to eq('stderr')
|
141
|
+
expect(SimpleCovMcp.active_log_file).to eq('stderr')
|
142
|
+
ensure
|
143
|
+
SimpleCovMcp.default_log_file = original_default
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'allows adjusting the active log target without touching the default' do
|
147
|
+
original_default = SimpleCovMcp.default_log_file
|
148
|
+
SimpleCovMcp.active_log_file = 'stdout'
|
149
|
+
expect(SimpleCovMcp.active_log_file).to eq('stdout')
|
150
|
+
expect(SimpleCovMcp.default_log_file).to eq(original_default)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/spec/version_spec.rb
CHANGED
@@ -2,14 +2,122 @@
|
|
2
2
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
|
-
RSpec.describe '
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
5
|
+
RSpec.describe 'SimpleCovMcp::VERSION' do
|
6
|
+
describe 'constant existence' do
|
7
|
+
it 'defines a VERSION constant' do
|
8
|
+
expect(SimpleCovMcp.const_defined?(:VERSION)).to be true
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'exposes VERSION as a non-empty string' do
|
12
|
+
expect(SimpleCovMcp::VERSION).to be_a(String)
|
13
|
+
expect(SimpleCovMcp::VERSION).not_to be_empty
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'is frozen (immutable)' do
|
17
|
+
expect(SimpleCovMcp::VERSION).to be_frozen
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'semantic versioning compliance' do
|
22
|
+
let(:version) { SimpleCovMcp::VERSION }
|
23
|
+
# Simplified semantic versioning regex
|
24
|
+
# Preserves key semver rules: no leading zeros on numeric parts, optional prerelease/build metadata
|
25
|
+
let(:semver_regex) do
|
26
|
+
%r{\A
|
27
|
+
(?<major>0|[1-9]\d*)\.
|
28
|
+
(?<minor>0|[1-9]\d*)\.
|
29
|
+
(?<patch>0|[1-9]\d*)
|
30
|
+
(?:-(?<prerelease>[0-9A-Za-z.-]+))?
|
31
|
+
(?:\+(?<buildmetadata>[0-9A-Za-z.-]+))?
|
32
|
+
\z}x
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'follows semantic versioning format' do
|
36
|
+
expect(version).to match(semver_regex)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'has valid major.minor.patch core version' do
|
40
|
+
match = version.match(semver_regex)
|
41
|
+
expect(match).not_to be_nil, "VERSION '#{version}' does not match semantic versioning format"
|
42
|
+
|
43
|
+
major = match[:major].to_i
|
44
|
+
minor = match[:minor].to_i
|
45
|
+
patch = match[:patch].to_i
|
46
|
+
|
47
|
+
expect(major).to be >= 0
|
48
|
+
expect(minor).to be >= 0
|
49
|
+
expect(patch).to be >= 0
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'when version has prerelease identifier' do
|
53
|
+
let(:prerelease_version) { '9.9.9-rc.1' }
|
54
|
+
|
55
|
+
before do
|
56
|
+
stub_const('SimpleCovMcp::VERSION', prerelease_version)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'has valid prerelease format' do
|
60
|
+
match = version.match(semver_regex)
|
61
|
+
prerelease = match[:prerelease]
|
62
|
+
expect(prerelease).not_to be_empty
|
63
|
+
expect(prerelease).not_to start_with('.')
|
64
|
+
expect(prerelease).not_to end_with('.')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when version has build metadata' do
|
69
|
+
let(:build_metadata_version) { '9.9.9+build.42' }
|
70
|
+
|
71
|
+
before do
|
72
|
+
stub_const('SimpleCovMcp::VERSION', build_metadata_version)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'has valid build metadata format' do
|
76
|
+
match = version.match(semver_regex)
|
77
|
+
buildmetadata = match[:buildmetadata]
|
78
|
+
expect(buildmetadata).not_to be_empty
|
79
|
+
expect(buildmetadata).to match(/\A[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*\z/)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'version consistency' do
|
85
|
+
it 'is accessible via require path' do
|
86
|
+
expect { SimpleCovMcp::VERSION }.not_to raise_error
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'matches the version referenced in gemspec' do
|
90
|
+
gemspec_path = File.expand_path('../simplecov-mcp.gemspec', __dir__)
|
91
|
+
gemspec_content = File.read(gemspec_path)
|
92
|
+
|
93
|
+
version_line = gemspec_content.lines.find { |line| line.include?('spec.version') }
|
94
|
+
expect(version_line).not_to be_nil, 'Could not find version line in gemspec'
|
95
|
+
expect(version_line).to include('SimpleCovMcp::VERSION')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe 'current version sanity check' do
|
100
|
+
it 'is not the initial 0.0.0 version' do
|
101
|
+
# Ensure this is a real release, not an uninitialized version
|
102
|
+
expect(SimpleCovMcp::VERSION).not_to eq('0.0.0')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'standalone version file load' do
|
107
|
+
it 'defines the module and VERSION constant when only version.rb is loaded' do
|
108
|
+
original_module = SimpleCovMcp
|
109
|
+
original_version = SimpleCovMcp::VERSION
|
110
|
+
|
111
|
+
Object.send(:remove_const, :SimpleCovMcp)
|
112
|
+
|
113
|
+
version_path = File.expand_path('../lib/simplecov_mcp/version.rb', __dir__)
|
114
|
+
load version_path
|
115
|
+
|
116
|
+
expect(Object.const_defined?(:SimpleCovMcp)).to be true
|
117
|
+
expect(SimpleCovMcp::VERSION).to eq(original_version)
|
118
|
+
ensure
|
119
|
+
Object.send(:remove_const, :SimpleCovMcp) if Object.const_defined?(:SimpleCovMcp)
|
120
|
+
Object.const_set(:SimpleCovMcp, original_module)
|
121
|
+
end
|
14
122
|
end
|
15
123
|
end
|
data/spec/version_tool_spec.rb
CHANGED
@@ -1,22 +1,143 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'spec_helper'
|
4
|
-
require '
|
4
|
+
require 'simplecov_mcp/tools/version_tool'
|
5
5
|
|
6
6
|
RSpec.describe SimpleCovMcp::Tools::VersionTool do
|
7
7
|
let(:server_context) { instance_double('ServerContext').as_null_object }
|
8
8
|
|
9
9
|
before do
|
10
|
-
|
10
|
+
setup_mcp_response_stub
|
11
11
|
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
describe '.call' do
|
14
|
+
it 'returns a text payload with the version string when called without arguments' do
|
15
|
+
response = described_class.call(server_context: server_context)
|
16
|
+
item = response.payload.first
|
17
|
+
expect(item[:type] || item['type']).to eq('text')
|
18
|
+
text = item[:text] || item['text']
|
19
|
+
expect(text).to eq("SimpleCovMcp version: #{SimpleCovMcp::VERSION}")
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'includes the exact version constant value' do
|
23
|
+
response = described_class.call(server_context: server_context)
|
24
|
+
item = response.payload.first
|
25
|
+
text = item[:text] || item['text']
|
26
|
+
expect(text).to include(SimpleCovMcp::VERSION)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'matches the expected format exactly' do
|
30
|
+
expected_format = "SimpleCovMcp version: #{SimpleCovMcp::VERSION}"
|
31
|
+
response = described_class.call(server_context: server_context)
|
32
|
+
item = response.payload.first
|
33
|
+
text = item[:text] || item['text']
|
34
|
+
expect(text).to eq(expected_format)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'returns an MCP::Tool::Response object' do
|
38
|
+
response = described_class.call(server_context: server_context)
|
39
|
+
expect(response).to be_a(MCP::Tool::Response)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'has a single payload item' do
|
43
|
+
response = described_class.call(server_context: server_context)
|
44
|
+
expect(response.payload).to be_an(Array)
|
45
|
+
expect(response.payload.size).to eq(1)
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when error_mode is specified' do
|
49
|
+
it 'accepts error_mode parameter without affecting output' do
|
50
|
+
response = described_class.call(error_mode: 'off', server_context: server_context)
|
51
|
+
item = response.payload.first
|
52
|
+
text = item[:text] || item['text']
|
53
|
+
expect(text).to eq("SimpleCovMcp version: #{SimpleCovMcp::VERSION}")
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'accepts error_mode "on" (default)' do
|
57
|
+
response = described_class.call(error_mode: 'on', server_context: server_context)
|
58
|
+
item = response.payload.first
|
59
|
+
expect(item[:type] || item['type']).to eq('text')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'accepts error_mode "trace"' do
|
63
|
+
response = described_class.call(error_mode: 'trace', server_context: server_context)
|
64
|
+
item = response.payload.first
|
65
|
+
expect(item[:type] || item['type']).to eq('text')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'when additional arguments are passed' do
|
70
|
+
it 'ignores additional arguments gracefully' do
|
71
|
+
response = described_class.call(
|
72
|
+
server_context: server_context,
|
73
|
+
extra_arg: 'value',
|
74
|
+
another: { nested: 'data' }
|
75
|
+
)
|
76
|
+
item = response.payload.first
|
77
|
+
text = item[:text] || item['text']
|
78
|
+
expect(text).to eq("SimpleCovMcp version: #{SimpleCovMcp::VERSION}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when an error occurs' do
|
83
|
+
it 'handles VERSION constant access errors and returns structured error response' do
|
84
|
+
# Force an error by overriding const_get to raise an error when VERSION is accessed
|
85
|
+
allow(SimpleCovMcp).to receive(:const_missing).with(:VERSION).and_raise(StandardError,
|
86
|
+
'Version access error')
|
87
|
+
|
88
|
+
# Clear the cached VERSION constant to trigger const_missing
|
89
|
+
SimpleCovMcp.send(:remove_const, :VERSION) if SimpleCovMcp.const_defined?(:VERSION)
|
90
|
+
|
91
|
+
response = described_class.call(error_mode: 'on', server_context: server_context)
|
92
|
+
|
93
|
+
# Should return error response in MCP format
|
94
|
+
expect(response).to be_a(MCP::Tool::Response)
|
95
|
+
item = response.payload.first
|
96
|
+
expect(item[:type] || item['type']).to eq('text')
|
97
|
+
|
98
|
+
error_text = item[:text] || item['text']
|
99
|
+
expect(error_text).to include('Error')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'handles errors in the response creation process' do
|
103
|
+
# Force an error by mocking string interpolation to fail
|
104
|
+
version_obj = double('VERSION')
|
105
|
+
allow(version_obj).to receive(:to_s).and_raise(StandardError, 'String conversion error')
|
106
|
+
|
107
|
+
# Replace VERSION with our mock object
|
108
|
+
stub_const('SimpleCovMcp::VERSION', version_obj)
|
109
|
+
|
110
|
+
response = described_class.call(error_mode: 'on', server_context: server_context)
|
111
|
+
|
112
|
+
# Should return error response in MCP format via the rescue block
|
113
|
+
expect(response).to be_a(MCP::Tool::Response)
|
114
|
+
item = response.payload.first
|
115
|
+
expect(item[:type] || item['type']).to eq('text')
|
116
|
+
|
117
|
+
error_text = item[:text] || item['text']
|
118
|
+
expect(error_text).to include('Error')
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'respects error_mode setting when handling errors' do
|
122
|
+
# Force an error using a mock VERSION that raises an exception
|
123
|
+
version_obj = double('VERSION')
|
124
|
+
allow(version_obj).to receive(:to_s).and_raise(StandardError, 'Version error')
|
125
|
+
stub_const('SimpleCovMcp::VERSION', version_obj)
|
126
|
+
|
127
|
+
# Test error_mode 'off' (should be silent but still return structured response)
|
128
|
+
response = described_class.call(error_mode: 'off', server_context: server_context)
|
129
|
+
expect(response).to be_a(MCP::Tool::Response)
|
130
|
+
item = response.payload.first
|
131
|
+
expect(item[:type] || item['type']).to eq('text')
|
132
|
+
|
133
|
+
# Test error_mode 'trace' (should include more detail)
|
134
|
+
response = described_class.call(error_mode: 'trace', server_context: server_context)
|
135
|
+
expect(response).to be_a(MCP::Tool::Response)
|
136
|
+
item = response.payload.first
|
137
|
+
expect(item[:type] || item['type']).to eq('text')
|
138
|
+
error_text = item[:text] || item['text']
|
139
|
+
expect(error_text).to include('Error')
|
140
|
+
end
|
141
|
+
end
|
20
142
|
end
|
21
143
|
end
|
22
|
-
|