simplecov-mcp 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +173 -356
- data/docs/ADVANCED_USAGE.md +967 -0
- data/docs/ARCHITECTURE.md +79 -0
- data/docs/BRANCH_ONLY_COVERAGE.md +81 -0
- data/docs/CLI_USAGE.md +637 -0
- data/docs/DEVELOPMENT.md +82 -0
- data/docs/ERROR_HANDLING.md +93 -0
- data/docs/EXAMPLES.md +430 -0
- data/docs/INSTALLATION.md +352 -0
- data/docs/LIBRARY_API.md +635 -0
- data/docs/MCP_INTEGRATION.md +488 -0
- data/docs/TROUBLESHOOTING.md +276 -0
- data/docs/arch-decisions/001-x-arch-decision.md +93 -0
- data/docs/arch-decisions/002-x-arch-decision.md +157 -0
- data/docs/arch-decisions/003-x-arch-decision.md +163 -0
- data/docs/arch-decisions/004-x-arch-decision.md +199 -0
- data/docs/arch-decisions/005-x-arch-decision.md +187 -0
- data/docs/arch-decisions/README.md +60 -0
- data/docs/presentations/simplecov-mcp-presentation.md +249 -0
- data/exe/simplecov-mcp +4 -4
- data/lib/simplecov_mcp/app_context.rb +26 -0
- data/lib/simplecov_mcp/base_tool.rb +74 -0
- data/lib/simplecov_mcp/cli.rb +234 -0
- data/lib/simplecov_mcp/cli_config.rb +56 -0
- data/lib/simplecov_mcp/commands/base_command.rb +78 -0
- data/lib/simplecov_mcp/commands/command_factory.rb +39 -0
- data/lib/simplecov_mcp/commands/detailed_command.rb +24 -0
- data/lib/simplecov_mcp/commands/list_command.rb +13 -0
- data/lib/simplecov_mcp/commands/raw_command.rb +22 -0
- data/lib/simplecov_mcp/commands/summary_command.rb +24 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +26 -0
- data/lib/simplecov_mcp/commands/version_command.rb +18 -0
- data/lib/simplecov_mcp/constants.rb +22 -0
- data/lib/simplecov_mcp/error_handler.rb +124 -0
- data/lib/simplecov_mcp/error_handler_factory.rb +31 -0
- data/lib/simplecov_mcp/errors.rb +179 -0
- data/lib/simplecov_mcp/formatters/source_formatter.rb +148 -0
- data/lib/simplecov_mcp/mcp_server.rb +40 -0
- data/lib/simplecov_mcp/mode_detector.rb +55 -0
- data/lib/simplecov_mcp/model.rb +300 -0
- data/lib/simplecov_mcp/option_normalizers.rb +92 -0
- data/lib/simplecov_mcp/option_parser_builder.rb +134 -0
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +50 -0
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +109 -0
- data/lib/simplecov_mcp/path_relativizer.rb +61 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +44 -0
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +52 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +126 -0
- data/lib/simplecov_mcp/resolvers/resolver_factory.rb +28 -0
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +78 -0
- data/lib/simplecov_mcp/resultset_loader.rb +136 -0
- data/lib/simplecov_mcp/staleness_checker.rb +243 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/all_files_coverage_tool.rb +31 -13
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_detailed_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_raw_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_summary_tool.rb +7 -7
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +90 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/help_tool.rb +13 -4
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/uncovered_lines_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/version_tool.rb +11 -3
- data/lib/simplecov_mcp/util.rb +82 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/version.rb +1 -1
- data/lib/simplecov_mcp.rb +144 -2
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +29 -25
- data/spec/base_tool_spec.rb +11 -10
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_config_spec.rb +137 -0
- data/spec/cli_enumerated_options_spec.rb +68 -0
- data/spec/cli_error_spec.rb +105 -47
- data/spec/cli_source_spec.rb +82 -23
- data/spec/cli_spec.rb +140 -5
- data/spec/cli_success_predicate_spec.rb +141 -0
- data/spec/cli_table_spec.rb +1 -1
- data/spec/cli_usage_spec.rb +10 -26
- data/spec/commands/base_command_spec.rb +187 -0
- data/spec/commands/command_factory_spec.rb +72 -0
- data/spec/commands/detailed_command_spec.rb +48 -0
- data/spec/commands/raw_command_spec.rb +46 -0
- data/spec/commands/summary_command_spec.rb +47 -0
- data/spec/commands/uncovered_command_spec.rb +49 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/coverage_table_tool_spec.rb +17 -33
- data/spec/error_handler_spec.rb +22 -13
- data/spec/error_mode_spec.rb +143 -0
- data/spec/errors_edge_cases_spec.rb +239 -0
- data/spec/errors_stale_spec.rb +2 -2
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +0 -1
- data/spec/fixtures/project1/lib/foo.rb +0 -1
- data/spec/help_tool_spec.rb +11 -17
- data/spec/integration_spec.rb +845 -0
- data/spec/logging_fallback_spec.rb +128 -0
- data/spec/mcp_logging_spec.rb +44 -0
- data/spec/mcp_server_integration_spec.rb +23 -0
- data/spec/mcp_server_spec.rb +15 -4
- data/spec/mode_detector_spec.rb +148 -0
- data/spec/model_error_handling_spec.rb +210 -0
- data/spec/model_staleness_spec.rb +40 -10
- data/spec/option_normalizers_spec.rb +204 -0
- data/spec/option_parsers/env_options_parser_spec.rb +233 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +83 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
- data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
- data/spec/presenters/project_coverage_presenter_spec.rb +86 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +57 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +55 -0
- data/spec/resultset_loader_spec.rb +167 -0
- data/spec/shared_examples/README.md +115 -0
- data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
- data/spec/shared_examples/file_based_mcp_tools.rb +174 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/simple_cov_mcp_module_spec.rb +16 -0
- data/spec/simplecov_mcp_model_spec.rb +340 -9
- data/spec/simplecov_mcp_opts_spec.rb +182 -0
- data/spec/spec_helper.rb +147 -4
- data/spec/staleness_checker_spec.rb +373 -0
- data/spec/staleness_more_spec.rb +16 -13
- data/spec/support/mcp_runner.rb +64 -0
- data/spec/tools_error_handling_spec.rb +144 -0
- data/spec/util_spec.rb +109 -34
- data/spec/version_spec.rb +117 -9
- data/spec/version_tool_spec.rb +131 -10
- metadata +120 -63
- data/lib/simple_cov/mcp.rb +0 -9
- data/lib/simple_cov_mcp/base_tool.rb +0 -70
- data/lib/simple_cov_mcp/cli.rb +0 -390
- data/lib/simple_cov_mcp/error_handler.rb +0 -131
- data/lib/simple_cov_mcp/error_handler_factory.rb +0 -38
- data/lib/simple_cov_mcp/errors.rb +0 -176
- data/lib/simple_cov_mcp/mcp_server.rb +0 -30
- data/lib/simple_cov_mcp/model.rb +0 -104
- data/lib/simple_cov_mcp/staleness_checker.rb +0 -125
- data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +0 -61
- data/lib/simple_cov_mcp/util.rb +0 -122
- data/lib/simple_cov_mcp.rb +0 -102
- data/spec/coverage_detailed_tool_spec.rb +0 -36
- data/spec/coverage_raw_tool_spec.rb +0 -32
- data/spec/coverage_summary_tool_spec.rb +0 -39
- data/spec/legacy_shim_spec.rb +0 -13
- data/spec/uncovered_lines_tool_spec.rb +0 -33
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'resolvers/resolver_factory'
|
4
|
+
|
5
|
+
module SimpleCovMcp
|
6
|
+
RESULTSET_CANDIDATES = [
|
7
|
+
'.resultset.json',
|
8
|
+
'coverage/.resultset.json',
|
9
|
+
'tmp/.resultset.json'
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
DEFAULT_LOG_FILESPEC = './simplecov_mcp.log'
|
13
|
+
|
14
|
+
module CovUtil
|
15
|
+
module_function
|
16
|
+
|
17
|
+
def log(msg)
|
18
|
+
log_file = SimpleCovMcp.active_log_file
|
19
|
+
|
20
|
+
case log_file
|
21
|
+
when 'stdout'
|
22
|
+
$stdout.puts "[#{Time.now.iso8601}] #{msg}"
|
23
|
+
when 'stderr'
|
24
|
+
$stderr.puts "[#{Time.now.iso8601}] #{msg}"
|
25
|
+
else
|
26
|
+
# Handles both nil (default) and custom file paths
|
27
|
+
path_to_log = log_file || DEFAULT_LOG_FILESPEC
|
28
|
+
File.open(File.expand_path(path_to_log), 'a') { |f| f.puts "[#{Time.now.iso8601}] #{msg}" }
|
29
|
+
end
|
30
|
+
rescue StandardError => e
|
31
|
+
# Fallback to stderr if file logging fails, but suppress in MCP mode
|
32
|
+
# to avoid interfering with JSON-RPC protocol
|
33
|
+
unless SimpleCovMcp.context.mcp_mode?
|
34
|
+
begin
|
35
|
+
$stderr.puts "[#{Time.now.iso8601}] LOGGING ERROR: #{e.message}"
|
36
|
+
$stderr.puts "[#{Time.now.iso8601}] #{msg}"
|
37
|
+
rescue StandardError
|
38
|
+
# Silently ignore only stderr fallback failures
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_resultset(root, resultset: nil)
|
44
|
+
Resolvers::ResolverFactory.find_resultset(root, resultset: resultset)
|
45
|
+
end
|
46
|
+
|
47
|
+
def lookup_lines(cov, file_abs)
|
48
|
+
Resolvers::ResolverFactory.lookup_lines(cov, file_abs)
|
49
|
+
end
|
50
|
+
|
51
|
+
def summary(arr)
|
52
|
+
total = 0
|
53
|
+
covered = 0
|
54
|
+
arr.compact.each do |hits|
|
55
|
+
total += 1
|
56
|
+
covered += 1 if hits.to_i > 0
|
57
|
+
end
|
58
|
+
pct = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
|
59
|
+
{ 'covered' => covered, 'total' => total, 'pct' => pct }
|
60
|
+
end
|
61
|
+
|
62
|
+
def uncovered(arr)
|
63
|
+
out = []
|
64
|
+
|
65
|
+
arr.each_with_index do |hits, i|
|
66
|
+
next if hits.nil?
|
67
|
+
|
68
|
+
out << (i + 1) if hits.to_i.zero?
|
69
|
+
end
|
70
|
+
out
|
71
|
+
end
|
72
|
+
|
73
|
+
def detailed(arr)
|
74
|
+
rows = []
|
75
|
+
arr.each_with_index do |hits, i|
|
76
|
+
h = hits&.to_i
|
77
|
+
rows << { 'line' => i + 1, 'hits' => h, 'covered' => h.positive? } if h
|
78
|
+
end
|
79
|
+
rows
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/simplecov_mcp.rb
CHANGED
@@ -1,4 +1,146 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require '
|
3
|
+
require 'json'
|
4
|
+
require 'time'
|
5
|
+
require 'pathname'
|
6
|
+
require 'optparse'
|
7
|
+
require 'mcp'
|
8
|
+
require 'mcp/server/transports/stdio_transport'
|
9
|
+
|
10
|
+
require_relative 'simplecov_mcp/version'
|
11
|
+
require_relative 'simplecov_mcp/app_context'
|
12
|
+
require_relative 'simplecov_mcp/util'
|
13
|
+
require_relative 'simplecov_mcp/errors'
|
14
|
+
require_relative 'simplecov_mcp/error_handler'
|
15
|
+
require_relative 'simplecov_mcp/error_handler_factory'
|
16
|
+
require_relative 'simplecov_mcp/path_relativizer'
|
17
|
+
require_relative 'simplecov_mcp/resultset_loader'
|
18
|
+
require_relative 'simplecov_mcp/mode_detector'
|
19
|
+
require_relative 'simplecov_mcp/model'
|
20
|
+
require_relative 'simplecov_mcp/base_tool'
|
21
|
+
require_relative 'simplecov_mcp/tools/coverage_raw_tool'
|
22
|
+
require_relative 'simplecov_mcp/tools/coverage_summary_tool'
|
23
|
+
require_relative 'simplecov_mcp/tools/uncovered_lines_tool'
|
24
|
+
require_relative 'simplecov_mcp/tools/coverage_detailed_tool'
|
25
|
+
require_relative 'simplecov_mcp/tools/all_files_coverage_tool'
|
26
|
+
require_relative 'simplecov_mcp/tools/coverage_table_tool'
|
27
|
+
require_relative 'simplecov_mcp/tools/version_tool'
|
28
|
+
require_relative 'simplecov_mcp/tools/help_tool'
|
29
|
+
require_relative 'simplecov_mcp/mcp_server'
|
30
|
+
require_relative 'simplecov_mcp/cli'
|
31
|
+
|
32
|
+
module SimpleCovMcp
|
33
|
+
class << self
|
34
|
+
THREAD_CONTEXT_KEY = :simplecov_mcp_context
|
35
|
+
|
36
|
+
def run(argv)
|
37
|
+
# Parse environment options for mode detection
|
38
|
+
env_opts = parse_env_opts_for_mode_detection
|
39
|
+
full_argv = env_opts + argv
|
40
|
+
|
41
|
+
if ModeDetector.cli_mode?(full_argv)
|
42
|
+
CoverageCLI.new.run(argv) # CLI will re-parse env opts internally
|
43
|
+
else
|
44
|
+
log_file = parse_log_file(full_argv)
|
45
|
+
|
46
|
+
if log_file == 'stdout'
|
47
|
+
raise ConfigurationError,
|
48
|
+
'Logging to stdout is not permitted in MCP server mode as it interferes with ' \
|
49
|
+
"the JSON-RPC protocol. Please use 'stderr' or a file path."
|
50
|
+
end
|
51
|
+
|
52
|
+
handler = ErrorHandlerFactory.for_mcp_server
|
53
|
+
context = create_context(error_handler: handler, log_target: log_file, mode: :mcp_server)
|
54
|
+
with_context(context) { MCPServer.new(context: context).run }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def with_context(context)
|
59
|
+
previous = Thread.current[THREAD_CONTEXT_KEY]
|
60
|
+
Thread.current[THREAD_CONTEXT_KEY] = context
|
61
|
+
yield
|
62
|
+
ensure
|
63
|
+
Thread.current[THREAD_CONTEXT_KEY] = previous
|
64
|
+
end
|
65
|
+
|
66
|
+
def context
|
67
|
+
Thread.current[THREAD_CONTEXT_KEY] || default_context
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_context(error_handler:, log_target: nil, mode: :library)
|
71
|
+
AppContext.new(
|
72
|
+
error_handler: error_handler,
|
73
|
+
log_target: log_target.nil? ? default_context.log_target : log_target,
|
74
|
+
mode: mode
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def default_log_file
|
79
|
+
default_context.log_target
|
80
|
+
end
|
81
|
+
|
82
|
+
def default_log_file=(value)
|
83
|
+
previous_default = default_context
|
84
|
+
@default_context = previous_default.with_log_target(value)
|
85
|
+
active = Thread.current[THREAD_CONTEXT_KEY]
|
86
|
+
if active.nil? || active.log_target == previous_default.log_target
|
87
|
+
Thread.current[THREAD_CONTEXT_KEY] = @default_context
|
88
|
+
end
|
89
|
+
value
|
90
|
+
end
|
91
|
+
|
92
|
+
def active_log_file
|
93
|
+
context.log_target
|
94
|
+
end
|
95
|
+
|
96
|
+
def active_log_file=(value)
|
97
|
+
current = Thread.current[THREAD_CONTEXT_KEY]
|
98
|
+
if current
|
99
|
+
Thread.current[THREAD_CONTEXT_KEY] = current.with_log_target(value)
|
100
|
+
else
|
101
|
+
Thread.current[THREAD_CONTEXT_KEY] = default_context.with_log_target(value)
|
102
|
+
end
|
103
|
+
value
|
104
|
+
end
|
105
|
+
|
106
|
+
def error_handler
|
107
|
+
context.error_handler
|
108
|
+
end
|
109
|
+
|
110
|
+
def error_handler=(handler)
|
111
|
+
@default_context = default_context.with_error_handler(handler)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def default_context
|
117
|
+
@default_context ||= AppContext.new(
|
118
|
+
error_handler: ErrorHandlerFactory.for_cli,
|
119
|
+
log_target: nil
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
def parse_log_file(argv)
|
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
|
135
|
+
require 'shellwords'
|
136
|
+
opts_string = ENV['SIMPLECOV_MCP_OPTS']
|
137
|
+
return [] unless opts_string && !opts_string.empty?
|
138
|
+
|
139
|
+
begin
|
140
|
+
Shellwords.split(opts_string)
|
141
|
+
rescue ArgumentError
|
142
|
+
[] # Ignore parsing errors for mode detection
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# MCP Server Protocol Integration Tests
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
This document describes the comprehensive integration tests added for the SimpleCov MCP server protocol in `spec/integration_spec.rb`.
|
6
|
+
|
7
|
+
## Test Coverage
|
8
|
+
|
9
|
+
The integration tests spawn the actual MCP server as a subprocess and communicate with it via JSON-RPC over stdio, testing the complete end-to-end protocol implementation.
|
10
|
+
|
11
|
+
### Tests Added (12 total)
|
12
|
+
|
13
|
+
1. **starts MCP server without errors** - Verifies the server starts and responds to basic requests without NameError or other initialization issues
|
14
|
+
2. **handles tools/list request** - Confirms all 8 expected tools are properly registered
|
15
|
+
3. **executes coverage_summary_tool via JSON-RPC** - Tests single-file coverage summary queries
|
16
|
+
4. **executes all_files_coverage_tool via JSON-RPC** - Tests project-wide coverage listing
|
17
|
+
5. **executes uncovered_lines_tool via JSON-RPC** - Tests uncovered line detection
|
18
|
+
6. **executes help_tool via JSON-RPC** - Tests help/documentation retrieval
|
19
|
+
7. **executes version_tool via JSON-RPC** - Tests version information queries
|
20
|
+
8. **handles error responses for invalid tool calls** - Verifies graceful error handling
|
21
|
+
9. **handles malformed JSON-RPC requests** - Tests robustness against invalid input
|
22
|
+
10. **respects --log-file configuration in MCP mode** - Tests logging configuration
|
23
|
+
11. **prohibits stdout logging in MCP mode** - Ensures stdout isn't corrupted
|
24
|
+
12. **handles multiple sequential requests** - Tests statelessness and multi-request handling
|
25
|
+
|
26
|
+
## Why These Tests Are Critical
|
27
|
+
|
28
|
+
### Issue #1 from Analysis: Missing `require 'optparse'`
|
29
|
+
|
30
|
+
The critical bug (missing `require 'optparse'` in `lib/simplecov_mcp.rb:110`) was not caught by existing tests because:
|
31
|
+
|
32
|
+
- Unit tests loaded the full gem which transitively required optparse through the CLI
|
33
|
+
- MCP tools were tested in-process without spawning the server
|
34
|
+
- No integration tests verified the MCP server startup sequence
|
35
|
+
|
36
|
+
### What These Tests Catch
|
37
|
+
|
38
|
+
* ✅ **Server Initialization Errors**: NameError, LoadError, missing requires
|
39
|
+
* ✅ **Protocol Compliance**: Valid JSON-RPC request/response format
|
40
|
+
* ✅ **Tool Registration**: All tools properly configured and accessible
|
41
|
+
* ✅ **Data Accuracy**: Coverage data correctly passed from fixtures
|
42
|
+
* ✅ **Error Handling**: Graceful responses for invalid requests
|
43
|
+
* ✅ **Configuration**: Environment variables and options properly handled
|
44
|
+
* ✅ **Statelessness**: Multiple requests handled independently
|
45
|
+
* ✅ **Stream Integrity**: Stdout not corrupted by logging
|
46
|
+
|
47
|
+
## Test Architecture
|
48
|
+
|
49
|
+
### Helper Methods
|
50
|
+
|
51
|
+
- **`run_mcp_request(request_hash, timeout: 5)`**: Spawns MCP server, sends JSON-RPC request, returns stdout/stderr/status
|
52
|
+
- **`parse_jsonrpc_response(output)`**: Extracts JSON-RPC response from output (handles mixed stderr/stdout)
|
53
|
+
|
54
|
+
### Test Fixtures
|
55
|
+
|
56
|
+
Uses `spec/fixtures/project1/` with known coverage data:
|
57
|
+
- `lib/foo.rb`: 66.67% coverage (2/3 lines, line 2 uncovered)
|
58
|
+
- `lib/bar.rb`: 33.33% coverage (1/3 lines)
|
59
|
+
|
60
|
+
### Test Execution
|
61
|
+
|
62
|
+
```bash
|
63
|
+
# Run all MCP integration tests
|
64
|
+
bundle exec rspec spec/integration_spec.rb --tag slow
|
65
|
+
|
66
|
+
# Run specific integration test
|
67
|
+
bundle exec rspec spec/integration_spec.rb:363
|
68
|
+
```
|
69
|
+
|
70
|
+
## Performance
|
71
|
+
|
72
|
+
- Total execution time: ~2.1 seconds for all 12 tests
|
73
|
+
- Tagged with `:slow` to allow exclusion from quick test runs
|
74
|
+
- Uses `Open3.popen3` for subprocess management
|
75
|
+
- 5-second timeout per request (configurable)
|
76
|
+
|
77
|
+
## Coverage Impact
|
78
|
+
|
79
|
+
These tests increased the overall test count from 272 to 284 examples and improved confidence in the MCP server mode, which is the primary use case for AI assistant integration.
|
80
|
+
|
81
|
+
### Before Integration Tests
|
82
|
+
- 272 examples
|
83
|
+
- Missing `require 'optparse'` bug went undetected
|
84
|
+
- MCP server mode untested end-to-end
|
85
|
+
|
86
|
+
### After Integration Tests
|
87
|
+
- 284 examples
|
88
|
+
- MCP server startup verified
|
89
|
+
- Full JSON-RPC protocol tested
|
90
|
+
- Would catch Issue #1 immediately
|
91
|
+
|
92
|
+
## Future Enhancements
|
93
|
+
|
94
|
+
Potential additions:
|
95
|
+
- Test connection lifecycle (startup, multiple sessions, shutdown)
|
96
|
+
- Test concurrent requests (if supported)
|
97
|
+
- Test large coverage datasets (performance)
|
98
|
+
- Test network transport (if added)
|
99
|
+
- Test authentication/authorization (if added)
|
100
|
+
|
101
|
+
## Related Files
|
102
|
+
|
103
|
+
- `spec/integration_spec.rb` - Main integration test file (lines 308-683)
|
104
|
+
- `lib/simplecov_mcp.rb` - Entry point with mode detection
|
105
|
+
- `lib/simplecov_mcp/mcp_server.rb` - MCP server implementation
|
106
|
+
- `exe/simplecov-mcp` - Executable entry point
|
107
|
+
|
108
|
+
## References
|
109
|
+
|
110
|
+
- [MCP Protocol Specification](https://modelcontextprotocol.io/)
|
111
|
+
- [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)
|
data/spec/TIMESTAMPS.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Test Timestamp Documentation
|
2
|
+
|
3
|
+
This document explains the timestamp constants used throughout the test suite for consistent and documented test data.
|
4
|
+
|
5
|
+
## Constants (defined in `spec_helper.rb`)
|
6
|
+
|
7
|
+
### `FIXTURE_COVERAGE_TIMESTAMP = 1_720_000_000`
|
8
|
+
- **Human readable**: 2024-07-03 16:26:40 UTC (July 3rd, 2024)
|
9
|
+
- **Purpose**: The "generated" timestamp for coverage data in `spec/fixtures/project1/coverage/.resultset.json`
|
10
|
+
- **Usage**: Used in tests that verify timestamp parsing and calculations with realistic coverage data
|
11
|
+
|
12
|
+
### `VERY_OLD_TIMESTAMP = 0`
|
13
|
+
- **Human readable**: 1970-01-01 00:00:00 UTC (Unix epoch)
|
14
|
+
- **Purpose**: Simulates extremely stale coverage data (much older than any real file)
|
15
|
+
- **Usage**: Used in staleness tests to force stale coverage scenarios
|
16
|
+
|
17
|
+
### `TEST_FILE_TIMESTAMP = 1_000`
|
18
|
+
- **Human readable**: 1970-01-01 00:16:40 UTC (16 minutes and 40 seconds after epoch)
|
19
|
+
- **Purpose**: Used for stale error formatting tests to create predictable time deltas
|
20
|
+
- **Usage**: Creates a 1000-second (16m 40s) difference from `VERY_OLD_TIMESTAMP` for delta calculations
|
21
|
+
|
22
|
+
## Conversion Reference
|
23
|
+
|
24
|
+
To convert timestamps for debugging:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
# Unix timestamp to human readable
|
28
|
+
date -d @1720000000
|
29
|
+
# Wed Jul 3 16:26:40 UTC 2024
|
30
|
+
|
31
|
+
# Human readable to Unix timestamp
|
32
|
+
date -d "2024-07-03 16:26:40 UTC" +%s
|
33
|
+
# 1720000000
|
34
|
+
```
|
35
|
+
|
36
|
+
## Why These Values?
|
37
|
+
|
38
|
+
- **Realistic but static**: `FIXTURE_COVERAGE_TIMESTAMP` is a realistic recent date that won't change
|
39
|
+
- **Predictable deltas**: The differences between timestamps create predictable test scenarios
|
40
|
+
- **Clear intent**: Named constants make it obvious what each timestamp represents in tests
|
41
|
+
|
42
|
+
## Files Using These Constants
|
43
|
+
|
44
|
+
- `spec/util_spec.rb` - Tests timestamp parsing from fixture
|
45
|
+
- `spec/model_staleness_spec.rb` - Tests staleness detection logic
|
46
|
+
- `spec/errors_stale_spec.rb` - Tests stale error message formatting
|
47
|
+
- `spec/cli_error_spec.rb` - Tests CLI error handling for stale coverage
|
48
|
+
- `spec/fixtures/project1/coverage/.resultset.json` - Contains the actual timestamp data
|
@@ -1,43 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'spec_helper'
|
4
|
-
require '
|
4
|
+
require 'simplecov_mcp/tools/all_files_coverage_tool'
|
5
5
|
|
6
6
|
RSpec.describe SimpleCovMcp::Tools::AllFilesCoverageTool do
|
7
|
-
let(:root) { (
|
7
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
8
8
|
let(:server_context) { instance_double('ServerContext').as_null_object }
|
9
9
|
|
10
10
|
before do
|
11
|
-
|
12
|
-
stub_const('MCP::Tool::Response', Class.new do
|
13
|
-
attr_reader :payload, :meta
|
14
|
-
def initialize(payload, meta: nil)
|
15
|
-
@payload = payload
|
16
|
-
@meta = meta
|
17
|
-
end
|
18
|
-
end)
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'returns files with counts (total/ok/stale) as JSON' do
|
11
|
+
setup_mcp_response_stub
|
22
12
|
model = instance_double(SimpleCovMcp::CoverageModel)
|
23
13
|
allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
|
24
|
-
allow(model).to receive(:all_files).and_return([
|
25
|
-
{ 'file' => "#{root}/lib/foo.rb", 'percentage' => 100.0, 'covered' => 10, 'total' => 10, 'stale' => false },
|
26
|
-
{ 'file' => "#{root}/lib/bar.rb", 'percentage' => 50.0, 'covered' => 5, 'total' => 10, 'stale' => true }
|
27
|
-
])
|
28
14
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
15
|
+
payload = {
|
16
|
+
'files' => [
|
17
|
+
{ 'file' => 'lib/foo.rb', 'percentage' => 100.0, 'covered' => 10, 'total' => 10,
|
18
|
+
'stale' => false },
|
19
|
+
{ 'file' => 'lib/bar.rb', 'percentage' => 50.0, 'covered' => 5, 'total' => 10,
|
20
|
+
'stale' => true }
|
21
|
+
],
|
22
|
+
'counts' => { 'total' => 2, 'ok' => 1, 'stale' => 1 }
|
23
|
+
}
|
24
|
+
|
25
|
+
presenter = instance_double(SimpleCovMcp::Presenters::ProjectCoveragePresenter)
|
26
|
+
allow(SimpleCovMcp::Presenters::ProjectCoveragePresenter).to receive(:new).and_return(presenter)
|
27
|
+
allow(presenter).to receive(:relativized_payload).and_return(payload)
|
28
|
+
end
|
29
|
+
|
30
|
+
subject { described_class.call(root: root, server_context: server_context) }
|
31
|
+
|
32
|
+
it_behaves_like 'an MCP tool that returns text JSON'
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
34
|
+
it 'returns all files coverage data with counts' do
|
35
|
+
response = subject
|
36
|
+
data, item = expect_mcp_text_json(response, expected_keys: ['files', 'counts'])
|
37
|
+
|
38
|
+
files = data['files']
|
39
|
+
counts = data['counts']
|
38
40
|
|
39
41
|
expect(files.length).to eq(2)
|
40
42
|
expect(counts).to include('total' => 2).or include(total: 2)
|
43
|
+
expect(files.map { |f| f['file'] }).to eq(['lib/foo.rb', 'lib/bar.rb'])
|
44
|
+
|
41
45
|
# ok + stale equals total
|
42
46
|
ok = counts[:ok] || counts['ok']
|
43
47
|
stale = counts[:stale] || counts['stale']
|
data/spec/base_tool_spec.rb
CHANGED
@@ -3,10 +3,11 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
RSpec.describe SimpleCovMcp::BaseTool do
|
6
|
-
let(:handler) { SimpleCovMcp::ErrorHandler.new(
|
6
|
+
let(:handler) { SimpleCovMcp::ErrorHandler.new(error_mode: :on, logger: test_logger) }
|
7
7
|
let(:test_logger) do
|
8
8
|
Class.new do
|
9
9
|
attr_reader :messages
|
10
|
+
|
10
11
|
def initialize; @messages = []; end
|
11
12
|
def error(msg); @messages << msg; end
|
12
13
|
end.new
|
@@ -19,12 +20,7 @@ RSpec.describe SimpleCovMcp::BaseTool do
|
|
19
20
|
nil
|
20
21
|
end
|
21
22
|
SimpleCovMcp.error_handler = handler
|
22
|
-
|
23
|
-
fake_resp = Class.new do
|
24
|
-
attr_reader :payload
|
25
|
-
def initialize(payload) = @payload = payload
|
26
|
-
end
|
27
|
-
stub_const('MCP::Tool::Response', fake_resp)
|
23
|
+
setup_mcp_response_stub
|
28
24
|
end
|
29
25
|
|
30
26
|
after do
|
@@ -32,11 +28,16 @@ RSpec.describe SimpleCovMcp::BaseTool do
|
|
32
28
|
end
|
33
29
|
|
34
30
|
shared_examples 'friendly response and logged' do
|
35
|
-
it 'returns friendly text
|
36
|
-
resp = described_class.handle_mcp_error(error, tool)
|
31
|
+
it 'returns friendly text' do
|
32
|
+
resp = described_class.handle_mcp_error(error, tool, error_mode: :on)
|
33
|
+
expect(resp).to be_a(MCP::Tool::Response)
|
34
|
+
expect(resp.payload.first[:text]).to match(expected_pattern)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'respects error_mode :off' do
|
38
|
+
resp = described_class.handle_mcp_error(error, tool, error_mode: :off)
|
37
39
|
expect(resp).to be_a(MCP::Tool::Response)
|
38
40
|
expect(resp.payload.first[:text]).to match(expected_pattern)
|
39
|
-
expect(test_logger.messages.join).to include(log_fragment)
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::CoverageCLI do
|
6
|
+
let(:root) { (FIXTURES_DIR / 'project1').to_s }
|
7
|
+
let(:cli) { described_class.new }
|
8
|
+
|
9
|
+
before do
|
10
|
+
cli.config.root = root
|
11
|
+
cli.config.resultset = 'coverage'
|
12
|
+
cli.config.stale_mode = :off
|
13
|
+
cli.config.tracked_globs = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#show_default_report' do
|
17
|
+
it 'prints JSON summary using relativized payload when json mode is enabled' do
|
18
|
+
cli.config.json = true
|
19
|
+
|
20
|
+
output = nil
|
21
|
+
silence_output do |stdout, _stderr|
|
22
|
+
cli.show_default_report(sort_order: :ascending, output: stdout)
|
23
|
+
output = stdout.string
|
24
|
+
end
|
25
|
+
|
26
|
+
payload = JSON.parse(output)
|
27
|
+
|
28
|
+
expect(payload['files']).to be_an(Array)
|
29
|
+
expect(payload['files'].first['file']).to eq('lib/bar.rb').or eq('lib/foo.rb')
|
30
|
+
expect(payload['counts']).to include('total', 'ok', 'stale')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::CLIConfig do
|
6
|
+
describe '#initialize' do
|
7
|
+
it 'creates a config with default values' do
|
8
|
+
config = described_class.new
|
9
|
+
expect(config.root).to eq('.')
|
10
|
+
expect(config.json).to be false
|
11
|
+
expect(config.sort_order).to eq(:ascending)
|
12
|
+
expect(config.source_context).to eq(2)
|
13
|
+
expect(config.error_mode).to eq(:on)
|
14
|
+
expect(config.stale_mode).to eq(:off)
|
15
|
+
expect(config.resultset).to be_nil
|
16
|
+
expect(config.source_mode).to be_nil
|
17
|
+
expect(config.tracked_globs).to be_nil
|
18
|
+
expect(config.log_file).to be_nil
|
19
|
+
expect(config.success_predicate).to be_nil
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'allows overriding defaults via keyword arguments' do
|
23
|
+
config = described_class.new(
|
24
|
+
root: '/custom',
|
25
|
+
json: true,
|
26
|
+
sort_order: :descending,
|
27
|
+
stale_mode: :error
|
28
|
+
)
|
29
|
+
expect(config.root).to eq('/custom')
|
30
|
+
expect(config.json).to be true
|
31
|
+
expect(config.sort_order).to eq(:descending)
|
32
|
+
expect(config.stale_mode).to eq(:error)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'is mutable (struct fields can be changed)' do
|
36
|
+
config = described_class.new
|
37
|
+
config.root = '/new/root'
|
38
|
+
config.json = true
|
39
|
+
expect(config.root).to eq('/new/root')
|
40
|
+
expect(config.json).to be true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#model_options' do
|
45
|
+
it 'returns hash suitable for CoverageModel.new' do
|
46
|
+
config = described_class.new(
|
47
|
+
root: '/custom/root',
|
48
|
+
resultset: '/custom/.resultset.json',
|
49
|
+
stale_mode: :error,
|
50
|
+
tracked_globs: ['lib/**/*.rb']
|
51
|
+
)
|
52
|
+
|
53
|
+
options = config.model_options
|
54
|
+
expect(options).to eq({
|
55
|
+
root: '/custom/root',
|
56
|
+
resultset: '/custom/.resultset.json',
|
57
|
+
staleness: :error,
|
58
|
+
tracked_globs: ['lib/**/*.rb']
|
59
|
+
})
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'handles nil values correctly' do
|
63
|
+
config = described_class.new
|
64
|
+
options = config.model_options
|
65
|
+
expect(options[:root]).to eq('.')
|
66
|
+
expect(options[:resultset]).to be_nil
|
67
|
+
expect(options[:staleness]).to eq(:off)
|
68
|
+
expect(options[:tracked_globs]).to be_nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#formatter_options' do
|
73
|
+
it 'returns hash suitable for SourceFormatter.new' do
|
74
|
+
config = described_class.new(color: true)
|
75
|
+
options = config.formatter_options
|
76
|
+
expect(options).to eq({ color_enabled: true })
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'handles false color setting' do
|
80
|
+
config = described_class.new(color: false)
|
81
|
+
options = config.formatter_options
|
82
|
+
expect(options).to eq({ color_enabled: false })
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'struct behavior' do
|
87
|
+
it 'supports equality comparison' do
|
88
|
+
config1 = described_class.new(root: '/foo', json: true)
|
89
|
+
config2 = described_class.new(root: '/foo', json: true)
|
90
|
+
config3 = described_class.new(root: '/bar', json: true)
|
91
|
+
|
92
|
+
expect(config1).to eq(config2)
|
93
|
+
expect(config1).not_to eq(config3)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'provides readable inspect output' do
|
97
|
+
config = described_class.new(root: '/test', json: true)
|
98
|
+
output = config.inspect
|
99
|
+
expect(output).to include('root="/test"')
|
100
|
+
expect(output).to include('json=true')
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'converts to hash' do
|
104
|
+
config = described_class.new(root: '/test', json: true)
|
105
|
+
hash = config.to_h
|
106
|
+
expect(hash).to be_a(Hash)
|
107
|
+
expect(hash[:root]).to eq('/test')
|
108
|
+
expect(hash[:json]).to be true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'symbol enumerated values' do
|
113
|
+
it 'uses symbols for sort_order' do
|
114
|
+
config = described_class.new(sort_order: :descending)
|
115
|
+
expect(config.sort_order).to eq(:descending)
|
116
|
+
expect(config.sort_order).to be_a(Symbol)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'uses symbols for stale_mode' do
|
120
|
+
config = described_class.new(stale_mode: :error)
|
121
|
+
expect(config.stale_mode).to eq(:error)
|
122
|
+
expect(config.stale_mode).to be_a(Symbol)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'uses symbols for error_mode' do
|
126
|
+
config = described_class.new(error_mode: :trace)
|
127
|
+
expect(config.error_mode).to eq(:trace)
|
128
|
+
expect(config.error_mode).to be_a(Symbol)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'uses symbols for source_mode' do
|
132
|
+
config = described_class.new(source_mode: :uncovered)
|
133
|
+
expect(config.source_mode).to eq(:uncovered)
|
134
|
+
expect(config.source_mode).to be_a(Symbol)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|