simplecov-mcp 0.3.0 → 1.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +173 -356
  4. data/docs/ADVANCED_USAGE.md +967 -0
  5. data/docs/ARCHITECTURE.md +79 -0
  6. data/docs/BRANCH_ONLY_COVERAGE.md +81 -0
  7. data/docs/CLI_USAGE.md +637 -0
  8. data/docs/DEVELOPMENT.md +82 -0
  9. data/docs/ERROR_HANDLING.md +93 -0
  10. data/docs/EXAMPLES.md +430 -0
  11. data/docs/INSTALLATION.md +352 -0
  12. data/docs/LIBRARY_API.md +635 -0
  13. data/docs/MCP_INTEGRATION.md +488 -0
  14. data/docs/TROUBLESHOOTING.md +276 -0
  15. data/docs/arch-decisions/001-x-arch-decision.md +93 -0
  16. data/docs/arch-decisions/002-x-arch-decision.md +157 -0
  17. data/docs/arch-decisions/003-x-arch-decision.md +163 -0
  18. data/docs/arch-decisions/004-x-arch-decision.md +199 -0
  19. data/docs/arch-decisions/005-x-arch-decision.md +187 -0
  20. data/docs/arch-decisions/README.md +60 -0
  21. data/docs/presentations/simplecov-mcp-presentation.md +249 -0
  22. data/exe/simplecov-mcp +4 -4
  23. data/lib/simplecov_mcp/app_context.rb +26 -0
  24. data/lib/simplecov_mcp/base_tool.rb +74 -0
  25. data/lib/simplecov_mcp/cli.rb +234 -0
  26. data/lib/simplecov_mcp/cli_config.rb +56 -0
  27. data/lib/simplecov_mcp/commands/base_command.rb +78 -0
  28. data/lib/simplecov_mcp/commands/command_factory.rb +39 -0
  29. data/lib/simplecov_mcp/commands/detailed_command.rb +24 -0
  30. data/lib/simplecov_mcp/commands/list_command.rb +13 -0
  31. data/lib/simplecov_mcp/commands/raw_command.rb +22 -0
  32. data/lib/simplecov_mcp/commands/summary_command.rb +24 -0
  33. data/lib/simplecov_mcp/commands/uncovered_command.rb +26 -0
  34. data/lib/simplecov_mcp/commands/version_command.rb +18 -0
  35. data/lib/simplecov_mcp/constants.rb +22 -0
  36. data/lib/simplecov_mcp/error_handler.rb +124 -0
  37. data/lib/simplecov_mcp/error_handler_factory.rb +31 -0
  38. data/lib/simplecov_mcp/errors.rb +179 -0
  39. data/lib/simplecov_mcp/formatters/source_formatter.rb +148 -0
  40. data/lib/simplecov_mcp/mcp_server.rb +40 -0
  41. data/lib/simplecov_mcp/mode_detector.rb +55 -0
  42. data/lib/simplecov_mcp/model.rb +300 -0
  43. data/lib/simplecov_mcp/option_normalizers.rb +92 -0
  44. data/lib/simplecov_mcp/option_parser_builder.rb +134 -0
  45. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +50 -0
  46. data/lib/simplecov_mcp/option_parsers/error_helper.rb +109 -0
  47. data/lib/simplecov_mcp/path_relativizer.rb +61 -0
  48. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +44 -0
  49. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +16 -0
  50. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +16 -0
  51. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +16 -0
  52. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +16 -0
  53. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +52 -0
  54. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +126 -0
  55. data/lib/simplecov_mcp/resolvers/resolver_factory.rb +28 -0
  56. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +78 -0
  57. data/lib/simplecov_mcp/resultset_loader.rb +136 -0
  58. data/lib/simplecov_mcp/staleness_checker.rb +243 -0
  59. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/all_files_coverage_tool.rb +31 -13
  60. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_detailed_tool.rb +7 -7
  61. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_raw_tool.rb +7 -7
  62. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_summary_tool.rb +7 -7
  63. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +90 -0
  64. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/help_tool.rb +13 -4
  65. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/uncovered_lines_tool.rb +7 -7
  66. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/version_tool.rb +11 -3
  67. data/lib/simplecov_mcp/util.rb +82 -0
  68. data/lib/{simple_cov_mcp → simplecov_mcp}/version.rb +1 -1
  69. data/lib/simplecov_mcp.rb +144 -2
  70. data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
  71. data/spec/TIMESTAMPS.md +48 -0
  72. data/spec/all_files_coverage_tool_spec.rb +29 -25
  73. data/spec/base_tool_spec.rb +11 -10
  74. data/spec/cli/show_default_report_spec.rb +33 -0
  75. data/spec/cli_config_spec.rb +137 -0
  76. data/spec/cli_enumerated_options_spec.rb +68 -0
  77. data/spec/cli_error_spec.rb +105 -47
  78. data/spec/cli_source_spec.rb +82 -23
  79. data/spec/cli_spec.rb +140 -5
  80. data/spec/cli_success_predicate_spec.rb +141 -0
  81. data/spec/cli_table_spec.rb +1 -1
  82. data/spec/cli_usage_spec.rb +10 -26
  83. data/spec/commands/base_command_spec.rb +187 -0
  84. data/spec/commands/command_factory_spec.rb +72 -0
  85. data/spec/commands/detailed_command_spec.rb +48 -0
  86. data/spec/commands/raw_command_spec.rb +46 -0
  87. data/spec/commands/summary_command_spec.rb +47 -0
  88. data/spec/commands/uncovered_command_spec.rb +49 -0
  89. data/spec/constants_spec.rb +61 -0
  90. data/spec/coverage_table_tool_spec.rb +17 -33
  91. data/spec/error_handler_spec.rb +22 -13
  92. data/spec/error_mode_spec.rb +143 -0
  93. data/spec/errors_edge_cases_spec.rb +239 -0
  94. data/spec/errors_stale_spec.rb +2 -2
  95. data/spec/file_based_mcp_tools_spec.rb +99 -0
  96. data/spec/fixtures/project1/lib/bar.rb +0 -1
  97. data/spec/fixtures/project1/lib/foo.rb +0 -1
  98. data/spec/help_tool_spec.rb +11 -17
  99. data/spec/integration_spec.rb +845 -0
  100. data/spec/logging_fallback_spec.rb +128 -0
  101. data/spec/mcp_logging_spec.rb +44 -0
  102. data/spec/mcp_server_integration_spec.rb +23 -0
  103. data/spec/mcp_server_spec.rb +15 -4
  104. data/spec/mode_detector_spec.rb +148 -0
  105. data/spec/model_error_handling_spec.rb +210 -0
  106. data/spec/model_staleness_spec.rb +40 -10
  107. data/spec/option_normalizers_spec.rb +204 -0
  108. data/spec/option_parsers/env_options_parser_spec.rb +233 -0
  109. data/spec/option_parsers/error_helper_spec.rb +222 -0
  110. data/spec/path_relativizer_spec.rb +83 -0
  111. data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
  112. data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
  113. data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
  114. data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
  115. data/spec/presenters/project_coverage_presenter_spec.rb +86 -0
  116. data/spec/resolvers/coverage_line_resolver_spec.rb +57 -0
  117. data/spec/resolvers/resolver_factory_spec.rb +61 -0
  118. data/spec/resolvers/resultset_path_resolver_spec.rb +55 -0
  119. data/spec/resultset_loader_spec.rb +167 -0
  120. data/spec/shared_examples/README.md +115 -0
  121. data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
  122. data/spec/shared_examples/file_based_mcp_tools.rb +174 -0
  123. data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
  124. data/spec/simple_cov_mcp_module_spec.rb +16 -0
  125. data/spec/simplecov_mcp_model_spec.rb +340 -9
  126. data/spec/simplecov_mcp_opts_spec.rb +182 -0
  127. data/spec/spec_helper.rb +147 -4
  128. data/spec/staleness_checker_spec.rb +373 -0
  129. data/spec/staleness_more_spec.rb +16 -13
  130. data/spec/support/mcp_runner.rb +64 -0
  131. data/spec/tools_error_handling_spec.rb +144 -0
  132. data/spec/util_spec.rb +109 -34
  133. data/spec/version_spec.rb +117 -9
  134. data/spec/version_tool_spec.rb +131 -10
  135. metadata +120 -63
  136. data/lib/simple_cov/mcp.rb +0 -9
  137. data/lib/simple_cov_mcp/base_tool.rb +0 -70
  138. data/lib/simple_cov_mcp/cli.rb +0 -390
  139. data/lib/simple_cov_mcp/error_handler.rb +0 -131
  140. data/lib/simple_cov_mcp/error_handler_factory.rb +0 -38
  141. data/lib/simple_cov_mcp/errors.rb +0 -176
  142. data/lib/simple_cov_mcp/mcp_server.rb +0 -30
  143. data/lib/simple_cov_mcp/model.rb +0 -104
  144. data/lib/simple_cov_mcp/staleness_checker.rb +0 -125
  145. data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +0 -61
  146. data/lib/simple_cov_mcp/util.rb +0 -122
  147. data/lib/simple_cov_mcp.rb +0 -102
  148. data/spec/coverage_detailed_tool_spec.rb +0 -36
  149. data/spec/coverage_raw_tool_spec.rb +0 -32
  150. data/spec/coverage_summary_tool_spec.rb +0 -39
  151. data/spec/legacy_shim_spec.rb +0 -13
  152. data/spec/uncovered_lines_tool_spec.rb +0 -33
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'simplecov_mcp/tools/help_tool'
5
+ require 'simplecov_mcp/tools/version_tool'
6
+ require 'simplecov_mcp/tools/coverage_summary_tool'
7
+ require 'simplecov_mcp/tools/coverage_raw_tool'
8
+ require 'simplecov_mcp/tools/uncovered_lines_tool'
9
+ require 'simplecov_mcp/tools/coverage_detailed_tool'
10
+
11
+ RSpec.describe 'MCP Tool error handling' do
12
+ let(:server_context) { instance_double('ServerContext').as_null_object }
13
+
14
+ before do
15
+ setup_mcp_response_stub
16
+ end
17
+
18
+ # Note: VersionTool error handling is difficult to test because the tool is so simple
19
+ # and doesn't have any complex logic that could fail. The rescue clause in the tool
20
+ # exists for consistency with other tools but is unlikely to be triggered in practice.
21
+
22
+ describe SimpleCovMcp::Tools::HelpTool do
23
+ it 'handles errors during query processing' do
24
+ # Simulate an error during filter_entries
25
+ allow(described_class).to receive(:filter_entries).and_raise(StandardError, 'Filter error')
26
+
27
+ response = described_class.call(query: 'test', error_mode: 'on',
28
+ server_context: server_context)
29
+
30
+ # Should return error response
31
+ expect(response).to be_a(MCP::Tool::Response)
32
+ item = response.payload.first
33
+ expect(item[:type] || item['type']).to eq('text')
34
+ expect(item[:text] || item['text']).to include('Error')
35
+ end
36
+
37
+ it 'returns empty array when tokens are empty after filtering' do
38
+ # Test the edge case where query contains only non-word characters
39
+ response = described_class.call(query: '!!!', server_context: server_context)
40
+
41
+ data = JSON.parse(response.payload.first['text'])
42
+ # With empty tokens, should return all entries (no filtering applied)
43
+ expect(data['tools']).not_to be_empty
44
+ end
45
+
46
+ it 'handles non-string, non-array values in filter' do
47
+ # Test value_matches? with values that are neither strings nor arrays
48
+ # This exercises the 'else false' branch
49
+ allow(described_class).to receive(:format_entry).and_return({
50
+ 'tool' => 'test_tool',
51
+ 'label' => nil, # Neither string nor array
52
+ 'use_when' => 123, # Neither string nor array
53
+ 'avoid_when' => true, # Neither string nor array
54
+ 'inputs' => {}, # Neither string nor array
55
+ 'example' => 'example'
56
+ })
57
+
58
+ response = described_class.call(query: 'test', server_context: server_context)
59
+
60
+ # Should not crash, should return response
61
+ expect(response).to be_a(MCP::Tool::Response)
62
+ data = JSON.parse(response.payload.first['text'])
63
+ expect(data).to have_key('tools')
64
+ end
65
+ end
66
+
67
+ describe SimpleCovMcp::Tools::CoverageSummaryTool do
68
+ it 'handles errors during model creation' do
69
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_raise(StandardError, 'Model error')
70
+
71
+ response = described_class.call(
72
+ path: 'lib/foo.rb',
73
+ error_mode: 'on',
74
+ server_context: server_context
75
+ )
76
+
77
+ # Should return error response
78
+ expect(response).to be_a(MCP::Tool::Response)
79
+ item = response.payload.first
80
+ expect(item[:type] || item['type']).to eq('text')
81
+ expect(item[:text] || item['text']).to include('Error')
82
+ end
83
+ end
84
+
85
+ describe SimpleCovMcp::Tools::CoverageRawTool do
86
+ it 'handles errors during raw data retrieval' do
87
+ model = instance_double(SimpleCovMcp::CoverageModel)
88
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
89
+ allow(model).to receive(:raw_for).and_raise(StandardError, 'Raw data error')
90
+
91
+ response = SimpleCovMcp::Tools::CoverageRawTool.call(
92
+ path: 'lib/foo.rb',
93
+ error_mode: 'on',
94
+ server_context: server_context
95
+ )
96
+
97
+ # Should return error response
98
+ expect(response).to be_a(MCP::Tool::Response)
99
+ item = response.payload.first
100
+ expect(item[:type] || item['type']).to eq('text')
101
+ expect(item[:text] || item['text']).to include('Error')
102
+ end
103
+ end
104
+
105
+ describe SimpleCovMcp::Tools::UncoveredLinesTool do
106
+ it 'handles errors during uncovered lines retrieval' do
107
+ model = instance_double(SimpleCovMcp::CoverageModel)
108
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
109
+ allow(model).to receive(:uncovered_for).and_raise(StandardError, 'Uncovered error')
110
+
111
+ response = SimpleCovMcp::Tools::UncoveredLinesTool.call(
112
+ path: 'lib/foo.rb',
113
+ error_mode: 'on',
114
+ server_context: server_context
115
+ )
116
+
117
+ # Should return error response
118
+ expect(response).to be_a(MCP::Tool::Response)
119
+ item = response.payload.first
120
+ expect(item[:type] || item['type']).to eq('text')
121
+ expect(item[:text] || item['text']).to include('Error')
122
+ end
123
+ end
124
+
125
+ describe SimpleCovMcp::Tools::CoverageDetailedTool do
126
+ it 'handles errors during detailed data retrieval' do
127
+ model = instance_double(SimpleCovMcp::CoverageModel)
128
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
129
+ allow(model).to receive(:detailed_for).and_raise(StandardError, 'Detailed error')
130
+
131
+ response = SimpleCovMcp::Tools::CoverageDetailedTool.call(
132
+ path: 'lib/foo.rb',
133
+ error_mode: 'on',
134
+ server_context: server_context
135
+ )
136
+
137
+ # Should return error response
138
+ expect(response).to be_a(MCP::Tool::Response)
139
+ item = response.payload.first
140
+ expect(item[:type] || item['type']).to eq('text')
141
+ expect(item[:text] || item['text']).to include('Error')
142
+ end
143
+ end
144
+ end
data/spec/util_spec.rb CHANGED
@@ -1,28 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'tempfile'
4
5
 
