cov-loupe 3.0.0 → 4.0.0.pre

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 (281) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +230 -0
  3. data/CLAUDE.md +5 -0
  4. data/CODE_OF_CONDUCT.md +62 -0
  5. data/CONTRIBUTING.md +102 -0
  6. data/GEMINI.md +5 -0
  7. data/README.md +154 -51
  8. data/RELEASE_NOTES.md +452 -0
  9. data/dev/images/cov-loupe-icon-lores.png +0 -0
  10. data/dev/images/cov-loupe-icon-square.png +0 -0
  11. data/dev/images/cov-loupe-icon.png +0 -0
  12. data/dev/images/cov-loupe-logo.png +0 -0
  13. data/dev/prompts/README.md +74 -0
  14. data/dev/prompts/archive/architectural-review-and-actions-prompt.md +53 -0
  15. data/dev/prompts/archive/investigate-and-report-issues-prompt.md +33 -0
  16. data/dev/prompts/archive/produce-action-items-prompt.md +25 -0
  17. data/dev/prompts/guidelines/ai-code-evaluator-guidelines.md +337 -0
  18. data/dev/prompts/improve/refactor-test-suite.md +18 -0
  19. data/dev/prompts/improve/simplify-code-logic.md +133 -0
  20. data/dev/prompts/improve/update-documentation.md +21 -0
  21. data/dev/prompts/review/comprehensive-codebase-review.md +176 -0
  22. data/dev/prompts/review/identify-action-items.md +143 -0
  23. data/dev/prompts/review/verify-code-changes.md +54 -0
  24. data/dev/prompts/validate/create-screencast-outline.md +234 -0
  25. data/dev/prompts/validate/test-documentation-examples.md +180 -0
  26. data/docs/QUICKSTART.md +63 -0
  27. data/docs/assets/images/cov-loupe-logo-lores.png +0 -0
  28. data/docs/assets/images/cov-loupe-logo.png +0 -0
  29. data/docs/assets/images/favicon.png +0 -0
  30. data/docs/assets/stylesheets/branding.css +16 -0
  31. data/docs/assets/stylesheets/extra.css +15 -0
  32. data/docs/code_of_conduct.md +1 -0
  33. data/docs/contributing.md +1 -0
  34. data/docs/dev/ARCHITECTURE.md +56 -11
  35. data/docs/dev/DEVELOPMENT.md +116 -12
  36. data/docs/dev/FUTURE_ENHANCEMENTS.md +14 -0
  37. data/docs/dev/README.md +3 -2
  38. data/docs/dev/RELEASING.md +2 -0
  39. data/docs/dev/arch-decisions/README.md +10 -7
  40. data/docs/dev/arch-decisions/application-architecture.md +259 -0
  41. data/docs/dev/arch-decisions/coverage-data-quality.md +193 -0
  42. data/docs/dev/arch-decisions/output-character-mode.md +217 -0
  43. data/docs/dev/arch-decisions/path-resolution.md +90 -0
  44. data/docs/dev/arch-decisions/{004-x-arch-decision.md → policy-validation.md} +32 -28
  45. data/docs/dev/arch-decisions/{005-x-arch-decision.md → simplecov-integration.md} +47 -44
  46. data/docs/dev/presentations/cov-loupe-presentation.md +15 -13
  47. data/docs/examples/mcp-inputs.md +3 -0
  48. data/docs/examples/prompts.md +3 -0
  49. data/docs/examples/success_predicates.md +3 -0
  50. data/docs/fixtures/demo_project/.resultset.json +170 -0
  51. data/docs/fixtures/demo_project/README.md +6 -0
  52. data/docs/fixtures/demo_project/app/controllers/admin/audit_logs_controller.rb +19 -0
  53. data/docs/fixtures/demo_project/app/controllers/orders_controller.rb +26 -0
  54. data/docs/fixtures/demo_project/app/models/order.rb +20 -0
  55. data/docs/fixtures/demo_project/app/models/user.rb +19 -0
  56. data/docs/fixtures/demo_project/lib/api/client.rb +22 -0
  57. data/docs/fixtures/demo_project/lib/ops/jobs/cleanup_job.rb +16 -0
  58. data/docs/fixtures/demo_project/lib/ops/jobs/report_job.rb +17 -0
  59. data/docs/fixtures/demo_project/lib/payments/processor.rb +15 -0
  60. data/docs/fixtures/demo_project/lib/payments/refund_service.rb +15 -0
  61. data/docs/fixtures/demo_project/lib/payments/reporting/exporter.rb +16 -0
  62. data/docs/index.md +1 -0
  63. data/docs/license.md +3 -0
  64. data/docs/release_notes.md +3 -0
  65. data/docs/user/ADVANCED_USAGE.md +208 -115
  66. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +2 -0
  67. data/docs/user/CLI_USAGE.md +276 -101
  68. data/docs/user/ERROR_HANDLING.md +4 -4
  69. data/docs/user/EXAMPLES.md +121 -128
  70. data/docs/user/INSTALLATION.md +9 -28
  71. data/docs/user/LIBRARY_API.md +227 -122
  72. data/docs/user/MCP_INTEGRATION.md +114 -203
  73. data/docs/user/README.md +5 -1
  74. data/docs/user/TROUBLESHOOTING.md +49 -27
  75. data/docs/user/installing-a-prelease-version-of-covloupe.md +43 -0
  76. data/docs/user/{V2-BREAKING-CHANGES.md → migrations/MIGRATING_TO_V2.md} +62 -72
  77. data/docs/user/migrations/MIGRATING_TO_V3.md +72 -0
  78. data/docs/user/migrations/MIGRATING_TO_V4.md +591 -0
  79. data/docs/user/migrations/README.md +22 -0
  80. data/docs/user/prompts/README.md +9 -0
  81. data/docs/user/prompts/non-web-coverage-analysis-prompt.md +103 -0
  82. data/docs/user/prompts/rails-coverage-analysis-prompt.md +94 -0
  83. data/docs/user/prompts/use-cli-not-mcp-prompt.md +53 -0
  84. data/examples/cli_demo.sh +77 -0
  85. data/examples/filter_and_table_demo-output.md +114 -0
  86. data/examples/filter_and_table_demo.rb +174 -0
  87. data/examples/fixtures/demo_project/coverage/.resultset.json +10 -0
  88. data/examples/mcp-inputs/README.md +66 -0
  89. data/examples/mcp-inputs/coverage_detailed.json +1 -0
  90. data/examples/mcp-inputs/coverage_raw.json +1 -0
  91. data/examples/mcp-inputs/coverage_summary.json +1 -0
  92. data/examples/mcp-inputs/list.json +1 -0
  93. data/examples/mcp-inputs/uncovered_lines.json +1 -0
  94. data/examples/prompts/README.md +27 -0
  95. data/examples/prompts/custom_resultset.txt +2 -0
  96. data/examples/prompts/detailed_with_source.txt +2 -0
  97. data/examples/prompts/list_lowest.txt +2 -0
  98. data/examples/prompts/summary.txt +2 -0
  99. data/examples/prompts/uncovered.txt +2 -0
  100. data/examples/success_predicates/README.md +198 -0
  101. data/examples/success_predicates/all_files_above_threshold_predicate.rb +21 -0
  102. data/examples/success_predicates/directory_specific_thresholds_predicate.rb +30 -0
  103. data/examples/success_predicates/project_coverage_minimum_predicate.rb +6 -0
  104. data/lib/cov_loupe/base_tool.rb +229 -20
  105. data/lib/cov_loupe/cli.rb +132 -23
  106. data/lib/cov_loupe/commands/base_command.rb +25 -6
  107. data/lib/cov_loupe/commands/command_factory.rb +0 -1
  108. data/lib/cov_loupe/commands/detailed_command.rb +10 -5
  109. data/lib/cov_loupe/commands/list_command.rb +2 -1
  110. data/lib/cov_loupe/commands/raw_command.rb +7 -5
  111. data/lib/cov_loupe/commands/summary_command.rb +12 -7
  112. data/lib/cov_loupe/commands/totals_command.rb +74 -10
  113. data/lib/cov_loupe/commands/uncovered_command.rb +7 -5
  114. data/lib/cov_loupe/commands/validate_command.rb +11 -3
  115. data/lib/cov_loupe/commands/version_command.rb +6 -4
  116. data/lib/cov_loupe/{app_config.rb → config/app_config.rb} +13 -5
  117. data/lib/cov_loupe/config/app_context.rb +43 -0
  118. data/lib/cov_loupe/config/boolean_type.rb +91 -0
  119. data/lib/cov_loupe/config/logger.rb +92 -0
  120. data/lib/cov_loupe/{option_normalizers.rb → config/option_normalizers.rb} +55 -24
  121. data/lib/cov_loupe/{option_parser_builder.rb → config/option_parser_builder.rb} +46 -24
  122. data/lib/cov_loupe/coverage/coverage_calculator.rb +53 -0
  123. data/lib/cov_loupe/coverage/coverage_reporter.rb +63 -0
  124. data/lib/cov_loupe/coverage/coverage_table_formatter.rb +133 -0
  125. data/lib/cov_loupe/{error_handler.rb → errors/error_handler.rb} +21 -33
  126. data/lib/cov_loupe/{errors.rb → errors/errors.rb} +48 -71
  127. data/lib/cov_loupe/formatters/formatters.rb +75 -0
  128. data/lib/cov_loupe/formatters/source_formatter.rb +18 -7
  129. data/lib/cov_loupe/formatters/table_formatter.rb +80 -0
  130. data/lib/cov_loupe/loaders/all.rb +15 -0
  131. data/lib/cov_loupe/loaders/all_cli.rb +10 -0
  132. data/lib/cov_loupe/loaders/all_mcp.rb +23 -0
  133. data/lib/cov_loupe/loaders/resultset_loader.rb +147 -0
  134. data/lib/cov_loupe/mcp_server.rb +3 -2
  135. data/lib/cov_loupe/model/model.rb +520 -0
  136. data/lib/cov_loupe/model/model_data.rb +13 -0
  137. data/lib/cov_loupe/model/model_data_cache.rb +116 -0
  138. data/lib/cov_loupe/option_parsers/env_options_parser.rb +17 -6
  139. data/lib/cov_loupe/option_parsers/error_helper.rb +16 -10
  140. data/lib/cov_loupe/output_chars.rb +192 -0
  141. data/lib/cov_loupe/paths/glob_utils.rb +100 -0
  142. data/lib/cov_loupe/{path_relativizer.rb → paths/path_relativizer.rb} +5 -13
  143. data/lib/cov_loupe/paths/path_utils.rb +265 -0
  144. data/lib/cov_loupe/paths/volume_case_sensitivity.rb +173 -0
  145. data/lib/cov_loupe/presenters/base_coverage_presenter.rb +9 -13
  146. data/lib/cov_loupe/presenters/coverage_payload_presenter.rb +21 -0
  147. data/lib/cov_loupe/presenters/payload_caching.rb +23 -0
  148. data/lib/cov_loupe/presenters/project_coverage_presenter.rb +73 -21
  149. data/lib/cov_loupe/presenters/project_totals_presenter.rb +16 -10
  150. data/lib/cov_loupe/repositories/coverage_repository.rb +149 -0
  151. data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +90 -76
  152. data/lib/cov_loupe/resolvers/{resolver_factory.rb → resolver_helpers.rb} +6 -5
  153. data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +40 -12
  154. data/lib/cov_loupe/scripts/command_execution.rb +113 -0
  155. data/lib/cov_loupe/scripts/latest_ci_status.rb +97 -0
  156. data/lib/cov_loupe/scripts/pre_release_check.rb +164 -0
  157. data/lib/cov_loupe/scripts/setup_doc_server.rb +23 -0
  158. data/lib/cov_loupe/scripts/start_doc_server.rb +24 -0
  159. data/lib/cov_loupe/staleness/stale_status.rb +23 -0
  160. data/lib/cov_loupe/staleness/staleness_checker.rb +328 -0
  161. data/lib/cov_loupe/staleness/staleness_message_formatter.rb +91 -0
  162. data/lib/cov_loupe/tools/coverage_detailed_tool.rb +14 -15
  163. data/lib/cov_loupe/tools/coverage_raw_tool.rb +14 -14
  164. data/lib/cov_loupe/tools/coverage_summary_tool.rb +16 -16
  165. data/lib/cov_loupe/tools/coverage_table_tool.rb +139 -21
  166. data/lib/cov_loupe/tools/coverage_totals_tool.rb +31 -13
  167. data/lib/cov_loupe/tools/help_tool.rb +16 -20
  168. data/lib/cov_loupe/tools/list_tool.rb +65 -0
  169. data/lib/cov_loupe/tools/uncovered_lines_tool.rb +14 -14
  170. data/lib/cov_loupe/tools/validate_tool.rb +18 -24
  171. data/lib/cov_loupe/tools/version_tool.rb +8 -3
  172. data/lib/cov_loupe/version.rb +1 -1
  173. data/lib/cov_loupe.rb +83 -55
  174. metadata +184 -154
  175. data/docs/dev/BRANCH_ONLY_COVERAGE.md +0 -158
  176. data/docs/dev/arch-decisions/001-x-arch-decision.md +0 -95
  177. data/docs/dev/arch-decisions/002-x-arch-decision.md +0 -159
  178. data/docs/dev/arch-decisions/003-x-arch-decision.md +0 -165
  179. data/lib/cov_loupe/app_context.rb +0 -26
  180. data/lib/cov_loupe/constants.rb +0 -22
  181. data/lib/cov_loupe/coverage_reporter.rb +0 -31
  182. data/lib/cov_loupe/formatters.rb +0 -51
  183. data/lib/cov_loupe/mode_detector.rb +0 -56
  184. data/lib/cov_loupe/model.rb +0 -339
  185. data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +0 -14
  186. data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +0 -14
  187. data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +0 -14
  188. data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +0 -14
  189. data/lib/cov_loupe/resultset_loader.rb +0 -131
  190. data/lib/cov_loupe/staleness_checker.rb +0 -247
  191. data/lib/cov_loupe/table_formatter.rb +0 -64
  192. data/lib/cov_loupe/tools/all_files_coverage_tool.rb +0 -51
  193. data/lib/cov_loupe/util.rb +0 -88
  194. data/spec/MCP_INTEGRATION_TESTS_README.md +0 -111
  195. data/spec/TIMESTAMPS.md +0 -48
  196. data/spec/all_files_coverage_tool_spec.rb +0 -53
  197. data/spec/app_config_spec.rb +0 -142
  198. data/spec/base_tool_spec.rb +0 -62
  199. data/spec/cli/show_default_report_spec.rb +0 -33
  200. data/spec/cli_enumerated_options_spec.rb +0 -90
  201. data/spec/cli_error_spec.rb +0 -184
  202. data/spec/cli_format_spec.rb +0 -123
  203. data/spec/cli_json_options_spec.rb +0 -50
  204. data/spec/cli_source_spec.rb +0 -44
  205. data/spec/cli_spec.rb +0 -192
  206. data/spec/cli_table_spec.rb +0 -28
  207. data/spec/cli_usage_spec.rb +0 -42
  208. data/spec/commands/base_command_spec.rb +0 -107
  209. data/spec/commands/command_factory_spec.rb +0 -76
  210. data/spec/commands/detailed_command_spec.rb +0 -34
  211. data/spec/commands/list_command_spec.rb +0 -28
  212. data/spec/commands/raw_command_spec.rb +0 -69
  213. data/spec/commands/summary_command_spec.rb +0 -34
  214. data/spec/commands/totals_command_spec.rb +0 -34
  215. data/spec/commands/uncovered_command_spec.rb +0 -55
  216. data/spec/commands/validate_command_spec.rb +0 -213
  217. data/spec/commands/version_command_spec.rb +0 -38
  218. data/spec/constants_spec.rb +0 -61
  219. data/spec/cov_loupe/formatters/source_formatter_spec.rb +0 -267
  220. data/spec/cov_loupe/formatters_spec.rb +0 -76
  221. data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +0 -79
  222. data/spec/cov_loupe_model_spec.rb +0 -454
  223. data/spec/cov_loupe_module_spec.rb +0 -37
  224. data/spec/cov_loupe_opts_spec.rb +0 -185
  225. data/spec/coverage_reporter_spec.rb +0 -102
  226. data/spec/coverage_table_tool_spec.rb +0 -59
  227. data/spec/coverage_totals_tool_spec.rb +0 -37
  228. data/spec/error_handler_spec.rb +0 -197
  229. data/spec/error_mode_spec.rb +0 -139
  230. data/spec/errors_edge_cases_spec.rb +0 -312
  231. data/spec/errors_stale_spec.rb +0 -83
  232. data/spec/file_based_mcp_tools_spec.rb +0 -99
  233. data/spec/help_tool_spec.rb +0 -26
  234. data/spec/integration_spec.rb +0 -789
  235. data/spec/logging_fallback_spec.rb +0 -128
  236. data/spec/mcp_logging_spec.rb +0 -44
  237. data/spec/mcp_server_integration_spec.rb +0 -23
  238. data/spec/mcp_server_spec.rb +0 -106
  239. data/spec/mode_detector_spec.rb +0 -153
  240. data/spec/model_error_handling_spec.rb +0 -269
  241. data/spec/model_staleness_spec.rb +0 -79
  242. data/spec/option_normalizers_spec.rb +0 -203
  243. data/spec/option_parsers/env_options_parser_spec.rb +0 -221
  244. data/spec/option_parsers/error_helper_spec.rb +0 -222
  245. data/spec/path_relativizer_spec.rb +0 -98
  246. data/spec/presenters/coverage_detailed_presenter_spec.rb +0 -19
  247. data/spec/presenters/coverage_raw_presenter_spec.rb +0 -15
  248. data/spec/presenters/coverage_summary_presenter_spec.rb +0 -15
  249. data/spec/presenters/coverage_uncovered_presenter_spec.rb +0 -16
  250. data/spec/presenters/project_coverage_presenter_spec.rb +0 -87
  251. data/spec/presenters/project_totals_presenter_spec.rb +0 -144
  252. data/spec/resolvers/coverage_line_resolver_spec.rb +0 -282
  253. data/spec/resolvers/resolver_factory_spec.rb +0 -61
  254. data/spec/resolvers/resultset_path_resolver_spec.rb +0 -60
  255. data/spec/resultset_loader_spec.rb +0 -167
  256. data/spec/shared_examples/README.md +0 -115
  257. data/spec/shared_examples/coverage_presenter_examples.rb +0 -66
  258. data/spec/shared_examples/file_based_mcp_tools.rb +0 -179
  259. data/spec/shared_examples/formatted_command_examples.rb +0 -64
  260. data/spec/shared_examples/mcp_tool_text_json_response.rb +0 -16
  261. data/spec/spec_helper.rb +0 -127
  262. data/spec/staleness_checker_spec.rb +0 -374
  263. data/spec/staleness_more_spec.rb +0 -42
  264. data/spec/support/cli_helpers.rb +0 -22
  265. data/spec/support/control_flow_helpers.rb +0 -20
  266. data/spec/support/fake_mcp.rb +0 -40
  267. data/spec/support/io_helpers.rb +0 -29
  268. data/spec/support/mcp_helpers.rb +0 -35
  269. data/spec/support/mcp_runner.rb +0 -66
  270. data/spec/support/mocking_helpers.rb +0 -30
  271. data/spec/table_format_spec.rb +0 -70
  272. data/spec/tools/validate_tool_spec.rb +0 -132
  273. data/spec/tools_error_handling_spec.rb +0 -130
  274. data/spec/util_spec.rb +0 -154
  275. data/spec/version_spec.rb +0 -123
  276. data/spec/version_tool_spec.rb +0 -141
  277. /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/bar.rb +0 -0
  278. /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/foo.rb +0 -0
  279. /data/lib/cov_loupe/{config_parser.rb → config/config_parser.rb} +0 -0
  280. /data/lib/cov_loupe/{predicate_evaluator.rb → config/predicate_evaluator.rb} +0 -0
  281. /data/lib/cov_loupe/{error_handler_factory.rb → errors/error_handler_factory.rb} +0 -0
