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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +173 -356
  4. data/docs/ADVANCED_USAGE.md +967 -0
  5. data/docs/ARCHITECTURE.md +79 -0
  6. data/docs/BRANCH_ONLY_COVERAGE.md +81 -0
  7. data/docs/CLI_USAGE.md +637 -0
  8. data/docs/DEVELOPMENT.md +82 -0
  9. data/docs/ERROR_HANDLING.md +93 -0
  10. data/docs/EXAMPLES.md +430 -0
  11. data/docs/INSTALLATION.md +352 -0
  12. data/docs/LIBRARY_API.md +635 -0
  13. data/docs/MCP_INTEGRATION.md +488 -0
  14. data/docs/TROUBLESHOOTING.md +276 -0
  15. data/docs/arch-decisions/001-x-arch-decision.md +93 -0
  16. data/docs/arch-decisions/002-x-arch-decision.md +157 -0
  17. data/docs/arch-decisions/003-x-arch-decision.md +163 -0
  18. data/docs/arch-decisions/004-x-arch-decision.md +199 -0
  19. data/docs/arch-decisions/005-x-arch-decision.md +187 -0
  20. data/docs/arch-decisions/README.md +60 -0
  21. data/docs/presentations/simplecov-mcp-presentation.md +249 -0
  22. data/exe/simplecov-mcp +4 -4
  23. data/lib/simplecov_mcp/app_context.rb +26 -0
  24. data/lib/simplecov_mcp/base_tool.rb +74 -0
  25. data/lib/simplecov_mcp/cli.rb +234 -0
  26. data/lib/simplecov_mcp/cli_config.rb +56 -0
  27. data/lib/simplecov_mcp/commands/base_command.rb +78 -0
  28. data/lib/simplecov_mcp/commands/command_factory.rb +39 -0
  29. data/lib/simplecov_mcp/commands/detailed_command.rb +24 -0
  30. data/lib/simplecov_mcp/commands/list_command.rb +13 -0
  31. data/lib/simplecov_mcp/commands/raw_command.rb +22 -0
  32. data/lib/simplecov_mcp/commands/summary_command.rb +24 -0
  33. data/lib/simplecov_mcp/commands/uncovered_command.rb +26 -0
  34. data/lib/simplecov_mcp/commands/version_command.rb +18 -0
  35. data/lib/simplecov_mcp/constants.rb +22 -0
  36. data/lib/simplecov_mcp/error_handler.rb +124 -0
  37. data/lib/simplecov_mcp/error_handler_factory.rb +31 -0
  38. data/lib/simplecov_mcp/errors.rb +179 -0
  39. data/lib/simplecov_mcp/formatters/source_formatter.rb +148 -0
  40. data/lib/simplecov_mcp/mcp_server.rb +40 -0
  41. data/lib/simplecov_mcp/mode_detector.rb +55 -0
  42. data/lib/simplecov_mcp/model.rb +300 -0
  43. data/lib/simplecov_mcp/option_normalizers.rb +92 -0
  44. data/lib/simplecov_mcp/option_parser_builder.rb +134 -0
  45. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +50 -0
  46. data/lib/simplecov_mcp/option_parsers/error_helper.rb +109 -0
  47. data/lib/simplecov_mcp/path_relativizer.rb +61 -0
  48. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +44 -0
  49. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +16 -0
  50. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +16 -0
  51. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +16 -0
  52. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +16 -0
  53. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +52 -0
  54. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +126 -0
  55. data/lib/simplecov_mcp/resolvers/resolver_factory.rb +28 -0
  56. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +78 -0
  57. data/lib/simplecov_mcp/resultset_loader.rb +136 -0
  58. data/lib/simplecov_mcp/staleness_checker.rb +243 -0
  59. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/all_files_coverage_tool.rb +31 -13
  60. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_detailed_tool.rb +7 -7
  61. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_raw_tool.rb +7 -7
  62. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_summary_tool.rb +7 -7
  63. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +90 -0
  64. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/help_tool.rb +13 -4
  65. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/uncovered_lines_tool.rb +7 -7
  66. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/version_tool.rb +11 -3
  67. data/lib/simplecov_mcp/util.rb +82 -0
  68. data/lib/{simple_cov_mcp → simplecov_mcp}/version.rb +1 -1
  69. data/lib/simplecov_mcp.rb +144 -2
  70. data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
  71. data/spec/TIMESTAMPS.md +48 -0
  72. data/spec/all_files_coverage_tool_spec.rb +29 -25
  73. data/spec/base_tool_spec.rb +11 -10
  74. data/spec/cli/show_default_report_spec.rb +33 -0
  75. data/spec/cli_config_spec.rb +137 -0
  76. data/spec/cli_enumerated_options_spec.rb +68 -0
  77. data/spec/cli_error_spec.rb +105 -47
  78. data/spec/cli_source_spec.rb +82 -23
  79. data/spec/cli_spec.rb +140 -5
  80. data/spec/cli_success_predicate_spec.rb +141 -0
  81. data/spec/cli_table_spec.rb +1 -1
  82. data/spec/cli_usage_spec.rb +10 -26
  83. data/spec/commands/base_command_spec.rb +187 -0
  84. data/spec/commands/command_factory_spec.rb +72 -0
  85. data/spec/commands/detailed_command_spec.rb +48 -0
  86. data/spec/commands/raw_command_spec.rb +46 -0
  87. data/spec/commands/summary_command_spec.rb +47 -0
  88. data/spec/commands/uncovered_command_spec.rb +49 -0
  89. data/spec/constants_spec.rb +61 -0
  90. data/spec/coverage_table_tool_spec.rb +17 -33
  91. data/spec/error_handler_spec.rb +22 -13
  92. data/spec/error_mode_spec.rb +143 -0
  93. data/spec/errors_edge_cases_spec.rb +239 -0
  94. data/spec/errors_stale_spec.rb +2 -2
  95. data/spec/file_based_mcp_tools_spec.rb +99 -0
  96. data/spec/fixtures/project1/lib/bar.rb +0 -1
  97. data/spec/fixtures/project1/lib/foo.rb +0 -1
  98. data/spec/help_tool_spec.rb +11 -17
  99. data/spec/integration_spec.rb +845 -0
  100. data/spec/logging_fallback_spec.rb +128 -0
  101. data/spec/mcp_logging_spec.rb +44 -0
  102. data/spec/mcp_server_integration_spec.rb +23 -0
  103. data/spec/mcp_server_spec.rb +15 -4
  104. data/spec/mode_detector_spec.rb +148 -0
  105. data/spec/model_error_handling_spec.rb +210 -0
  106. data/spec/model_staleness_spec.rb +40 -10
  107. data/spec/option_normalizers_spec.rb +204 -0
  108. data/spec/option_parsers/env_options_parser_spec.rb +233 -0
  109. data/spec/option_parsers/error_helper_spec.rb +222 -0
  110. data/spec/path_relativizer_spec.rb +83 -0
  111. data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
  112. data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
  113. data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
  114. data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
  115. data/spec/presenters/project_coverage_presenter_spec.rb +86 -0
  116. data/spec/resolvers/coverage_line_resolver_spec.rb +57 -0
  117. data/spec/resolvers/resolver_factory_spec.rb +61 -0
  118. data/spec/resolvers/resultset_path_resolver_spec.rb +55 -0
  119. data/spec/resultset_loader_spec.rb +167 -0
  120. data/spec/shared_examples/README.md +115 -0
  121. data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
  122. data/spec/shared_examples/file_based_mcp_tools.rb +174 -0
  123. data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
  124. data/spec/simple_cov_mcp_module_spec.rb +16 -0
  125. data/spec/simplecov_mcp_model_spec.rb +340 -9
  126. data/spec/simplecov_mcp_opts_spec.rb +182 -0
  127. data/spec/spec_helper.rb +147 -4
  128. data/spec/staleness_checker_spec.rb +373 -0
  129. data/spec/staleness_more_spec.rb +16 -13
  130. data/spec/support/mcp_runner.rb +64 -0
  131. data/spec/tools_error_handling_spec.rb +144 -0
  132. data/spec/util_spec.rb +109 -34
  133. data/spec/version_spec.rb +117 -9
  134. data/spec/version_tool_spec.rb +131 -10
  135. metadata +120 -63
  136. data/lib/simple_cov/mcp.rb +0 -9
  137. data/lib/simple_cov_mcp/base_tool.rb +0 -70
  138. data/lib/simple_cov_mcp/cli.rb +0 -390
  139. data/lib/simple_cov_mcp/error_handler.rb +0 -131
  140. data/lib/simple_cov_mcp/error_handler_factory.rb +0 -38
  141. data/lib/simple_cov_mcp/errors.rb +0 -176
  142. data/lib/simple_cov_mcp/mcp_server.rb +0 -30
  143. data/lib/simple_cov_mcp/model.rb +0 -104
  144. data/lib/simple_cov_mcp/staleness_checker.rb +0 -125
  145. data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +0 -61
  146. data/lib/simple_cov_mcp/util.rb +0 -122
  147. data/lib/simple_cov_mcp.rb +0 -102
  148. data/spec/coverage_detailed_tool_spec.rb +0 -36
  149. data/spec/coverage_raw_tool_spec.rb +0 -32
  150. data/spec/coverage_summary_tool_spec.rb +0 -39
  151. data/spec/legacy_shim_spec.rb +0 -13
  152. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCovMcp
