simplecov-mcp 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -50
  3. data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
  4. data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
  5. data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
  6. data/docs/dev/README.md +10 -0
  7. data/docs/dev/RELEASING.md +146 -0
  8. data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
  9. data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
  10. data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
  11. data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
  12. data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
  13. data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
  14. data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
  15. data/docs/fixtures/demo_project/README.md +9 -0
  16. data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
  17. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
  18. data/docs/user/CLI_USAGE.md +750 -0
  19. data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
  20. data/docs/user/EXAMPLES.md +588 -0
  21. data/docs/user/INSTALLATION.md +130 -0
  22. data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
  23. data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
  24. data/docs/user/README.md +14 -0
  25. data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
  26. data/docs/user/V2-BREAKING-CHANGES.md +472 -0
  27. data/exe/simplecov-mcp +1 -1
  28. data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
  29. data/lib/simplecov_mcp/app_context.rb +1 -1
  30. data/lib/simplecov_mcp/base_tool.rb +66 -38
  31. data/lib/simplecov_mcp/cli.rb +67 -123
  32. data/lib/simplecov_mcp/commands/base_command.rb +16 -27
  33. data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
  34. data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
  35. data/lib/simplecov_mcp/commands/list_command.rb +1 -1
  36. data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
  37. data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
  38. data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
  39. data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
  40. data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
  41. data/lib/simplecov_mcp/commands/version_command.rb +19 -4
  42. data/lib/simplecov_mcp/config_parser.rb +32 -0
  43. data/lib/simplecov_mcp/constants.rb +3 -3
  44. data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
  45. data/lib/simplecov_mcp/error_handler.rb +81 -40
  46. data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
  47. data/lib/simplecov_mcp/errors.rb +12 -19
  48. data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
  49. data/lib/simplecov_mcp/formatters.rb +51 -0
  50. data/lib/simplecov_mcp/mcp_server.rb +9 -7
  51. data/lib/simplecov_mcp/mode_detector.rb +6 -5
  52. data/lib/simplecov_mcp/model.rb +122 -88
  53. data/lib/simplecov_mcp/option_normalizers.rb +39 -18
  54. data/lib/simplecov_mcp/option_parser_builder.rb +82 -65
  55. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
  56. data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
  57. data/lib/simplecov_mcp/path_relativizer.rb +17 -14
  58. data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
  59. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
  60. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
  61. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
  62. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
  63. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
  64. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
  65. data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
  66. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
  67. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
  68. data/lib/simplecov_mcp/resultset_loader.rb +20 -25
  69. data/lib/simplecov_mcp/staleness_checker.rb +50 -46
  70. data/lib/simplecov_mcp/table_formatter.rb +64 -0
  71. data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
  72. data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
  73. data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
  74. data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
  75. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
  76. data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
  77. data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
  78. data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
  79. data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
  80. data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
  81. data/lib/simplecov_mcp/util.rb +18 -12
  82. data/lib/simplecov_mcp/version.rb +1 -1
  83. data/lib/simplecov_mcp.rb +23 -29
  84. data/spec/all_files_coverage_tool_spec.rb +4 -3
  85. data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
  86. data/spec/base_tool_spec.rb +17 -14
  87. data/spec/cli/show_default_report_spec.rb +2 -2
  88. data/spec/cli_enumerated_options_spec.rb +31 -9
  89. data/spec/cli_error_spec.rb +46 -23
  90. data/spec/cli_format_spec.rb +123 -0
  91. data/spec/cli_json_options_spec.rb +50 -0
  92. data/spec/cli_source_spec.rb +11 -63
  93. data/spec/cli_spec.rb +82 -97
  94. data/spec/cli_usage_spec.rb +15 -15
  95. data/spec/commands/base_command_spec.rb +12 -92
  96. data/spec/commands/command_factory_spec.rb +7 -3
  97. data/spec/commands/detailed_command_spec.rb +10 -24
  98. data/spec/commands/list_command_spec.rb +28 -0
  99. data/spec/commands/raw_command_spec.rb +43 -20
  100. data/spec/commands/summary_command_spec.rb +10 -23
  101. data/spec/commands/totals_command_spec.rb +34 -0
  102. data/spec/commands/uncovered_command_spec.rb +29 -23
  103. data/spec/commands/validate_command_spec.rb +213 -0
  104. data/spec/commands/version_command_spec.rb +38 -0
  105. data/spec/constants_spec.rb +3 -3
  106. data/spec/coverage_reporter_spec.rb +102 -0
  107. data/spec/coverage_table_tool_spec.rb +21 -10
  108. data/spec/coverage_totals_tool_spec.rb +37 -0
  109. data/spec/error_handler_spec.rb +120 -4
  110. data/spec/error_mode_spec.rb +18 -22
  111. data/spec/errors_edge_cases_spec.rb +101 -28
  112. data/spec/errors_stale_spec.rb +34 -0
  113. data/spec/file_based_mcp_tools_spec.rb +6 -6
  114. data/spec/fixtures/project1/lib/bar.rb +2 -0
  115. data/spec/fixtures/project1/lib/foo.rb +2 -0
  116. data/spec/help_tool_spec.rb +2 -18
  117. data/spec/integration_spec.rb +103 -161
  118. data/spec/logging_fallback_spec.rb +3 -3
  119. data/spec/mcp_server_integration_spec.rb +1 -1
  120. data/spec/mcp_server_spec.rb +70 -53
  121. data/spec/mode_detector_spec.rb +46 -41
  122. data/spec/model_error_handling_spec.rb +139 -78
  123. data/spec/model_staleness_spec.rb +13 -13
  124. data/spec/option_normalizers_spec.rb +111 -112
  125. data/spec/option_parsers/env_options_parser_spec.rb +25 -37
  126. data/spec/option_parsers/error_helper_spec.rb +56 -56
  127. data/spec/path_relativizer_spec.rb +15 -0
  128. data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
  129. data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
  130. data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
  131. data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
  132. data/spec/presenters/project_totals_presenter_spec.rb +144 -0
  133. data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
  134. data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
  135. data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
  136. data/spec/shared_examples/formatted_command_examples.rb +64 -0
  137. data/spec/simple_cov_mcp_module_spec.rb +24 -3
  138. data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
  139. data/spec/simplecov_mcp/formatters_spec.rb +76 -0
  140. data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
  141. data/spec/simplecov_mcp_model_spec.rb +97 -47
  142. data/spec/simplecov_mcp_opts_spec.rb +42 -39
  143. data/spec/spec_helper.rb +27 -92
  144. data/spec/staleness_checker_spec.rb +10 -9
  145. data/spec/staleness_more_spec.rb +4 -4
  146. data/spec/support/cli_helpers.rb +22 -0
  147. data/spec/support/control_flow_helpers.rb +20 -0
  148. data/spec/support/fake_mcp.rb +40 -0
  149. data/spec/support/io_helpers.rb +29 -0
  150. data/spec/support/mcp_helpers.rb +35 -0
  151. data/spec/support/mcp_runner.rb +10 -8
  152. data/spec/support/mocking_helpers.rb +30 -0
  153. data/spec/table_format_spec.rb +70 -0
  154. data/spec/tools/validate_tool_spec.rb +132 -0
  155. data/spec/tools_error_handling_spec.rb +34 -48
  156. data/spec/util_spec.rb +5 -4
  157. data/spec/version_spec.rb +7 -7
  158. data/spec/version_tool_spec.rb +20 -22
  159. metadata +90 -23
  160. data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
  161. data/docs/CLI_USAGE.md +0 -637
  162. data/docs/EXAMPLES.md +0 -430
  163. data/docs/INSTALLATION.md +0 -352
  164. data/spec/cli_success_predicate_spec.rb +0 -141
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SimpleCovMcp::CoverageCLI, 'format option' do
6
+ let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
+
8
+ def run_cli(*argv)
9
+ cli = SimpleCovMcp::CoverageCLI.new
10
+ output = nil
11
+ silence_output do |stdout, _stderr|
12
+ cli.send(:run, argv)
13
+ output = stdout.string
14
+ end
15
+ output
16
+ end
17
+
18
+ describe 'format normalization' do
19
+ it 'normalizes short format aliases' do
20
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'j', 'list')
21
+ expect(output).to include('"files":', '"percentage":')
22
+ data = JSON.parse(output)
23
+ expect(data['files']).to be_an(Array)
24
+ end
25
+
26
+ it 'normalizes table format' do
27
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 't', 'list')
28
+ expect(output).to include('File', '%') # Table output
29
+ expect(output).not_to include('"files"') # Not JSON
30
+ end
31
+
32
+ it 'supports yaml format' do
33
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'y', 'list')
34
+ expect(output).to include('---', 'files:', 'file:')
35
+ end
36
+
37
+ it 'supports awesome_print format' do
38
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'a', 'list')
39
+ # AwesomePrint output contains colored/formatted structure
40
+ expect(output).to match(/:files|"files"/)
41
+ end
42
+ end
43
+
44
+ describe 'option order requirements' do
45
+ it 'works with format option before subcommand' do
46
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'json', 'list')
47
+ data = JSON.parse(output)
48
+ expect(data).to have_key('files')
49
+ end
50
+
51
+ it 'shows helpful error when global option comes after subcommand' do
52
+ _out, err, status = run_cli_with_status(
53
+ '--root', root, '--resultset', 'coverage', 'list', '--format', 'json'
54
+ )
55
+ expect(status).to eq(1)
56
+ expect(err).to include(
57
+ 'Global option(s) must come BEFORE the subcommand',
58
+ 'You used: list --format',
59
+ 'Correct: --format list',
60
+ 'Example:'
61
+ )
62
+ end
63
+ end
64
+
65
+ describe 'format with different subcommands' do
66
+ it 'works with totals subcommand' do
67
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'json', 'totals')
68
+ data = JSON.parse(output)
69
+ expect(data).to have_key('lines')
70
+ expect(data).to have_key('percentage')
71
+ end
72
+
73
+ it 'works with summary subcommand' do
74
+ output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'json',
75
+ 'summary', 'lib/foo.rb')
76
+ data = JSON.parse(output)
77
+ expect(data).to have_key('file')
78
+ expect(data).to have_key('summary')
79
+ end
80
+
81
+ it 'works with version subcommand' do
82
+ output = run_cli('--format', 'json', 'version')
83
+ data = JSON.parse(output)
84
+ expect(data).to have_key('version')
85
+ expect(data).to have_key('gem_root')
86
+ end
87
+ end
88
+
89
+ describe 'comprehensive misplaced option detection' do
90
+ # Array of test cases: [description, args_array, expected_option_in_error]
91
+ [
92
+ # Short-form options
93
+ ['short -f after list', ['list', '-f', 'json'], '-f'],
94
+ ['short -r after totals', ['totals', '-r', '.resultset.json'], '-r'],
95
+ ['short -R after list', ['list', '-R', '/tmp'], '-R'],
96
+ ['short -o after list', ['list', '-o', 'a'], '-o'],
97
+ ['short -s after list', ['list', '-s', 'full'], '-s'],
98
+ ['short -S after list', ['list', '-S', 'error'], '-S'],
99
+
100
+ # Long-form options
101
+ ['--sort-order after list', ['list', '--sort-order', 'ascending'], '--sort-order'],
102
+ ['--source after list', ['list', '--source', 'full'], '--source'],
103
+ ['--staleness after totals', ['totals', '--staleness', 'error'], '--staleness'],
104
+ ['--color after list', ['list', '--color'], '--color'],
105
+ ['--no-color after list', ['list', '--no-color'], '--no-color'],
106
+ ['--log-file after list', ['list', '--log-file', '/tmp/test.log'], '--log-file'],
107
+
108
+ # Different subcommands
109
+ ['option after version', ['version', '--format', 'json'], '--format'],
110
+ ['option after summary', ['summary', 'lib/foo.rb', '--format', 'json'], '--format'],
111
+ ['option after raw', ['raw', 'lib/foo.rb', '-f', 'json'], '-f'],
112
+ ['option after detailed', ['detailed', 'lib/foo.rb', '-f', 'json'], '-f'],
113
+ ['option after uncovered', ['uncovered', 'lib/foo.rb', '--root', '/tmp'], '--root']
114
+ ].each do |desc, args, option|
115
+ it "detects #{desc}" do
116
+ _out, err, status = run_cli_with_status(*args)
117
+ expect(status).to eq(1)
118
+ expect(err).to include('Global option(s) must come BEFORE the subcommand')
119
+ expect(err).to include(option)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SimpleCovMcp::CoverageCLI, 'json format options' do
6
+ let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
+
8
+ def run_cli_output(*argv)
9
+ cli = SimpleCovMcp::CoverageCLI.new
10
+ output = nil
11
+ silence_output do |stdout, _stderr|
12
+ cli.send(:run, argv)
13
+ output = stdout.string
14
+ end
15
+ output
16
+ end
17
+
18
+ describe 'JSON format options' do
19
+ it 'produces compact JSON with -f j' do
20
+ output = run_cli_output('--root', root, '--resultset', 'coverage', '-f', 'j', 'list')
21
+
22
+ expect(output.strip.lines.count).to eq(1)
23
+ data = JSON.parse(output)
24
+ expect(data['files']).to be_an(Array)
25
+ end
26
+
27
+ it 'produces pretty JSON with -f pretty-json' do
28
+ output = run_cli_output('--root', root, '--resultset', 'coverage', '-f', 'pretty-json',
29
+ 'list')
30
+ expect(output.strip.lines.count).to be > 1
31
+ data = JSON.parse(output)
32
+ expect(data['files']).to be_an(Array)
33
+ end
34
+
35
+ it 'produces pretty JSON with -f pretty_json (underscore variant)' do
36
+ output = run_cli_output('--root', root, '--resultset', 'coverage', '-f', 'pretty_json',
37
+ 'list')
38
+ expect(output.strip.lines.count).to be > 1
39
+ data = JSON.parse(output)
40
+ expect(data['files']).to be_an(Array)
41
+ end
42
+
43
+ it 'produces compact JSON with -f json' do
44
+ output = run_cli_output('--root', root, '--resultset', 'coverage', '-f', 'json', 'list')
45
+ expect(output.strip.lines.count).to eq(1)
46
+ data = JSON.parse(output)
47
+ expect(data['files']).to be_an(Array)
48
+ end
49
+ end
50
+ end
@@ -7,90 +7,38 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
7
7
 
