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,185 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe 'COV_LOUPE_OPTS Environment Variable' do
6
- let(:cli) { CovLoupe::CoverageCLI.new }
7
-
8
- around do |example|
9
- original_value = ENV['COV_LOUPE_OPTS']
10
- example.run
11
- ensure
12
- ENV['COV_LOUPE_OPTS'] = original_value
13
- end
14
-
15
- describe 'CLI option parsing from environment' do
16
- it 'parses simple options from COV_LOUPE_OPTS' do
17
- ENV['COV_LOUPE_OPTS'] = '--error-mode off --format json'
18
- env_opts = CovLoupe.send(:extract_env_opts)
19
-
20
- swallow_system_exit do
21
- silence_output do
22
- cli.send(:run, env_opts + ['summary', 'lib/foo.rb'])
23
- end
24
- end
25
- rescue CovLoupe::Error => e
26
- # Expected to fail due to missing file, but options should be parsed
27
- puts "DEBUG: Caught exception: #{e.class}: #{e.message}" if ENV['DEBUG']
28
- ensure
29
- expect(cli.config.error_mode).to eq(:off)
30
- expect(cli.config.format).to eq(:json)
31
- end
32
-
33
- it 'handles quoted options with spaces' do
34
- test_path = File.join(Dir.tmpdir, 'test path with spaces', '.resultset.json')
35
- ENV['COV_LOUPE_OPTS'] = "--resultset \"#{test_path}\""
36
- env_opts = CovLoupe.send(:extract_env_opts)
37
-
38
- exit_status = swallow_system_exit do
39
- silence_output do
40
- cli.send(:run, env_opts + ['--help'])
41
- end
42
- end
43
-
44
- expect(exit_status).to eq(0) # --help exits cleanly
45
- expect(cli.config.resultset).to eq(test_path)
46
- end
47
-
48
- it 'supports setting log-file to stdout from environment' do
49
- ENV['COV_LOUPE_OPTS'] = '--log-file stdout'
50
- env_opts = CovLoupe.send(:extract_env_opts)
51
-
52
- swallow_system_exit do
53
- silence_output do
54
- cli.send(:run, env_opts + ['--help'])
55
- end
56
- end
57
-
58
- expect(cli.config.log_file).to eq('stdout')
59
- end
60
-
61
- it 'command line arguments override environment options' do
62
- ENV['COV_LOUPE_OPTS'] = '--error-mode off'
63
- env_opts = CovLoupe.send(:extract_env_opts)
64
-
65
- begin
66
- args = env_opts + ['--error-mode', 'debug', 'summary', 'lib/foo.rb']
67
- silence_output { cli.send(:run, args) }
68
- rescue SystemExit, CovLoupe::Error
69
- # Expected to fail, but options should be parsed
70
- end
71
-
72
- # Command line should override environment
73
- expect(cli.config.error_mode).to eq(:debug)
74
- end
75
-
76
- it 'handles malformed COV_LOUPE_OPTS gracefully' do
77
- ENV['COV_LOUPE_OPTS'] = '--option "unclosed quote'
78
-
79
- # Should catch the ConfigurationError and exit cleanly
80
- _out, _err, status = run_cli_with_status('summary', 'lib/foo.rb')
81
- expect(status).not_to eq(0)
82
- end
83
-
84
- it 'returns empty array when COV_LOUPE_OPTS is not set' do
85
- # ENV is already cleared by around block
86
- opts = CovLoupe.send(:extract_env_opts)
87
- expect(opts).to eq([])
88
- end
89
-
90
- it 'returns empty array when COV_LOUPE_OPTS is empty' do
91
- ENV['COV_LOUPE_OPTS'] = ''
92
- opts = CovLoupe.send(:extract_env_opts)
93
- expect(opts).to eq([])
94
- end
95
- end
96
-
97
- describe 'CLI mode detection with COV_LOUPE_OPTS' do
98
- it 'respects --force-cli from environment variable' do
99
- ENV['COV_LOUPE_OPTS'] = '--force-cli'
100
-
101
- # This would normally be MCP mode (no TTY, no subcommand)
102
- stdin = double('stdin', tty?: false)
103
-
104
- env_opts = CovLoupe.send(:extract_env_opts)
105
- full_argv = env_opts + []
106
-
107
- expect(CovLoupe::ModeDetector.cli_mode?(full_argv, stdin: stdin)).to be true
108
- end
109
-
110
- it 'handles parse errors gracefully in mode detection' do
111
- ENV['COV_LOUPE_OPTS'] = '--option "unclosed quote'
112
-
113
- # Should return empty array and not crash
114
- opts = CovLoupe.send(:extract_env_opts)
115
- expect(opts).to eq([])
116
- end
117
-
118
- it 'actually runs CLI when --force-cli is in COV_LOUPE_OPTS' do
119
- ENV['COV_LOUPE_OPTS'] = '--force-cli'
120
-
121
- # Mock STDIN to not be a TTY (would normally trigger MCP server mode)
122
- allow($stdin).to receive(:tty?).and_return(false)
123
-
124
- # Run with --help which should produce help output
125
- output = nil
126
- silence_output do |out, err|
127
- swallow_system_exit do
128
- CovLoupe.run(['--help'])
129
- end
130
- output = out.string + err.string
131
- end
132
-
133
- # Verify CLI actually ran by checking for help text
134
- expect(output).to include('Usage:')
135
- expect(output).to include('cov-loupe')
136
- end
137
-
138
- it 'actually runs MCP server mode when no CLI indicators present' do
139
- ENV['COV_LOUPE_OPTS'] = ''
140
-
141
- # Mock STDIN to not be a TTY and to provide valid JSON-RPC
142
- allow($stdin).to receive(:tty?).and_return(false)
143
-
144
- # Provide a minimal JSON-RPC request that the server can handle
145
- json_request = JSON.generate({
146
- jsonrpc: '2.0',
147
- id: 1,
148
- method: 'initialize',
149
- params: {
150
- protocolVersion: '2024-11-05',
151
- capabilities: {},
152
- clientInfo: { name: 'test', version: '1.0' }
153
- }
154
- })
155
-
156
- allow($stdin).to receive(:gets).and_return(json_request, nil)
157
-
158
- # Capture output to verify MCP server response
159
- output = nil
160
- silence_output do |out, err|
161
- CovLoupe.run([])
162
- output = out.string + err.string
163
- end
164
-
165
- # Verify MCP server ran by checking for JSON-RPC response
166
- expect(output).to include('"jsonrpc"')
167
- expect(output).to include('"result"')
168
- end
169
- end
170
-
171
- describe 'integration with actual CLI usage' do
172
- it 'works end-to-end with --resultset option' do
173
- test_resultset = File.join(Dir.tmpdir, 'test_coverage', '.resultset.json')
174
- ENV['COV_LOUPE_OPTS'] = "--resultset #{test_resultset} --format json"
175
- env_opts = CovLoupe.send(:extract_env_opts)
176
-
177
- swallow_system_exit do
178
- silence_output { cli.send(:run, env_opts + ['--help']) }
179
- end
180
-
181
- expect(cli.config.resultset).to eq(test_resultset)
182
- expect(cli.config.format).to eq(:json)
183
- end
184
- end
185
- end
@@ -1,102 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::CoverageReporter do
6
- let(:model) { instance_double(CovLoupe::CoverageModel) }
7
- # Data is pre-sorted by percentage ascending (as model.all_files returns)
8
- let(:all_files_data) do
9
- [
10
- { 'file' => '/project/lib/zero.rb', 'percentage' => 0.0, 'covered' => 0, 'total' => 10 },
11
- { 'file' => '/project/lib/low.rb', 'percentage' => 25.0, 'covered' => 5, 'total' => 20 },
12
- { 'file' => '/project/lib/medium.rb', 'percentage' => 60.0, 'covered' => 12, 'total' => 20 },
13
- { 'file' => '/project/lib/high.rb', 'percentage' => 95.0, 'covered' => 19, 'total' => 20 }
14
- ]
15
- end
16
-
17
- before do
18
- allow(model).to receive(:all_files).with(sort_order: :ascending).and_return(all_files_data)
19
- allow(model).to receive(:relativize) do |files|
20
- files.map { |f| f.merge('file' => f['file'].sub('/project/', '')) }
21
- end
22
- end
23
-
24
- describe '.report' do
25
- it 'returns formatted low coverage files string' do
26
- result = described_class.report(threshold: 80, count: 5, model: model)
27
-
28
- expect(result).to be_a(String)
29
- expect(result).to include('Lowest coverage files (< 80%):')
30
- expect(result).to include('lib/zero.rb')
31
- end
32
-
33
- it 'includes files below threshold sorted by coverage ascending' do
34
- result = described_class.report(threshold: 80, count: 5, model: model)
35
-
36
- expect(result).to include('lib/zero.rb', 'lib/low.rb', 'lib/medium.rb')
37
- expect(result).not_to include('lib/high.rb')
38
- end
39
-
40
- it 'respects count parameter' do
41
- result = described_class.report(threshold: 80, count: 2, model: model)
42
-
43
- expect(result).to include('lib/zero.rb')
44
- expect(result).to include('lib/low.rb')
45
- expect(result).not_to include('lib/medium.rb')
46
- end
47
-
48
- it 'returns nil when no files below threshold' do
49
- result = described_class.report(threshold: 0, count: 5, model: model)
50
-
51
- expect(result).to be_nil
52
- end
53
-
54
- it 'uses threshold in header' do
55
- result = described_class.report(threshold: 90, count: 5, model: model)
56
-
57
- expect(result).to include('< 90%')
58
- end
59
-
60
- it 'uses default threshold of 80' do
61
- result = described_class.report(count: 5, model: model)
62
-
63
- expect(result).to include('< 80%')
64
- expect(result).not_to include('lib/high.rb')
65
- end
66
-
67
- it 'uses default count of 5' do
68
- result = described_class.report(threshold: 100, model: model)
69
-
70
- # All 4 files are below 100%
71
- expect(result).to include('lib/zero.rb')
72
- expect(result).to include('lib/high.rb')
73
- end
74
-
75
- it 'relativizes file paths' do
76
- result = described_class.report(threshold: 80, count: 5, model: model)
77
-
78
- expect(result).to include('lib/zero.rb')
79
- expect(result).not_to include('/project/')
80
- end
81
-
82
- it 'aligns percentages correctly' do
83
- result = described_class.report(threshold: 100, count: 5, model: model)
84
- lines = result.split("\n")
85
-
86
- # lines[0] is empty (leading newline), lines[1] is header, lines[2..] are data
87
- expect(lines[2]).to match(/^\s+0\.0%/)
88
- expect(lines[3]).to match(/^\s+25\.0%/)
89
- end
90
- end
91
-
92
- describe 'module_function behavior' do
93
- it 'report is available as a module method' do
94
- expect(described_class).to respond_to(:report)
95
- end
96
-
97
- it 'report is available as a private instance method when included' do
98
- klass = Class.new { include CovLoupe::CoverageReporter }
99
- expect(klass.private_instance_methods).to include(:report)
100
- end
101
- end
102
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
- require 'cov_loupe/tools/coverage_table_tool'
5
-
6
- RSpec.describe CovLoupe::Tools::CoverageTableTool do
7
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
8
- let(:server_context) { instance_double('ServerContext').as_null_object }
9
-
10
- before do
11
- setup_mcp_response_stub
12
- end
13
-
14
- def run_tool(staleness: :off)
15
- # Let real CoverageModel work to test actual format_table behavior
16
- described_class.call(root: root, staleness: staleness,
17
- server_context: server_context).payload.first['text']
18
- end
19
-
20
- it 'returns a formatted table as a string' do
21
- output = run_tool
22
-
23
- # Contains table structure, headers, and file data
24
- expect(output).to include(
25
- '┌', '┬', '┐', '│', '├', '┼', '┤', '└', '┴', '┘',
26
- 'File', 'Covered', 'Total', ' │ Stale │',
27
- 'lib/foo.rb', 'lib/bar.rb',
28
- 'Files: total 2, ok 0, stale 2'
29
- )
30
- end
31
-
32
- it 'configures CLI to enforce stale checking when requested' do
33
- model = instance_double(CovLoupe::CoverageModel,
34
- all_files: [
35
- { 'file' => "#{root}/lib/foo.rb", 'percentage' => 100.0, 'covered' => 10, 'total' => 10,
36
- 'stale' => false }
37
- ],
38
- relativize: ->(payload) { payload },
39
- format_table: 'Mock table output'
40
- )
41
- allow(CovLoupe::CoverageModel).to receive(:new).with(
42
- root: root,
43
- resultset: nil,
44
- staleness: :error,
45
- tracked_globs: nil
46
- ).and_return(model)
47
- allow(model).to receive(:format_table).and_return('Mock table output')
48
-
49
- described_class.call(root: root, staleness: :error, server_context: server_context)
50
-
51
- expect(CovLoupe::CoverageModel).to have_received(:new).with(
52
- root: root,
53
- resultset: nil,
54
- staleness: :error,
55
- tracked_globs: nil
56
- )
57
- expect(model).to have_received(:format_table)
58
- end
59
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
- require 'cov_loupe/tools/coverage_totals_tool'
5
-
6
- RSpec.describe CovLoupe::Tools::CoverageTotalsTool do
7
- subject(:tool_response) { 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
- 'lines' => { 'total' => 42, 'covered' => 40, 'uncovered' => 2 },
19
- 'percentage' => 95.24,
20
- 'files' => { 'total' => 4, 'ok' => 4, 'stale' => 0 }
21
- }
22
-
23
- presenter = instance_double(CovLoupe::Presenters::ProjectTotalsPresenter)
24
- allow(CovLoupe::Presenters::ProjectTotalsPresenter).to receive(:new).and_return(presenter)
25
- allow(presenter).to receive(:relativized_payload).and_return(payload)
26
- end
27
-
28
- it_behaves_like 'an MCP tool that returns text JSON'
29
-
30
- it 'returns aggregated totals' do
31
- data, = expect_mcp_text_json(tool_response, expected_keys: ['lines', 'percentage', 'files'])
32
-
33
- expect(data['lines']).to include('total' => 42, 'covered' => 40, 'uncovered' => 2)
34
- expect(data['files']).to include('total' => 4, 'stale' => 0)
35
- expect(data['percentage']).to eq(95.24)
36
- end
37
- end
@@ -1,197 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::ErrorHandler do
6
- subject(:handler) { described_class.new(error_mode: :log, logger: logger) }
7
-
8
- let(:logger) do
9
- Class.new do
10
- attr_reader :messages
11
-
12
- def initialize = @messages = []
13
- def error(msg) = @messages << msg
14
- end.new
15
- end
16
-
17
-
18
- it 'maps filesystem errors to friendly custom errors' do
19
- e = handler.convert_standard_error(Errno::EISDIR.new('Is a directory @ rb_sysopen - a_dir'))
20
- expect(e).to be_a(CovLoupe::NotAFileError)
21
-
22
- e = handler.convert_standard_error(
23
- Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.txt')
24
- )
25
- expect(e).to be_a(CovLoupe::FileNotFoundError)
26
-
27
- e = handler.convert_standard_error(Errno::EACCES.new('Permission denied @ rb_sysopen - secret'))
28
- expect(e).to be_a(CovLoupe::FilePermissionError)
29
- end
30
-
31
- it 'maps JSON::ParserError to CoverageDataError' do
32
- e = handler.convert_standard_error(JSON::ParserError.new('unexpected token'))
33
- expect(e).to be_a(CovLoupe::CoverageDataError)
34
- expect(e.user_friendly_message).to include('Invalid coverage data format')
35
- end
36
-
37
- it 'maps ArgumentError by message' do
38
- e = handler.convert_standard_error(
39
- ArgumentError.new('wrong number of arguments (given 1, expected 2)')
40
- )
41
- expect(e).to be_a(CovLoupe::UsageError)
42
-
43
- e = handler.convert_standard_error(ArgumentError.new('invalid option'))
44
- expect(e).to be_a(CovLoupe::ConfigurationError)
45
- end
46
-
47
- it 'maps NoMethodError to CoverageDataError with helpful info' do
48
- e = handler.convert_standard_error(
49
- NoMethodError.new("undefined method `fetch' for #<Hash:0x123>")
50
- )
51
- expect(e).to be_a(CovLoupe::CoverageDataError)
52
- expect(e.user_friendly_message).to include('Invalid coverage data structure')
53
- end
54
-
55
- it 'maps runtime strings from util to friendly errors' do
56
- e = handler.convert_standard_error(
57
- RuntimeError.new('Could not find .resultset.json under /path; run tests')
58
- )
59
- expect(e).to be_a(CovLoupe::CoverageDataError)
60
- expect(e.user_friendly_message).to include('run your tests first')
61
-
62
- e = handler.convert_standard_error(
63
- RuntimeError.new('No .resultset.json found in directory: /path')
64
- )
65
- expect(e).to be_a(CovLoupe::CoverageDataError)
66
-
67
- e = handler.convert_standard_error(
68
- RuntimeError.new('Specified resultset not found: /nowhere/file.json')
69
- )
70
- expect(e).to be_a(CovLoupe::ResultsetNotFoundError)
71
- end
72
-
73
- it 'logs via provided logger' do
74
- begin
75
- handler.handle_error(Errno::ENOENT.new('No such file or directory @ rb_sysopen - x'),
76
- context: 'test', reraise: false)
77
- rescue
78
- # reraise disabled
79
- end
80
- expect(logger.messages.join).to include('Error in test')
81
- end
82
-
83
- it 'converts TypeError to CoverageDataError for invalid data structures' do
84
- error = TypeError.new('wrong argument type')
85
- result = handler.convert_standard_error(error)
86
-
87
- expect(result).to be_a(CovLoupe::CoverageDataError)
88
- expect(result.user_friendly_message).to include('Invalid coverage data structure')
89
- end
90
-
91
- it 'returns generic Error for unrecognized SystemCallError' do
92
- error = Errno::EEXIST.new('File exists')
93
- result = handler.convert_standard_error(error)
94
-
95
- expect(result).to be_a(CovLoupe::Error)
96
- expect(result.user_friendly_message).to include('An unexpected error occurred')
97
- end
98
-
99
- it 'handles NoMethodError with non-standard message format' do
100
- error = NoMethodError.new('some weird error message without the expected pattern')
101
- result = handler.convert_standard_error(error)
102
-
103
- expect(result).to be_a(CovLoupe::CoverageDataError)
104
- expect(result.user_friendly_message).to include('some weird error message')
105
- end
106
-
107
- describe 'else branch for non-StandardError exceptions' do
108
- # This tests the else clause in convert_standard_error for exceptions
109
- # that don't inherit from StandardError
110
- it 'returns generic Error for Exception subclasses not inheriting from StandardError' do
111
- # Create a custom exception that inherits from Exception, not StandardError
112
- custom_exception_class = Class.new(StandardError) do
113
- def message
114
- 'Custom non-standard exception'
115
- end
116
- end
117
-
118
- error = custom_exception_class.new
119
- result = handler.convert_standard_error(error)
120
-
121
- expect(result).to be_a(CovLoupe::Error)
122
- expect(result.user_friendly_message).to include('An unexpected error occurred')
123
- expect(result.user_friendly_message).to include('Custom non-standard exception')
124
- end
125
-
126
- it 'returns generic Error for ScriptError subclasses' do
127
- # ScriptError inherits from Exception, not StandardError
128
- error = NotImplementedError.new('This feature is not implemented')
129
- result = handler.convert_standard_error(error)
130
-
131
- expect(result).to be_a(CovLoupe::Error)
132
- expect(result.user_friendly_message).to include('An unexpected error occurred')
133
- end
134
- end
135
-
136
- describe 'extract_method_info fallback' do
137
- # This tests the fallback path in extract_method_info when NoMethodError
138
- # message doesn't match the expected pattern
139
- it 'returns original message when pattern does not match' do
140
- # Test various NoMethodError formats that won't match the regex
141
- test_messages = [
142
- 'method not found',
143
- 'private method called',
144
- 'undefined local variable or method',
145
- ''
146
- ]
147
-
148
- test_messages.each do |msg|
149
- error = NoMethodError.new(msg)
150
- result = handler.convert_standard_error(error)
151
-
152
- expect(result).to be_a(CovLoupe::CoverageDataError)
153
- # The original message should be preserved
154
- expect(result.message).to include(msg) unless msg.empty?
155
- end
156
- end
157
- end
158
-
159
- # ErrorHandler#convert_runtime_error handles RuntimeErrors differently based on context:
160
- # - :coverage_loading assumes errors relate to coverage data and maps them to
161
- # CoverageDataError or ResultsetNotFoundError
162
- # - :general (or any other context) maps unrecognized errors to generic Error
163
- # This tests the final else branch in convert_runtime_error.
164
- describe 'convert_runtime_error with general context' do
165
- it 'converts RuntimeError with unrecognized message to generic Error' do
166
- error = RuntimeError.new('Some completely unexpected runtime error')
167
-
168
- result = handler.convert_standard_error(error, context: :general)
169
-
170
- expect(result).to be_a(CovLoupe::Error)
171
- expect(result.user_friendly_message)
172
- .to include('An unexpected error occurred', 'unexpected runtime error')
173
- end
174
- end
175
-
176
- describe '#handle_error with reraise' do
177
- it 're-raises CovLoupe::Error when reraise is true' do
178
- error = CovLoupe::FileNotFoundError.new('Test file not found')
179
-
180
- expect { handler.handle_error(error, context: 'test', reraise: true) }
181
- .to raise_error(CovLoupe::FileNotFoundError, 'Test file not found')
182
-
183
- # Verify it was logged
184
- expect(logger.messages.join).to include('Error in test')
185
- end
186
-
187
- it 'converts and re-raises StandardError when reraise is true' do
188
- error = Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb')
189
-
190
- expect { handler.handle_error(error, context: 'test', reraise: true) }
191
- .to raise_error(CovLoupe::FileNotFoundError)
192
-
193
- # Verify it was logged
194
- expect(logger.messages.join).to include('Error in test')
195
- end
196
- end
197
- end