4
- VERSION = '0.3.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/simplecov_mcp.rb CHANGED
@@ -1,4 +1,146 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Convenience single-level require path (backcompat)
4
- require 'simple_cov_mcp'
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)
@@ -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 'simple_cov_mcp/tools/all_files_coverage_tool'
4
+ require 'simplecov_mcp/tools/all_files_coverage_tool'
5
5
 
6
6
  RSpec.describe SimpleCovMcp::Tools::AllFilesCoverageTool do
7
- let(:root) { (FIXTURES / 'project1').to_s }
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
- # Stub a response object that accepts a meta keyword, like the real one
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
- response = described_class.call(root: root, server_context: server_context)
30
- entry = response.payload.first
31
- expect(entry['type']).to eq('resource')
32
- expect(entry['resource']).to include('mimeType' => 'application/json')
33
- json = JSON.parse(entry['resource']['text'])
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
- expect(json).to have_key('files')
36
- files = json['files']
37
- counts = json['counts']
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']
@@ -3,10 +3,11 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  RSpec.describe SimpleCovMcp::BaseTool do
6
- let(:handler) { SimpleCovMcp::ErrorHandler.new(log_errors: true, show_stack_traces: false, logger: test_logger) }
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
- # Stub MCP::Tool::Response once for all examples; capture the payload
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 and logs' do
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