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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +329 -0
- data/docs/dev/ARCHITECTURE.md +80 -0
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/dev/DEVELOPMENT.md +83 -0
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/dev/arch-decisions/001-x-arch-decision.md +95 -0
- data/docs/dev/arch-decisions/002-x-arch-decision.md +159 -0
- data/docs/dev/arch-decisions/003-x-arch-decision.md +165 -0
- data/docs/dev/arch-decisions/004-x-arch-decision.md +203 -0
- data/docs/dev/arch-decisions/005-x-arch-decision.md +189 -0
- data/docs/dev/arch-decisions/README.md +60 -0
- data/docs/dev/presentations/cov-loupe-presentation.md +255 -0
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/user/ADVANCED_USAGE.md +777 -0
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/user/ERROR_HANDLING.md +93 -0
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/user/LIBRARY_API.md +693 -0
- data/docs/user/MCP_INTEGRATION.md +490 -0
- data/docs/user/README.md +14 -0
- data/docs/user/TROUBLESHOOTING.md +197 -0
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/cov-loupe +23 -0
- data/lib/cov_loupe/app_config.rb +56 -0
- data/lib/cov_loupe/app_context.rb +26 -0
- data/lib/cov_loupe/base_tool.rb +102 -0
- data/lib/cov_loupe/cli.rb +178 -0
- data/lib/cov_loupe/commands/base_command.rb +67 -0
- data/lib/cov_loupe/commands/command_factory.rb +45 -0
- data/lib/cov_loupe/commands/detailed_command.rb +38 -0
- data/lib/cov_loupe/commands/list_command.rb +13 -0
- data/lib/cov_loupe/commands/raw_command.rb +38 -0
- data/lib/cov_loupe/commands/summary_command.rb +41 -0
- data/lib/cov_loupe/commands/totals_command.rb +53 -0
- data/lib/cov_loupe/commands/uncovered_command.rb +45 -0
- data/lib/cov_loupe/commands/validate_command.rb +60 -0
- data/lib/cov_loupe/commands/version_command.rb +33 -0
- data/lib/cov_loupe/config_parser.rb +32 -0
- data/lib/cov_loupe/constants.rb +22 -0
- data/lib/cov_loupe/coverage_reporter.rb +31 -0
- data/lib/cov_loupe/error_handler.rb +165 -0
- data/lib/cov_loupe/error_handler_factory.rb +31 -0
- data/lib/cov_loupe/errors.rb +191 -0
- data/lib/cov_loupe/formatters/source_formatter.rb +152 -0
- data/lib/cov_loupe/formatters.rb +51 -0
- data/lib/cov_loupe/mcp_server.rb +42 -0
- data/lib/cov_loupe/mode_detector.rb +56 -0
- data/lib/cov_loupe/model.rb +339 -0
- data/lib/cov_loupe/option_normalizers.rb +113 -0
- data/lib/cov_loupe/option_parser_builder.rb +147 -0
- data/lib/cov_loupe/option_parsers/env_options_parser.rb +48 -0
- data/lib/cov_loupe/option_parsers/error_helper.rb +110 -0
- data/lib/cov_loupe/path_relativizer.rb +64 -0
- data/lib/cov_loupe/predicate_evaluator.rb +72 -0
- data/lib/cov_loupe/presenters/base_coverage_presenter.rb +42 -0
- data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/project_coverage_presenter.rb +50 -0
- data/lib/cov_loupe/presenters/project_totals_presenter.rb +27 -0
- data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +122 -0
- data/lib/cov_loupe/resolvers/resolver_factory.rb +28 -0
- data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +76 -0
- data/lib/cov_loupe/resultset_loader.rb +131 -0
- data/lib/cov_loupe/staleness_checker.rb +247 -0
- data/lib/cov_loupe/table_formatter.rb +64 -0
- data/lib/cov_loupe/tools/all_files_coverage_tool.rb +51 -0
- data/lib/cov_loupe/tools/coverage_detailed_tool.rb +35 -0
- data/lib/cov_loupe/tools/coverage_raw_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_summary_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_table_tool.rb +50 -0
- data/lib/cov_loupe/tools/coverage_totals_tool.rb +44 -0
- data/lib/cov_loupe/tools/help_tool.rb +115 -0
- data/lib/cov_loupe/tools/uncovered_lines_tool.rb +34 -0
- data/lib/cov_loupe/tools/validate_tool.rb +72 -0
- data/lib/cov_loupe/tools/version_tool.rb +32 -0
- data/lib/cov_loupe/util.rb +88 -0
- data/lib/cov_loupe/version.rb +5 -0
- data/lib/cov_loupe.rb +140 -0
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +53 -0
- data/spec/app_config_spec.rb +142 -0
- data/spec/base_tool_spec.rb +62 -0
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_enumerated_options_spec.rb +90 -0
- data/spec/cli_error_spec.rb +184 -0
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +44 -0
- data/spec/cli_spec.rb +192 -0
- data/spec/cli_table_spec.rb +28 -0
- data/spec/cli_usage_spec.rb +42 -0
- data/spec/commands/base_command_spec.rb +107 -0
- data/spec/commands/command_factory_spec.rb +76 -0
- data/spec/commands/detailed_command_spec.rb +34 -0
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +69 -0
- data/spec/commands/summary_command_spec.rb +34 -0
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +55 -0
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/cov_loupe/formatters/source_formatter_spec.rb +267 -0
- data/spec/cov_loupe/formatters_spec.rb +76 -0
- data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/cov_loupe_model_spec.rb +454 -0
- data/spec/cov_loupe_module_spec.rb +37 -0
- data/spec/cov_loupe_opts_spec.rb +185 -0
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +59 -0
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +197 -0
- data/spec/error_mode_spec.rb +139 -0
- data/spec/errors_edge_cases_spec.rb +312 -0
- data/spec/errors_stale_spec.rb +83 -0
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +5 -0
- data/spec/fixtures/project1/lib/foo.rb +6 -0
- data/spec/help_tool_spec.rb +26 -0
- data/spec/integration_spec.rb +789 -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 +106 -0
- data/spec/mode_detector_spec.rb +153 -0
- data/spec/model_error_handling_spec.rb +269 -0
- data/spec/model_staleness_spec.rb +79 -0
- data/spec/option_normalizers_spec.rb +203 -0
- data/spec/option_parsers/env_options_parser_spec.rb +221 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +98 -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 +87 -0
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +282 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +60 -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 +179 -0
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/spec_helper.rb +127 -0
- data/spec/staleness_checker_spec.rb +374 -0
- data/spec/staleness_more_spec.rb +42 -0
- data/spec/support/cli_helpers.rb +22 -0
- data/spec/support/control_flow_helpers.rb +20 -0
- data/spec/support/fake_mcp.rb +40 -0
- data/spec/support/io_helpers.rb +29 -0
- data/spec/support/mcp_helpers.rb +35 -0
- data/spec/support/mcp_runner.rb +66 -0
- data/spec/support/mocking_helpers.rb +30 -0
- data/spec/table_format_spec.rb +70 -0
- data/spec/tools/validate_tool_spec.rb +132 -0
- data/spec/tools_error_handling_spec.rb +130 -0
- data/spec/util_spec.rb +154 -0
- data/spec/version_spec.rb +123 -0
- data/spec/version_tool_spec.rb +141 -0
- metadata +290 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe do
|
|
6
|
+
describe CovLoupe::ConfigurationError do
|
|
7
|
+
describe '#user_friendly_message' do
|
|
8
|
+
it 'prefixes message with "Configuration error:"' do
|
|
9
|
+
error = described_class.new('Invalid option value')
|
|
10
|
+
|
|
11
|
+
expect(error.user_friendly_message).to eq('Configuration error: Invalid option value')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'handles empty message' do
|
|
15
|
+
error = described_class.new('')
|
|
16
|
+
|
|
17
|
+
expect(error.user_friendly_message).to eq('Configuration error: ')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'handles nil message' do
|
|
21
|
+
# When nil is passed to StandardError, it uses the class name as the message
|
|
22
|
+
error = described_class.new(nil)
|
|
23
|
+
|
|
24
|
+
expect(error.user_friendly_message).to eq('Configuration error: CovLoupe::ConfigurationError')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
describe CovLoupe::ResultsetNotFoundError do
|
|
31
|
+
describe '#user_friendly_message' do
|
|
32
|
+
it 'includes helpful tips in CLI mode' do
|
|
33
|
+
# Create a CLI context (not MCP mode)
|
|
34
|
+
error_handler = CovLoupe::ErrorHandlerFactory.for_cli
|
|
35
|
+
context = CovLoupe.create_context(error_handler: error_handler, mode: :cli)
|
|
36
|
+
CovLoupe.with_context(context) do
|
|
37
|
+
error = described_class.new('Coverage data not found')
|
|
38
|
+
message = error.user_friendly_message
|
|
39
|
+
|
|
40
|
+
expect(message).to include(
|
|
41
|
+
'File error: Coverage data not found',
|
|
42
|
+
'Try one of the following:',
|
|
43
|
+
'cd to a directory containing coverage/.resultset.json',
|
|
44
|
+
'Specify a resultset: cov-loupe -r PATH',
|
|
45
|
+
'Use -h for help: cov-loupe -h'
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'does not include helpful tips in MCP mode' do
|
|
51
|
+
# Create an MCP context
|
|
52
|
+
error_handler = CovLoupe::ErrorHandlerFactory.for_mcp_server
|
|
53
|
+
context = CovLoupe.create_context(error_handler: error_handler, mode: :mcp)
|
|
54
|
+
CovLoupe.with_context(context) do
|
|
55
|
+
error = described_class.new('Coverage data not found')
|
|
56
|
+
message = error.user_friendly_message
|
|
57
|
+
|
|
58
|
+
expect(message).to eq('File error: Coverage data not found')
|
|
59
|
+
expect(message).not_to include('Try one of the following:')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe CovLoupe::CoverageDataStaleError do
|
|
66
|
+
describe 'time formatting edge cases' do
|
|
67
|
+
it 'handles invalid epoch seconds gracefully in rescue path' do
|
|
68
|
+
# Create an object that responds to to_i but breaks Time.at
|
|
69
|
+
bad_timestamp = Object.new
|
|
70
|
+
def bad_timestamp.to_i
|
|
71
|
+
raise ArgumentError, "Can't convert"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
error = described_class.new(
|
|
75
|
+
'Test error',
|
|
76
|
+
nil,
|
|
77
|
+
file_path: 'test.rb',
|
|
78
|
+
file_mtime: Time.at(1000),
|
|
79
|
+
cov_timestamp: bad_timestamp,
|
|
80
|
+
src_len: 10,
|
|
81
|
+
cov_len: 8
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
message = error.user_friendly_message
|
|
85
|
+
expect(message).to include('Coverage data stale')
|
|
86
|
+
expect(message).to include('Test error')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'handles time that breaks Time.parse but has valid to_s' do
|
|
90
|
+
# Create an object that can't be parsed but has valid to_s
|
|
91
|
+
bad_time = Object.new
|
|
92
|
+
def bad_time.to_s
|
|
93
|
+
'unparseable_time_string'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
error = described_class.new(
|
|
97
|
+
'Test error',
|
|
98
|
+
nil,
|
|
99
|
+
file_path: 'test.rb',
|
|
100
|
+
file_mtime: bad_time,
|
|
101
|
+
cov_timestamp: 1000,
|
|
102
|
+
src_len: 10,
|
|
103
|
+
cov_len: 8
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
message = error.user_friendly_message
|
|
107
|
+
expect(message).to include('Coverage data stale')
|
|
108
|
+
expect(message).to include('Test error')
|
|
109
|
+
# Should fallback to string representation
|
|
110
|
+
expect(message).to include('unparseable_time_string')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'handles delta calculation with invalid values in rescue path' do
|
|
114
|
+
# Create objects that break arithmetic
|
|
115
|
+
bad_time = Object.new
|
|
116
|
+
def bad_time.to_i
|
|
117
|
+
raise ArgumentError, "Can't convert"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
bad_timestamp = Object.new
|
|
121
|
+
def bad_timestamp.to_i
|
|
122
|
+
raise ArgumentError, "Can't convert"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
error = described_class.new(
|
|
126
|
+
'Test error',
|
|
127
|
+
nil,
|
|
128
|
+
file_path: 'test.rb',
|
|
129
|
+
file_mtime: bad_time,
|
|
130
|
+
cov_timestamp: bad_timestamp,
|
|
131
|
+
src_len: 10,
|
|
132
|
+
cov_len: 8
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
message = error.user_friendly_message
|
|
136
|
+
expect(message).to include('Coverage data stale')
|
|
137
|
+
# Delta line should not appear when calculation fails
|
|
138
|
+
expect(message).not_to match(/Delta\s+- file is/)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe 'default message generation' do
|
|
143
|
+
it 'uses default message when message is nil with file_path' do
|
|
144
|
+
error = described_class.new(
|
|
145
|
+
nil, # No message provided - triggers default_message
|
|
146
|
+
nil,
|
|
147
|
+
file_path: 'test.rb',
|
|
148
|
+
file_mtime: Time.at(2000),
|
|
149
|
+
cov_timestamp: 1000
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
message = error.user_friendly_message
|
|
153
|
+
# default_message returns "Coverage data appears stale for test.rb"
|
|
154
|
+
expect(message).to include('Coverage data appears stale for test.rb')
|
|
155
|
+
# File path should appear in the details section
|
|
156
|
+
expect(message).to match(/File\s+-/)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'uses generic default message when file_path is nil' do
|
|
160
|
+
# This tests the fallback path when file_path is nil: fp = file_path || 'file'
|
|
161
|
+
error = described_class.new(
|
|
162
|
+
nil, # No message - triggers default_message
|
|
163
|
+
nil,
|
|
164
|
+
file_path: nil, # No file path - triggers 'file' fallback
|
|
165
|
+
file_mtime: Time.at(2000),
|
|
166
|
+
cov_timestamp: 1000
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
message = error.user_friendly_message
|
|
170
|
+
# When file_path is nil, default_message returns "Coverage data appears stale for file"
|
|
171
|
+
expect(message).to include('Coverage data appears stale for file')
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe CovLoupe::CoverageDataProjectStaleError do
|
|
177
|
+
describe 'default message generation' do
|
|
178
|
+
# These tests exercise the private default_message method
|
|
179
|
+
it 'includes project stale info when message is nil' do
|
|
180
|
+
error = described_class.new(
|
|
181
|
+
nil, # StandardError sets message to class name when nil
|
|
182
|
+
nil,
|
|
183
|
+
cov_timestamp: 1000,
|
|
184
|
+
newer_files: ['file1.rb', 'file2.rb']
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
message = error.user_friendly_message
|
|
188
|
+
# user_friendly_message prefixes with "Coverage data stale (project):"
|
|
189
|
+
expect(message).to include('Coverage data stale (project)')
|
|
190
|
+
expect(message).to include('Newer files')
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'exercises default_message directly via send' do
|
|
194
|
+
# Directly test the private default_message method for coverage
|
|
195
|
+
# This is necessary because user_friendly_message uses `message || default_message`
|
|
196
|
+
# and StandardError sets message to class name when initialized with nil
|
|
197
|
+
error = described_class.new(
|
|
198
|
+
'explicit message',
|
|
199
|
+
nil,
|
|
200
|
+
cov_timestamp: 1000
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Call the private default_message method directly
|
|
204
|
+
result = error.send(:default_message)
|
|
205
|
+
expect(result).to eq('Coverage data appears stale for project')
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe 'large file list truncation' do
|
|
210
|
+
it 'shows all files when there are 10 or fewer deleted files' do
|
|
211
|
+
deleted_files = (1..10).map { |i| "deleted_file_#{i}.rb" }
|
|
212
|
+
error = described_class.new(
|
|
213
|
+
'Test error',
|
|
214
|
+
nil,
|
|
215
|
+
cov_timestamp: 1000,
|
|
216
|
+
deleted_files: deleted_files
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
message = error.user_friendly_message
|
|
220
|
+
expect(message).to include('Coverage-only files (deleted or moved in project, 10):')
|
|
221
|
+
deleted_files.each do |file|
|
|
222
|
+
expect(message).to include(" - #{file}")
|
|
223
|
+
end
|
|
224
|
+
expect(message).not_to include('...')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'truncates and shows ellipsis when there are more than 10 deleted files' do
|
|
228
|
+
deleted_files = (1..15).map { |i| "deleted_file_#{i}.rb" }
|
|
229
|
+
error = described_class.new(
|
|
230
|
+
'Test error',
|
|
231
|
+
nil,
|
|
232
|
+
cov_timestamp: 1000,
|
|
233
|
+
deleted_files: deleted_files
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
message = error.user_friendly_message
|
|
237
|
+
expect(message).to include('Coverage-only files (deleted or moved in project, 15):')
|
|
238
|
+
# Should show first 10 files
|
|
239
|
+
deleted_files[0..9].each do |file|
|
|
240
|
+
expect(message).to include(" - #{file}")
|
|
241
|
+
end
|
|
242
|
+
# Should not show files beyond 10
|
|
243
|
+
deleted_files[10..14].each do |file|
|
|
244
|
+
expect(message).not_to include(" - #{file}")
|
|
245
|
+
end
|
|
246
|
+
# Should show ellipsis
|
|
247
|
+
expect(message).to include('...')
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'shows all files when there are 10 or fewer missing files' do
|
|
251
|
+
missing_files = (1..10).map { |i| "missing_file_#{i}.rb" }
|
|
252
|
+
error = described_class.new(
|
|
253
|
+
'Test error',
|
|
254
|
+
nil,
|
|
255
|
+
cov_timestamp: 1000,
|
|
256
|
+
missing_files: missing_files
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
message = error.user_friendly_message
|
|
260
|
+
expect(message).to include('Missing files (new in project, not in coverage, 10):')
|
|
261
|
+
missing_files.each do |file|
|
|
262
|
+
expect(message).to include(" - #{file}")
|
|
263
|
+
end
|
|
264
|
+
expect(message).not_to include('...')
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it 'truncates and shows ellipsis when there are more than 10 missing files' do
|
|
268
|
+
missing_files = (1..12).map { |i| "missing_file_#{i}.rb" }
|
|
269
|
+
error = described_class.new(
|
|
270
|
+
'Test error',
|
|
271
|
+
nil,
|
|
272
|
+
cov_timestamp: 1000,
|
|
273
|
+
missing_files: missing_files
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
message = error.user_friendly_message
|
|
277
|
+
expect(message).to include('Missing files (new in project, not in coverage, 12):')
|
|
278
|
+
# Should show first 10 files
|
|
279
|
+
missing_files[0..9].each do |file|
|
|
280
|
+
expect(message).to include(" - #{file}")
|
|
281
|
+
end
|
|
282
|
+
# Should not show files beyond 10
|
|
283
|
+
expect(message).not_to include(" - #{missing_files[11]}")
|
|
284
|
+
# Should show ellipsis
|
|
285
|
+
expect(message).to include('...')
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it 'truncates and shows ellipsis when there are more than 10 newer files' do
|
|
289
|
+
newer_files = (1..20).map { |i| "newer_file_#{i}.rb" }
|
|
290
|
+
error = described_class.new(
|
|
291
|
+
'Test error',
|
|
292
|
+
nil,
|
|
293
|
+
cov_timestamp: 1000,
|
|
294
|
+
newer_files: newer_files
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
message = error.user_friendly_message
|
|
298
|
+
expect(message).to include('Newer files (20):')
|
|
299
|
+
# Should show first 10 files
|
|
300
|
+
newer_files[0..9].each do |file|
|
|
301
|
+
expect(message).to include(" - #{file}")
|
|
302
|
+
end
|
|
303
|
+
# Should not show files beyond 10
|
|
304
|
+
newer_files[10..19].each do |file|
|
|
305
|
+
expect(message).not_to include(" - #{file}")
|
|
306
|
+
end
|
|
307
|
+
# Should show ellipsis
|
|
308
|
+
expect(message).to include('...')
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CovLoupe::CoverageDataStaleError do
|
|
6
|
+
it 'formats a detailed, user-friendly message with UTC/local, delta, and resultset' do
|
|
7
|
+
file_time = Time.at(TEST_FILE_TIMESTAMP) # 1970-01-01T00:16:40Z
|
|
8
|
+
cov_epoch = VERY_OLD_TIMESTAMP # 1970-01-01T00:00:00Z
|
|
9
|
+
err = described_class.new(
|
|
10
|
+
'Coverage data appears stale for foo.rb',
|
|
11
|
+
nil,
|
|
12
|
+
file_path: 'foo.rb',
|
|
13
|
+
file_mtime: file_time,
|
|
14
|
+
cov_timestamp: cov_epoch,
|
|
15
|
+
src_len: 10,
|
|
16
|
+
cov_len: 8,
|
|
17
|
+
resultset_path: '/path/to/coverage/.resultset.json'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
msg = err.user_friendly_message
|
|
21
|
+
|
|
22
|
+
expect(msg).to include('Coverage data stale: Coverage data appears stale for foo.rb')
|
|
23
|
+
expect(msg).to match(/File\s*-\s*time:\s*1970-01-01T00:16:40Z/)
|
|
24
|
+
expect(msg).to include('(local ') # do not assert exact local tz
|
|
25
|
+
expect(msg).to match(/Coverage\s*-\s*time:\s*1970-01-01T00:00:00Z/)
|
|
26
|
+
expect(msg).to match(/lines:\s*10/)
|
|
27
|
+
expect(msg).to match(/lines:\s*8/)
|
|
28
|
+
expect(msg).to match(/Delta\s*- file is \+1000s newer than coverage/)
|
|
29
|
+
expect(msg).to include('Resultset - /path/to/coverage/.resultset.json')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'handles missing timestamps gracefully' do
|
|
33
|
+
err = described_class.new(
|
|
34
|
+
'Coverage data appears stale for bar.rb',
|
|
35
|
+
nil,
|
|
36
|
+
file_path: 'bar.rb',
|
|
37
|
+
file_mtime: nil,
|
|
38
|
+
cov_timestamp: nil,
|
|
39
|
+
src_len: 1,
|
|
40
|
+
cov_len: 0,
|
|
41
|
+
resultset_path: nil
|
|
42
|
+
)
|
|
43
|
+
msg = err.user_friendly_message
|
|
44
|
+
expect(msg).to include('Coverage data stale: Coverage data appears stale for bar.rb')
|
|
45
|
+
expect(msg).to match(/File\s*-\s*time:\s*not found.*lines: 1/m)
|
|
46
|
+
expect(msg).to match(/Coverage\s*-\s*time:\s*not found.*lines: 0/m)
|
|
47
|
+
expect(msg).not_to include('Delta')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'uses default message when message is nil' do
|
|
51
|
+
err = described_class.new(
|
|
52
|
+
nil,
|
|
53
|
+
nil,
|
|
54
|
+
file_path: 'lib/example.rb',
|
|
55
|
+
file_mtime: Time.now,
|
|
56
|
+
cov_timestamp: Time.now.to_i - 1000,
|
|
57
|
+
src_len: 10,
|
|
58
|
+
cov_len: 8,
|
|
59
|
+
resultset_path: '/coverage/.resultset.json'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
msg = err.user_friendly_message
|
|
63
|
+
expect(msg).to include('Coverage data stale:')
|
|
64
|
+
expect(msg).to include('Coverage data appears stale for lib/example.rb')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'uses "file" in default message when file_path is also nil' do
|
|
68
|
+
err = described_class.new(
|
|
69
|
+
nil,
|
|
70
|
+
nil,
|
|
71
|
+
file_path: nil,
|
|
72
|
+
file_mtime: nil,
|
|
73
|
+
cov_timestamp: nil,
|
|
74
|
+
src_len: 0,
|
|
75
|
+
cov_len: 0,
|
|
76
|
+
resultset_path: nil
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
msg = err.user_friendly_message
|
|
80
|
+
expect(msg).to include('Coverage data stale:')
|
|
81
|
+
expect(msg).to include('Coverage data appears stale for file')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative 'shared_examples/file_based_mcp_tools'
|
|
5
|
+
|
|
6
|
+
# Load all the tool classes that will be tested
|
|
7
|
+
require 'cov_loupe/tools/coverage_summary_tool'
|
|
8
|
+
require 'cov_loupe/tools/coverage_raw_tool'
|
|
9
|
+
require 'cov_loupe/tools/uncovered_lines_tool'
|
|
10
|
+
require 'cov_loupe/tools/coverage_detailed_tool'
|
|
11
|
+
|
|
12
|
+
RSpec.describe 'File-based MCP Tools' do
|
|
13
|
+
# Test each file-based tool using the shared example with its specific configuration
|
|
14
|
+
FILE_BASED_TOOL_CONFIGS.each_value do |config|
|
|
15
|
+
describe config[:tool_class] do
|
|
16
|
+
it_behaves_like 'a file-based MCP tool', config
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Test that all file-based tools handle the same parameters consistently
|
|
21
|
+
describe 'parameter consistency' do
|
|
22
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
23
|
+
|
|
24
|
+
before do
|
|
25
|
+
setup_mcp_response_stub
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'all file-based tools accept the same basic parameters' do
|
|
29
|
+
# Test that all tools can be called with the same parameter signature
|
|
30
|
+
FILE_BASED_TOOL_CONFIGS.each_value do |config|
|
|
31
|
+
model = instance_double(CovLoupe::CoverageModel)
|
|
32
|
+
allow(CovLoupe::CoverageModel).to receive(:new).and_return(model)
|
|
33
|
+
allow(model).to receive(config[:model_method]).and_return(config[:mock_data])
|
|
34
|
+
allow(model).to receive(:relativize) { |payload| payload }
|
|
35
|
+
allow(model).to receive(:staleness_for).and_return(false)
|
|
36
|
+
|
|
37
|
+
expect do
|
|
38
|
+
config[:tool_class].call(
|
|
39
|
+
path: 'lib/example.rb',
|
|
40
|
+
root: '.',
|
|
41
|
+
resultset: 'coverage',
|
|
42
|
+
server_context: server_context
|
|
43
|
+
)
|
|
44
|
+
end.not_to raise_error
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'all file-based tools return JSON resources with consistent structure' do
|
|
49
|
+
FILE_BASED_TOOL_CONFIGS.each_value do |config|
|
|
50
|
+
model = instance_double(CovLoupe::CoverageModel)
|
|
51
|
+
allow(CovLoupe::CoverageModel).to receive(:new).and_return(model)
|
|
52
|
+
allow(model).to receive(config[:model_method]).and_return(config[:mock_data])
|
|
53
|
+
allow(model).to receive(:relativize) { |payload| payload }
|
|
54
|
+
allow(model).to receive(:staleness_for).and_return(false)
|
|
55
|
+
|
|
56
|
+
response = config[:tool_class].call(path: 'lib/foo.rb', server_context: server_context)
|
|
57
|
+
|
|
58
|
+
# All should have the same basic MCP text structure
|
|
59
|
+
expect(response.payload).to be_an(Array)
|
|
60
|
+
expect(response.payload.first['type']).to eq('text')
|
|
61
|
+
expect(response.payload.first).to have_key('text')
|
|
62
|
+
|
|
63
|
+
# All should return valid JSON
|
|
64
|
+
expect { JSON.parse(response.payload.first['text']) }.not_to raise_error
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Performance/behavior comparison tests
|
|
70
|
+
describe 'cross-tool consistency' do
|
|
71
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
72
|
+
|
|
73
|
+
before do
|
|
74
|
+
setup_mcp_response_stub
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'tools that include summary data return consistent summary format' do
|
|
78
|
+
summary_tools = FILE_BASED_TOOL_CONFIGS.select do |_, config|
|
|
79
|
+
config[:expected_keys].include?('summary')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
summary_tools.each_value do |config|
|
|
83
|
+
model = instance_double(CovLoupe::CoverageModel)
|
|
84
|
+
allow(CovLoupe::CoverageModel).to receive(:new).and_return(model)
|
|
85
|
+
allow(model).to receive(config[:model_method]).and_return(config[:mock_data])
|
|
86
|
+
allow(model).to receive(:relativize) { |payload| payload }
|
|
87
|
+
allow(model).to receive(:staleness_for).and_return(false)
|
|
88
|
+
|
|
89
|
+
response = config[:tool_class].call(path: 'lib/foo.rb', server_context: server_context)
|
|
90
|
+
data = JSON.parse(response.payload.first['text'])
|
|
91
|
+
|
|
92
|
+
if data.key?('summary')
|
|
93
|
+
expect(data['summary']).to include('covered', 'total', 'percentage')
|
|
94
|
+
expect(data['summary']['percentage']).to be_a(Numeric)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'cov_loupe/tools/help_tool'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::Tools::HelpTool do
|
|
7
|
+
let(:server_context) { instance_double('ServerContext').as_null_object }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
setup_mcp_response_stub
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'returns guidance for each registered tool' do
|
|
14
|
+
response = described_class.call(server_context: server_context)
|
|
15
|
+
expect(response.meta).to be_nil
|
|
16
|
+
|
|
17
|
+
payload = response.payload.first
|
|
18
|
+
expect(payload['type']).to eq('text')
|
|
19
|
+
data = JSON.parse(payload['text'])
|
|
20
|
+
tool_names = data['tools'].map { |entry| entry['tool'] }
|
|
21
|
+
|
|
22
|
+
expect(tool_names).to include('coverage_summary_tool', 'uncovered_lines_tool',
|
|
23
|
+
'all_files_coverage_tool', 'coverage_totals_tool', 'coverage_table_tool', 'version_tool')
|
|
24
|
+
expect(data['tools']).to all(include('use_when', 'avoid_when', 'inputs'))
|
|
25
|
+
end
|
|
26
|
+
end
|