Skip to content

Commit 09d238e

Browse files
justin808claude
andcommitted
Fix critical error handling bug and improve test suite
This commit addresses critical issues identified in code review: 1. **Fix error handling in silent mode** (CRITICAL) - Changed from warn/\$stderr to STDERR.puts constant - Bypasses capture_output redirection to ensure errors always appear - Added RuboCop disable pragmas with detailed comments explaining why 2. **Refactor test suite to mock at boundaries** - Removed mocking of private methods (run_rake_task_directly) - Now mocks Rake::Task and system calls directly - Tests verify actual behavior, not implementation details - More resilient to refactoring 3. **Add comprehensive error handling tests** - Test that errors appear in stderr even in silent mode - Test backtrace inclusion when DEBUG env is set - Test stdout suppression in silent mode - Test Rails fallback when not available 4. **Improve test coverage and quality** - 10 examples, all passing - Tests now verify error output reaches STDERR correctly - Added context for Rails not being available All tests pass (10 examples, 0 failures) RuboCop passes with no offenses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 5ec7f8c commit 09d238e

File tree

2 files changed

+106
-11
lines changed

2 files changed

+106
-11
lines changed

lib/react_on_rails/dev/pack_generator.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ def handle_rake_error(error, _silent)
106106
error_msg += "\n#{error.backtrace.join("\n")}" if ENV["DEBUG"]
107107

108108
# Always write to stderr, even in silent mode
109-
warn error_msg
109+
# Use STDERR constant instead of warn/$stderr to bypass capture_output redirection
110+
# rubocop:disable Style/StderrPuts, Style/GlobalStdStream
111+
STDERR.puts error_msg
112+
# rubocop:enable Style/StderrPuts, Style/GlobalStdStream
110113
end
111114

112115
def run_via_bundle_exec(silent: false)

spec/react_on_rails/dev/pack_generator_spec.rb

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,107 @@
66
RSpec.describe ReactOnRails::Dev::PackGenerator do
77
describe ".generate" do
88
context "when in Bundler context with Rails available" do
9+
let(:mock_task) { instance_double(Rake::Task) }
10+
let(:mock_rails_app) do
11+
# rubocop:disable RSpec/VerifiedDoubles
12+
double("Rails.application").tap do |app|
13+
allow(app).to receive(:load_tasks)
14+
allow(app).to receive(:respond_to?).with(:load_tasks).and_return(true)
15+
end
16+
# rubocop:enable RSpec/VerifiedDoubles
17+
end
18+
919
before do
20+
# Setup Bundler context
1021
stub_const("Bundler", Module.new)
1122
allow(ENV).to receive(:[]).and_call_original
1223
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
13-
allow(described_class).to receive(:rails_available?).and_return(true)
24+
25+
# Setup Rails availability
26+
app = mock_rails_app
27+
rails_module = Module.new do
28+
define_singleton_method(:application) { app }
29+
define_singleton_method(:respond_to?) { |method, *| method == :application }
30+
end
31+
stub_const("Rails", rails_module)
32+
33+
# Mock Rake::Task at the boundary
34+
allow(Rake::Task).to receive(:task_defined?).with("react_on_rails:generate_packs").and_return(false)
35+
allow(Rake::Task).to receive(:[]).with("react_on_rails:generate_packs").and_return(mock_task)
36+
allow(mock_task).to receive(:reenable)
37+
allow(mock_task).to receive(:invoke)
1438
end
1539

1640
it "runs pack generation successfully in verbose mode using direct rake execution" do
17-
allow(described_class).to receive(:run_rake_task_directly).and_return(true)
18-
1941
expect { described_class.generate(verbose: true) }
2042
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
21-
expect(described_class).to have_received(:run_rake_task_directly)
43+
44+
expect(mock_task).to have_received(:invoke)
45+
expect(mock_rails_app).to have_received(:load_tasks)
2246
end
2347

2448
it "runs pack generation successfully in quiet mode using direct rake execution" do
25-
allow(described_class).to receive(:run_rake_task_directly).and_return(true)
26-
2749
expect { described_class.generate(verbose: false) }
2850
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
29-
expect(described_class).to have_received(:run_rake_task_directly)
51+
52+
expect(mock_task).to have_received(:invoke)
3053
end
3154

3255
it "exits with error when pack generation fails" do
33-
allow(described_class).to receive(:run_rake_task_directly).and_return(false)
56+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed"))
57+
58+
# Mock STDERR.puts to capture output
59+
error_output = []
60+
# rubocop:disable Style/GlobalStdStream
61+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
62+
# rubocop:enable Style/GlobalStdStream
3463

3564
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
65+
expect(error_output.join("\n")).to match(/Error generating packs: Task failed/)
66+
end
67+
68+
it "outputs errors to stderr even in silent mode" do
69+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error"))
70+
71+
# Mock STDERR.puts to capture output
72+
error_output = []
73+
# rubocop:disable Style/GlobalStdStream
74+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
75+
# rubocop:enable Style/GlobalStdStream
76+
77+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
78+
expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/)
79+
end
80+
81+
it "includes backtrace in error output when DEBUG env is set" do
82+
allow(ENV).to receive(:[]).with("DEBUG").and_return("true")
83+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Debug error"))
84+
85+
# Mock STDERR.puts to capture output
86+
error_output = []
87+
# rubocop:disable Style/GlobalStdStream
88+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
89+
# rubocop:enable Style/GlobalStdStream
90+
91+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
92+
expect(error_output.join("\n")).to match(/Error generating packs: Debug error.*pack_generator_spec\.rb/m)
93+
end
94+
95+
it "suppresses stdout in silent mode" do
96+
# Mock task to produce output
97+
allow(mock_task).to receive(:invoke) do
98+
puts "This should be suppressed"
99+
end
100+
101+
expect { described_class.generate(verbose: false) }
102+
.not_to output(/This should be suppressed/).to_stdout_from_any_process
36103
end
37104
end
38105

39106
context "when not in Bundler context" do
40107
before do
41-
stub_const("Bundler", nil) unless defined?(Bundler)
42-
allow(described_class).to receive(:should_run_directly?).and_return(false)
108+
# Ensure we're not in Bundler context
109+
hide_const("Bundler") if defined?(Bundler)
43110
end
44111

45112
it "runs pack generation successfully in verbose mode using bundle exec" do
@@ -48,6 +115,8 @@
48115

49116
expect { described_class.generate(verbose: true) }
50117
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
118+
119+
expect(described_class).to have_received(:system).with(command)
51120
end
52121

53122
it "runs pack generation successfully in quiet mode using bundle exec" do
@@ -56,6 +125,8 @@
56125

57126
expect { described_class.generate(verbose: false) }
58127
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
128+
129+
expect(described_class).to have_received(:system).with(command)
59130
end
60131

61132
it "exits with error when pack generation fails" do
@@ -65,5 +136,26 @@
65136
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
66137
end
67138
end
139+
140+
context "when Rails is not available" do
141+
before do
142+
stub_const("Bundler", Module.new)
143+
allow(ENV).to receive(:[]).and_call_original
144+
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
145+
146+
# Rails not available
147+
hide_const("Rails") if defined?(Rails)
148+
end
149+
150+
it "falls back to bundle exec when Rails is not defined" do
151+
command = "bundle exec rake react_on_rails:generate_packs"
152+
allow(described_class).to receive(:system).with(command).and_return(true)
153+
154+
expect { described_class.generate(verbose: true) }
155+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
156+
157+
expect(described_class).to have_received(:system).with(command)
158+
end
159+
end
68160
end
69161
end

0 commit comments

Comments
 (0)