ruact 0.0.1 → 0.0.3
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 +4 -4
- data/.codecov.yml +31 -0
- data/.github/workflows/ci.yml +160 -94
- data/.github/workflows/server-functions-bench.yml +54 -0
- data/.rubocop.yml +19 -1
- data/.rubocop_todo.yml +175 -0
- data/CHANGELOG.md +86 -5
- data/README.md +2 -0
- data/RELEASING.md +9 -3
- data/bench/server_functions_dispatch_bench.rb +309 -0
- data/bench/server_functions_dispatch_bench.results.md +121 -0
- data/docs/internal/README.md +9 -0
- data/docs/internal/decisions/server-functions-api.md +1680 -0
- data/lib/generators/ruact/install/install_generator.rb +43 -0
- data/lib/generators/ruact/install/templates/application.jsx.tt +1 -1
- data/lib/generators/ruact/install/templates/initializer.rb.tt +1 -1
- data/lib/ruact/client_manifest.rb +125 -12
- data/lib/ruact/configuration.rb +264 -23
- data/lib/ruact/controller.rb +459 -32
- data/lib/ruact/doctor.rb +34 -2
- data/lib/ruact/erb_preprocessor.rb +6 -6
- data/lib/ruact/errors.rb +89 -0
- data/lib/ruact/flight/serializer.rb +2 -2
- data/lib/ruact/html_converter.rb +131 -31
- data/lib/ruact/query.rb +107 -0
- data/lib/ruact/railtie.rb +220 -3
- data/lib/ruact/render_context.rb +30 -0
- data/lib/ruact/render_pipeline.rb +201 -59
- data/lib/ruact/routing.rb +81 -0
- data/lib/ruact/serializable.rb +11 -11
- data/lib/ruact/server.rb +341 -0
- data/lib/ruact/server_action.rb +131 -0
- data/lib/ruact/server_functions/backtrace_cleaner.rb +32 -0
- data/lib/ruact/server_functions/bucket_two_payload.rb +109 -0
- data/lib/ruact/server_functions/codegen.rb +330 -0
- data/lib/ruact/server_functions/codegen_v2.rb +176 -0
- data/lib/ruact/server_functions/endpoint_controller.rb +237 -0
- data/lib/ruact/server_functions/error_payload.rb +93 -0
- data/lib/ruact/server_functions/error_rendering.rb +188 -0
- data/lib/ruact/server_functions/error_suggestion.rb +38 -0
- data/lib/ruact/server_functions/name_bridge.rb +113 -0
- data/lib/ruact/server_functions/query_context.rb +62 -0
- data/lib/ruact/server_functions/query_dispatch.rb +248 -0
- data/lib/ruact/server_functions/registry.rb +148 -0
- data/lib/ruact/server_functions/registry_entry.rb +26 -0
- data/lib/ruact/server_functions/route_source.rb +201 -0
- data/lib/ruact/server_functions/snapshot.rb +195 -0
- data/lib/ruact/server_functions/snapshot_writer.rb +65 -0
- data/lib/ruact/server_functions/standalone_context.rb +103 -0
- data/lib/ruact/server_functions/standalone_dispatcher.rb +178 -0
- data/lib/ruact/server_functions.rb +75 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +17 -9
- data/lib/ruact.rb +85 -6
- data/lib/rubocop/cop/ruact/no_shared_state.rb +1 -1
- data/lib/tasks/benchmark.rake +15 -11
- data/lib/tasks/ruact.rake +81 -0
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +1 -1
- data/spec/fixtures/flight/README.md +55 -7
- data/spec/fixtures/flight/bigint.txt +1 -0
- data/spec/fixtures/flight/infinity.txt +1 -0
- data/spec/fixtures/flight/nan.txt +1 -0
- data/spec/fixtures/flight/negative_infinity.txt +1 -0
- data/spec/fixtures/flight/undefined.txt +1 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +108 -0
- data/spec/ruact/configuration_spec.rb +501 -0
- data/spec/ruact/controller_request_spec.rb +204 -0
- data/spec/ruact/controller_spec.rb +427 -39
- data/spec/ruact/doctor_spec.rb +118 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +3 -3
- data/spec/ruact/erb_preprocessor_spec.rb +7 -7
- data/spec/ruact/errors_spec.rb +95 -0
- data/spec/ruact/flight/renderer_spec.rb +14 -3
- data/spec/ruact/flight/serializer_spec.rb +129 -88
- data/spec/ruact/html_converter_spec.rb +183 -5
- data/spec/ruact/install_generator_spec.rb +93 -0
- data/spec/ruact/query_request_spec.rb +446 -0
- data/spec/ruact/query_spec.rb +105 -0
- data/spec/ruact/railtie_spec.rb +2 -3
- data/spec/ruact/render_context_spec.rb +58 -0
- data/spec/ruact/render_pipeline_concurrency_spec.rb +78 -0
- data/spec/ruact/render_pipeline_spec.rb +784 -330
- data/spec/ruact/serializable_spec.rb +8 -8
- data/spec/ruact/server_bucket_request_spec.rb +352 -0
- data/spec/ruact/server_function_name_spec.rb +53 -0
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +63 -0
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +200 -0
- data/spec/ruact/server_functions/codegen_spec.rb +429 -0
- data/spec/ruact/server_functions/csrf_request_spec.rb +380 -0
- data/spec/ruact/server_functions/dispatch_request_spec.rb +819 -0
- data/spec/ruact/server_functions/error_payload_spec.rb +222 -0
- data/spec/ruact/server_functions/error_suggestion_spec.rb +79 -0
- data/spec/ruact/server_functions/name_bridge_spec.rb +188 -0
- data/spec/ruact/server_functions/query_context_spec.rb +72 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +345 -0
- data/spec/ruact/server_functions/rake_spec.rb +86 -0
- data/spec/ruact/server_functions/registry_spec.rb +199 -0
- data/spec/ruact/server_functions/route_source_spec.rb +202 -0
- data/spec/ruact/server_functions/snapshot_spec.rb +256 -0
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +71 -0
- data/spec/ruact/server_functions/standalone_action_spec.rb +224 -0
- data/spec/ruact/server_functions/standalone_context_spec.rb +142 -0
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +273 -0
- data/spec/ruact/server_rescue_request_spec.rb +416 -0
- data/spec/ruact/server_spec.rb +180 -0
- data/spec/ruact/server_upload_request_spec.rb +311 -0
- data/spec/ruact/view_helper_spec.rb +23 -17
- data/spec/spec_helper.rb +52 -1
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +135 -0
- data/spec/support/flight_wire_parser_spec.rb +93 -0
- data/spec/support/matchers/flight_fixture_matcher.rb +356 -0
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +250 -0
- data/spec/support/rails_stub.rb +75 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +139 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.js +438 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +827 -0
- data/vendor/javascript/ruact-server-functions-runtime/package.json +22 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +3 -2
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +1429 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +15 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +761 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +866 -0
- data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +15 -0
- metadata +87 -6
- data/Users/luiz/workspace/rails-rsc/gem/vendor/javascript/vite-plugin-ruact/index.js +0 -163
- data/lib/ruact/component_registry.rb +0 -31
- data/lib/tasks/rsc.rake +0 -9
|
@@ -62,10 +62,10 @@ module Ruact
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
describe "client component via registry" do
|
|
65
|
-
it "replaces
|
|
65
|
+
it "replaces Ruact comment placeholder with the registered ReactElement" do
|
|
66
66
|
ref = Flight::ClientReference.new(module_id: "./LikeButton", export_name: "LikeButton")
|
|
67
|
-
registry = [{ token: "
|
|
68
|
-
result = convert.call("<!--
|
|
67
|
+
registry = [{ token: "__RUACT_0__", name: "LikeButton", ref: ref, props: { "postId" => 1 } }]
|
|
68
|
+
result = convert.call("<!-- __RUACT_0__ -->", registry)
|
|
69
69
|
|
|
70
70
|
expect(result).to be_a(Flight::ReactElement)
|
|
71
71
|
expect(result.type).to eq(ref)
|
|
@@ -74,8 +74,8 @@ module Ruact
|
|
|
74
74
|
|
|
75
75
|
it "wraps a client component inside a parent DOM element" do
|
|
76
76
|
ref = Flight::ClientReference.new(module_id: "./Button", export_name: "Button")
|
|
77
|
-
registry = [{ token: "
|
|
78
|
-
result = convert.call('<div class="wrapper"><!--
|
|
77
|
+
registry = [{ token: "__RUACT_0__", name: "Button", ref: ref, props: {} }]
|
|
78
|
+
result = convert.call('<div class="wrapper"><!-- __RUACT_0__ --></div>', registry)
|
|
79
79
|
|
|
80
80
|
expect(result.type).to eq("div")
|
|
81
81
|
child = result.props["children"]
|
|
@@ -143,5 +143,183 @@ module Ruact
|
|
|
143
143
|
})
|
|
144
144
|
end
|
|
145
145
|
end
|
|
146
|
+
|
|
147
|
+
describe "input validation (Story 7.4)" do
|
|
148
|
+
describe "nil input" do
|
|
149
|
+
it "raises Ruact::HtmlConverterError" do
|
|
150
|
+
expect { described_class.convert(nil) }.to raise_error(Ruact::HtmlConverterError)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "message contains 'received nil; expected a String of HTML'" do
|
|
154
|
+
expect { described_class.convert(nil) }
|
|
155
|
+
.to raise_error(Ruact::HtmlConverterError,
|
|
156
|
+
/HtmlConverter\.convert received nil; expected a String of HTML/)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "message contains the 'Most likely cause' hint" do
|
|
160
|
+
expect { described_class.convert(nil) }
|
|
161
|
+
.to raise_error(Ruact::HtmlConverterError, /Most likely cause:/)
|
|
162
|
+
expect { described_class.convert(nil) }
|
|
163
|
+
.to raise_error(Ruact::HtmlConverterError, /missing yield, an empty respond_to branch/)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "message contains the documentation pointer" do
|
|
167
|
+
expect { described_class.convert(nil) }
|
|
168
|
+
.to raise_error(Ruact::HtmlConverterError,
|
|
169
|
+
/See HtmlConverter\.convert documentation for the canonical contract\./)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "message points at the spec's file:line, not html_converter.rb (Called from)" do
|
|
173
|
+
expected_line = __LINE__ + 1
|
|
174
|
+
expect { described_class.convert(nil) }
|
|
175
|
+
.to raise_error(Ruact::HtmlConverterError,
|
|
176
|
+
/Called from: #{Regexp.escape(__FILE__)}:#{expected_line}/)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "does not invoke Nokogiri::HTML::DocumentFragment.parse" do
|
|
180
|
+
allow(Nokogiri::HTML::DocumentFragment).to receive(:parse).and_call_original
|
|
181
|
+
expect { described_class.convert(nil) }.to raise_error(Ruact::HtmlConverterError)
|
|
182
|
+
expect(Nokogiri::HTML::DocumentFragment).not_to have_received(:parse)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
describe "non-String inputs" do
|
|
187
|
+
it "raises with 'got Integer' and 'Received: 42' for an Integer" do
|
|
188
|
+
expect { described_class.convert(42) }
|
|
189
|
+
.to raise_error(Ruact::HtmlConverterError,
|
|
190
|
+
/HtmlConverter\.convert expected a String of HTML; got Integer/)
|
|
191
|
+
expect { described_class.convert(42) }
|
|
192
|
+
.to raise_error(Ruact::HtmlConverterError, /Received: 42/)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "raises with 'got Symbol' for a Symbol" do
|
|
196
|
+
expect { described_class.convert(:symbol) }
|
|
197
|
+
.to raise_error(Ruact::HtmlConverterError, /got Symbol/)
|
|
198
|
+
expect { described_class.convert(:symbol) }
|
|
199
|
+
.to raise_error(Ruact::HtmlConverterError, /Received: :symbol/)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "raises with 'got Array' for an Array" do
|
|
203
|
+
expect { described_class.convert(["array"]) }
|
|
204
|
+
.to raise_error(Ruact::HtmlConverterError, /got Array/)
|
|
205
|
+
expect { described_class.convert(["array"]) }
|
|
206
|
+
.to raise_error(Ruact::HtmlConverterError, /Received: \["array"\]/)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "raises with 'got Hash' for a Hash" do
|
|
210
|
+
expect { described_class.convert({ hash: 1 }) }
|
|
211
|
+
.to raise_error(Ruact::HtmlConverterError, /got Hash/)
|
|
212
|
+
expect { described_class.convert({ hash: 1 }) }
|
|
213
|
+
.to raise_error(Ruact::HtmlConverterError, /Received: \{.*hash.*1.*\}/)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "raises with 'got Object' for a generic Object" do
|
|
217
|
+
expect { described_class.convert(Object.new) }
|
|
218
|
+
.to raise_error(Ruact::HtmlConverterError, /got Object/)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "message points at the spec's file:line for non-nil non-String input" do
|
|
222
|
+
expected_line = __LINE__ + 1
|
|
223
|
+
expect { described_class.convert(42) }
|
|
224
|
+
.to raise_error(Ruact::HtmlConverterError,
|
|
225
|
+
/Called from: #{Regexp.escape(__FILE__)}:#{expected_line}/)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "truncates large input previews to ≤ 84 chars (80 + '...')" do
|
|
229
|
+
large_value = (1..10_000).to_a
|
|
230
|
+
message = nil
|
|
231
|
+
begin
|
|
232
|
+
described_class.convert(large_value)
|
|
233
|
+
rescue Ruact::HtmlConverterError => e
|
|
234
|
+
message = e.message
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
received_line = message.lines.find { |l| l.include?("Received:") }
|
|
238
|
+
expect(received_line).not_to be_nil
|
|
239
|
+
preview = received_line.sub(/^\s*Received:\s/, "").strip
|
|
240
|
+
expect(preview.length).to be <= 84
|
|
241
|
+
expect(preview).to end_with("...")
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
describe "subclasses and edge cases" do
|
|
246
|
+
it "accepts String subclasses (e.g. ActionView SafeBuffer-like)" do
|
|
247
|
+
safe_buffer_class = Class.new(String)
|
|
248
|
+
input = safe_buffer_class.new("<div>hi</div>")
|
|
249
|
+
expect { described_class.convert(input) }.not_to raise_error
|
|
250
|
+
|
|
251
|
+
result = described_class.convert(input)
|
|
252
|
+
expect(result).to be_a(Ruact::Flight::ReactElement)
|
|
253
|
+
expect(result.type).to eq("div")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "accepts an empty string without raising" do
|
|
257
|
+
expect { described_class.convert("") }.not_to raise_error
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "raises Ruact::HtmlConverterError (not NoMethodError) for BasicObject inputs" do
|
|
261
|
+
basic = BasicObject.new
|
|
262
|
+
expect { described_class.convert(basic) }
|
|
263
|
+
.to raise_error(Ruact::HtmlConverterError, /HtmlConverter\.convert expected a String of HTML/)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "produces a readable message even when value#inspect raises" do
|
|
267
|
+
hostile = Class.new do
|
|
268
|
+
def inspect
|
|
269
|
+
raise "I refuse to introspect"
|
|
270
|
+
end
|
|
271
|
+
end.new
|
|
272
|
+
|
|
273
|
+
expect { described_class.convert(hostile) }
|
|
274
|
+
.to raise_error(Ruact::HtmlConverterError, /Received: <inspect raised>/)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it "produces a readable message even when value#class raises (BasicObject)" do
|
|
278
|
+
basic = BasicObject.new
|
|
279
|
+
expect { described_class.convert(basic) }
|
|
280
|
+
.to raise_error(Ruact::HtmlConverterError) do |error|
|
|
281
|
+
expect(error.message).to include("got ")
|
|
282
|
+
expect(error.message).not_to match(/got\s*$/)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
describe "backtrace shape" do
|
|
288
|
+
it "frame 0 of the backtrace points outside the gem" do
|
|
289
|
+
described_class.convert(nil)
|
|
290
|
+
rescue Ruact::HtmlConverterError => e
|
|
291
|
+
expect(e.backtrace.first).not_to include("/lib/ruact/html_converter.rb")
|
|
292
|
+
expect(e.backtrace.first).to include(__FILE__)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it "frame 0 references the test file when called from a spec" do
|
|
296
|
+
described_class.convert(42)
|
|
297
|
+
rescue Ruact::HtmlConverterError => e
|
|
298
|
+
expect(e.backtrace.first).to start_with(__FILE__)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Detailed coverage of HtmlConverter input validation (nil + non-String inputs,
|
|
304
|
+
# backtrace shape, BasicObject handling) lives in the "input validation (Story 7.4)"
|
|
305
|
+
# block above (lines 147-300). This regression suite is intentionally minimal — two
|
|
306
|
+
# smoke specs that name the suite for greppability via `:story_7_7` and the
|
|
307
|
+
# "Story 7.7" describe substring; do NOT duplicate the 7.4 detailed coverage here.
|
|
308
|
+
describe "edge cases (Story 7.7) — code-review regression suite", :story_7_7 do
|
|
309
|
+
it "raises Ruact::HtmlConverterError for nil input (smoke; full coverage at :147-184)" do
|
|
310
|
+
expect { described_class.convert(nil) }.to raise_error(Ruact::HtmlConverterError) do |error|
|
|
311
|
+
expect(error.message).to include("received nil")
|
|
312
|
+
expect(error.message).to include("expected a String of HTML")
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it "raises Ruact::HtmlConverterError for a non-String input (smoke; full coverage at :186-243)" do
|
|
317
|
+
expect { described_class.convert(42) }.to raise_error(Ruact::HtmlConverterError) do |error|
|
|
318
|
+
expect(error.message).to include("expected a String of HTML")
|
|
319
|
+
expect(error.message).to include("got Integer")
|
|
320
|
+
expect(error.message).to include("Received: 42")
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
146
324
|
end
|
|
147
325
|
end
|
|
@@ -208,5 +208,98 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
|
|
|
208
208
|
expect(content).not_to include('from "vite-plugin-ruact"')
|
|
209
209
|
end
|
|
210
210
|
end
|
|
211
|
+
|
|
212
|
+
describe "server-functions scaffold (Story 8.0a — AC8)", :story_8_0a do
|
|
213
|
+
# Reproduces append_gitignore_entries logic from the install generator
|
|
214
|
+
# so the spec can be expressed without a full Rails::Generators::TestCase.
|
|
215
|
+
let(:gitignore_entries) do
|
|
216
|
+
[
|
|
217
|
+
"app/javascript/.ruact/server-functions.ts",
|
|
218
|
+
# Story 9.3 — route-driven (v2) parallel inspection target.
|
|
219
|
+
"app/javascript/.ruact/server-functions.next.ts",
|
|
220
|
+
"tmp/cache/ruact/"
|
|
221
|
+
]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Mirror of the real generator's append_gitignore_entries logic
|
|
225
|
+
# (gem/lib/generators/ruact/install/install_generator.rb). Kept in sync
|
|
226
|
+
# at line-set membership granularity — the previous substring-based
|
|
227
|
+
# version drifted from the real generator after the Chunk 1 review's
|
|
228
|
+
# line-set fix and the Re-run 2026-05-14 patch brings this helper
|
|
229
|
+
# back into parity.
|
|
230
|
+
def append_gitignore_entries(dest_root)
|
|
231
|
+
gitignore = File.join(dest_root, ".gitignore")
|
|
232
|
+
return :no_gitignore unless File.exist?(gitignore)
|
|
233
|
+
|
|
234
|
+
existing_lines = File.read(gitignore).each_line.to_set { |line| line.chomp.strip }
|
|
235
|
+
new_entries = gitignore_entries.reject { |e| existing_lines.include?(e) }
|
|
236
|
+
return :already_present if new_entries.empty?
|
|
237
|
+
|
|
238
|
+
File.open(gitignore, "a") do |io|
|
|
239
|
+
io.puts
|
|
240
|
+
io.puts "# ruact (Story 8.0a — auto-generated server-functions module)"
|
|
241
|
+
new_entries.each { |entry| io.puts entry }
|
|
242
|
+
end
|
|
243
|
+
:appended
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def create_gitkeep(dest_root)
|
|
247
|
+
keep = File.join(dest_root, "app/javascript/.ruact/.gitkeep")
|
|
248
|
+
FileUtils.mkdir_p(File.dirname(keep))
|
|
249
|
+
return :already_present if File.exist?(keep)
|
|
250
|
+
|
|
251
|
+
File.write(keep, "")
|
|
252
|
+
:created
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "creates .ruact/.gitkeep so the directory is checkable in (Story 8.0a)" do
|
|
256
|
+
result = create_gitkeep(tmpdir)
|
|
257
|
+
expect(result).to eq(:created)
|
|
258
|
+
expect(File).to exist(File.join(tmpdir, "app/javascript/.ruact/.gitkeep"))
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it "appends both .gitignore entries when missing (Story 8.0a)", :aggregate_failures do
|
|
262
|
+
write_file(".gitignore", "/tmp\n")
|
|
263
|
+
append_gitignore_entries(tmpdir)
|
|
264
|
+
content = read_file(".gitignore")
|
|
265
|
+
expect(content).to include("app/javascript/.ruact/server-functions.ts")
|
|
266
|
+
expect(content).to include("app/javascript/.ruact/server-functions.next.ts")
|
|
267
|
+
expect(content).to include("tmp/cache/ruact/")
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it "is idempotent — running twice does not duplicate entries (Story 8.0a — pitfall #5)" do
|
|
271
|
+
write_file(".gitignore", "/tmp\n")
|
|
272
|
+
append_gitignore_entries(tmpdir)
|
|
273
|
+
append_gitignore_entries(tmpdir)
|
|
274
|
+
|
|
275
|
+
content = read_file(".gitignore")
|
|
276
|
+
expect(content.scan("app/javascript/.ruact/server-functions.ts").size).to eq(1)
|
|
277
|
+
expect(content.scan("tmp/cache/ruact/").size).to eq(1)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "does not write to .gitignore when both entries already exist" do
|
|
281
|
+
write_file(".gitignore", "/tmp\napp/javascript/.ruact/server-functions.ts\n" \
|
|
282
|
+
"app/javascript/.ruact/server-functions.next.ts\ntmp/cache/ruact/\n")
|
|
283
|
+
result = append_gitignore_entries(tmpdir)
|
|
284
|
+
expect(result).to eq(:already_present)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
it "still appends 'tmp/cache/ruact/' when only a longer prefix-matching path is present " \
|
|
288
|
+
"(Re-run 2026-05-14 — line-set semantics, not substring)", :aggregate_failures do
|
|
289
|
+
# The Chunk 1 review's line-set fix makes the real generator append
|
|
290
|
+
# `tmp/cache/ruact/` even when the file already contains a deeper
|
|
291
|
+
# path like `tmp/cache/ruact/some-cache.bin`. The helper used to
|
|
292
|
+
# use substring matching, which would skip the entry — a silent
|
|
293
|
+
# drift between helper and generator. This test pins the helper
|
|
294
|
+
# to the same semantics.
|
|
295
|
+
write_file(".gitignore", "/tmp\ntmp/cache/ruact/some-cache.bin\n")
|
|
296
|
+
append_gitignore_entries(tmpdir)
|
|
297
|
+
content = read_file(".gitignore")
|
|
298
|
+
# Exact-line match: a new "tmp/cache/ruact/" line is appended even
|
|
299
|
+
# though the file already contains "tmp/cache/ruact/some-cache.bin"
|
|
300
|
+
expect(content.each_line.to_a).to include("tmp/cache/ruact/\n")
|
|
301
|
+
expect(content).to include("app/javascript/.ruact/server-functions.ts")
|
|
302
|
+
end
|
|
303
|
+
end
|
|
211
304
|
end
|
|
212
305
|
end
|