5
6
  RSpec.describe SimpleCovMcp::CovUtil do
6
- let(:root) { (FIXTURES / 'project1').to_s }
7
+ let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
8
  let(:resultset_file) { File.join(root, 'coverage', '.resultset.json') }
8
9
 
9
- it 'latest_timestamp returns integer from fixture' do
10
- ts = described_class.latest_timestamp(root, resultset: 'coverage')
11
- expect(ts).to be_a(Integer)
12
- expect(ts).to eq(1_720_000_000)
13
- end
14
10
 
15
- it 'find_resultset honors SIMPLECOV_RESULTSET file path' do
16
- begin
17
- ENV['SIMPLECOV_RESULTSET'] = resultset_file
18
- path = described_class.find_resultset(root)
19
- expect(path).to eq(File.absolute_path(resultset_file, root))
20
- ensure
21
- ENV.delete('SIMPLECOV_RESULTSET')
22
- end
23
- end
24
11
 
25
- it 'lookup_lines supports cwd-stripping and basename fallbacks' do
12
+ it 'lookup_lines supports cwd-stripping' do
26
13
  lines = [1, 0]
27
14
 
28
15
  # Exact key
@@ -30,29 +17,29 @@ RSpec.describe SimpleCovMcp::CovUtil do
30
17
  expect(described_class.lookup_lines(cov, '/abs/path/foo.rb')).to eq(lines)