8
8
  it 'renders uncovered source without error for fixture file' do
9
9
  out, err, status = run_cli_with_status(
10
- 'uncovered', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
11
- '--source=uncovered', '--source-context', '1', '--no-color'
10
+ '--root', root, '--resultset', 'coverage', '--source', 'uncovered', '--context-lines', '1',
11
+ '--no-color', 'uncovered', 'lib/foo.rb'
12
12
  )
13
13
  expect(status).to eq(0)
14
14
  expect(err).to eq('')
15
15
  expect(out).to match(/File:\s+lib\/foo\.rb/)
16
- expect(out).to match(/Uncovered lines:\s*2\b/)
16
+ expect(out).to include('│') # Table format
17
17
  expect(out).to show_source_table_or_fallback
18
18
  end
19
19
 
20
20
  it 'renders full source for uncovered command without brittle spacing' do
21
21
  out, err, status = run_cli_with_status(
22
- 'uncovered', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
23
- '--source=full', '--no-color'
22
+ '--root', root, '--resultset', 'coverage', '--source', 'full', '--no-color',
23
+ 'uncovered', 'lib/foo.rb'
24
24
  )
25
25
  expect(status).to eq(0)
26
26
  expect(err).to eq('')
27
- # Summary line with flexible spacing
28
- expect(out).to match(/Summary:\s*\d+\.\d{2}%\s*\d+\/\d+/)
27
+ expect(out).to include('│') # Table format
28
+ expect(out).to include('66.67%')
29
29
  expect(out).to show_source_table_or_fallback