@@ -1,139 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe 'Error Mode System' do
6
- let(:test_logger) do
7
- Class.new do
8
- attr_reader :messages
9
-
10
- def initialize = @messages = []
11
- def error(msg) = @messages << msg
12
- end.new
13
- end
14
-
15
- let(:test_error) { StandardError.new('Test error message') }
16
-
17
- describe 'ErrorHandler error modes' do
18
- context 'with error_mode: :off' do
19
- subject(:handler) { CovLoupe::ErrorHandler.new(error_mode: :off, logger: test_logger) }
20
-
21
- it 'does not log errors' do
22
- expect(handler.log_errors?).to be false
23
- expect(handler.show_stack_traces?).to be false
24
-
25
- handler.handle_error(test_error, context: 'test', reraise: false)
26
- expect(test_logger.messages).to be_empty
27
- end
28
- end
29
-
30
- context 'with error_mode: :log' do
31
- subject(:handler) { CovLoupe::ErrorHandler.new(error_mode: :log, logger: test_logger) }
32
-
33
- it 'logs errors but not stack traces' do
34
- expect(handler.log_errors?).to be true
35
- expect(handler.show_stack_traces?).to be false
36
-
37
- handler.handle_error(test_error, context: 'test', reraise: false)
38
- logged_message = test_logger.messages.join
39
- expect(logged_message).to include('Error in test: StandardError: Test error message')
40
- expect(logged_message).not_to include('spec/error_mode_spec.rb') # No stack trace
41
- end
42
- end
43
-
44
- context 'with error_mode: :debug' do
45
- subject(:handler) { CovLoupe::ErrorHandler.new(error_mode: :debug, logger: test_logger) }
46
-
47
- it 'logs errors with stack traces' do
48
- expect(handler.log_errors?).to be true
49
- expect(handler.show_stack_traces?).to be true
50
-
51
- # Create an error with a proper backtrace
52
- begin
53
- raise StandardError, 'Test error message'
54
- rescue => e
55
- handler.handle_error(e, context: 'test', reraise: false)
56
- end
57
-
58
- logged_message = test_logger.messages.join
59
- expect(logged_message).to include('Error in test: StandardError: Test error message')
60
- expect(logged_message).to include('spec/error_mode_spec.rb') # Stack trace included
61
- end
62
- end
63
- end
64
-
65
- describe 'ErrorHandlerFactory' do
66
- it 'creates handlers with correct modes' do
67
- cli_handler = CovLoupe::ErrorHandlerFactory.for_cli(error_mode: :debug)
68
- expect(cli_handler.error_mode).to eq(:debug)
69
-
70
- lib_handler = CovLoupe::ErrorHandlerFactory.for_library(error_mode: :off)
71
- expect(lib_handler.error_mode).to eq(:off)
72
-
73
- mcp_handler = CovLoupe::ErrorHandlerFactory.for_mcp_server(error_mode: :log)
74
- expect(mcp_handler.error_mode).to eq(:log)
75
- end
76
- end
77
-
78
- describe 'MCP Tools error mode support' do
79
- before { setup_mcp_response_stub }
80
-
81
- it 'BaseTool.handle_mcp_error respects error modes' do
82
- test_error = StandardError.new('Test MCP error')
83
-
84
- # Test different error modes
85
- [:off, :log, :debug].each do |mode|
86
- expect(CovLoupe::ErrorHandlerFactory)
87
- .to receive(:for_mcp_server).with(error_mode: mode).and_call_original
88
-
89
- response = CovLoupe::BaseTool.handle_mcp_error(test_error, 'TestTool', error_mode: mode)
90
- expect(response).to be_a(MCP::Tool::Response)
91
- expect(response.payload.first['text']).to include('Error:')
92
- end
93
- end
94
- end
95
-
96
- describe 'CLI error mode support' do
97
- let(:project_dir) { File.join(__dir__, 'fixtures', 'project1') }
98
-
99
- it 'accepts --error-mode flag' do
100
- cli = CovLoupe::CoverageCLI.new
101
-
102
- # Test that the option parser accepts the flag
103
- expect do
104
- cli.send(:parse_options!, ['--error-mode', 'debug', 'summary', 'lib/foo.rb'])
105
- end.not_to raise_error
106
-
107
- expect(cli.config.error_mode).to eq(:debug)
108
- end
109
-
110
- it 'creates error handler with specified mode' do
111
- cli = CovLoupe::CoverageCLI.new
112
- cli.send(:parse_options!, ['--error-mode', 'off', 'summary', 'lib/foo.rb'])
113
-
114
- expect(cli.send(:error_handler).error_mode).to eq(:off)
115
- end
116
-
117
- it 'validates error mode values' do
118
- cli = CovLoupe::CoverageCLI.new
119
-
120
- expect do
121
- cli.send(:parse_options!, ['--error-mode', 'invalid', 'summary', 'lib/foo.rb'])
122
- end.to raise_error(OptionParser::InvalidArgument)
123
- end
124
- end
125
-
126
- describe 'Error mode validation' do
127
- it 'raises ArgumentError for invalid error modes' do
128
- expect do
129
- CovLoupe::ErrorHandler.new(error_mode: :invalid)
130
- end.to raise_error(ArgumentError, /Invalid error_mode: :invalid/)
131
- end
132
-
133
- it 'accepts all valid error modes' do
134
- expect { CovLoupe::ErrorHandler.new(error_mode: :off) }.not_to raise_error
135
- expect { CovLoupe::ErrorHandler.new(error_mode: :log) }.not_to raise_error
136
- expect { CovLoupe::ErrorHandler.new(error_mode: :debug) }.not_to raise_error
137
- end
138
- end
139
- end
@@ -1,312 +0,0 @@
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
@@ -1,83 +0,0 @@
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
@@ -1,99 +0,0 @@
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
@@ -1,26 +0,0 @@
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