31
18
 
32
19
  # CWD strip fallback
33
- begin
34
- allow(Dir).to receive(:pwd).and_return('/cwd')
35
- cov = { 'sub/foo.rb' => { 'lines' => lines } }
36
- expect(described_class.lookup_lines(cov, '/cwd/sub/foo.rb')).to eq(lines)
37
- ensure
38
- # no-op
39
- end
20
+ allow(Dir).to receive(:pwd).and_return('/cwd')
21
+ cov = { 'sub/foo.rb' => { 'lines' => lines } }
22
+ expect(described_class.lookup_lines(cov, '/cwd/sub/foo.rb')).to eq(lines)
40
23
 
41
- # Basename fallback
24
+ # Different paths with same basename should not match
42
25
  cov = { '/some/where/else/foo.rb' => { 'lines' => lines } }
43
- expect(described_class.lookup_lines(cov, '/another/place/foo.rb')).to eq(lines)
26
+ expect do
27
+ described_class.lookup_lines(cov, '/another/place/foo.rb')
28
+ end.to raise_error(SimpleCovMcp::FileError, /No coverage entry found/)
44
29
 
45
- # Missing raises a helpful string error
30
+ # Missing raises a FileError
46
31
  cov = {}
47
- expect {
32
+ expect do
48
33
  described_class.lookup_lines(cov, '/nowhere/foo.rb')
49
- }.to raise_error(RuntimeError, /No coverage entry found/)
34
+ end.to raise_error(SimpleCovMcp::FileError, /No coverage entry found/)
50
35
  end