30
30
  end
31
31
 
32
32
  it 'renders source for summary with uncovered mode without crashing' do
33
33
  out, err, status = run_cli_with_status(
34
- 'summary', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
35
- '--source=uncovered', '--source-context', '1', '--no-color'
34
+ '--root', root, '--resultset', 'coverage', '--source', 'uncovered', '--context-lines', '1',
35
+ '--no-color', 'summary', 'lib/foo.rb'
36
36
  )
37
37
  expect(status).to eq(0)
38
38
  expect(err).to eq('')
39
39
  expect(out).to include('lib/foo.rb')
40
- # Presence of percentage and counts, spacing-agnostic
41
- expect(out).to match(/66\.67%/)
42
- expect(out).to match(/\b2\/3\b/)
40
+ expect(out).to include('66.67%')
41
+ expect(out).to include('│') # Table format
43
42
  expect(out).to show_source_table_or_fallback
44
43
  end
45
-
46
- context 'source option without equals sign' do
47
- it 'parses --source uncovered correctly (space-separated argument)' do
48
- out, err, status = run_cli_with_status(
49
- 'summary', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
50
- '--source', 'uncovered', '--source-context', '1', '--no-color'
51
- )
52
- expect(status).to eq(0)
53
- expect(err).to eq('')
54
- expect(out).to include('lib/foo.rb')
55
- expect(out).to match(/66\.67%/)
56
- expect(out).to match(/\b2\/3\b/)
57
- expect(out).to show_source_table_or_fallback
58
- end
59
-
60
- it 'parses -s full correctly (short form with space-separated argument)' do
61
- out, err, status = run_cli_with_status(
62
- 'uncovered', 'lib/foo.rb', '--root', root, '--resultset', 'coverage',
63
- '-s', 'full', '--no-color'
64
- )
65
- expect(status).to eq(0)
66
- expect(err).to eq('')
67
- expect(out).to match(/Summary:\s*\d+\.\d{2}%\s*\d+\/\d+/)
68
- expect(out).to show_source_table_or_fallback
69
- end
70
-
71
- it 'handles --source uncovered in default report (no subcommand)' do
72
- out, err, status = run_cli_with_status(
73
- '--root', root, '--resultset', 'coverage',
74
- '--source', 'uncovered', '--no-color'
75
- )
76
- expect(status).to eq(0)
77
- expect(err).to eq('')
78
- expect(out).to match(/66\.67%/)
79
- # Default report doesn't show source tables, that's OK - just check it parses correctly
80
- expect(out).not_to include('Unknown subcommand')
81
- end
82
-
83
- it 'does not misinterpret following token as subcommand when using --source' do
84
- # This test specifically addresses the bug where --source uncovered
85
- # was interpreting 'uncovered' as a subcommand
86
- out, err, status = run_cli_with_status(
87
- '--root', root, '--resultset', 'coverage',
88
- '--source', 'uncovered'
89
- )
90
- expect(status).to eq(0)
91
- expect(err).to eq('')
92
- expect(out).not_to include('Unknown subcommand')
93
- expect(out).to match(/66\.67%/)
94
- end
95
- end
96
44
  end
