simplecov-mcp 1.0.0 → 2.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/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 +32 -20
- 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 -83
- data/lib/simplecov_mcp/option_normalizers.rb +39 -18
- data/lib/simplecov_mcp/option_parser_builder.rb +82 -65
- 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 +114 -170
- 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 +141 -82
- 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 +99 -49
- 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/lib/simplecov_mcp.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'time'
|
|
5
5
|
require 'pathname'
|
|
6
|
+
require 'set' # rubocop:disable Lint/RedundantRequireStatement -- Ruby >= 3.4 requires explicit require for set; RuboCop targets 3.2
|
|
6
7
|
require 'optparse'
|
|
7
8
|
require 'mcp'
|
|
8
9
|
require 'mcp/server/transports/stdio_transport'
|
|
@@ -17,13 +18,16 @@ require_relative 'simplecov_mcp/path_relativizer'
|
|
|
17
18
|
require_relative 'simplecov_mcp/resultset_loader'
|
|
18
19
|
require_relative 'simplecov_mcp/mode_detector'
|
|
19
20
|
require_relative 'simplecov_mcp/model'
|
|
21
|
+
require_relative 'simplecov_mcp/coverage_reporter'
|
|
20
22
|
require_relative 'simplecov_mcp/base_tool'
|
|
21
23
|
require_relative 'simplecov_mcp/tools/coverage_raw_tool'
|
|
22
24
|
require_relative 'simplecov_mcp/tools/coverage_summary_tool'
|
|
23
25
|
require_relative 'simplecov_mcp/tools/uncovered_lines_tool'
|
|
24
26
|
require_relative 'simplecov_mcp/tools/coverage_detailed_tool'
|
|
25
27
|
require_relative 'simplecov_mcp/tools/all_files_coverage_tool'
|
|
28
|
+
require_relative 'simplecov_mcp/tools/coverage_totals_tool'
|
|
26
29
|
require_relative 'simplecov_mcp/tools/coverage_table_tool'
|
|
30
|
+
require_relative 'simplecov_mcp/tools/validate_tool'
|
|
27
31
|
require_relative 'simplecov_mcp/tools/version_tool'
|
|
28
32
|
require_relative 'simplecov_mcp/tools/help_tool'
|
|
29
33
|
require_relative 'simplecov_mcp/mcp_server'
|
|
@@ -34,23 +38,26 @@ module SimpleCovMcp
|
|
|
34
38
|
THREAD_CONTEXT_KEY = :simplecov_mcp_context
|
|
35
39
|
|
|
36
40
|
def run(argv)
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
full_argv = env_opts + argv
|
|
41
|
+
# Prepend environment options once at entry point
|
|
42
|
+
full_argv = extract_env_opts + argv
|
|
40
43
|
|
|
41
44
|
if ModeDetector.cli_mode?(full_argv)
|
|
42
|
-
|
|
45
|
+
# CLI mode: pass merged argv to CoverageCLI
|
|
46
|
+
CoverageCLI.new.run(full_argv)
|
|
43
47
|
else
|
|
44
|
-
|
|
48
|
+
# MCP server mode: parse config once from full_argv
|
|
49
|
+
require_relative 'simplecov_mcp/config_parser'
|
|
50
|
+
config = ConfigParser.parse(full_argv)
|
|
45
51
|
|
|
46
|
-
if log_file == 'stdout'
|
|
52
|
+
if config.log_file == 'stdout'
|
|
47
53
|
raise ConfigurationError,
|
|
48
54
|
'Logging to stdout is not permitted in MCP server mode as it interferes with ' \
|
|
49
55
|
"the JSON-RPC protocol. Please use 'stderr' or a file path."
|
|
50
56
|
end
|
|
51
57
|
|
|
52
|
-
handler = ErrorHandlerFactory.for_mcp_server
|
|
53
|
-
context = create_context(error_handler: handler, log_target: log_file,
|
|
58
|
+
handler = ErrorHandlerFactory.for_mcp_server(error_mode: config.error_mode)
|
|
59
|
+
context = create_context(error_handler: handler, log_target: config.log_file,
|
|
60
|
+
mode: :mcp)
|
|
54
61
|
with_context(context) { MCPServer.new(context: context).run }
|
|
55
62
|
end
|
|
56
63
|
end
|
|
@@ -86,7 +93,7 @@ module SimpleCovMcp
|
|
|
86
93
|
if active.nil? || active.log_target == previous_default.log_target
|
|
87
94
|
Thread.current[THREAD_CONTEXT_KEY] = @default_context
|
|
88
95
|
end
|
|
89
|
-
value
|
|
96
|
+
value # rubocop:disable Lint/Void -- return assigned log target for symmetry
|
|
90
97
|
end
|
|
91
98
|
|
|
92
99
|
def active_log_file
|
|
@@ -95,12 +102,12 @@ module SimpleCovMcp
|
|
|
95
102
|
|
|
96
103
|
def active_log_file=(value)
|
|
97
104
|
current = Thread.current[THREAD_CONTEXT_KEY]
|
|
98
|
-
if current
|
|
99
|
-
|
|
105
|
+
Thread.current[THREAD_CONTEXT_KEY] = if current
|
|
106
|
+
current.with_log_target(value)
|
|
100
107
|
else
|
|
101
|
-
|
|
108
|
+
default_context.with_log_target(value)
|
|
102
109
|
end
|
|
103
|
-
value
|
|
110
|
+
value # rubocop:disable Lint/Void -- return assigned log target for symmetry
|
|
104
111
|
end
|
|
105
112
|
|
|
106
113
|
def error_handler
|
|
@@ -111,27 +118,14 @@ module SimpleCovMcp
|
|
|
111
118
|
@default_context = default_context.with_error_handler(handler)
|
|
112
119
|
end
|
|
113
120
|
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
def default_context
|
|
121
|
+
private def default_context
|
|
117
122
|
@default_context ||= AppContext.new(
|
|
118
123
|
error_handler: ErrorHandlerFactory.for_cli,
|
|
119
124
|
log_target: nil
|
|
120
125
|
)
|
|
121
126
|
end
|
|
122
127
|
|
|
123
|
-
def
|
|
124
|
-
log_file = nil
|
|
125
|
-
parser = OptionParser.new do |o|
|
|
126
|
-
# Define the option we're looking for
|
|
127
|
-
o.on('-l', '--log-file PATH') { |v| log_file = v }
|
|
128
|
-
end
|
|
129
|
-
# Parse arguments, but ignore errors and stop at the first non-option
|
|
130
|
-
parser.order!(argv.dup) {} rescue nil
|
|
131
|
-
log_file
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def parse_env_opts_for_mode_detection
|
|
128
|
+
private def extract_env_opts
|
|
135
129
|
require 'shellwords'
|
|
136
130
|
opts_string = ENV['SIMPLECOV_MCP_OPTS']
|
|
137
131
|
return [] unless opts_string && !opts_string.empty?
|
|
@@ -139,7 +133,7 @@ module SimpleCovMcp
|
|
|
139
133
|
begin
|
|
140
134
|
Shellwords.split(opts_string)
|
|
141
135
|
rescue ArgumentError
|
|
142
|
-
[] # Ignore parsing errors
|
|
136
|
+
[] # Ignore parsing errors
|
|
143
137
|
end
|
|
144
138
|
end
|
|
145
139
|
end
|
|
@@ -4,6 +4,8 @@ require 'spec_helper'
|
|
|
4
4
|
require 'simplecov_mcp/tools/all_files_coverage_tool'
|
|
5
5
|
|
|
6
6
|
RSpec.describe SimpleCovMcp::Tools::AllFilesCoverageTool do
|
|
7
|
+
subject(:call_tool) { described_class.call(root: root, server_context: server_context) }
|
|
8
|
+
|
|
7
9
|
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
8
10
|
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
9
11
|
|
|
@@ -27,13 +29,12 @@ RSpec.describe SimpleCovMcp::Tools::AllFilesCoverageTool do
|
|
|
27
29
|
allow(presenter).to receive(:relativized_payload).and_return(payload)
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
subject { described_class.call(root: root, server_context: server_context) }
|
|
31
32
|
|
|
32
33
|
it_behaves_like 'an MCP tool that returns text JSON'
|
|
33
34
|
|
|
34
35
|
it 'returns all files coverage data with counts' do
|
|
35
|
-
response =
|
|
36
|
-
data,
|
|
36
|
+
response = call_tool
|
|
37
|
+
data, _item = expect_mcp_text_json(response, expected_keys: ['files', 'counts'])
|
|
37
38
|
|
|
38
39
|
files = data['files']
|
|
39
40
|
counts = data['counts']
|
|
@@ -2,42 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
|
-
RSpec.describe SimpleCovMcp::
|
|
5
|
+
RSpec.describe SimpleCovMcp::AppConfig do
|
|
6
6
|
describe '#initialize' do
|
|
7
7
|
it 'creates a config with default values' do
|
|
8
8
|
config = described_class.new
|
|
9
9
|
expect(config.root).to eq('.')
|
|
10
|
-
expect(config.
|
|
11
|
-
expect(config.sort_order).to eq(:
|
|
10
|
+
expect(config.format).to eq(:table)
|
|
11
|
+
expect(config.sort_order).to eq(:descending)
|
|
12
12
|
expect(config.source_context).to eq(2)
|
|
13
|
-
expect(config.error_mode).to eq(:
|
|
14
|
-
expect(config.
|
|
13
|
+
expect(config.error_mode).to eq(:log)
|
|
14
|
+
expect(config.staleness).to eq(:off)
|
|
15
15
|
expect(config.resultset).to be_nil
|
|
16
16
|
expect(config.source_mode).to be_nil
|
|
17
17
|
expect(config.tracked_globs).to be_nil
|
|
18
18
|
expect(config.log_file).to be_nil
|
|
19
|
-
expect(config.success_predicate).to be_nil
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
it 'allows overriding defaults via keyword arguments' do
|
|
23
22
|
config = described_class.new(
|
|
24
23
|
root: '/custom',
|
|
25
|
-
|
|
24
|
+
format: :json,
|
|
26
25
|
sort_order: :descending,
|
|
27
|
-
|
|
26
|
+
staleness: :error
|
|
28
27
|
)
|
|
29
28
|
expect(config.root).to eq('/custom')
|
|
30
|
-
expect(config.
|
|
29
|
+
expect(config.format).to eq(:json)
|
|
31
30
|
expect(config.sort_order).to eq(:descending)
|
|
32
|
-
expect(config.
|
|
31
|
+
expect(config.staleness).to eq(:error)
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
it 'is mutable (struct fields can be changed)' do
|
|
36
35
|
config = described_class.new
|
|
37
36
|
config.root = '/new/root'
|
|
38
|
-
config.
|
|
37
|
+
config.format = :json
|
|
39
38
|
expect(config.root).to eq('/new/root')
|
|
40
|
-
expect(config.
|
|
39
|
+
expect(config.format).to eq(:json)
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
42
|
|
|
@@ -46,7 +45,7 @@ RSpec.describe SimpleCovMcp::CLIConfig do
|
|
|
46
45
|
config = described_class.new(
|
|
47
46
|
root: '/custom/root',
|
|
48
47
|
resultset: '/custom/.resultset.json',
|
|
49
|
-
|
|
48
|
+
staleness: :error,
|
|
50
49
|
tracked_globs: ['lib/**/*.rb']
|
|
51
50
|
)
|
|
52
51
|
|
|
@@ -85,46 +84,52 @@ RSpec.describe SimpleCovMcp::CLIConfig do
|
|
|
85
84
|
|
|
86
85
|
describe 'struct behavior' do
|
|
87
86
|
it 'supports equality comparison' do
|
|
88
|
-
config1 = described_class.new(root: '/foo',
|
|
89
|
-
config2 = described_class.new(root: '/foo',
|
|
90
|
-
config3 = described_class.new(root: '/bar',
|
|
87
|
+
config1 = described_class.new(root: '/foo', format: :json)
|
|
88
|
+
config2 = described_class.new(root: '/foo', format: :json)
|
|
89
|
+
config3 = described_class.new(root: '/bar', format: :json)
|
|
91
90
|
|
|
92
91
|
expect(config1).to eq(config2)
|
|
93
92
|
expect(config1).not_to eq(config3)
|
|
94
93
|
end
|
|
95
94
|
|
|
96
95
|
it 'provides readable inspect output' do
|
|
97
|
-
config = described_class.new(root: '/test',
|
|
96
|
+
config = described_class.new(root: '/test', format: :json)
|
|
98
97
|
output = config.inspect
|
|
99
98
|
expect(output).to include('root="/test"')
|
|
100
|
-
expect(output).to include('json
|
|
99
|
+
expect(output).to include('format=:json')
|
|
101
100
|
end
|
|
102
101
|
|
|
103
102
|
it 'converts to hash' do
|
|
104
|
-
config = described_class.new(root: '/test',
|
|
103
|
+
config = described_class.new(root: '/test', format: :json)
|
|
105
104
|
hash = config.to_h
|
|
106
105
|
expect(hash).to be_a(Hash)
|
|
107
106
|
expect(hash[:root]).to eq('/test')
|
|
108
|
-
expect(hash[:
|
|
107
|
+
expect(hash[:format]).to eq(:json)
|
|
109
108
|
end
|
|
110
109
|
end
|
|
111
110
|
|
|
112
111
|
describe 'symbol enumerated values' do
|
|
112
|
+
it 'uses symbols for format' do
|
|
113
|
+
config = described_class.new(format: :json)
|
|
114
|
+
expect(config.format).to eq(:json)
|
|
115
|
+
expect(config.format).to be_a(Symbol)
|
|
116
|
+
end
|
|
117
|
+
|
|
113
118
|
it 'uses symbols for sort_order' do
|
|
114
119
|
config = described_class.new(sort_order: :descending)
|
|
115
120
|
expect(config.sort_order).to eq(:descending)
|
|
116
121
|
expect(config.sort_order).to be_a(Symbol)
|
|
117
122
|
end
|
|
118
123
|
|
|
119
|
-
it 'uses symbols for
|
|
120
|
-
config = described_class.new(
|
|
121
|
-
expect(config.
|
|
122
|
-
expect(config.
|
|
124
|
+
it 'uses symbols for staleness' do
|
|
125
|
+
config = described_class.new(staleness: :error)
|
|
126
|
+
expect(config.staleness).to eq(:error)
|
|
127
|
+
expect(config.staleness).to be_a(Symbol)
|
|
123
128
|
end
|
|
124
129
|
|
|
125
130
|
it 'uses symbols for error_mode' do
|
|
126
|
-
config = described_class.new(error_mode: :
|
|
127
|
-
expect(config.error_mode).to eq(:
|
|
131
|
+
config = described_class.new(error_mode: :debug)
|
|
132
|
+
expect(config.error_mode).to eq(:debug)
|
|
128
133
|
expect(config.error_mode).to be_a(Symbol)
|
|
129
134
|
end
|
|
130
135
|
|
data/spec/base_tool_spec.rb
CHANGED
|
@@ -3,41 +3,42 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
RSpec.describe SimpleCovMcp::BaseTool do
|
|
6
|
-
let(:handler) { SimpleCovMcp::ErrorHandler.new(error_mode: :
|
|
6
|
+
let(:handler) { SimpleCovMcp::ErrorHandler.new(error_mode: :log, logger: test_logger) }
|
|
7
7
|
let(:test_logger) do
|
|
8
8
|
Class.new do
|
|
9
9
|
attr_reader :messages
|
|
10
10
|
|
|
11
|
-
def initialize
|
|
12
|
-
def error(msg)
|
|
11
|
+
def initialize = @messages = []
|
|
12
|
+
def error(msg) = @messages << msg
|
|
13
13
|
end.new
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
let(:orig_handler) do
|
|
17
|
+
SimpleCovMcp.error_handler
|
|
18
|
+
rescue
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
before do
|
|
17
|
-
@orig_handler = begin
|
|
18
|
-
SimpleCovMcp.error_handler
|
|
19
|
-
rescue StandardError
|
|
20
|
-
nil
|
|
21
|
-
end
|
|
22
23
|
SimpleCovMcp.error_handler = handler
|
|
23
24
|
setup_mcp_response_stub
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
after do
|
|
27
|
-
SimpleCovMcp.error_handler =
|
|
28
|
+
SimpleCovMcp.error_handler = orig_handler if orig_handler
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
shared_examples 'friendly response and logged' do
|
|
31
32
|
it 'returns friendly text' do
|
|
32
|
-
resp = described_class.handle_mcp_error(error, tool, error_mode: :
|
|
33
|
+
resp = described_class.handle_mcp_error(error, tool, error_mode: :log)
|
|
33
34
|
expect(resp).to be_a(MCP::Tool::Response)
|
|
34
|
-
expect(resp.payload.first[
|
|
35
|
+
expect(resp.payload.first['text']).to match(expected_pattern)
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
it 'respects error_mode :off' do
|
|
38
39
|
resp = described_class.handle_mcp_error(error, tool, error_mode: :off)
|
|
39
40
|
expect(resp).to be_a(MCP::Tool::Response)
|
|
40
|
-
expect(resp.payload.first[
|
|
41
|
+
expect(resp.payload.first['text']).to match(expected_pattern)
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -46,7 +47,8 @@ RSpec.describe SimpleCovMcp::BaseTool do
|
|
|
46
47
|
let(:tool) { 'coverage_summary' }
|
|
47
48
|
let(:expected_pattern) { /Error: invalid args/ }
|
|
48
49
|
let(:log_fragment) { 'invalid args' }
|
|
49
|
-
|
|
50
|
+
|
|
51
|
+
it_behaves_like 'friendly response and logged'
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
context 'with standard error' do
|
|
@@ -54,6 +56,7 @@ RSpec.describe SimpleCovMcp::BaseTool do
|
|
|
54
56
|
let(:tool) { 'coverage_raw' }
|
|
55
57
|
let(:expected_pattern) { /Error: .*File not found: missing.rb/ }
|
|
56
58
|
let(:log_fragment) { 'File not found' }
|
|
57
|
-
|
|
59
|
+
|
|
60
|
+
it_behaves_like 'friendly response and logged'
|
|
58
61
|
end
|
|
59
62
|
end
|
|
@@ -9,13 +9,13 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
9
9
|
before do
|
|
10
10
|
cli.config.root = root
|
|
11
11
|
cli.config.resultset = 'coverage'
|
|
12
|
-
cli.config.
|
|
12
|
+
cli.config.staleness = :off
|
|
13
13
|
cli.config.tracked_globs = nil
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
describe '#show_default_report' do
|
|
17
17
|
it 'prints JSON summary using relativized payload when json mode is enabled' do
|
|
18
|
-
cli.config.
|
|
18
|
+
cli.config.format = :json
|
|
19
19
|
|
|
20
20
|
output = nil
|
|
21
21
|
silence_output do |stdout, _stderr|
|
|
@@ -18,18 +18,22 @@ RSpec.describe 'CLI enumerated option parsing' do
|
|
|
18
18
|
expected: :descending },
|
|
19
19
|
|
|
20
20
|
{ argv: ['--source', 'f', 'summary', 'lib/foo.rb'], accessor: :source_mode, expected: :full },
|
|
21
|
-
{ argv: ['--source
|
|
21
|
+
{ argv: ['--source', 'u', 'summary', 'lib/foo.rb'], accessor: :source_mode,
|
|
22
22
|
expected: :uncovered },
|
|
23
|
-
{ argv: ['--source
|
|
24
|
-
|
|
23
|
+
{ argv: ['--source', 'full', 'summary', 'lib/foo.rb'], accessor: :source_mode,
|
|
24
|
+
expected: :full },
|
|
25
|
+
{ argv: ['--source', 'uncovered', 'summary', 'lib/foo.rb'], accessor: :source_mode,
|
|
25
26
|
expected: :uncovered },
|
|
26
27
|
|
|
27
|
-
{ argv: ['-S', 'e', 'list'], accessor: :
|
|
28
|
-
{ argv: ['-S', 'o', 'list'], accessor: :
|
|
28
|
+
{ argv: ['-S', 'e', 'list'], accessor: :staleness, expected: :error },
|
|
29
|
+
{ argv: ['-S', 'o', 'list'], accessor: :staleness, expected: :off },
|
|
30
|
+
{ argv: ['--staleness', 'e', 'list'], accessor: :staleness, expected: :error },
|
|
31
|
+
{ argv: ['--staleness', 'o', 'list'], accessor: :staleness, expected: :off },
|
|
29
32
|
|
|
30
33
|
{ argv: ['--error-mode', 'off', 'list'], accessor: :error_mode, expected: :off },
|
|
31
|
-
{ argv: ['--error-mode', '
|
|
32
|
-
{ argv: ['--error-mode', '
|
|
34
|
+
{ argv: ['--error-mode', 'o', 'list'], accessor: :error_mode, expected: :off },
|
|
35
|
+
{ argv: ['--error-mode', 'log', 'list'], accessor: :error_mode, expected: :log },
|
|
36
|
+
{ argv: ['--error-mode', 'debug', 'list'], accessor: :error_mode, expected: :debug }
|
|
33
37
|
]
|
|
34
38
|
|
|
35
39
|
cases.each do |c|
|
|
@@ -43,9 +47,12 @@ RSpec.describe 'CLI enumerated option parsing' do
|
|
|
43
47
|
describe 'rejects invalid values' do
|
|
44
48
|
invalid_cases = [
|
|
45
49
|
{ argv: ['--sort-order', 'asc', 'list'] },
|
|
46
|
-
{ argv: ['--source
|
|
50
|
+
{ argv: ['--source', 'x', 'summary', 'lib/foo.rb'] },
|
|
47
51
|
{ argv: ['-S', 'x', 'list'] },
|
|
48
|
-
{ argv: ['--
|
|
52
|
+
{ argv: ['--staleness', 'x', 'list'] },
|
|
53
|
+
{ argv: ['--error-mode', 'bad', 'list'] },
|
|
54
|
+
{ argv: ['--error-mode', 'on', 'list'] },
|
|
55
|
+
{ argv: ['--error-mode', 'trace', 'list'] }
|
|
49
56
|
]
|
|
50
57
|
|
|
51
58
|
invalid_cases.each do |c|
|
|
@@ -64,5 +71,20 @@ RSpec.describe 'CLI enumerated option parsing' do
|
|
|
64
71
|
expect(status).to eq(1)
|
|
65
72
|
expect(err).to include('invalid argument')
|
|
66
73
|
end
|
|
74
|
+
|
|
75
|
+
it 'exits 1 when --staleness is provided without a value' do
|
|
76
|
+
_out, err, status = run_cli_with_status('--staleness', 'list')
|
|
77
|
+
expect(status).to eq(1)
|
|
78
|
+
expect(err).to include('invalid argument')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'exits 1 when --source is provided without a value' do
|
|
82
|
+
_out, err, status = run_cli_with_status('--source', 'summary', 'lib/foo.rb')
|
|
83
|
+
expect(status).to eq(1)
|
|
84
|
+
# Depending on OptParse implementation for required argument, it might say "missing argument"
|
|
85
|
+
# But usually it consumes next arg. If 'summary' is consumed as argument for source:
|
|
86
|
+
# normalize_source_mode('summary') -> raises InvalidArgument.
|
|
87
|
+
expect(err).to include('invalid argument')
|
|
88
|
+
end
|
|
67
89
|
end
|
|
68
90
|
end
|
data/spec/cli_error_spec.rb
CHANGED
|
@@ -9,9 +9,11 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
9
9
|
out, err, status = run_cli_with_status('--help')
|
|
10
10
|
expect(status).to eq(0)
|
|
11
11
|
expect(out).to match(/Usage:.*simplecov-mcp/)
|
|
12
|
-
expect(out).to include(
|
|
12
|
+
expect(out).to include(
|
|
13
|
+
'Repository: https://github.com/keithrbennett/simplecov-mcp',
|
|
14
|
+
'Subcommands:'
|
|
15
|
+
)
|
|
13
16
|
expect(out).to match(/Version:.*#{SimpleCovMcp::VERSION}/)
|
|
14
|
-
expect(out).to include('Subcommands:')
|
|
15
17
|
expect(err).to eq('')
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -19,7 +21,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
19
21
|
before do
|
|
20
22
|
# Build a fake model that raises the specified error from the specified method
|
|
21
23
|
fake_model = Class.new do
|
|
22
|
-
def initialize(*)
|
|
24
|
+
def initialize(*)
|
|
25
|
+
end
|
|
23
26
|
end
|
|
24
27
|
error_to_raise = raised_error
|
|
25
28
|
fake_model.define_method(model_method) { |*| raise error_to_raise }
|
|
@@ -33,20 +36,22 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
context '
|
|
39
|
+
context 'when mapping ENOENT' do
|
|
37
40
|
let(:model_method) { :summary_for }
|
|
38
41
|
let(:raised_error) { Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb') }
|
|
39
|
-
let(:invoke_args) { ['
|
|
42
|
+
let(:invoke_args) { ['--root', root, '--resultset', 'coverage', 'summary', 'lib/missing.rb'] }
|
|
40
43
|
let(:expected_message) { 'File error: File not found: lib/missing.rb' }
|
|
41
|
-
|
|
44
|
+
|
|
45
|
+
it_behaves_like 'maps error to exit 1 with message'
|
|
42
46
|
end
|
|
43
47
|
|
|
44
|
-
context '
|
|
48
|
+
context 'when mapping EACCES' do
|
|
45
49
|
let(:model_method) { :raw_for }
|
|
46
50
|
let(:raised_error) { Errno::EACCES.new('Permission denied @ rb_sysopen - secret.rb') }
|
|
47
|
-
let(:invoke_args) { ['
|
|
51
|
+
let(:invoke_args) { ['--root', root, '--resultset', 'coverage', 'raw', 'lib/secret.rb'] }
|
|
48
52
|
let(:expected_message) { 'Permission denied: lib/secret.rb' }
|
|
49
|
-
|
|
53
|
+
|
|
54
|
+
it_behaves_like 'maps error to exit 1 with message'
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
it 'emits detailed stale coverage info and exits 1' do
|
|
@@ -54,8 +59,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
54
59
|
File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, 1] }
|
|
55
60
|
})
|
|
56
61
|
|
|
57
|
-
_out, err, status = run_cli_with_status('
|
|
58
|
-
'
|
|
62
|
+
_out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage',
|
|
63
|
+
'--staleness', 'error', 'summary', 'lib/foo.rb')
|
|
59
64
|
expect(status).to eq(1)
|
|
60
65
|
expect(err).to include('Coverage data stale:')
|
|
61
66
|
expect(err).to match(/File\s+- time:/)
|
|
@@ -69,8 +74,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
69
74
|
File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, 1] }
|
|
70
75
|
})
|
|
71
76
|
|
|
72
|
-
_out, err, status = run_cli_with_status('
|
|
73
|
-
'
|
|
77
|
+
_out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage',
|
|
78
|
+
'--staleness', 'off', 'summary', 'lib/foo.rb')
|
|
74
79
|
expect(status).to eq(0)
|
|
75
80
|
expect(err).to eq('')
|
|
76
81
|
end
|
|
@@ -80,22 +85,22 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
80
85
|
# This is a regression test for the "can't convert nil into Integer" crash
|
|
81
86
|
# that was previously mentioned in comments
|
|
82
87
|
out, err, status = run_cli_with_status(
|
|
83
|
-
'
|
|
84
|
-
'--
|
|
88
|
+
'--root', root, '--resultset', 'coverage', '--source', 'uncovered', '--context-lines', '2',
|
|
89
|
+
'--no-color', 'uncovered', 'lib/foo.rb'
|
|
85
90
|
)
|
|
86
91
|
|
|
87
92
|
expect(status).to eq(0)
|
|
88
93
|
expect(err).to eq('')
|
|
89
94
|
expect(out).to match(/File:\s+lib\/foo\.rb/)
|
|
90
|
-
expect(out).to include('
|
|
95
|
+
expect(out).to include('│') # Table format
|
|
91
96
|
expect(out).to show_source_table_or_fallback
|
|
92
97
|
end
|
|
93
98
|
|
|
94
99
|
it 'renders source with full mode without crashing' do
|
|
95
100
|
# Additional regression test for source rendering with full mode
|
|
96
101
|
out, err, status = run_cli_with_status(
|
|
97
|
-
'
|
|
98
|
-
'
|
|
102
|
+
'--root', root, '--resultset', 'coverage', '--source', 'full', '--no-color',
|
|
103
|
+
'summary', 'lib/foo.rb'
|
|
99
104
|
)
|
|
100
105
|
|
|
101
106
|
expect(status).to eq(0)
|
|
@@ -115,8 +120,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
115
120
|
File.rename(foo_path, temp_path) if File.exist?(foo_path)
|
|
116
121
|
|
|
117
122
|
out, err, status = run_cli_with_status(
|
|
118
|
-
'
|
|
119
|
-
'
|
|
123
|
+
'--root', root, '--resultset', 'coverage', '--source', 'full', '--no-color',
|
|
124
|
+
'summary', 'lib/foo.rb'
|
|
120
125
|
)
|
|
121
126
|
|
|
122
127
|
expect(status).to eq(0)
|
|
@@ -141,13 +146,13 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
141
146
|
end
|
|
142
147
|
|
|
143
148
|
it 'reports invalid enum value for --opt=value' do
|
|
144
|
-
_out, err, status = run_cli_with_status('
|
|
149
|
+
_out, err, status = run_cli_with_status('--staleness=bogus', 'list')
|
|
145
150
|
expect(status).to eq(1)
|
|
146
|
-
expect(err).to include('invalid argument: --
|
|
151
|
+
expect(err).to include('invalid argument: --staleness=bogus')
|
|
147
152
|
end
|
|
148
153
|
|
|
149
154
|
it 'reports invalid enum value for --opt value' do
|
|
150
|
-
_out, err, status = run_cli_with_status('
|
|
155
|
+
_out, err, status = run_cli_with_status('--staleness', 'bogus', 'list')
|
|
151
156
|
expect(status).to eq(1)
|
|
152
157
|
expect(err).to include('invalid argument: bogus')
|
|
153
158
|
end
|
|
@@ -158,4 +163,22 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
|
|
|
158
163
|
expect(err).to include('Error: invalid option: --no-such-option')
|
|
159
164
|
end
|
|
160
165
|
end
|
|
166
|
+
|
|
167
|
+
describe 'subcommand error handling' do
|
|
168
|
+
it 'handles generic exceptions from subcommands' do
|
|
169
|
+
# Stub the CommandFactory to return a command that raises a StandardError
|
|
170
|
+
fake_command = Class.new do
|
|
171
|
+
def initialize(_cli) = nil
|
|
172
|
+
def execute(_args) = raise(StandardError, 'Unexpected error in subcommand')
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
allow(SimpleCovMcp::Commands::CommandFactory).to receive(:create)
|
|
176
|
+
.and_return(fake_command.new(nil))
|
|
177
|
+
|
|
178
|
+
_out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage', 'summary',
|
|
179
|
+
'lib/foo.rb')
|
|
180
|
+
expect(status).to eq(1)
|
|
181
|
+
expect(err).to include('Unexpected error in subcommand')
|
|
182
|
+
end
|
|
183
|
+
end
|
|
161
184
|
end
|