cov-loupe 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +329 -0
- data/docs/dev/ARCHITECTURE.md +80 -0
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/dev/DEVELOPMENT.md +83 -0
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/dev/arch-decisions/001-x-arch-decision.md +95 -0
- data/docs/dev/arch-decisions/002-x-arch-decision.md +159 -0
- data/docs/dev/arch-decisions/003-x-arch-decision.md +165 -0
- data/docs/dev/arch-decisions/004-x-arch-decision.md +203 -0
- data/docs/dev/arch-decisions/005-x-arch-decision.md +189 -0
- data/docs/dev/arch-decisions/README.md +60 -0
- data/docs/dev/presentations/cov-loupe-presentation.md +255 -0
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/user/ADVANCED_USAGE.md +777 -0
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/user/ERROR_HANDLING.md +93 -0
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/user/LIBRARY_API.md +693 -0
- data/docs/user/MCP_INTEGRATION.md +490 -0
- data/docs/user/README.md +14 -0
- data/docs/user/TROUBLESHOOTING.md +197 -0
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/cov-loupe +23 -0
- data/lib/cov_loupe/app_config.rb +56 -0
- data/lib/cov_loupe/app_context.rb +26 -0
- data/lib/cov_loupe/base_tool.rb +102 -0
- data/lib/cov_loupe/cli.rb +178 -0
- data/lib/cov_loupe/commands/base_command.rb +67 -0
- data/lib/cov_loupe/commands/command_factory.rb +45 -0
- data/lib/cov_loupe/commands/detailed_command.rb +38 -0
- data/lib/cov_loupe/commands/list_command.rb +13 -0
- data/lib/cov_loupe/commands/raw_command.rb +38 -0
- data/lib/cov_loupe/commands/summary_command.rb +41 -0
- data/lib/cov_loupe/commands/totals_command.rb +53 -0
- data/lib/cov_loupe/commands/uncovered_command.rb +45 -0
- data/lib/cov_loupe/commands/validate_command.rb +60 -0
- data/lib/cov_loupe/commands/version_command.rb +33 -0
- data/lib/cov_loupe/config_parser.rb +32 -0
- data/lib/cov_loupe/constants.rb +22 -0
- data/lib/cov_loupe/coverage_reporter.rb +31 -0
- data/lib/cov_loupe/error_handler.rb +165 -0
- data/lib/cov_loupe/error_handler_factory.rb +31 -0
- data/lib/cov_loupe/errors.rb +191 -0
- data/lib/cov_loupe/formatters/source_formatter.rb +152 -0
- data/lib/cov_loupe/formatters.rb +51 -0
- data/lib/cov_loupe/mcp_server.rb +42 -0
- data/lib/cov_loupe/mode_detector.rb +56 -0
- data/lib/cov_loupe/model.rb +339 -0
- data/lib/cov_loupe/option_normalizers.rb +113 -0
- data/lib/cov_loupe/option_parser_builder.rb +147 -0
- data/lib/cov_loupe/option_parsers/env_options_parser.rb +48 -0
- data/lib/cov_loupe/option_parsers/error_helper.rb +110 -0
- data/lib/cov_loupe/path_relativizer.rb +64 -0
- data/lib/cov_loupe/predicate_evaluator.rb +72 -0
- data/lib/cov_loupe/presenters/base_coverage_presenter.rb +42 -0
- data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +14 -0
- data/lib/cov_loupe/presenters/project_coverage_presenter.rb +50 -0
- data/lib/cov_loupe/presenters/project_totals_presenter.rb +27 -0
- data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +122 -0
- data/lib/cov_loupe/resolvers/resolver_factory.rb +28 -0
- data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +76 -0
- data/lib/cov_loupe/resultset_loader.rb +131 -0
- data/lib/cov_loupe/staleness_checker.rb +247 -0
- data/lib/cov_loupe/table_formatter.rb +64 -0
- data/lib/cov_loupe/tools/all_files_coverage_tool.rb +51 -0
- data/lib/cov_loupe/tools/coverage_detailed_tool.rb +35 -0
- data/lib/cov_loupe/tools/coverage_raw_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_summary_tool.rb +34 -0
- data/lib/cov_loupe/tools/coverage_table_tool.rb +50 -0
- data/lib/cov_loupe/tools/coverage_totals_tool.rb +44 -0
- data/lib/cov_loupe/tools/help_tool.rb +115 -0
- data/lib/cov_loupe/tools/uncovered_lines_tool.rb +34 -0
- data/lib/cov_loupe/tools/validate_tool.rb +72 -0
- data/lib/cov_loupe/tools/version_tool.rb +32 -0
- data/lib/cov_loupe/util.rb +88 -0
- data/lib/cov_loupe/version.rb +5 -0
- data/lib/cov_loupe.rb +140 -0
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +53 -0
- data/spec/app_config_spec.rb +142 -0
- data/spec/base_tool_spec.rb +62 -0
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_enumerated_options_spec.rb +90 -0
- data/spec/cli_error_spec.rb +184 -0
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +44 -0
- data/spec/cli_spec.rb +192 -0
- data/spec/cli_table_spec.rb +28 -0
- data/spec/cli_usage_spec.rb +42 -0
- data/spec/commands/base_command_spec.rb +107 -0
- data/spec/commands/command_factory_spec.rb +76 -0
- data/spec/commands/detailed_command_spec.rb +34 -0
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +69 -0
- data/spec/commands/summary_command_spec.rb +34 -0
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +55 -0
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/cov_loupe/formatters/source_formatter_spec.rb +267 -0
- data/spec/cov_loupe/formatters_spec.rb +76 -0
- data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/cov_loupe_model_spec.rb +454 -0
- data/spec/cov_loupe_module_spec.rb +37 -0
- data/spec/cov_loupe_opts_spec.rb +185 -0
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +59 -0
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +197 -0
- data/spec/error_mode_spec.rb +139 -0
- data/spec/errors_edge_cases_spec.rb +312 -0
- data/spec/errors_stale_spec.rb +83 -0
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +5 -0
- data/spec/fixtures/project1/lib/foo.rb +6 -0
- data/spec/help_tool_spec.rb +26 -0
- data/spec/integration_spec.rb +789 -0
- data/spec/logging_fallback_spec.rb +128 -0
- data/spec/mcp_logging_spec.rb +44 -0
- data/spec/mcp_server_integration_spec.rb +23 -0
- data/spec/mcp_server_spec.rb +106 -0
- data/spec/mode_detector_spec.rb +153 -0
- data/spec/model_error_handling_spec.rb +269 -0
- data/spec/model_staleness_spec.rb +79 -0
- data/spec/option_normalizers_spec.rb +203 -0
- data/spec/option_parsers/env_options_parser_spec.rb +221 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +98 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
- data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
- data/spec/presenters/project_coverage_presenter_spec.rb +87 -0
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +282 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +60 -0
- data/spec/resultset_loader_spec.rb +167 -0
- data/spec/shared_examples/README.md +115 -0
- data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
- data/spec/shared_examples/file_based_mcp_tools.rb +179 -0
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/spec_helper.rb +127 -0
- data/spec/staleness_checker_spec.rb +374 -0
- data/spec/staleness_more_spec.rb +42 -0
- data/spec/support/cli_helpers.rb +22 -0
- data/spec/support/control_flow_helpers.rb +20 -0
- data/spec/support/fake_mcp.rb +40 -0
- data/spec/support/io_helpers.rb +29 -0
- data/spec/support/mcp_helpers.rb +35 -0
- data/spec/support/mcp_runner.rb +66 -0
- data/spec/support/mocking_helpers.rb +30 -0
- data/spec/table_format_spec.rb +70 -0
- data/spec/tools/validate_tool_spec.rb +132 -0
- data/spec/tools_error_handling_spec.rb +130 -0
- data/spec/util_spec.rb +154 -0
- data/spec/version_spec.rb +123 -0
- data/spec/version_tool_spec.rb +141 -0
- metadata +290 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Logging Fallback Behavior' do
|
|
6
|
+
describe 'CovUtil.log error handling' do
|
|
7
|
+
context 'when file logging fails in library mode' do
|
|
8
|
+
it 'falls back to stderr with error message' do
|
|
9
|
+
# Set up library mode context
|
|
10
|
+
context = CovLoupe.create_context(
|
|
11
|
+
error_handler: CovLoupe::ErrorHandlerFactory.for_library,
|
|
12
|
+
log_target: '/invalid/path/that/does/not/exist.log',
|
|
13
|
+
mode: :library
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
stderr_output = nil
|
|
17
|
+
CovLoupe.with_context(context) do
|
|
18
|
+
silence_output do |_stdout, stderr|
|
|
19
|
+
CovLoupe::CovUtil.log('test message')
|
|
20
|
+
stderr_output = stderr.string
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
expect(stderr_output).to include('LOGGING ERROR')
|
|
25
|
+
expect(stderr_output).to include('test message')
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
context 'when file logging fails in CLI mode' do
|
|
30
|
+
it 'falls back to stderr with error message' do
|
|
31
|
+
# Set up CLI mode context
|
|
32
|
+
context = CovLoupe.create_context(
|
|
33
|
+
error_handler: CovLoupe::ErrorHandlerFactory.for_cli,
|
|
34
|
+
log_target: '/invalid/path/that/does/not/exist.log',
|
|
35
|
+
mode: :cli
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
stderr_output = nil
|
|
39
|
+
CovLoupe.with_context(context) do
|
|
40
|
+
silence_output do |_stdout, stderr|
|
|
41
|
+
CovLoupe::CovUtil.log('test message')
|
|
42
|
+
stderr_output = stderr.string
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
expect(stderr_output).to include('LOGGING ERROR')
|
|
47
|
+
expect(stderr_output).to include('test message')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when file logging fails in MCP server mode' do
|
|
52
|
+
it 'suppresses stderr output to avoid interfering with JSON-RPC' do
|
|
53
|
+
# Set up MCP server mode context
|
|
54
|
+
context = CovLoupe.create_context(
|
|
55
|
+
error_handler: CovLoupe::ErrorHandlerFactory.for_mcp_server,
|
|
56
|
+
log_target: '/invalid/path/that/does/not/exist.log',
|
|
57
|
+
mode: :mcp
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
stderr_output = nil
|
|
61
|
+
CovLoupe.with_context(context) do
|
|
62
|
+
silence_output do |_stdout, stderr|
|
|
63
|
+
CovLoupe::CovUtil.log('test message')
|
|
64
|
+
stderr_output = stderr.string
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
expect(stderr_output).to be_empty
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context 'when logging succeeds' do
|
|
73
|
+
it 'does not write to stderr' do
|
|
74
|
+
Dir.mktmpdir do |dir|
|
|
75
|
+
log_file = File.join(dir, 'test.log')
|
|
76
|
+
context = CovLoupe.create_context(
|
|
77
|
+
error_handler: CovLoupe::ErrorHandlerFactory.for_library,
|
|
78
|
+
log_target: log_file,
|
|
79
|
+
mode: :library
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
stderr_output = nil
|
|
83
|
+
CovLoupe.with_context(context) do
|
|
84
|
+
silence_output do |_stdout, stderr|
|
|
85
|
+
CovLoupe::CovUtil.log('test message')
|
|
86
|
+
stderr_output = stderr.string
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
expect(stderr_output).to be_empty
|
|
91
|
+
expect(File.read(log_file)).to include('test message')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe 'AppContext mode predicates' do
|
|
98
|
+
it 'correctly identifies library mode' do
|
|
99
|
+
context = CovLoupe.create_context(
|
|
100
|
+
error_handler: CovLoupe::ErrorHandlerFactory.for_library,
|
|
101
|
+
mode: :library
|
|
102
|
+
)
|
|
103
|
+
expect(context.library_mode?).to be true
|
|
104
|
+
expect(context.cli_mode?).to be false
|
|
105
|
+
expect(context.mcp_mode?).to be false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'correctly identifies CLI mode' do
|
|
109
|
+
context = CovLoupe.create_context(
|
|
110
|
+
error_handler: CovLoupe::ErrorHandlerFactory.for_cli,
|
|
111
|
+
mode: :cli
|
|
112
|
+
)
|
|
113
|
+
expect(context.library_mode?).to be false
|
|
114
|
+
expect(context.cli_mode?).to be true
|
|
115
|
+
expect(context.mcp_mode?).to be false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'correctly identifies MCP mode' do
|
|
119
|
+
context = CovLoupe.create_context(
|
|
120
|
+
error_handler: CovLoupe::ErrorHandlerFactory.for_mcp_server,
|
|
121
|
+
mode: :mcp
|
|
122
|
+
)
|
|
123
|
+
expect(context.library_mode?).to be false
|
|
124
|
+
expect(context.cli_mode?).to be false
|
|
125
|
+
expect(context.mcp_mode?).to be true
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'MCP Mode Logging' do
|
|
6
|
+
it 'raises a configuration error when --log-file is stdout' do
|
|
7
|
+
argv = ['--log-file', 'stdout']
|
|
8
|
+
|
|
9
|
+
# Mock ModeDetector to force MCP mode
|
|
10
|
+
allow(CovLoupe::ModeDetector).to receive(:cli_mode?).and_return(false)
|
|
11
|
+
|
|
12
|
+
expect do
|
|
13
|
+
CovLoupe.run(argv)
|
|
14
|
+
end.to raise_error(CovLoupe::ConfigurationError,
|
|
15
|
+
/Logging to stdout is not permitted in MCP server mode/)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'allows stderr logging in MCP mode' do
|
|
19
|
+
argv = ['--log-file', 'stderr']
|
|
20
|
+
original_target = CovLoupe.active_log_file
|
|
21
|
+
|
|
22
|
+
# Mock ModeDetector to force MCP mode
|
|
23
|
+
allow(CovLoupe::ModeDetector).to receive(:cli_mode?).and_return(false)
|
|
24
|
+
|
|
25
|
+
# The server would normally start here; stub it so we can capture the context without side effects.
|
|
26
|
+
mcp_server_double = instance_double(CovLoupe::MCPServer, run: true)
|
|
27
|
+
captured_context = nil
|
|
28
|
+
allow(CovLoupe::MCPServer).to receive(:new) do |context:|
|
|
29
|
+
# Record the context that the MCP server receives to ensure the log target was honored.
|
|
30
|
+
captured_context = context
|
|
31
|
+
mcp_server_double
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
expect do
|
|
35
|
+
CovLoupe.run(argv)
|
|
36
|
+
end.not_to raise_error
|
|
37
|
+
|
|
38
|
+
# Server boot should have been given a context that points stdout logging to stderr.
|
|
39
|
+
expect(captured_context).not_to be_nil
|
|
40
|
+
expect(captured_context.log_target).to eq('stderr')
|
|
41
|
+
# After the run, the original active context should be restored.
|
|
42
|
+
expect(CovLoupe.active_log_file).to eq(original_target)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'MCP Server Bootstrap' do
|
|
6
|
+
it 'does not crash on startup in non-TTY environments' do
|
|
7
|
+
# Simulate a non-TTY environment, which should trigger MCP mode
|
|
8
|
+
allow($stdin).to receive(:tty?).and_return(false)
|
|
9
|
+
|
|
10
|
+
# The server will try to run, but we only need to ensure it gets past
|
|
11
|
+
# the point where the NameError would have occurred. We can mock the
|
|
12
|
+
# server's run method to prevent it from hanging while waiting for input.
|
|
13
|
+
mcp_server_instance = instance_double(CovLoupe::MCPServer)
|
|
14
|
+
allow(CovLoupe::MCPServer).to receive(:new).and_return(mcp_server_instance)
|
|
15
|
+
allow(mcp_server_instance).to receive(:run)
|
|
16
|
+
|
|
17
|
+
# The key assertion is that this code executes without raising a NameError
|
|
18
|
+
# or any other exception related to the bootstrap process.
|
|
19
|
+
expect { CovLoupe.run([]) }.not_to raise_error
|
|
20
|
+
|
|
21
|
+
expect(mcp_server_instance).to have_received(:run)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/fake_mcp'
|
|
5
|
+
|
|
6
|
+
RSpec.describe CovLoupe::MCPServer do
|
|
7
|
+
# This spec verifies the MCP server boot path without requiring the real
|
|
8
|
+
# MCP runtime. We stub the MCP::Server and its stdio transport to capture
|
|
9
|
+
# constructor parameters and observe that `open` is invoked.
|
|
10
|
+
it 'sets error handler and boots server with expected tools' do
|
|
11
|
+
# Prepare fakes for MCP server and transport
|
|
12
|
+
module ::MCP; end unless defined?(::MCP)
|
|
13
|
+
|
|
14
|
+
stub_const('MCP::Server', FakeMCP::Server)
|
|
15
|
+
stub_const('MCP::Server::Transports::StdioTransport', FakeMCP::StdioTransport)
|
|
16
|
+
|
|
17
|
+
server_context = CovLoupe.create_context(
|
|
18
|
+
error_handler: CovLoupe::ErrorHandlerFactory.for_mcp_server,
|
|
19
|
+
log_target: 'stderr'
|
|
20
|
+
)
|
|
21
|
+
server = described_class.new(context: server_context)
|
|
22
|
+
baseline_context = CovLoupe.context
|
|
23
|
+
|
|
24
|
+
# Run should construct server and open transport
|
|
25
|
+
server.run
|
|
26
|
+
# Server should restore the caller's context after execution.
|
|
27
|
+
expect(CovLoupe.context).to eq(baseline_context)
|
|
28
|
+
|
|
29
|
+
# Fetch the instances created during `run` via the class-level hooks.
|
|
30
|
+
fake_server = FakeMCP::Server.last_instance
|
|
31
|
+
fake_transport = FakeMCP::StdioTransport.last_instance
|
|
32
|
+
|
|
33
|
+
expect(fake_transport).not_to be_nil
|
|
34
|
+
expect(fake_transport).to be_opened
|
|
35
|
+
expect(fake_server).not_to be_nil
|
|
36
|
+
|
|
37
|
+
expect(fake_server.params[:name]).to eq('cov-loupe')
|
|
38
|
+
# Ensure expected tools are registered
|
|
39
|
+
tool_names = fake_server.params[:tools].map { |t| t.name.split('::').last }
|
|
40
|
+
expect(tool_names).to include(
|
|
41
|
+
'AllFilesCoverageTool',
|
|
42
|
+
'CoverageDetailedTool',
|
|
43
|
+
'CoverageRawTool',
|
|
44
|
+
'CoverageSummaryTool',
|
|
45
|
+
'CoverageTotalsTool',
|
|
46
|
+
'UncoveredLinesTool',
|
|
47
|
+
'CoverageTableTool',
|
|
48
|
+
'HelpTool',
|
|
49
|
+
'VersionTool'
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe 'TOOLSET and TOOL_GUIDE consistency' do
|
|
54
|
+
it 'includes all tools documented in HelpTool TOOL_GUIDE' do
|
|
55
|
+
# Get tool classes from TOOLSET
|
|
56
|
+
toolset_classes = described_class::TOOLSET
|
|
57
|
+
|
|
58
|
+
# Get tool classes from TOOL_GUIDE
|
|
59
|
+
tool_guide_classes = CovLoupe::Tools::HelpTool::TOOL_GUIDE.map { |guide| guide[:tool] }
|
|
60
|
+
|
|
61
|
+
# Every tool in TOOL_GUIDE should be in TOOLSET
|
|
62
|
+
tool_guide_classes.each do |tool_class|
|
|
63
|
+
expect(toolset_classes).to include(tool_class),
|
|
64
|
+
"Expected TOOLSET to include #{tool_class.name}, but it was missing. " \
|
|
65
|
+
'Add it to MCPServer::TOOLSET or remove from HelpTool::TOOL_GUIDE.'
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'has corresponding TOOL_GUIDE entry for all tools (except HelpTool itself)' do
|
|
70
|
+
toolset_classes = described_class::TOOLSET
|
|
71
|
+
tool_guide_classes = CovLoupe::Tools::HelpTool::TOOL_GUIDE.map { |guide| guide[:tool] }
|
|
72
|
+
|
|
73
|
+
# Every tool in TOOLSET should be in TOOL_GUIDE (except HelpTool which documents itself)
|
|
74
|
+
toolset_classes.each do |tool_class|
|
|
75
|
+
# HelpTool doesn't need an entry about itself
|
|
76
|
+
next if tool_class == CovLoupe::Tools::HelpTool
|
|
77
|
+
|
|
78
|
+
expect(tool_guide_classes).to include(tool_class),
|
|
79
|
+
"Expected TOOL_GUIDE to document #{tool_class.name}, but it was missing. " \
|
|
80
|
+
'Add documentation for this tool to HelpTool::TOOL_GUIDE.'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'registers the expected number of tools' do
|
|
85
|
+
expect(described_class::TOOLSET.length).to eq(10)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'registers all tool classes defined in CovLoupe::Tools module' do
|
|
89
|
+
# This test catches the bug where a tool file is created, required in
|
|
90
|
+
# cov_loupe.rb, but not added to MCPServer::TOOLSET.
|
|
91
|
+
#
|
|
92
|
+
# Get all classes in the Tools module that inherit from BaseTool
|
|
93
|
+
tool_classes = CovLoupe::Tools.constants
|
|
94
|
+
.map { |const_name| CovLoupe::Tools.const_get(const_name) }
|
|
95
|
+
.select { |const| const.is_a?(Class) && const < CovLoupe::BaseTool }
|
|
96
|
+
|
|
97
|
+
toolset_classes = described_class::TOOLSET
|
|
98
|
+
|
|
99
|
+
tool_classes.each do |tool_class|
|
|
100
|
+
expect(toolset_classes).to include(tool_class),
|
|
101
|
+
"Expected TOOLSET to include #{tool_class.name}, but it was missing. " \
|
|
102
|
+
'The tool class exists in CovLoupe::Tools but is not registered in MCPServer::TOOLSET.'
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
# Array-driven test cases for comprehensive coverage
|
|
6
|
+
# Format: [argv, tty?, expected_result, description]
|
|
7
|
+
CLI_MODE_SCENARIOS = [
|
|
8
|
+
# Priority 1: --force-cli flag (highest priority)
|
|
9
|
+
[['--force-cli'], false, true, '--force-cli with piped input'],
|
|
10
|
+
[['--force-cli', '--format', 'json'], false, true, '--force-cli with other flags'],
|
|
11
|
+
|
|
12
|
+
# Priority 2: Valid subcommands (must be first arg)
|
|
13
|
+
[['list'], false, true, 'list subcommand'],
|
|
14
|
+
[['summary', 'lib/foo.rb'], false, true, 'summary with path'],
|
|
15
|
+
[['version'], false, true, 'version subcommand'],
|
|
16
|
+
[['total'], false, true, 'total subcommand'],
|
|
17
|
+
[['list', '--format', 'json'], false, true, 'subcommand with trailing flags'],
|
|
18
|
+
|
|
19
|
+
# Priority 3: Invalid subcommand attempts (must be first non-flag arg)
|
|
20
|
+
[['invalid-command'], false, true, 'invalid subcommand (shows error)'],
|
|
21
|
+
[['lib/foo.rb'], false, true, 'file path (shows error)'],
|
|
22
|
+
|
|
23
|
+
# Priority 4: TTY determines mode when no subcommand/force-cli
|
|
24
|
+
[[], true, true, 'empty args with TTY'],
|
|
25
|
+
[[], false, false, 'empty args with piped input'],
|
|
26
|
+
[['--format', 'json'], true, true, 'flags only with TTY'],
|
|
27
|
+
[['--format', 'json'], false, false, 'flags only with piped input'],
|
|
28
|
+
[['-r', 'foo', '--format', 'json'], false, false, 'multiple flags with piped input'],
|
|
29
|
+
|
|
30
|
+
# Edge cases: flags before subcommands should now be detected as CLI mode
|
|
31
|
+
[['--format', 'json', 'list'], false, true, 'flag first = CLI mode'],
|
|
32
|
+
[['-r', 'foo', 'summary'], false, true, 'option first = CLI mode'],
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# Simpler test cases for the inverse method
|
|
36
|
+
MCP_SCENARIOS = [
|
|
37
|
+
[[], false, true, 'piped input, no args'],
|
|
38
|
+
[['--format', 'json'], false, true, 'piped input with flags'],
|
|
39
|
+
[[], true, false, 'TTY, no args'],
|
|
40
|
+
[['--force-cli'], false, false, '--force-cli flag'],
|
|
41
|
+
[['list'], false, false, 'subcommand'],
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
RSpec.describe CovLoupe::ModeDetector do
|
|
45
|
+
describe '.cli_mode?' do
|
|
46
|
+
CLI_MODE_SCENARIOS.each do |argv, is_tty, expected, description|
|
|
47
|
+
it "#{expected ? 'CLI' : 'MCP'}: #{description}" do
|
|
48
|
+
stdin = double('stdin', tty?: is_tty)
|
|
49
|
+
result = described_class.cli_mode?(argv, stdin: stdin)
|
|
50
|
+
expect(result).to be(expected),
|
|
51
|
+
"Expected cli_mode?(#{argv.inspect}, tty: #{is_tty}) to be #{expected}, got #{result}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Test all subcommands dynamically
|
|
56
|
+
context 'with all valid subcommands' do
|
|
57
|
+
CovLoupe::ModeDetector::SUBCOMMANDS.each do |subcommand|
|
|
58
|
+
it "CLI mode for '#{subcommand}' (no TTY)" do
|
|
59
|
+
stdin = double('stdin', tty?: false)
|
|
60
|
+
expect(described_class.cli_mode?([subcommand], stdin: stdin)).to be true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'uses STDIN by default when no stdin parameter given' do
|
|
66
|
+
allow($stdin).to receive(:tty?).and_return(true)
|
|
67
|
+
expect(described_class.cli_mode?([])).to be true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '.mcp_server_mode?' do
|
|
72
|
+
MCP_SCENARIOS.each do |argv, is_tty, expected, description|
|
|
73
|
+
it "#{expected ? 'MCP' : 'CLI'}: #{description}" do
|
|
74
|
+
stdin = double('stdin', tty?: is_tty)
|
|
75
|
+
expect(described_class.mcp_server_mode?(argv, stdin: stdin)).to be expected
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'is the logical inverse of cli_mode?' do
|
|
80
|
+
[[[], true], [[], false], [['list'], false]].each do |argv, is_tty|
|
|
81
|
+
stdin = double('stdin', tty?: is_tty)
|
|
82
|
+
cli = described_class.cli_mode?(argv, stdin: stdin)
|
|
83
|
+
mcp = described_class.mcp_server_mode?(argv, stdin: stdin)
|
|
84
|
+
expect(mcp).to eq(!cli)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe 'priority order' do
|
|
90
|
+
let(:stdin) { double('stdin', tty?: false) }
|
|
91
|
+
|
|
92
|
+
it '1. --force-cli overrides everything' do
|
|
93
|
+
expect(described_class.cli_mode?(['--force-cli'], stdin: stdin)).to be true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it '2. subcommand (first arg) overrides TTY' do
|
|
97
|
+
expect(described_class.cli_mode?(['list'], stdin: stdin)).to be true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it '3. invalid first arg (not flag) triggers CLI' do
|
|
101
|
+
expect(described_class.cli_mode?(['invalid'], stdin: stdin)).to be true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it '4. TTY is checked last (when first arg is flag or empty)' do
|
|
105
|
+
tty = double('stdin', tty?: true)
|
|
106
|
+
no_tty = double('stdin', tty?: false)
|
|
107
|
+
|
|
108
|
+
expect(described_class.cli_mode?([], stdin: tty)).to be true
|
|
109
|
+
expect(described_class.cli_mode?([], stdin: no_tty)).to be false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe 'consistency checks' do
|
|
114
|
+
it 'SUBCOMMANDS matches CoverageCLI' do
|
|
115
|
+
expect(CovLoupe::ModeDetector::SUBCOMMANDS).to eq(CovLoupe::CoverageCLI::SUBCOMMANDS)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'all SUBCOMMANDS are lowercase without dashes' do
|
|
119
|
+
CovLoupe::ModeDetector::SUBCOMMANDS.each do |cmd|
|
|
120
|
+
expect(cmd).to eq(cmd.downcase)
|
|
121
|
+
expect(cmd).not_to start_with('-')
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe 'regression tests for non-TTY environment' do
|
|
127
|
+
let(:stdin) { double('stdin', tty?: false) }
|
|
128
|
+
|
|
129
|
+
it 'chooses CLI mode for --help' do
|
|
130
|
+
expect(described_class.cli_mode?(['--help'], stdin: stdin)).to be true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'chooses CLI mode for -h' do
|
|
134
|
+
expect(described_class.cli_mode?(['-h'], stdin: stdin)).to be true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'chooses CLI mode for --version' do
|
|
138
|
+
expect(described_class.cli_mode?(['--version'], stdin: stdin)).to be true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'chooses CLI mode for -v' do
|
|
142
|
+
expect(described_class.cli_mode?(['-v'], stdin: stdin)).to be true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'chooses CLI mode for --json list' do
|
|
146
|
+
expect(described_class.cli_mode?(['--format', 'json', 'list'], stdin: stdin)).to be true
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'chooses MCP mode for flags without a subcommand' do
|
|
150
|
+
expect(described_class.cli_mode?(['--format', 'json'], stdin: stdin)).to be false
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|