data/spec/cli_spec.rb CHANGED
@@ -9,48 +9,65 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
9
9
  def run_cli(*argv)
10
10
  cli = described_class.new
11
11
  silence_output do |out, _err|
12
- cli.run(argv.flatten)
12
+ begin
13
+ cli.run(argv.flatten)
14
+ rescue SystemExit
15
+ # Ignore exit, just capture output
16
+ end
13
17
  return out.string
14
18
  end
15
19
  end
16
20
 
17
- it 'prints summary as JSON for a file' do
18
- output = run_cli('summary', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage')
19
- data = JSON.parse(output)
20
- expect(data['file']).to end_with('lib/foo.rb')
21
- expect(data['summary']).to include('covered' => 2, 'total' => 3)
22
- end
21
+ describe 'JSON output' do
22
+ def with_json_output(command, *args)
23
+ output = run_cli('--format', 'json', '--root', root, '--resultset', 'coverage',
24
+ command, *args)
25
+ yield JSON.parse(output)
26
+ end
23
27
 
24
- it 'prints raw lines as JSON' do
25
- output = run_cli('raw', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage')
26
- data = JSON.parse(output)
27
- expect(data['file']).to end_with('lib/foo.rb')
28
- expect(data['lines']).to eq([1, 0, nil, 2])
29
- end
28
+ it 'prints summary as JSON' do
29
+ with_json_output('summary', 'lib/foo.rb') do |data|
30
+ expect(data['summary']).to include('covered' => 2)
31
+ end
32
+ end
30
33
 
31
- it 'prints raw lines as text' do
32
- output = run_cli('raw', 'lib/foo.rb', '--root', root, '--resultset', 'coverage')
33
- expect(output).to include('File: lib/foo.rb')
34
- expect(output).to include('[1, 0, nil, 2]')
35
- end
34
+ it 'prints raw as JSON' do
35
+ with_json_output('raw', 'lib/foo.rb') do |data|
36
+ expect(data['lines']).to eq([1, 0, nil, 2])
37
+ end
38
+ end
36
39
 
37
- it 'prints uncovered lines as JSON' do
38
- output = run_cli('uncovered', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage')
39
- data = JSON.parse(output)
40
- expect(data['uncovered']).to eq([2])
41
- expect(data['summary']).to include('total' => 3)
40
+ it 'prints uncovered as JSON' do
41
+ with_json_output('uncovered', 'lib/foo.rb') do |data|
42
+ expect(data['uncovered']).to eq([2])
43
+ end
44
+ end
45
+
46
+ it 'prints detailed as JSON' do
47
+ with_json_output('detailed', 'lib/foo.rb') do |data|
48
+ expect(data['lines']).to be_an(Array)
49
+ end
50
+ end
51
+
52
+ it 'prints totals as JSON' do
53
+ with_json_output('totals') do |data|
54
+ expect(data['lines']).to include('total' => 6, 'covered' => 3, 'uncovered' => 3)
55
+ expect(data['files']).to include('total' => 2)
56
+ expect(data['files']['ok'] + data['files']['stale']).to eq(data['files']['total'])
57
+ end
58
+ end
42
59
  end
43
60
 
44
- it 'prints detailed rows as JSON' do
45
- output = run_cli('detailed', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage')
46
- data = JSON.parse(output)
47
- expect(data['lines']).to be_an(Array)
48
- expect(data['lines'].first).to include('line', 'hits', 'covered')
61
+ it 'prints raw lines as text' do
62
+ output = run_cli('--root', root, '--resultset', 'coverage', 'raw', 'lib/foo.rb')
63
+ expect(output).to include('File: lib/foo.rb')
64
+ expect(output).to include('│') # Table format
49
65
  end
50
66
 
51
67
  it 'list subcommand with --json outputs JSON with sort order' do
52
- output = run_cli('list', '--json', '--root', root, '--resultset', 'coverage', '--sort-order',
53
- 'a')
68
+ output = run_cli(
69
+ '--format', 'json', '--root', root, '--resultset', 'coverage', '--sort-order', 'a', 'list'
70
+ )
54
71
  asc = JSON.parse(output)
55
72
  expect(asc['files']).to be_an(Array)
56
73
  expect(asc['files'].first['file']).to end_with('lib/bar.rb')
@@ -63,14 +80,15 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
63
80
  expect(total).to eq(asc['files'].length)
64
81
  expect(ok + stale).to eq(total)
65
82
 
66
- output = run_cli('list', '--json', '--root', root, '--resultset', 'coverage', '--sort-order',
67
- 'd')
83
+ output = run_cli(
84
+ '--format', 'json', '--root', root, '--resultset', 'coverage', '--sort-order', 'd', 'list'
85
+ )
68
86
  desc = JSON.parse(output)
69
87
  expect(desc['files'].first['file']).to end_with('lib/foo.rb')
70
88
  end
71
89
 
72
90
  it 'list subcommand outputs formatted table' do
73
- output = run_cli('list', '--root', root, '--resultset', 'coverage')
91
+ output = run_cli('--root', root, '--resultset', 'coverage', 'list')
74
92
  expect(output).to include('File')
75
93
  expect(output).to include('lib/foo.rb')
76
94
  expect(output).to include('lib/bar.rb')
@@ -79,20 +97,23 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
79
97
 
80
98
  it 'list subcommand retains rows when using an absolute tracked glob' do
81
99
  absolute_glob = File.join(root, 'lib', '**', '*.rb')
82
- output = run_cli('list', '--root', root, '--resultset', 'coverage', '--tracked-globs',
83
- absolute_glob)
100
+ output = run_cli('--root', root, '--resultset', 'coverage', '--tracked-globs',
101
+ absolute_glob, 'list')
84
102
  expect(output).not_to include('No coverage data found')
85
103
  expect(output).to include('lib/foo.rb')
86
104
  expect(output).to include('lib/bar.rb')
87
105
  end
88
106
 
89
- it 'exposes expected subcommands via constant' do
90
- expect(described_class::SUBCOMMANDS).to eq(%w[list summary raw uncovered detailed version])
107
+ it 'totals subcommand prints a readable summary by default' do
108
+ output = run_cli('--root', root, '--resultset', 'coverage', 'totals')
109
+ expect(output).to include('│') # Table format
110
+ expect(output).to include('Lines')
111
+ # expect(output).to include('Average coverage:') # Not in table version
91
112
  end
92
113
 
93
114
  it 'can include source in JSON payload (nil if file missing)' do
94
- output = run_cli('summary', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage',
95
- '--source')
115
+ output = run_cli('--format', 'json', '--root', root, '--resultset', 'coverage',
116
+ '--source', 'full', 'summary', 'lib/foo.rb')
96
117
  data = JSON.parse(output)
97
118
  expect(data).to have_key('source')
98
119
  end
@@ -108,8 +129,8 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
108
129
  m.call(error_handler: error_handler, log_target: log_target, mode: mode)
109
130
  end
110
131
  original_target = SimpleCovMcp.active_log_file
111
- run_cli('summary', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage',
112
- '--log-file', log_path)
132
+ run_cli('--format', 'json', '--root', root, '--resultset', 'coverage',
133
+ '--log-file', log_path, 'summary', 'lib/foo.rb')
113
134
  expect(SimpleCovMcp.active_log_file).to eq(original_target)
114
135
  end
115
136
  end
@@ -122,86 +143,50 @@ RSpec.describe SimpleCovMcp::CoverageCLI do
122
143
  m.call(error_handler: error_handler, log_target: log_target, mode: mode)
123
144
  end
124
145
  original_target = SimpleCovMcp.active_log_file
125
- run_cli('summary', 'lib/foo.rb', '--json', '--root', root, '--resultset', 'coverage',
126
- '--log-file', 'stdout')
146
+ run_cli('--format', 'json', '--root', root, '--resultset', 'coverage',
147
+ '--log-file', 'stdout', 'summary', 'lib/foo.rb')
127
148
  expect(SimpleCovMcp.active_log_file).to eq(original_target)
128
149
  end
129
150
  end
130
151
 
131
- describe '#load_success_predicate' do
132
- let(:cli) { described_class.new }
133
152
 
134
- def with_temp_predicate(content)
135
- Tempfile.create(['predicate', '.rb']) do |file|
136
- file.write(content)
137
- file.flush
138
- yield file.path
139
- end
140
- end
141
153
 
142
- it 'loads a callable predicate from file' do
143
- with_temp_predicate("->(model) { model }\n") do |path|
144
- predicate = cli.send(:load_success_predicate, path)
145
- expect(predicate).to respond_to(:call)
146
- expect(predicate.call(:ok)).to eq(:ok)
147
- end
148
- end
149
-
150
- it 'raises when file does not return callable' do
151
- with_temp_predicate(":not_callable\n") do |path|
152
- expect { cli.send(:load_success_predicate, path) }
153
- .to raise_error(RuntimeError, include('Success predicate must be callable'))
154
- end
155
- end
156
154
 
157
- it 'wraps syntax errors with friendly message' do
158
- with_temp_predicate("->(model) {\n") do |path|
159
- expect { cli.send(:load_success_predicate, path) }
160
- .to raise_error(RuntimeError, include('Syntax error in success predicate file'))
161
- end
162
- end
163
- end
164
-
165
- describe '#extract_subcommand!' do
166
- let(:cli) { described_class.new }
167
-
168
- around do |example|
169
- original = ENV['SIMPLECOV_MCP_OPTS']
170
- example.run
171
- ensure
172
- ENV['SIMPLECOV_MCP_OPTS'] = original
173
- end
174
-
175
- it 'picks up subcommands that appear after env-provided options' do
176
- ENV['SIMPLECOV_MCP_OPTS'] = '--resultset coverage'
177
- argv = cli.send(:parse_env_opts) + ['summary', 'lib/foo.rb']
178
-
179
- expect do
180
- cli.send(:extract_subcommand!, argv)
181
- end.to change { cli.instance_variable_get(:@cmd) }.from(nil).to('summary')
182
- end
183
- end
184
155
 
185
156
  describe 'version command' do
186
157
  it 'prints version as plain text by default' do
187
158
  output = run_cli('version')
188
- expect(output).to include('SimpleCovMcp version')
159
+ expect(output).to include('') # Table format
189
160
  expect(output).to include(SimpleCovMcp::VERSION)
190
161
  expect(output).not_to include('{')
191
162
  expect(output).not_to include('}')
192
163
  end
193
164
 
194
165
  it 'prints version as JSON when --json flag is used' do
195
- output = run_cli('version', '--json')
166
+ output = run_cli('--format', 'json', 'version')
196
167
  data = JSON.parse(output)
197
168
  expect(data).to have_key('version')
198
169
  expect(data['version']).to eq(SimpleCovMcp::VERSION)
199
170
  end
200
171
 
201
172
  it 'works with version command and other flags' do
202
- output = run_cli('version', '--root', root)
203
- expect(output).to include('SimpleCovMcp version')
173
+ output = run_cli('--root', root, 'version')
174
+ expect(output).to include('') # Table format
175
+ expect(output).to include(SimpleCovMcp::VERSION)
176
+ end
177
+ end
178
+
179
+ describe 'version option (-v)' do
180
+ it 'prints the same version info as the version subcommand' do
181
+ output = run_cli('-v')
182
+ expect(output).to include('│') # Table format
204
183
  expect(output).to include(SimpleCovMcp::VERSION)
205
184
  end
185
+
186
+ it 'respects --json when -v is used' do
187
+ output = run_cli('-v', '--format', 'json')
188
+ data = JSON.parse(output)
189
+ expect(data['version']).to eq(SimpleCovMcp::VERSION)
190
+ end
206
191
  end
207
192
  end
@@ -1,42 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'tempfile'
4
5
 
5
6
  RSpec.describe SimpleCovMcp::CoverageCLI do
6
7
  let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
8
 
8
9
  it 'errors with usage when summary path is missing' do
9
- _out, err, status = run_cli_with_status('summary', '--root', root, '--resultset', 'coverage')
10
+ _out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage', 'summary')
10
11
  expect(status).to eq(1)
11
12
  expect(err).to include('Usage: simplecov-mcp summary <path>')
12
13
  end
13
14
 
14
15
  it 'errors with meaningful message for unknown subcommand' do
15
- out, err, status = run_cli_with_status('bogus', '--root', root, '--resultset', 'coverage')
16
+ _out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage', 'bogus')
16
17
  expect(status).to eq(1)
17
- expect(err).to include("Unknown subcommand: 'bogus'")
18
- expect(err).to include('Valid subcommands:')
18
+ expect(err).to include("Unknown subcommand: 'bogus'", 'Valid subcommands:')
19
19
  end
20
20
 
21
21
  it 'list honors stale=error and tracked_globs by exiting 1 when project is stale' do
22
- tmp = File.join(root, 'lib', 'brand_new_file_for_cli_usage_spec.rb')
23
- begin
24
- File.write(tmp, "# new file\n")
25
- _out, err, status = run_cli_with_status('list', '--root', root, '--resultset', 'coverage',
26
- '--stale', 'error', '--tracked-globs', 'lib/**/*.rb')
22
+ Tempfile.create(['brand_new_file_for_cli_usage_spec', '.rb'], File.join(root, 'lib')) do |f|
23
+ f.write("# new file\n")
24
+ f.flush
25
+ _out, err, status = run_cli_with_status(
26
+ '--root', root, '--resultset', 'coverage', '--staleness', 'error', '--tracked-globs',
27
+ 'lib/**/*.rb', 'list'
28
+ )
27
29
  expect(status).to eq(1)
28
30
  expect(err).to include('Coverage data stale (project)')
29
- ensure
30
- File.delete(tmp) if File.exist?(tmp)
31
31
  end
32
32
  end
33
33
 
34
34
  it 'list with stale=off prints table and exits 0' do
35
- out, err, status = run_cli_with_status('list', '--root', root, '--resultset', 'coverage',
36
- '--stale', 'off')
35
+ out, err, status = run_cli_with_status(
36
+ '--root', root, '--resultset', 'coverage', '--staleness', 'off', 'list'
37
+ )
37
38
  expect(status).to eq(0)
38
39
  expect(err).to eq('')
39
- expect(out).to include('File')
40
- expect(out).to include('lib/foo.rb')
40
+ expect(out).to include('File', 'lib/foo.rb')
41
41
  end
42
42
  end