cov-loupe 3.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 (171) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +329 -0
  4. data/docs/dev/ARCHITECTURE.md +80 -0
  5. data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
  6. data/docs/dev/DEVELOPMENT.md +83 -0
  7. data/docs/dev/README.md +10 -0
  8. data/docs/dev/RELEASING.md +146 -0
  9. data/docs/dev/arch-decisions/001-x-arch-decision.md +95 -0
  10. data/docs/dev/arch-decisions/002-x-arch-decision.md +159 -0
  11. data/docs/dev/arch-decisions/003-x-arch-decision.md +165 -0
  12. data/docs/dev/arch-decisions/004-x-arch-decision.md +203 -0
  13. data/docs/dev/arch-decisions/005-x-arch-decision.md +189 -0
  14. data/docs/dev/arch-decisions/README.md +60 -0
  15. data/docs/dev/presentations/cov-loupe-presentation.md +255 -0
  16. data/docs/fixtures/demo_project/README.md +9 -0
  17. data/docs/user/ADVANCED_USAGE.md +777 -0
  18. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
  19. data/docs/user/CLI_USAGE.md +750 -0
  20. data/docs/user/ERROR_HANDLING.md +93 -0
  21. data/docs/user/EXAMPLES.md +588 -0
  22. data/docs/user/INSTALLATION.md +130 -0
  23. data/docs/user/LIBRARY_API.md +693 -0
  24. data/docs/user/MCP_INTEGRATION.md +490 -0
  25. data/docs/user/README.md +14 -0
  26. data/docs/user/TROUBLESHOOTING.md +197 -0
  27. data/docs/user/V2-BREAKING-CHANGES.md +472 -0
  28. data/exe/cov-loupe +23 -0
  29. data/lib/cov_loupe/app_config.rb +56 -0
  30. data/lib/cov_loupe/app_context.rb +26 -0
  31. data/lib/cov_loupe/base_tool.rb +102 -0
  32. data/lib/cov_loupe/cli.rb +178 -0
  33. data/lib/cov_loupe/commands/base_command.rb +67 -0
  34. data/lib/cov_loupe/commands/command_factory.rb +45 -0
  35. data/lib/cov_loupe/commands/detailed_command.rb +38 -0
  36. data/lib/cov_loupe/commands/list_command.rb +13 -0
  37. data/lib/cov_loupe/commands/raw_command.rb +38 -0
  38. data/lib/cov_loupe/commands/summary_command.rb +41 -0
  39. data/lib/cov_loupe/commands/totals_command.rb +53 -0
  40. data/lib/cov_loupe/commands/uncovered_command.rb +45 -0
  41. data/lib/cov_loupe/commands/validate_command.rb +60 -0
  42. data/lib/cov_loupe/commands/version_command.rb +33 -0
  43. data/lib/cov_loupe/config_parser.rb +32 -0
  44. data/lib/cov_loupe/constants.rb +22 -0
  45. data/lib/cov_loupe/coverage_reporter.rb +31 -0
  46. data/lib/cov_loupe/error_handler.rb +165 -0
  47. data/lib/cov_loupe/error_handler_factory.rb +31 -0
  48. data/lib/cov_loupe/errors.rb +191 -0
  49. data/lib/cov_loupe/formatters/source_formatter.rb +152 -0
  50. data/lib/cov_loupe/formatters.rb +51 -0
  51. data/lib/cov_loupe/mcp_server.rb +42 -0
  52. data/lib/cov_loupe/mode_detector.rb +56 -0
  53. data/lib/cov_loupe/model.rb +339 -0
  54. data/lib/cov_loupe/option_normalizers.rb +113 -0
  55. data/lib/cov_loupe/option_parser_builder.rb +147 -0
  56. data/lib/cov_loupe/option_parsers/env_options_parser.rb +48 -0
  57. data/lib/cov_loupe/option_parsers/error_helper.rb +110 -0
  58. data/lib/cov_loupe/path_relativizer.rb +64 -0
  59. data/lib/cov_loupe/predicate_evaluator.rb +72 -0
  60. data/lib/cov_loupe/presenters/base_coverage_presenter.rb +42 -0
  61. data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +14 -0
  62. data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +14 -0
  63. data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +14 -0
  64. data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +14 -0
  65. data/lib/cov_loupe/presenters/project_coverage_presenter.rb +50 -0
  66. data/lib/cov_loupe/presenters/project_totals_presenter.rb +27 -0
  67. data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +122 -0
  68. data/lib/cov_loupe/resolvers/resolver_factory.rb +28 -0
  69. data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +76 -0
  70. data/lib/cov_loupe/resultset_loader.rb +131 -0
  71. data/lib/cov_loupe/staleness_checker.rb +247 -0
  72. data/lib/cov_loupe/table_formatter.rb +64 -0
  73. data/lib/cov_loupe/tools/all_files_coverage_tool.rb +51 -0
  74. data/lib/cov_loupe/tools/coverage_detailed_tool.rb +35 -0
  75. data/lib/cov_loupe/tools/coverage_raw_tool.rb +34 -0
  76. data/lib/cov_loupe/tools/coverage_summary_tool.rb +34 -0
  77. data/lib/cov_loupe/tools/coverage_table_tool.rb +50 -0
  78. data/lib/cov_loupe/tools/coverage_totals_tool.rb +44 -0
  79. data/lib/cov_loupe/tools/help_tool.rb +115 -0
  80. data/lib/cov_loupe/tools/uncovered_lines_tool.rb +34 -0
  81. data/lib/cov_loupe/tools/validate_tool.rb +72 -0
  82. data/lib/cov_loupe/tools/version_tool.rb +32 -0
  83. data/lib/cov_loupe/util.rb +88 -0
  84. data/lib/cov_loupe/version.rb +5 -0
  85. data/lib/cov_loupe.rb +140 -0
  86. data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
  87. data/spec/TIMESTAMPS.md +48 -0
  88. data/spec/all_files_coverage_tool_spec.rb +53 -0
  89. data/spec/app_config_spec.rb +142 -0
  90. data/spec/base_tool_spec.rb +62 -0
  91. data/spec/cli/show_default_report_spec.rb +33 -0
  92. data/spec/cli_enumerated_options_spec.rb +90 -0
  93. data/spec/cli_error_spec.rb +184 -0
  94. data/spec/cli_format_spec.rb +123 -0
  95. data/spec/cli_json_options_spec.rb +50 -0
  96. data/spec/cli_source_spec.rb +44 -0
  97. data/spec/cli_spec.rb +192 -0
  98. data/spec/cli_table_spec.rb +28 -0
  99. data/spec/cli_usage_spec.rb +42 -0
  100. data/spec/commands/base_command_spec.rb +107 -0
  101. data/spec/commands/command_factory_spec.rb +76 -0
  102. data/spec/commands/detailed_command_spec.rb +34 -0
  103. data/spec/commands/list_command_spec.rb +28 -0
  104. data/spec/commands/raw_command_spec.rb +69 -0
  105. data/spec/commands/summary_command_spec.rb +34 -0
  106. data/spec/commands/totals_command_spec.rb +34 -0
  107. data/spec/commands/uncovered_command_spec.rb +55 -0
  108. data/spec/commands/validate_command_spec.rb +213 -0
  109. data/spec/commands/version_command_spec.rb +38 -0
  110. data/spec/constants_spec.rb +61 -0
  111. data/spec/cov_loupe/formatters/source_formatter_spec.rb +267 -0
  112. data/spec/cov_loupe/formatters_spec.rb +76 -0
  113. data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +79 -0
  114. data/spec/cov_loupe_model_spec.rb +454 -0
  115. data/spec/cov_loupe_module_spec.rb +37 -0
  116. data/spec/cov_loupe_opts_spec.rb +185 -0
  117. data/spec/coverage_reporter_spec.rb +102 -0
  118. data/spec/coverage_table_tool_spec.rb +59 -0
  119. data/spec/coverage_totals_tool_spec.rb +37 -0
  120. data/spec/error_handler_spec.rb +197 -0
  121. data/spec/error_mode_spec.rb +139 -0
  122. data/spec/errors_edge_cases_spec.rb +312 -0
  123. data/spec/errors_stale_spec.rb +83 -0
  124. data/spec/file_based_mcp_tools_spec.rb +99 -0
  125. data/spec/fixtures/project1/lib/bar.rb +5 -0
  126. data/spec/fixtures/project1/lib/foo.rb +6 -0
  127. data/spec/help_tool_spec.rb +26 -0
  128. data/spec/integration_spec.rb +789 -0
  129. data/spec/logging_fallback_spec.rb +128 -0
  130. data/spec/mcp_logging_spec.rb +44 -0
  131. data/spec/mcp_server_integration_spec.rb +23 -0
  132. data/spec/mcp_server_spec.rb +106 -0
  133. data/spec/mode_detector_spec.rb +153 -0
  134. data/spec/model_error_handling_spec.rb +269 -0
  135. data/spec/model_staleness_spec.rb +79 -0
  136. data/spec/option_normalizers_spec.rb +203 -0
  137. data/spec/option_parsers/env_options_parser_spec.rb +221 -0
  138. data/spec/option_parsers/error_helper_spec.rb +222 -0
  139. data/spec/path_relativizer_spec.rb +98 -0
  140. data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
  141. data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
  142. data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
  143. data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
  144. data/spec/presenters/project_coverage_presenter_spec.rb +87 -0
  145. data/spec/presenters/project_totals_presenter_spec.rb +144 -0
  146. data/spec/resolvers/coverage_line_resolver_spec.rb +282 -0
  147. data/spec/resolvers/resolver_factory_spec.rb +61 -0
  148. data/spec/resolvers/resultset_path_resolver_spec.rb +60 -0
  149. data/spec/resultset_loader_spec.rb +167 -0
  150. data/spec/shared_examples/README.md +115 -0
  151. data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
  152. data/spec/shared_examples/file_based_mcp_tools.rb +179 -0
  153. data/spec/shared_examples/formatted_command_examples.rb +64 -0
  154. data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
  155. data/spec/spec_helper.rb +127 -0
  156. data/spec/staleness_checker_spec.rb +374 -0
  157. data/spec/staleness_more_spec.rb +42 -0
  158. data/spec/support/cli_helpers.rb +22 -0
  159. data/spec/support/control_flow_helpers.rb +20 -0
  160. data/spec/support/fake_mcp.rb +40 -0
  161. data/spec/support/io_helpers.rb +29 -0
  162. data/spec/support/mcp_helpers.rb +35 -0
  163. data/spec/support/mcp_runner.rb +66 -0
  164. data/spec/support/mocking_helpers.rb +30 -0
  165. data/spec/table_format_spec.rb +70 -0
  166. data/spec/tools/validate_tool_spec.rb +132 -0
  167. data/spec/tools_error_handling_spec.rb +130 -0
  168. data/spec/util_spec.rb +154 -0
  169. data/spec/version_spec.rb +123 -0
  170. data/spec/version_tool_spec.rb +141 -0
  171. metadata +290 -0
