simplecov-mcp 1.0.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +98 -50
- data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
- data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
- data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
- data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
- data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
- data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
- data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
- data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
- data/docs/user/README.md +14 -0
- data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/simplecov-mcp +1 -1
- data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
- data/lib/simplecov_mcp/app_context.rb +1 -1
- data/lib/simplecov_mcp/base_tool.rb +66 -38
- data/lib/simplecov_mcp/cli.rb +67 -123
- data/lib/simplecov_mcp/commands/base_command.rb +16 -27
- data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
- data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
- data/lib/simplecov_mcp/commands/list_command.rb +1 -1
- data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
- data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
- data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
- data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
- data/lib/simplecov_mcp/commands/version_command.rb +19 -4
- data/lib/simplecov_mcp/config_parser.rb +32 -0
- data/lib/simplecov_mcp/constants.rb +3 -3
- data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
- data/lib/simplecov_mcp/error_handler.rb +81 -40
- data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
- data/lib/simplecov_mcp/errors.rb +12 -19
- data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
- data/lib/simplecov_mcp/formatters.rb +51 -0
- data/lib/simplecov_mcp/mcp_server.rb +9 -7
- data/lib/simplecov_mcp/mode_detector.rb +6 -5
- data/lib/simplecov_mcp/model.rb +122 -88
- data/lib/simplecov_mcp/option_normalizers.rb +39 -18
- data/lib/simplecov_mcp/option_parser_builder.rb +85 -72
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
- data/lib/simplecov_mcp/path_relativizer.rb +17 -14
- data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
- data/lib/simplecov_mcp/resultset_loader.rb +20 -25
- data/lib/simplecov_mcp/staleness_checker.rb +50 -46
- data/lib/simplecov_mcp/table_formatter.rb +64 -0
- data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
- data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
- data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
- data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
- data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
- data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
- data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
- data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
- data/lib/simplecov_mcp/util.rb +18 -12
- data/lib/simplecov_mcp/version.rb +1 -1
- data/lib/simplecov_mcp.rb +23 -29
- data/spec/all_files_coverage_tool_spec.rb +4 -3
- data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
- data/spec/base_tool_spec.rb +17 -14
- data/spec/cli/show_default_report_spec.rb +2 -2
- data/spec/cli_enumerated_options_spec.rb +31 -9
- data/spec/cli_error_spec.rb +46 -23
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +11 -63
- data/spec/cli_spec.rb +82 -97
- data/spec/cli_usage_spec.rb +15 -15
- data/spec/commands/base_command_spec.rb +12 -92
- data/spec/commands/command_factory_spec.rb +7 -3
- data/spec/commands/detailed_command_spec.rb +10 -24
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +43 -20
- data/spec/commands/summary_command_spec.rb +10 -23
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +29 -23
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +3 -3
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +21 -10
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +120 -4
- data/spec/error_mode_spec.rb +18 -22
- data/spec/errors_edge_cases_spec.rb +101 -28
- data/spec/errors_stale_spec.rb +34 -0
- data/spec/file_based_mcp_tools_spec.rb +6 -6
- data/spec/fixtures/project1/lib/bar.rb +2 -0
- data/spec/fixtures/project1/lib/foo.rb +2 -0
- data/spec/help_tool_spec.rb +2 -18
- data/spec/integration_spec.rb +103 -161
- data/spec/logging_fallback_spec.rb +3 -3
- data/spec/mcp_server_integration_spec.rb +1 -1
- data/spec/mcp_server_spec.rb +70 -53
- data/spec/mode_detector_spec.rb +46 -41
- data/spec/model_error_handling_spec.rb +139 -78
- data/spec/model_staleness_spec.rb +13 -13
- data/spec/option_normalizers_spec.rb +111 -112
- data/spec/option_parsers/env_options_parser_spec.rb +25 -37
- data/spec/option_parsers/error_helper_spec.rb +56 -56
- data/spec/path_relativizer_spec.rb +15 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
- data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
- data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
- data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/simple_cov_mcp_module_spec.rb +24 -3
- data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
- data/spec/simplecov_mcp/formatters_spec.rb +76 -0
- data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/simplecov_mcp_model_spec.rb +97 -47
- data/spec/simplecov_mcp_opts_spec.rb +42 -39
- data/spec/spec_helper.rb +27 -92
- data/spec/staleness_checker_spec.rb +10 -9
- data/spec/staleness_more_spec.rb +4 -4
- data/spec/support/cli_helpers.rb +22 -0
- data/spec/support/control_flow_helpers.rb +20 -0
- data/spec/support/fake_mcp.rb +40 -0
- data/spec/support/io_helpers.rb +29 -0
- data/spec/support/mcp_helpers.rb +35 -0
- data/spec/support/mcp_runner.rb +10 -8
- data/spec/support/mocking_helpers.rb +30 -0
- data/spec/table_format_spec.rb +70 -0
- data/spec/tools/validate_tool_spec.rb +132 -0
- data/spec/tools_error_handling_spec.rb +34 -48
- data/spec/util_spec.rb +5 -4
- data/spec/version_spec.rb +7 -7
- data/spec/version_tool_spec.rb +20 -22
- metadata +90 -23
- data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
- data/docs/CLI_USAGE.md +0 -637
- data/docs/EXAMPLES.md +0 -430
- data/docs/INSTALLATION.md +0 -352
- data/spec/cli_success_predicate_spec.rb +0 -141
data/spec/staleness_more_spec.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
|
-
RSpec.describe
|
|
5
|
+
RSpec.describe SimpleCovMcp do
|
|
6
6
|
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
7
7
|
|
|
8
8
|
describe SimpleCovMcp::CoverageModel do
|
|
@@ -11,7 +11,7 @@ RSpec.describe 'Additional staleness cases' do
|
|
|
11
11
|
mock_resultset_with_timestamp(root, Time.now.to_i, coverage: {
|
|
12
12
|
File.join(root, 'lib', 'bar.rb') => { 'lines' => [1, 1] } # 2 entries vs 3 lines in source
|
|
13
13
|
})
|
|
14
|
-
model =
|
|
14
|
+
model = described_class.new(root: root, resultset: 'coverage', staleness: :error)
|
|
15
15
|
# bar.rb has 2 coverage entries but 3 source lines in fixtures
|
|
16
16
|
expect do
|
|
17
17
|
model.summary_for('lib/bar.rb')
|
|
@@ -21,7 +21,7 @@ RSpec.describe 'Additional staleness cases' do
|
|
|
21
21
|
|
|
22
22
|
describe SimpleCovMcp::StalenessChecker do
|
|
23
23
|
it 'flags deleted files present only in coverage' do
|
|
24
|
-
checker = described_class.new(root: root, resultset: 'coverage', mode:
|
|
24
|
+
checker = described_class.new(root: root, resultset: 'coverage', mode: :error,
|
|
25
25
|
timestamp: Time.now.to_i)
|
|
26
26
|
coverage_map = {
|
|
27
27
|
File.join(root, 'lib', 'does_not_exist_anymore.rb') => { 'lines' => [1] }
|
|
@@ -32,7 +32,7 @@ RSpec.describe 'Additional staleness cases' do
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
it 'does not raise for empty tracked_globs when nothing else is stale' do
|
|
35
|
-
checker = described_class.new(root: root, resultset: 'coverage', mode:
|
|
35
|
+
checker = described_class.new(root: root, resultset: 'coverage', mode: :error,
|
|
36
36
|
tracked_globs: [], timestamp: Time.now.to_i)
|
|
37
37
|
expect do
|
|
38
38
|
checker.check_project!({})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CLI test helpers
|
|
4
|
+
module CLITestHelpers
|
|
5
|
+
# Run CLI with the given arguments and return [stdout, stderr, exit_status]
|
|
6
|
+
def run_cli_with_status(*argv)
|
|
7
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
|
8
|
+
status = nil
|
|
9
|
+
out_str = err_str = nil
|
|
10
|
+
silence_output do |out, err|
|
|
11
|
+
begin
|
|
12
|
+
cli.run(argv.flatten)
|
|
13
|
+
status = 0
|
|
14
|
+
rescue SystemExit => e
|
|
15
|
+
status = e.status
|
|
16
|
+
end
|
|
17
|
+
out_str = out.string
|
|
18
|
+
err_str = err.string
|
|
19
|
+
end
|
|
20
|
+
[out_str, err_str, status]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers for managing control flow in RSpec tests.
|
|
4
|
+
module ControlFlowHelpers
|
|
5
|
+
# Execute a block that's expected to call exit() without terminating the test.
|
|
6
|
+
# Useful for testing CLI commands that normally exit.
|
|
7
|
+
# Returns the exit status code if exit was called, otherwise returns the block's value.
|
|
8
|
+
#
|
|
9
|
+
# Examples:
|
|
10
|
+
# status = swallow_system_exit { cli.run(['--help']) }
|
|
11
|
+
# expect(status).to eq(0) # --help calls exit(0)
|
|
12
|
+
#
|
|
13
|
+
# result = swallow_system_exit { some_computation }
|
|
14
|
+
# expect(result).to eq(expected_value) # no exit, returns block value
|
|
15
|
+
def swallow_system_exit
|
|
16
|
+
yield
|
|
17
|
+
rescue SystemExit => e
|
|
18
|
+
e.status
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FakeMCP
|
|
4
|
+
# Fake server captures the last created instance so we can assert on the
|
|
5
|
+
# name/version/tools passed in by SimpleCovMcp::MCPServer.
|
|
6
|
+
class Server
|
|
7
|
+
class << self
|
|
8
|
+
attr_accessor :last_instance
|
|
9
|
+
end
|
|
10
|
+
attr_reader :params
|
|
11
|
+
|
|
12
|
+
def initialize(name:, version:, tools:)
|
|
13
|
+
@params = { name: name, version: version, tools: tools }
|
|
14
|
+
self.class.last_instance = self
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Fake stdio transport records whether `open` was called and the server
|
|
19
|
+
# it was initialized with.
|
|
20
|
+
class StdioTransport
|
|
21
|
+
class << self
|
|
22
|
+
attr_accessor :last_instance
|
|
23
|
+
end
|
|
24
|
+
attr_reader :server, :opened
|
|
25
|
+
|
|
26
|
+
def initialize(server)
|
|
27
|
+
@server = server
|
|
28
|
+
@opened = false
|
|
29
|
+
self.class.last_instance = self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def open
|
|
33
|
+
@opened = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def opened?
|
|
37
|
+
@opened
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared test helpers for I/O operations (e.g., capturing stdout/stderr).
|
|
4
|
+
module TestIOHelpers
|
|
5
|
+
# Suppress stdout/stderr within the given block, yielding the StringIOs
|
|
6
|
+
def silence_output
|
|
7
|
+
original_stdout = $stdout
|
|
8
|
+
original_stderr = $stderr
|
|
9
|
+
$stdout = StringIO.new
|
|
10
|
+
$stderr = StringIO.new
|
|
11
|
+
yield $stdout, $stderr
|
|
12
|
+
ensure
|
|
13
|
+
$stdout = original_stdout
|
|
14
|
+
$stderr = original_stderr
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Capture the output of a command execution
|
|
18
|
+
# @param command [SimpleCovMcp::Commands::BaseCommand] The command instance to execute
|
|
19
|
+
# @param args [Array] The arguments to pass to execute
|
|
20
|
+
# @return [String] The captured output
|
|
21
|
+
def capture_command_output(command, args)
|
|
22
|
+
output = nil
|
|
23
|
+
silence_output do |stdout, _stderr|
|
|
24
|
+
command.execute(args.dup)
|
|
25
|
+
output = stdout.string
|
|
26
|
+
end
|
|
27
|
+
output
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# MCP Tool shared examples and helpers
|
|
4
|
+
module MCPToolTestHelpers
|
|
5
|
+
def setup_mcp_response_stub
|
|
6
|
+
# Standardized MCP::Tool::Response stub that works for all tools
|
|
7
|
+
response_class = Class.new do
|
|
8
|
+
attr_reader :payload, :meta
|
|
9
|
+
|
|
10
|
+
def initialize(payload, meta: nil)
|
|
11
|
+
@payload = payload
|
|
12
|
+
@meta = meta
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
stub_const('MCP::Tool::Response', response_class)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def expect_mcp_text_json(response, expected_keys: [])
|
|
19
|
+
item = response.payload.first
|
|
20
|
+
|
|
21
|
+
# Check for a 'text' part
|
|
22
|
+
expect(item['type']).to eq('text')
|
|
23
|
+
expect(item).to have_key('text')
|
|
24
|
+
|
|
25
|
+
# Parse and validate JSON content
|
|
26
|
+
data = JSON.parse(item['text'])
|
|
27
|
+
|
|
28
|
+
# Check for expected keys
|
|
29
|
+
expected_keys.each do |key|
|
|
30
|
+
expect(data).to have_key(key)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
[data, item] # Return for additional custom assertions
|
|
34
|
+
end
|
|
35
|
+
end
|
data/spec/support/mcp_runner.rb
CHANGED
|
@@ -15,9 +15,7 @@ module Spec
|
|
|
15
15
|
# always returns a hash containing those streams plus the `Process::Status`
|
|
16
16
|
# so callers can make assertions without duplicating the boilerplate.
|
|
17
17
|
|
|
18
|
-
module_function
|
|
19
|
-
|
|
20
|
-
def call(requests: nil, input: nil, env: {}, lib_path:, exe_path:, timeout: 5,
|
|
18
|
+
module_function def call(requests: nil, input: nil, env: {}, lib_path:, exe_path:, timeout: 5,
|
|
21
19
|
close_stdin: true)
|
|
22
20
|
payload = build_payload(requests, input)
|
|
23
21
|
|
|
@@ -44,15 +42,19 @@ module Spec
|
|
|
44
42
|
raise "MCP server timed out after #{timeout} seconds"
|
|
45
43
|
end
|
|
46
44
|
|
|
47
|
-
def call_json(request_hash,
|
|
48
|
-
|
|
45
|
+
module_function def call_json(request_hash, input: nil, env: {}, lib_path:, exe_path:,
|
|
46
|
+
timeout: 5, close_stdin: true)
|
|
47
|
+
call(requests: request_hash, input: input, env: env, lib_path: lib_path,
|
|
48
|
+
exe_path: exe_path, timeout: timeout, close_stdin: close_stdin)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
def call_json_stream(request_hashes,
|
|
52
|
-
|
|
51
|
+
module_function def call_json_stream(request_hashes, input: nil, env: {}, lib_path:,
|
|
52
|
+
exe_path:, timeout: 5, close_stdin: true)
|
|
53
|
+
call(requests: Array(request_hashes), input: input, env: env, lib_path: lib_path,
|
|
54
|
+
exe_path: exe_path, timeout: timeout, close_stdin: close_stdin)
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
def build_payload(requests, input)
|
|
57
|
+
module_function def build_payload(requests, input)
|
|
56
58
|
return input unless requests
|
|
57
59
|
|
|
58
60
|
normalized = requests.is_a?(Array) ? requests : [requests]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers for mocking and stubbing objects in RSpec tests.
|
|
4
|
+
module MockingHelpers
|
|
5
|
+
# Stub staleness checking to return a specific value
|
|
6
|
+
# @param value [String, false] The staleness value to return ('L', 'T', 'M', or false)
|
|
7
|
+
def stub_staleness_check(value)
|
|
8
|
+
checker_double = instance_double(SimpleCovMcp::StalenessChecker)
|
|
9
|
+
allow(checker_double).to receive_messages(
|
|
10
|
+
stale_for_file?: value,
|
|
11
|
+
off?: false
|
|
12
|
+
)
|
|
13
|
+
allow(checker_double).to receive(:check_file!)
|
|
14
|
+
allow(SimpleCovMcp::StalenessChecker).to receive(:new).and_return(checker_double)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Stub a presenter with specific payload data
|
|
18
|
+
# @param presenter_class [Class] The presenter class to stub (e.g., SimpleCovMcp::Presenters::CoverageRawPresenter)
|
|
19
|
+
# @param absolute_payload [Hash] The data hash to return from #absolute_payload
|
|
20
|
+
# @param relative_path [String] The path to return from #relative_path
|
|
21
|
+
def mock_presenter(presenter_class, absolute_payload:, relative_path:)
|
|
22
|
+
presenter_double = instance_double(presenter_class)
|
|
23
|
+
allow(presenter_double).to receive_messages(
|
|
24
|
+
absolute_payload: absolute_payload,
|
|
25
|
+
relative_path: relative_path
|
|
26
|
+
)
|
|
27
|
+
allow(presenter_class).to receive(:new).and_return(presenter_double)
|
|
28
|
+
presenter_double
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe SimpleCovMcp::CoverageCLI, 'table format for all commands' do
|
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
7
|
+
|
|
8
|
+
def run_cli(*argv)
|
|
9
|
+
cli = SimpleCovMcp::CoverageCLI.new
|
|
10
|
+
output = nil
|
|
11
|
+
silence_output do |stdout, _stderr|
|
|
12
|
+
cli.send(:run, argv)
|
|
13
|
+
output = stdout.string
|
|
14
|
+
end
|
|
15
|
+
output
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe 'table format consistency' do
|
|
19
|
+
it 'list command produces formatted table' do
|
|
20
|
+
output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table', 'list')
|
|
21
|
+
expect(output).to include('│') # Box drawing character
|
|
22
|
+
expect(output).to include('File')
|
|
23
|
+
expect(output).to include('%')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'summary command produces formatted table' do
|
|
27
|
+
output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table',
|
|
28
|
+
'summary', 'lib/foo.rb')
|
|
29
|
+
expect(output).to include('│') # Box drawing character
|
|
30
|
+
expect(output).to include('File')
|
|
31
|
+
expect(output).to include('%')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'totals command produces formatted table' do
|
|
35
|
+
output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table', 'totals')
|
|
36
|
+
expect(output).to include('│') # Box drawing character
|
|
37
|
+
expect(output).to include('Lines')
|
|
38
|
+
expect(output).to include('%')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'detailed command produces formatted table' do
|
|
42
|
+
output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table',
|
|
43
|
+
'detailed', 'lib/foo.rb')
|
|
44
|
+
expect(output).to include('│') # Box drawing character
|
|
45
|
+
expect(output).to include('Line')
|
|
46
|
+
expect(output).to include('Hits')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'uncovered command produces formatted table' do
|
|
50
|
+
output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table',
|
|
51
|
+
'uncovered', 'lib/bar.rb') # bar.rb has uncovered lines
|
|
52
|
+
expect(output).to include('│') # Box drawing character
|
|
53
|
+
expect(output).to include('Line')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'raw command produces formatted table' do
|
|
57
|
+
output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'table',
|
|
58
|
+
'raw', 'lib/foo.rb')
|
|
59
|
+
expect(output).to include('│') # Box drawing character
|
|
60
|
+
expect(output).to include('Line')
|
|
61
|
+
expect(output).to include('Coverage')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'version command produces formatted table' do
|
|
65
|
+
output = run_cli('--format', 'table', 'version')
|
|
66
|
+
expect(output).to include('│') # Box drawing character
|
|
67
|
+
expect(output).to include('Version')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
require 'simplecov_mcp/tools/validate_tool'
|
|
7
|
+
|
|
8
|
+
RSpec.describe SimpleCovMcp::Tools::ValidateTool do
|
|
9
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
|
10
|
+
let(:resultset) { 'coverage' }
|
|
11
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
setup_mcp_response_stub
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call_tool(**params)
|
|
18
|
+
described_class.call(**params, root: root, resultset: resultset, server_context: server_context)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def response_text(response)
|
|
22
|
+
item = response.payload.first
|
|
23
|
+
item['text']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def with_predicate_file(content, dir: nil)
|
|
27
|
+
Tempfile.create(['predicate', '.rb'], dir) do |file|
|
|
28
|
+
file.write(content)
|
|
29
|
+
file.flush
|
|
30
|
+
yield file
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
shared_examples 'syntax error handling' do |_source, error_message_fragment|
|
|
35
|
+
it 'returns an error for syntax errors' do
|
|
36
|
+
response = call_with_predicate('->(_m) { 1 + }')
|
|
37
|
+
|
|
38
|
+
expect(response_text(response)).to include(error_message_fragment)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
shared_examples 'non-callable handling' do |_source, content|
|
|
43
|
+
it 'returns an error when the predicate is not callable' do
|
|
44
|
+
response = call_with_predicate(content)
|
|
45
|
+
|
|
46
|
+
expect(response_text(response)).to include('Predicate must be callable')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
shared_examples 'false result' do
|
|
51
|
+
it 'returns false when the predicate evaluates to false' do
|
|
52
|
+
response = call_with_predicate('->(_m) { false }')
|
|
53
|
+
|
|
54
|
+
data, = expect_mcp_text_json(response, expected_keys: ['result'])
|
|
55
|
+
expect(data['result']).to be(false)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '.call' do
|
|
60
|
+
context 'with inline code' do
|
|
61
|
+
def call_with_predicate(code)
|
|
62
|
+
call_tool(code: code)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'evaluates the predicate against the coverage model' do
|
|
66
|
+
expect(SimpleCovMcp::CoverageModel).to receive(:new).and_call_original
|
|
67
|
+
|
|
68
|
+
# Realistic coverage policy: foo.rb must have at least 50% coverage
|
|
69
|
+
response = call_with_predicate(
|
|
70
|
+
'->(m) { m.all_files.detect { |f| f["file"].include?("foo.rb") }["percentage"] >= 50.0 }'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
data, = expect_mcp_text_json(response, expected_keys: ['result'])
|
|
74
|
+
expect(data['result']).to be(true)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it_behaves_like 'false result'
|
|
78
|
+
it_behaves_like 'syntax error handling', :code, 'Syntax error in predicate code'
|
|
79
|
+
it_behaves_like 'non-callable handling', :code, '123'
|
|
80
|
+
|
|
81
|
+
it 'returns an error when the predicate raises during execution' do
|
|
82
|
+
response = call_with_predicate("->(_m) { raise 'Boom' }")
|
|
83
|
+
|
|
84
|
+
text = response_text(response)
|
|
85
|
+
expect(text).to include('Error:', 'Boom')
|
|
86
|
+
# Verify it's an error response, not a JSON result
|
|
87
|
+
expect(text).not_to match(/\{"result"/)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
context 'with a predicate file' do
|
|
92
|
+
def call_with_predicate(content)
|
|
93
|
+
with_predicate_file(content) do |file|
|
|
94
|
+
call_tool(file: file.path)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it_behaves_like 'false result'
|
|
99
|
+
it_behaves_like 'syntax error handling', :file, 'Syntax error in predicate file'
|
|
100
|
+
it_behaves_like 'non-callable handling', :file, 'true'
|
|
101
|
+
|
|
102
|
+
it 'expands relative paths from the provided root before evaluation' do
|
|
103
|
+
with_predicate_file('->(_m) { true }', dir: root) do |file|
|
|
104
|
+
relative_path = Pathname.new(file.path).relative_path_from(Pathname.new(root)).to_s
|
|
105
|
+
allow(SimpleCovMcp::PredicateEvaluator)
|
|
106
|
+
.to receive(:evaluate_file)
|
|
107
|
+
.and_return(true)
|
|
108
|
+
|
|
109
|
+
response = call_tool(file: relative_path)
|
|
110
|
+
|
|
111
|
+
expect(SimpleCovMcp::PredicateEvaluator)
|
|
112
|
+
.to have_received(:evaluate_file)
|
|
113
|
+
.with(file.path, kind_of(SimpleCovMcp::CoverageModel))
|
|
114
|
+
data, = expect_mcp_text_json(response, expected_keys: ['result'])
|
|
115
|
+
expect(data['result']).to be(true)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'returns an error when the predicate file is missing' do
|
|
120
|
+
response = call_tool(file: 'missing_predicate.rb')
|
|
121
|
+
|
|
122
|
+
expect(response_text(response)).to include('Predicate file not found')
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'returns an error when neither code nor file is provided' do
|
|
127
|
+
response = call_tool
|
|
128
|
+
|
|
129
|
+
expect(response_text(response)).to include("Either 'code' or 'file' must be provided")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -7,60 +7,30 @@ require 'simplecov_mcp/tools/coverage_summary_tool'
|
|
|
7
7
|
require 'simplecov_mcp/tools/coverage_raw_tool'
|
|
8
8
|
require 'simplecov_mcp/tools/uncovered_lines_tool'
|
|
9
9
|
require 'simplecov_mcp/tools/coverage_detailed_tool'
|
|
10
|
+
require 'simplecov_mcp/tools/coverage_totals_tool'
|
|
10
11
|
|
|
11
|
-
RSpec.describe
|
|
12
|
+
RSpec.describe SimpleCovMcp::Tools do
|
|
12
13
|
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
13
14
|
|
|
14
15
|
before do
|
|
15
16
|
setup_mcp_response_stub
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
#
|
|
19
|
+
# NOTE: VersionTool error handling is difficult to test because the tool is so simple
|
|
19
20
|
# and doesn't have any complex logic that could fail. The rescue clause in the tool
|
|
20
21
|
# exists for consistency with other tools but is unlikely to be triggered in practice.
|
|
21
22
|
|
|
22
23
|
describe SimpleCovMcp::Tools::HelpTool do
|
|
23
|
-
it '
|
|
24
|
-
|
|
25
|
-
allow(described_class).to receive(:filter_entries).and_raise(StandardError, 'Filter error')
|
|
24
|
+
it 'returns tool information without errors' do
|
|
25
|
+
response = described_class.call(error_mode: 'log', server_context: server_context)
|
|
26
26
|
|
|
27
|
-
response = described_class.call(query: 'test', error_mode: 'on',
|
|
28
|
-
server_context: server_context)
|
|
29
|
-
|
|
30
|
-
# Should return error response
|
|
31
27
|
expect(response).to be_a(MCP::Tool::Response)
|
|
32
28
|
item = response.payload.first
|
|
33
29
|
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
30
|
|
|
46
|
-
|
|
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'])
|
|
31
|
+
data = JSON.parse(item['text'])
|
|
63
32
|
expect(data).to have_key('tools')
|
|
33
|
+
expect(data['tools']).not_to be_empty
|
|
64
34
|
end
|
|
65
35
|
end
|
|
66
36
|
|
|
@@ -70,7 +40,7 @@ RSpec.describe 'MCP Tool error handling' do
|
|
|
70
40
|
|
|
71
41
|
response = described_class.call(
|
|
72
42
|
path: 'lib/foo.rb',
|
|
73
|
-
error_mode: '
|
|
43
|
+
error_mode: 'log',
|
|
74
44
|
server_context: server_context
|
|
75
45
|
)
|
|
76
46
|
|
|
@@ -78,7 +48,7 @@ RSpec.describe 'MCP Tool error handling' do
|
|
|
78
48
|
expect(response).to be_a(MCP::Tool::Response)
|
|
79
49
|
item = response.payload.first
|
|
80
50
|
expect(item[:type] || item['type']).to eq('text')
|
|
81
|
-
expect(item[
|
|
51
|
+
expect(item['text']).to include('Error')
|
|
82
52
|
end
|
|
83
53
|
end
|
|
84
54
|
|
|
@@ -88,9 +58,9 @@ RSpec.describe 'MCP Tool error handling' do
|
|
|
88
58
|
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
|
89
59
|
allow(model).to receive(:raw_for).and_raise(StandardError, 'Raw data error')
|
|
90
60
|
|
|
91
|
-
response =
|
|
61
|
+
response = described_class.call(
|
|
92
62
|
path: 'lib/foo.rb',
|
|
93
|
-
error_mode: '
|
|
63
|
+
error_mode: 'log',
|
|
94
64
|
server_context: server_context
|
|
95
65
|
)
|
|
96
66
|
|
|
@@ -98,7 +68,7 @@ RSpec.describe 'MCP Tool error handling' do
|
|
|
98
68
|
expect(response).to be_a(MCP::Tool::Response)
|
|
99
69
|
item = response.payload.first
|
|
100
70
|
expect(item[:type] || item['type']).to eq('text')
|
|
101
|
-
expect(item[
|
|
71
|
+
expect(item['text']).to include('Error')
|
|
102
72
|
end
|
|
103
73
|
end
|
|
104
74
|
|
|
@@ -108,9 +78,9 @@ RSpec.describe 'MCP Tool error handling' do
|
|
|
108
78
|
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
|
109
79
|
allow(model).to receive(:uncovered_for).and_raise(StandardError, 'Uncovered error')
|
|
110
80
|
|
|
111
|
-
response =
|
|
81
|
+
response = described_class.call(
|
|
112
82
|
path: 'lib/foo.rb',
|
|
113
|
-
error_mode: '
|
|
83
|
+
error_mode: 'log',
|
|
114
84
|
server_context: server_context
|
|
115
85
|
)
|
|
116
86
|
|
|
@@ -118,7 +88,7 @@ RSpec.describe 'MCP Tool error handling' do
|
|
|
118
88
|
expect(response).to be_a(MCP::Tool::Response)
|
|
119
89
|
item = response.payload.first
|
|
120
90
|
expect(item[:type] || item['type']).to eq('text')
|
|
121
|
-
expect(item[
|
|
91
|
+
expect(item['text']).to include('Error')
|
|
122
92
|
end
|
|
123
93
|
end
|
|
124
94
|
|
|
@@ -128,9 +98,9 @@ RSpec.describe 'MCP Tool error handling' do
|
|
|
128
98
|
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
|
129
99
|
allow(model).to receive(:detailed_for).and_raise(StandardError, 'Detailed error')
|
|
130
100
|
|
|
131
|
-
response =
|
|
101
|
+
response = described_class.call(
|
|
132
102
|
path: 'lib/foo.rb',
|
|
133
|
-
error_mode: '
|
|
103
|
+
error_mode: 'log',
|
|
134
104
|
server_context: server_context
|
|
135
105
|
)
|
|
136
106
|
|
|
@@ -138,7 +108,23 @@ RSpec.describe 'MCP Tool error handling' do
|
|
|
138
108
|
expect(response).to be_a(MCP::Tool::Response)
|
|
139
109
|
item = response.payload.first
|
|
140
110
|
expect(item[:type] || item['type']).to eq('text')
|
|
141
|
-
expect(item[
|
|
111
|
+
expect(item['text']).to include('Error')
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe SimpleCovMcp::Tools::CoverageTotalsTool do
|
|
116
|
+
it 'handles errors during totals calculation' do
|
|
117
|
+
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_raise(StandardError, 'Model error')
|
|
118
|
+
|
|
119
|
+
response = described_class.call(
|
|
120
|
+
error_mode: 'log',
|
|
121
|
+
server_context: server_context
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
expect(response).to be_a(MCP::Tool::Response)
|
|
125
|
+
item = response.payload.first
|
|
126
|
+
expect(item[:type] || item['type']).to eq('text')
|
|
127
|
+
expect(item['text']).to include('Error')
|
|
142
128
|
end
|
|
143
129
|
end
|
|
144
130
|
end
|
data/spec/util_spec.rb
CHANGED
|
@@ -35,11 +35,12 @@ RSpec.describe SimpleCovMcp::CovUtil do
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
it 'summary handles edge cases and coercion' do
|
|
38
|
-
expect(described_class.summary([]))
|
|
38
|
+
expect(described_class.summary([]))
|
|
39
|
+
.to include('percentage' => 100.0, 'total' => 0, 'covered' => 0)
|
|
39
40
|
expect(described_class.summary([nil, nil]))
|
|
40
|
-
.to include('
|
|
41
|
+
.to include('percentage' => 100.0, 'total' => 0, 'covered' => 0)
|
|
41
42
|
expect(described_class.summary(['1', '0', nil]))
|
|
42
|
-
.to include('
|
|
43
|
+
.to include('percentage' => 50.0, 'total' => 2, 'covered' => 1)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
it 'uncovered and detailed ignore nils' do
|
|
@@ -65,7 +66,7 @@ RSpec.describe SimpleCovMcp::CovUtil do
|
|
|
65
66
|
describe 'logging configuration' do
|
|
66
67
|
let(:test_message) { 'test log message' }
|
|
67
68
|
|
|
68
|
-
around
|
|
69
|
+
around do |example|
|
|
69
70
|
# Reset logging settings so each example starts clean.
|
|
70
71
|
old_default = SimpleCovMcp.default_log_file
|
|
71
72
|
old_active = SimpleCovMcp.active_log_file
|