51
36
 
52
37
  it 'summary handles edge cases and coercion' do
53
38
  expect(described_class.summary([])).to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
54
- expect(described_class.summary([nil, nil])).to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
55
- expect(described_class.summary(['1', '0', nil])).to include('pct' => 50.0, 'total' => 2, 'covered' => 1)
39
+ expect(described_class.summary([nil, nil]))
40
+ .to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
41
+ expect(described_class.summary(['1', '0', nil]))
42
+ .to include('pct' => 50.0, 'total' => 2, 'covered' => 1)
56
43
  end
57
44
 
58
45
  it 'uncovered and detailed ignore nils' do
@@ -65,14 +52,102 @@ RSpec.describe SimpleCovMcp::CovUtil do
65
52
  ])
66
53
  end
67
54
 
68
- it 'load_latest_coverage raises CoverageDataError on invalid JSON via model' do
55
+ it 'load_coverage raises CoverageDataError on invalid JSON via model' do
69
56
  Dir.mktmpdir do |dir|
70
57
  bad = File.join(dir, '.resultset.json')
71
58
  File.write(bad, '{not-json')
72
- expect {
59
+ expect do
73
60
  SimpleCovMcp::CoverageModel.new(root: root, resultset: dir)
74
- }.to raise_error(SimpleCovMcp::CoverageDataError, /Invalid coverage data format/)
61
+ end.to raise_error(SimpleCovMcp::CoverageDataError, /Invalid coverage data format/)
75
62
  end