@@ -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/cov_loupe.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/cov_loupe.rb` - Entry point with mode detection
105
+ - `lib/cov_loupe/mcp_server.rb` - MCP server implementation
106
+ - `exe/cov-loupe` - 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
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'cov_loupe/tools/all_files_coverage_tool'
5
+
6
+ RSpec.describe CovLoupe::Tools::AllFilesCoverageTool do
7
+ subject(:call_tool) { described_class.call(root: root, server_context: server_context) }
8
+
9
+ let(:root) { (FIXTURES_DIR / 'project1').to_s }
10
+ let(:server_context) { instance_double('ServerContext').as_null_object }
11
+
12
+ before do
13
+ setup_mcp_response_stub
14
+ model = instance_double(CovLoupe::CoverageModel)
15
+ allow(CovLoupe::CoverageModel).to receive(:new).and_return(model)
16
+
17
+ payload = {
18
+ 'files' => [
19
+ { 'file' => 'lib/foo.rb', 'percentage' => 100.0, 'covered' => 10, 'total' => 10,
20
+ 'stale' => false },
21
+ { 'file' => 'lib/bar.rb', 'percentage' => 50.0, 'covered' => 5, 'total' => 10,
22
+ 'stale' => true }
23
+ ],
24
+ 'counts' => { 'total' => 2, 'ok' => 1, 'stale' => 1 }
25
+ }
26
+
27
+ presenter = instance_double(CovLoupe::Presenters::ProjectCoveragePresenter)
28
+ allow(CovLoupe::Presenters::ProjectCoveragePresenter).to receive(:new).and_return(presenter)
29
+ allow(presenter).to receive(:relativized_payload).and_return(payload)
30
+ end
31
+
32
+
33
+ it_behaves_like 'an MCP tool that returns text JSON'
34
+
35
+ it 'returns all files coverage data with counts' do
36
+ response = call_tool
37
+ data, _item = expect_mcp_text_json(response, expected_keys: ['files', 'counts'])
38
+
39
+ files = data['files']
40
+ counts = data['counts']
41
+
42
+ expect(files.length).to eq(2)
43
+ expect(counts).to include('total' => 2).or include(total: 2)
44
+ expect(files.map { |f| f['file'] }).to eq(['lib/foo.rb', 'lib/bar.rb'])
45
+
46
+ # ok + stale equals total
47
+ ok = counts[:ok] || counts['ok']
48
+ stale = counts[:stale] || counts['stale']
49
+ total = counts[:total] || counts['total']
50
+ expect(ok + stale).to eq(total)
51
+ expect(stale).to eq(1)
52
+ end
53
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CovLoupe::AppConfig 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.format).to eq(:table)
11
+ expect(config.sort_order).to eq(:descending)
12
+ expect(config.source_context).to eq(2)
13
+ expect(config.error_mode).to eq(:log)
14
+ expect(config.staleness).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
+ end
20
+
21
+ it 'allows overriding defaults via keyword arguments' do
22
+ config = described_class.new(
23
+ root: '/custom',
24
+ format: :json,
25
+ sort_order: :descending,
26
+ staleness: :error
27
+ )
28
+ expect(config.root).to eq('/custom')
29
+ expect(config.format).to eq(:json)
30
+ expect(config.sort_order).to eq(:descending)
31
+ expect(config.staleness).to eq(:error)
32
+ end
33
+
34
+ it 'is mutable (struct fields can be changed)' do
35
+ config = described_class.new
36
+ config.root = '/new/root'
37
+ config.format = :json
38
+ expect(config.root).to eq('/new/root')
39
+ expect(config.format).to eq(:json)
40
+ end
41
+ end
42
+
43
+ describe '#model_options' do
44
+ it 'returns hash suitable for CoverageModel.new' do
45
+ config = described_class.new(
46
+ root: '/custom/root',
47
+ resultset: '/custom/.resultset.json',
48
+ staleness: :error,
49
+ tracked_globs: ['lib/**/*.rb']
50
+ )
51
+
52
+ options = config.model_options
53
+ expect(options).to eq({
54
+ root: '/custom/root',
55
+ resultset: '/custom/.resultset.json',
56
+ staleness: :error,
57
+ tracked_globs: ['lib/**/*.rb']
58
+ })
59
+ end
60
+
61
+ it 'handles nil values correctly' do
62
+ config = described_class.new
63
+ options = config.model_options
64
+ expect(options[:root]).to eq('.')
65
+ expect(options[:resultset]).to be_nil
66
+ expect(options[:staleness]).to eq(:off)
67
+ expect(options[:tracked_globs]).to be_nil
68
+ end
69
+ end
70
+
71
+ describe '#formatter_options' do
72
+ it 'returns hash suitable for SourceFormatter.new' do
73
+ config = described_class.new(color: true)
74
+ options = config.formatter_options
75
+ expect(options).to eq({ color_enabled: true })
76
+ end
77
+
78
+ it 'handles false color setting' do
79
+ config = described_class.new(color: false)
80
+ options = config.formatter_options
81
+ expect(options).to eq({ color_enabled: false })
82
+ end
83
+ end
84
+
85
+ describe 'struct behavior' do
86
+ it 'supports equality comparison' do
87
+ config1 = described_class.new(root: '/foo', format: :json)
88
+ config2 = described_class.new(root: '/foo', format: :json)
89
+ config3 = described_class.new(root: '/bar', format: :json)
90
+
91
+ expect(config1).to eq(config2)
92
+ expect(config1).not_to eq(config3)
93
+ end
94
+
95
+ it 'provides readable inspect output' do
96
+ config = described_class.new(root: '/test', format: :json)
97
+ output = config.inspect
98
+ expect(output).to include('root="/test"')
99
+ expect(output).to include('format=:json')
100
+ end
101
+
102
+ it 'converts to hash' do
103
+ config = described_class.new(root: '/test', format: :json)
104
+ hash = config.to_h
105
+ expect(hash).to be_a(Hash)
106
+ expect(hash[:root]).to eq('/test')
107
+ expect(hash[:format]).to eq(:json)
108
+ end
109
+ end
110
+
111
+ describe 'symbol enumerated values' do
112
+ it 'uses symbols for format' do
113
+ config = described_class.new(format: :json)
114
+ expect(config.format).to eq(:json)
115
+ expect(config.format).to be_a(Symbol)
116
+ end
117
+
118
+ it 'uses symbols for sort_order' do
119
+ config = described_class.new(sort_order: :descending)
120
+ expect(config.sort_order).to eq(:descending)
121
+ expect(config.sort_order).to be_a(Symbol)
122
+ end
123
+
124
+ it 'uses symbols for staleness' do
125
+ config = described_class.new(staleness: :error)
126
+ expect(config.staleness).to eq(:error)
127
+ expect(config.staleness).to be_a(Symbol)
128
+ end
129
+
130
+ it 'uses symbols for error_mode' do
131
+ config = described_class.new(error_mode: :debug)
132
+ expect(config.error_mode).to eq(:debug)
133
+ expect(config.error_mode).to be_a(Symbol)
134
+ end
135
+
136
+ it 'uses symbols for source_mode' do
137
+ config = described_class.new(source_mode: :uncovered)
138
+ expect(config.source_mode).to eq(:uncovered)
139
+ expect(config.source_mode).to be_a(Symbol)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CovLoupe::BaseTool do
6
+ let(:handler) { CovLoupe::ErrorHandler.new(error_mode: :log, logger: test_logger) }
7
+ let(:test_logger) do
8
+ Class.new do
9
+ attr_reader :messages
10
+
11
+ def initialize = @messages = []
12
+ def error(msg) = @messages << msg
13
+ end.new
14
+ end
15
+
16
+ let(:orig_handler) do
17
+ CovLoupe.error_handler
18
+ rescue
19
+ nil
20
+ end
21
+
22
+ before do
23
+ CovLoupe.error_handler = handler
24
+ setup_mcp_response_stub
25
+ end
26
+
27
+ after do
28
+ CovLoupe.error_handler = orig_handler if orig_handler
29
+ end
30
+
31
+ shared_examples 'friendly response and logged' do
32
+ it 'returns friendly text' do
33
+ resp = described_class.handle_mcp_error(error, tool, error_mode: :log)
34
+ expect(resp).to be_a(MCP::Tool::Response)
35
+ expect(resp.payload.first['text']).to match(expected_pattern)
36
+ end
37
+
38
+ it 'respects error_mode :off' do
39
+ resp = described_class.handle_mcp_error(error, tool, error_mode: :off)
40
+ expect(resp).to be_a(MCP::Tool::Response)
41
+ expect(resp.payload.first['text']).to match(expected_pattern)
42
+ end
43
+ end
44
+
45
+ context 'with CovLoupe::Error' do
46
+ let(:error) { CovLoupe::UsageError.new('invalid args') }
47
+ let(:tool) { 'coverage_summary' }
48
+ let(:expected_pattern) { /Error: invalid args/ }
49
+ let(:log_fragment) { 'invalid args' }
50
+
51
+ it_behaves_like 'friendly response and logged'
52
+ end
53
+
54
+ context 'with standard error' do
55
+ let(:error) { Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb') }
56
+ let(:tool) { 'coverage_raw' }
57
+ let(:expected_pattern) { /Error: .*File not found: missing.rb/ }
58
+ let(:log_fragment) { 'File not found' }
59
+
60
+ it_behaves_like 'friendly response and logged'
61
+ end
62
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CovLoupe::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.staleness = :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.format = :json
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,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'CLI enumerated option parsing' do
6
+ def parse!(argv)
7
+ cli = CovLoupe::CoverageCLI.new
8
+ cli.send(:parse_options!, argv.dup)
9
+ cli
10
+ end
11
+
12
+ describe 'accepts short and long forms' do
13
+ cases = [
14
+ { argv: ['--sort-order', 'a', 'list'], accessor: :sort_order, expected: :ascending },
15
+ { argv: ['--sort-order', 'd', 'list'], accessor: :sort_order, expected: :descending },
16
+ { argv: ['--sort-order', 'ascending', 'list'], accessor: :sort_order, expected: :ascending },
17
+ { argv: ['--sort-order', 'descending', 'list'], accessor: :sort_order,
18
+ expected: :descending },
19
+
20
+ { argv: ['--source', 'f', 'summary', 'lib/foo.rb'], accessor: :source_mode, expected: :full },
21
+ { argv: ['--source', 'u', 'summary', 'lib/foo.rb'], accessor: :source_mode,
22
+ expected: :uncovered },
23
+ { argv: ['--source', 'full', 'summary', 'lib/foo.rb'], accessor: :source_mode,
24
+ expected: :full },
25
+ { argv: ['--source', 'uncovered', 'summary', 'lib/foo.rb'], accessor: :source_mode,
26
+ expected: :uncovered },
27
+
28
+ { argv: ['-S', 'e', 'list'], accessor: :staleness, expected: :error },
29
+ { argv: ['-S', 'o', 'list'], accessor: :staleness, expected: :off },
30
+ { argv: ['--staleness', 'e', 'list'], accessor: :staleness, expected: :error },
31
+ { argv: ['--staleness', 'o', 'list'], accessor: :staleness, expected: :off },
32
+
33
+ { argv: ['--error-mode', 'off', 'list'], accessor: :error_mode, expected: :off },
34
+ { argv: ['--error-mode', 'o', 'list'], accessor: :error_mode, expected: :off },
35
+ { argv: ['--error-mode', 'log', 'list'], accessor: :error_mode, expected: :log },
36
+ { argv: ['--error-mode', 'debug', 'list'], accessor: :error_mode, expected: :debug }
37
+ ]
38
+
39
+ cases.each do |c|
40
+ it "parses #{c[:argv].join(' ')}" do
41
+ cli = parse!(c[:argv])
42
+ expect(cli.config.public_send(c[:accessor])).to eq(c[:expected])
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'rejects invalid values' do
48
+ invalid_cases = [
49
+ { argv: ['--sort-order', 'asc', 'list'] },
50
+ { argv: ['--source', 'x', 'summary', 'lib/foo.rb'] },
51
+ { argv: ['-S', 'x', 'list'] },
52
+ { argv: ['--staleness', 'x', 'list'] },
53
+ { argv: ['--error-mode', 'bad', 'list'] },
54
+ { argv: ['--error-mode', 'on', 'list'] },
55
+ { argv: ['--error-mode', 'trace', 'list'] }
56
+ ]
57
+
58
+ invalid_cases.each do |c|
59
+ it "exits 1 for #{c[:argv].join(' ')}" do
60
+ _out, err, status = run_cli_with_status(*c[:argv])
61
+ expect(status).to eq(1)
62
+ expect(err).to include('Error:')
63
+ expect(err).to include('invalid argument')
64
+ end
65
+ end
66
+ end
67
+
68
+ describe 'missing value hints' do
69
+ it 'exits 1 when -S is provided without a value' do
70
+ _out, err, status = run_cli_with_status('-S', 'list')
71
+ expect(status).to eq(1)
72
+ expect(err).to include('invalid argument')
73
+ end
74
+
75
+ it 'exits 1 when --staleness is provided without a value' do
76
+ _out, err, status = run_cli_with_status('--staleness', 'list')
77
+ expect(status).to eq(1)
78
+ expect(err).to include('invalid argument')
79
+ end
80
+
81
+ it 'exits 1 when --source is provided without a value' do
82
+ _out, err, status = run_cli_with_status('--source', 'summary', 'lib/foo.rb')
83
+ expect(status).to eq(1)
84
+ # Depending on OptParse implementation for required argument, it might say "missing argument"
85
+ # But usually it consumes next arg. If 'summary' is consumed as argument for source:
86
+ # normalize_source_mode('summary') -> raises InvalidArgument.
87
+ expect(err).to include('invalid argument')
88
+ end
89
+ end
90
+ end