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
|
@@ -5,6 +5,7 @@ require 'fileutils'
|
|
|
5
5
|
|
|
6
6
|
RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
7
7
|
let(:tmpdir) { Dir.mktmpdir('scmcp-stale') }
|
|
8
|
+
|
|
8
9
|
after { FileUtils.remove_entry(tmpdir) if tmpdir && File.directory?(tmpdir) }
|
|
9
10
|
|
|
10
11
|
def write_file(path, lines)
|
|
@@ -57,8 +58,8 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
|
57
58
|
end
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
context '
|
|
61
|
-
|
|
61
|
+
context 'when computing file staleness details' do
|
|
62
|
+
it_behaves_like 'a staleness check',
|
|
62
63
|
description: 'detects newer file vs coverage timestamp',
|
|
63
64
|
file_lines: ['a', 'b'],
|
|
64
65
|
coverage_lines: [1, 1],
|
|
@@ -75,7 +76,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
|
75
76
|
expected_stale_char: 'T',
|
|
76
77
|
expected_error: SimpleCovMcp::CoverageDataStaleError
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
it_behaves_like 'a staleness check',
|
|
79
80
|
description: 'detects length mismatch between source and coverage',
|
|
80
81
|
file_lines: ['a', 'b', 'c', 'd'],
|
|
81
82
|
coverage_lines: [1, 1],
|
|
@@ -92,7 +93,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
|
92
93
|
expected_stale_char: 'L',
|
|
93
94
|
expected_error: SimpleCovMcp::CoverageDataStaleError
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
it_behaves_like 'a staleness check',
|
|
96
97
|
description: 'treats missing file as stale',
|
|
97
98
|
file_lines: nil,
|
|
98
99
|
coverage_lines: [1, 1, 1],
|
|
@@ -107,7 +108,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
|
107
108
|
expected_stale_char: 'M',
|
|
108
109
|
expected_error: SimpleCovMcp::CoverageDataStaleError
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
it_behaves_like 'a staleness check',
|
|
111
112
|
description: 'is not stale when timestamps and lengths match',
|
|
112
113
|
file_lines: ['a', 'b', 'c'],
|
|
113
114
|
coverage_lines: [1, 0, nil],
|
|
@@ -123,7 +124,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
|
123
124
|
expected_error: nil
|
|
124
125
|
end
|
|
125
126
|
|
|
126
|
-
context 'missing_trailing_newline? edge cases' do
|
|
127
|
+
context 'when handling missing_trailing_newline? edge cases' do
|
|
127
128
|
let(:checker) do
|
|
128
129
|
described_class.new(root: tmpdir, resultset: nil, mode: 'off', timestamp: Time.now)
|
|
129
130
|
end
|
|
@@ -192,7 +193,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
|
192
193
|
end
|
|
193
194
|
end
|
|
194
195
|
|
|
195
|
-
context 'line count
|
|
196
|
+
context 'when adjusting line count with missing trailing newline' do
|
|
196
197
|
let(:checker) do
|
|
197
198
|
described_class.new(root: tmpdir, resultset: nil, mode: 'off', timestamp: Time.now)
|
|
198
199
|
end
|
|
@@ -246,7 +247,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
|
246
247
|
end
|
|
247
248
|
end
|
|
248
249
|
|
|
249
|
-
context 'safe_count_lines edge cases' do
|
|
250
|
+
context 'when handling safe_count_lines edge cases' do
|
|
250
251
|
let(:checker) do
|
|
251
252
|
described_class.new(root: tmpdir, resultset: nil, mode: 'off', timestamp: Time.now)
|
|
252
253
|
end
|
|
@@ -287,7 +288,7 @@ RSpec.describe SimpleCovMcp::StalenessChecker do
|
|
|
287
288
|
end
|
|
288
289
|
end
|
|
289
290
|
|
|
290
|
-
context 'rel
|
|
291
|
+
context 'when rel has path prefix mismatches' do
|
|
291
292
|
let(:checker) do
|
|
292
293
|
described_class.new(root: tmpdir, resultset: nil, mode: 'off', timestamp: Time.now)
|
|
293
294
|
end
|
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
|