76
63
  end
77
- end
78
64
 
65
+ describe 'logging configuration' do
66
+ let(:test_message) { 'test log message' }
67
+
68
+ around(:each) do |example|
69
+ # Reset logging settings so each example starts clean.
70
+ old_default = SimpleCovMcp.default_log_file
71
+ old_active = SimpleCovMcp.active_log_file
72
+ SimpleCovMcp.default_log_file = nil
73
+ SimpleCovMcp.active_log_file = nil
74
+
75
+ example.run
76
+
77
+ # Restore state
78
+ SimpleCovMcp.default_log_file = old_default
79
+ SimpleCovMcp.active_log_file = old_active
80
+ end
81
+
82
+
83
+
84
+ it "logs to stdout when active_log_file is 'stdout'" do
85
+ SimpleCovMcp.active_log_file = 'stdout'
86
+ expect(File).not_to receive(:open)
87
+ expect { described_class.log(test_message) }
88
+ .to output(/#{Regexp.escape(test_message)}/).to_stdout
89
+ end
90
+
91
+ it "logs to stderr when active_log_file is 'stderr'" do
92
+ SimpleCovMcp.active_log_file = 'stderr'
93
+ expect(File).not_to receive(:open)
94
+ expect { described_class.log(test_message) }
95
+ .to output(/#{Regexp.escape(test_message)}/).to_stderr
96
+ end
97
+
98
+ it 'log writes to file when path is configured' do
99
+ tmp = Tempfile.new('simplecov_mcp-log')
100
+ log_path = tmp.path
101
+ tmp.close
102
+
103
+ SimpleCovMcp.active_log_file = log_path
104
+
105
+ described_class.log(test_message)
106
+
107
+ expect(File.exist?(log_path)).to be true
108
+ content = File.read(log_path)
109
+ expect(content).to include(test_message)
110
+ expect(content).to match(TIMESTAMP_REGEX)
111
+ ensure
112
+ tmp&.unlink
113
+ end
114
+
115
+ it 'log respects runtime changes disabling logging mid-run' do
116
+ tmp = Tempfile.new('simplecov_mcp-log')
117
+ log_path = tmp.path
118
+ tmp.close
119
+
120
+ SimpleCovMcp.active_log_file = log_path
121
+
122
+ described_class.log('first entry')
123
+ expect(File.exist?(log_path)).to be true
124
+ first_content = File.read(log_path)
125
+ expect(first_content).to include('first entry')
126
+
127
+ SimpleCovMcp.active_log_file = 'stderr'
128
+
129
+ expect { described_class.log('second entry') }
130
+ .to output(/second entry/).to_stderr
131
+ expect(File.exist?(log_path)).to be true
132
+ expect(File.read(log_path)).to eq(first_content)
133
+ ensure
134
+ tmp&.unlink
135
+ end
136
+
137
+ it 'exposes default log file configuration separately' do
138
+ original_default = SimpleCovMcp.default_log_file
139
+ SimpleCovMcp.default_log_file = 'stderr'
140
+ expect(SimpleCovMcp.default_log_file).to eq('stderr')
141
+ expect(SimpleCovMcp.active_log_file).to eq('stderr')
142
+ ensure
143
+ SimpleCovMcp.default_log_file = original_default
144
+ end
145
+
146
+ it 'allows adjusting the active log target without touching the default' do
147
+ original_default = SimpleCovMcp.default_log_file
148
+ SimpleCovMcp.active_log_file = 'stdout'
149
+ expect(SimpleCovMcp.active_log_file).to eq('stdout')
150
+ expect(SimpleCovMcp.default_log_file).to eq(original_default)
151
+ end
152
+ end
153
+ end
data/spec/version_spec.rb CHANGED
@@ -2,14 +2,122 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- RSpec.describe 'Version constant' do
6
- it 'exposes a semver-like version string' do
7
- expect(SimpleCovMcp::VERSION).to be_a(String)
8
- # Named fragments for readability (simplified SemVer)
9
- CORE = /\d+\.\d+\.\d+/
10
- ID = /[[:alnum:].-]+/ # ASCII alnum plus dot/hyphen
11
- SEMVER = /\A#{CORE.source}(?:-#{ID.source})?(?:\+#{ID.source})?\z/
12
-
13
- expect(SimpleCovMcp::VERSION).to match(SEMVER)
5
+ RSpec.describe 'SimpleCovMcp::VERSION' do
6
+ describe 'constant existence' do
7
+ it 'defines a VERSION constant' do
8
+ expect(SimpleCovMcp.const_defined?(:VERSION)).to be true
9
+ end
10
+
11
+ it 'exposes VERSION as a non-empty string' do
12
+ expect(SimpleCovMcp::VERSION).to be_a(String)
13
+ expect(SimpleCovMcp::VERSION).not_to be_empty
14
+ end
15
+
16
+ it 'is frozen (immutable)' do
17
+ expect(SimpleCovMcp::VERSION).to be_frozen
18
+ end
19
+ end
20
+
21
+ describe 'semantic versioning compliance' do
22
+ let(:version) { SimpleCovMcp::VERSION }
23
+ # Simplified semantic versioning regex
24
+ # Preserves key semver rules: no leading zeros on numeric parts, optional prerelease/build metadata
25
+ let(:semver_regex) do
26
+ %r{\A
27
+ (?<major>0|[1-9]\d*)\.
28
+ (?<minor>0|[1-9]\d*)\.
29
+ (?<patch>0|[1-9]\d*)
30
+ (?:-(?<prerelease>[0-9A-Za-z.-]+))?
31
+ (?:\+(?<buildmetadata>[0-9A-Za-z.-]+))?
32
+ \z}x
33
+ end
34
+
35
+ it 'follows semantic versioning format' do
36
+ expect(version).to match(semver_regex)
37
+ end
38
+
39
+ it 'has valid major.minor.patch core version' do
40
+ match = version.match(semver_regex)
41
+ expect(match).not_to be_nil, "VERSION '#{version}' does not match semantic versioning format"
42
+
43
+ major = match[:major].to_i
44
+ minor = match[:minor].to_i
45
+ patch = match[:patch].to_i
46
+
47
+ expect(major).to be >= 0
48
+ expect(minor).to be >= 0
49
+ expect(patch).to be >= 0
50
+ end
51
+
52
+ context 'when version has prerelease identifier' do
53
+ let(:prerelease_version) { '9.9.9-rc.1' }
54
+
55
+ before do
56
+ stub_const('SimpleCovMcp::VERSION', prerelease_version)
57
+ end
58
+
59
+ it 'has valid prerelease format' do
60
+ match = version.match(semver_regex)
61
+ prerelease = match[:prerelease]
62
+ expect(prerelease).not_to be_empty
63
+ expect(prerelease).not_to start_with('.')
64
+ expect(prerelease).not_to end_with('.')
65
+ end
66
+ end
67
+
68
+ context 'when version has build metadata' do
69
+ let(:build_metadata_version) { '9.9.9+build.42' }
70
+
71
+ before do
72
+ stub_const('SimpleCovMcp::VERSION', build_metadata_version)
73
+ end
74
+
75
+ it 'has valid build metadata format' do
76
+ match = version.match(semver_regex)
77
+ buildmetadata = match[:buildmetadata]
78
+ expect(buildmetadata).not_to be_empty
79
+ expect(buildmetadata).to match(/\A[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*\z/)
80
+ end
81
+ end
82
+ end
83
+
84
+ describe 'version consistency' do
85
+ it 'is accessible via require path' do
86
+ expect { SimpleCovMcp::VERSION }.not_to raise_error
87
+ end
88
+
89
+ it 'matches the version referenced in gemspec' do
90
+ gemspec_path = File.expand_path('../simplecov-mcp.gemspec', __dir__)
91
+ gemspec_content = File.read(gemspec_path)
92
+
93
+ version_line = gemspec_content.lines.find { |line| line.include?('spec.version') }
94
+ expect(version_line).not_to be_nil, 'Could not find version line in gemspec'
95
+ expect(version_line).to include('SimpleCovMcp::VERSION')
96
+ end
97
+ end
98
+
99
+ describe 'current version sanity check' do
100
+ it 'is not the initial 0.0.0 version' do
101
+ # Ensure this is a real release, not an uninitialized version
102
+ expect(SimpleCovMcp::VERSION).not_to eq('0.0.0')
103
+ end
104
+ end
105
+
106
+ describe 'standalone version file load' do
107
+ it 'defines the module and VERSION constant when only version.rb is loaded' do
108
+ original_module = SimpleCovMcp
109
+ original_version = SimpleCovMcp::VERSION
110
+
111
+ Object.send(:remove_const, :SimpleCovMcp)
112
+
113
+ version_path = File.expand_path('../lib/simplecov_mcp/version.rb', __dir__)
114
+ load version_path
115
+
116
+ expect(Object.const_defined?(:SimpleCovMcp)).to be true
117
+ expect(SimpleCovMcp::VERSION).to eq(original_version)
118
+ ensure
119
+ Object.send(:remove_const, :SimpleCovMcp) if Object.const_defined?(:SimpleCovMcp)
120
+ Object.const_set(:SimpleCovMcp, original_module)
121
+ end
14
122
  end
15
123
  end
@@ -1,22 +1,143 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
- require 'simple_cov_mcp/tools/version_tool'
4
+ require 'simplecov_mcp/tools/version_tool'
5
5
 
6
6
  RSpec.describe SimpleCovMcp::Tools::VersionTool do
7
7
  let(:server_context) { instance_double('ServerContext').as_null_object }
8
8
 
9
9
  before do
10
- stub_const('MCP::Tool::Response', Struct.new(:payload))
10
+ setup_mcp_response_stub
11
11
  end
12
12
 
13
- it 'returns a text payload with the version string' do
14
- response = described_class.call(server_context: server_context)
15
- item = response.payload.first
16
- expect(item[:type] || item['type']).to eq('text')
17
- text = item[:text] || item['text']
18
- expect(text).to include('SimpleCovMcp version:')
19
- expect(text).to include(SimpleCovMcp::VERSION)
13
+ describe '.call' do
14
+ it 'returns a text payload with the version string when called without arguments' do
15
+ response = described_class.call(server_context: server_context)
16
+ item = response.payload.first
17
+ expect(item[:type] || item['type']).to eq('text')
18
+ text = item[:text] || item['text']
19
+ expect(text).to eq("SimpleCovMcp version: #{SimpleCovMcp::VERSION}")
20
+ end
21
+
22
+ it 'includes the exact version constant value' do
23
+ response = described_class.call(server_context: server_context)
24
+ item = response.payload.first
25
+ text = item[:text] || item['text']
26
+ expect(text).to include(SimpleCovMcp::VERSION)
27
+ end
28
+
29
+ it 'matches the expected format exactly' do
30
+ expected_format = "SimpleCovMcp version: #{SimpleCovMcp::VERSION}"
31
+ response = described_class.call(server_context: server_context)
32
+ item = response.payload.first
33
+ text = item[:text] || item['text']
34
+ expect(text).to eq(expected_format)
35
+ end
36
+
37
+ it 'returns an MCP::Tool::Response object' do
38
+ response = described_class.call(server_context: server_context)
39
+ expect(response).to be_a(MCP::Tool::Response)
40
+ end
41
+
42
+ it 'has a single payload item' do
43
+ response = described_class.call(server_context: server_context)
44
+ expect(response.payload).to be_an(Array)
45
+ expect(response.payload.size).to eq(1)
46
+ end
47
+
48
+ context 'when error_mode is specified' do
49
+ it 'accepts error_mode parameter without affecting output' do
50
+ response = described_class.call(error_mode: 'off', server_context: server_context)
51
+ item = response.payload.first
52
+ text = item[:text] || item['text']
53
+ expect(text).to eq("SimpleCovMcp version: #{SimpleCovMcp::VERSION}")
54
+ end
55
+
56
+ it 'accepts error_mode "on" (default)' do
57
+ response = described_class.call(error_mode: 'on', server_context: server_context)
58
+ item = response.payload.first
59
+ expect(item[:type] || item['type']).to eq('text')
60
+ end
61
+
62
+ it 'accepts error_mode "trace"' do
63
+ response = described_class.call(error_mode: 'trace', server_context: server_context)
64
+ item = response.payload.first
65
+ expect(item[:type] || item['type']).to eq('text')
66
+ end
67
+ end
68
+
69
+ context 'when additional arguments are passed' do
70
+ it 'ignores additional arguments gracefully' do
71
+ response = described_class.call(
72
+ server_context: server_context,
73
+ extra_arg: 'value',
74
+ another: { nested: 'data' }
75
+ )
76
+ item = response.payload.first
77
+ text = item[:text] || item['text']
78
+ expect(text).to eq("SimpleCovMcp version: #{SimpleCovMcp::VERSION}")
79
+ end
80
+ end
81
+
82
+ context 'when an error occurs' do
83
+ it 'handles VERSION constant access errors and returns structured error response' do
84
+ # Force an error by overriding const_get to raise an error when VERSION is accessed
85
+ allow(SimpleCovMcp).to receive(:const_missing).with(:VERSION).and_raise(StandardError,
86
+ 'Version access error')
87
+
88
+ # Clear the cached VERSION constant to trigger const_missing
89
+ SimpleCovMcp.send(:remove_const, :VERSION) if SimpleCovMcp.const_defined?(:VERSION)
90
+
91
+ response = described_class.call(error_mode: 'on', server_context: server_context)
92
+
93
+ # Should return error response in MCP format
94
+ expect(response).to be_a(MCP::Tool::Response)
95
+ item = response.payload.first
96
+ expect(item[:type] || item['type']).to eq('text')
97
+
98
+ error_text = item[:text] || item['text']
99
+ expect(error_text).to include('Error')
100
+ end
101
+
102
+ it 'handles errors in the response creation process' do
103
+ # Force an error by mocking string interpolation to fail
104
+ version_obj = double('VERSION')
105
+ allow(version_obj).to receive(:to_s).and_raise(StandardError, 'String conversion error')
106
+
107
+ # Replace VERSION with our mock object
108
+ stub_const('SimpleCovMcp::VERSION', version_obj)
109
+
110
+ response = described_class.call(error_mode: 'on', server_context: server_context)
111
+
112
+ # Should return error response in MCP format via the rescue block
113
+ expect(response).to be_a(MCP::Tool::Response)
114
+ item = response.payload.first
115
+ expect(item[:type] || item['type']).to eq('text')
116
+
117
+ error_text = item[:text] || item['text']
118
+ expect(error_text).to include('Error')
119
+ end
120
+
121
+ it 'respects error_mode setting when handling errors' do
122
+ # Force an error using a mock VERSION that raises an exception
123
+ version_obj = double('VERSION')
124
+ allow(version_obj).to receive(:to_s).and_raise(StandardError, 'Version error')
125
+ stub_const('SimpleCovMcp::VERSION', version_obj)
126
+
127
+ # Test error_mode 'off' (should be silent but still return structured response)
128
+ response = described_class.call(error_mode: 'off', server_context: server_context)
129
+ expect(response).to be_a(MCP::Tool::Response)
130
+ item = response.payload.first
131
+ expect(item[:type] || item['type']).to eq('text')
132
+
133
+ # Test error_mode 'trace' (should include more detail)
134
+ response = described_class.call(error_mode: 'trace', server_context: server_context)
135
+ expect(response).to be_a(MCP::Tool::Response)
136
+ item = response.payload.first
137
+ expect(item[:type] || item['type']).to eq('text')
138
+ error_text = item[:text] || item['text']
139
+ expect(error_text).to include('Error')
140
+ end
141
+ end
20
142
  end
21
143
  end
22
-