ruact 0.0.11 → 0.0.12

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -48
  3. data/CONTRIBUTING.md +233 -0
  4. data/README.md +14 -4
  5. data/RELEASING.md +125 -139
  6. data/lib/ruact/packaging.rb +68 -0
  7. data/lib/ruact/version.rb +1 -1
  8. data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
  9. data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
  10. data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
  11. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
  12. metadata +6 -107
  13. data/.codecov.yml +0 -31
  14. data/.github/workflows/ci.yml +0 -284
  15. data/.github/workflows/server-functions-bench.yml +0 -54
  16. data/.rubocop.yml +0 -107
  17. data/.rubocop_todo.yml +0 -63
  18. data/Rakefile +0 -10
  19. data/bench/server_functions_dispatch_bench.rb +0 -276
  20. data/bench/server_functions_dispatch_bench.results.md +0 -150
  21. data/docs/internal/README.md +0 -9
  22. data/docs/internal/decisions/server-functions-api.md +0 -2236
  23. data/spec/benchmarks/baseline.json +0 -12
  24. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
  25. data/spec/fixtures/flight/README.md +0 -136
  26. data/spec/fixtures/flight/array.txt +0 -1
  27. data/spec/fixtures/flight/as_json_object.txt +0 -2
  28. data/spec/fixtures/flight/bigint.txt +0 -1
  29. data/spec/fixtures/flight/boolean_false.txt +0 -1
  30. data/spec/fixtures/flight/boolean_true.txt +0 -1
  31. data/spec/fixtures/flight/client_component_with_props.txt +0 -2
  32. data/spec/fixtures/flight/client_reference.txt +0 -2
  33. data/spec/fixtures/flight/hash.txt +0 -1
  34. data/spec/fixtures/flight/infinity.txt +0 -1
  35. data/spec/fixtures/flight/nan.txt +0 -1
  36. data/spec/fixtures/flight/negative_infinity.txt +0 -1
  37. data/spec/fixtures/flight/nil.txt +0 -1
  38. data/spec/fixtures/flight/number_float.txt +0 -1
  39. data/spec/fixtures/flight/number_integer.txt +0 -1
  40. data/spec/fixtures/flight/react_element_no_props.txt +0 -1
  41. data/spec/fixtures/flight/redirect_row.txt +0 -1
  42. data/spec/fixtures/flight/serializable_object.txt +0 -2
  43. data/spec/fixtures/flight/string_basic.txt +0 -1
  44. data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
  45. data/spec/fixtures/flight/undefined.txt +0 -1
  46. data/spec/fixtures/readme/children-error.html.erb +0 -3
  47. data/spec/fixtures/readme/children-error.txt +0 -1
  48. data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
  49. data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
  50. data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
  51. data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
  52. data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
  53. data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
  54. data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
  55. data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
  56. data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
  57. data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
  58. data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
  59. data/spec/readme_demo_message_spec.rb +0 -67
  60. data/spec/readme_spec.rb +0 -282
  61. data/spec/ruact/client_manifest_spec.rb +0 -270
  62. data/spec/ruact/component_contract_spec.rb +0 -119
  63. data/spec/ruact/configuration_spec.rb +0 -518
  64. data/spec/ruact/controller_request_spec.rb +0 -671
  65. data/spec/ruact/controller_spec.rb +0 -343
  66. data/spec/ruact/doctor_spec.rb +0 -769
  67. data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
  68. data/spec/ruact/erb_preprocessor_spec.rb +0 -361
  69. data/spec/ruact/errors_spec.rb +0 -93
  70. data/spec/ruact/flight/renderer_spec.rb +0 -133
  71. data/spec/ruact/flight/serializer_spec.rb +0 -494
  72. data/spec/ruact/html_converter_spec.rb +0 -375
  73. data/spec/ruact/install_generator_spec.rb +0 -1549
  74. data/spec/ruact/layout_source_spec.rb +0 -108
  75. data/spec/ruact/manifest_resolver_spec.rb +0 -174
  76. data/spec/ruact/query_request_spec.rb +0 -706
  77. data/spec/ruact/query_spec.rb +0 -105
  78. data/spec/ruact/railtie_spec.rb +0 -155
  79. data/spec/ruact/render_context_spec.rb +0 -58
  80. data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
  81. data/spec/ruact/render_pipeline_spec.rb +0 -928
  82. data/spec/ruact/scaffold_generator_spec.rb +0 -1849
  83. data/spec/ruact/serializable_spec.rb +0 -179
  84. data/spec/ruact/server_bucket_request_spec.rb +0 -785
  85. data/spec/ruact/server_function_name_spec.rb +0 -53
  86. data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
  87. data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
  88. data/spec/ruact/server_functions/codegen_spec.rb +0 -397
  89. data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
  90. data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
  91. data/spec/ruact/server_functions/introspection_spec.rb +0 -135
  92. data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
  93. data/spec/ruact/server_functions/query_context_spec.rb +0 -72
  94. data/spec/ruact/server_functions/query_source_spec.rb +0 -193
  95. data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
  96. data/spec/ruact/server_functions/rake_spec.rb +0 -86
  97. data/spec/ruact/server_functions/route_source_spec.rb +0 -202
  98. data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
  99. data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
  100. data/spec/ruact/server_rescue_request_spec.rb +0 -416
  101. data/spec/ruact/server_spec.rb +0 -179
  102. data/spec/ruact/server_upload_request_spec.rb +0 -311
  103. data/spec/ruact/signed_references_spec.rb +0 -164
  104. data/spec/ruact/string_distance_spec.rb +0 -38
  105. data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
  106. data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
  107. data/spec/ruact/testing/no_production_load_spec.rb +0 -41
  108. data/spec/ruact/validation_errors_spec.rb +0 -116
  109. data/spec/ruact/view_helper_spec.rb +0 -131
  110. data/spec/spec_helper.rb +0 -77
  111. data/spec/support/fixtures/pixel.png +0 -0
  112. data/spec/support/flight_wire_parser.rb +0 -21
  113. data/spec/support/flight_wire_parser_spec.rb +0 -93
  114. data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
  115. data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
  116. data/spec/support/rails_stub.rb +0 -115
@@ -1,518 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- RSpec.describe Configuration do
7
- # Reset the singleton + boot flag around every example so order randomization
8
- # is safe and the warning-on-second-call contract can be exercised cleanly.
9
- # Save and restore the original Rails.logger so we never clobber a logger a
10
- # later randomized example expects to find in place.
11
- around do |example|
12
- Ruact.instance_variable_set(:@config, nil)
13
- Ruact.instance_variable_set(:@configured_at_least_once, false)
14
- original_rails_logger = Rails.respond_to?(:logger) ? Rails.logger : nil
15
- example.run
16
- ensure
17
- Ruact.instance_variable_set(:@config, nil)
18
- Ruact.instance_variable_set(:@configured_at_least_once, false)
19
- Rails.logger = original_rails_logger if Rails.respond_to?(:logger=)
20
- end
21
-
22
- # Default: silence the [ruact] re-configuration warning so unrelated specs
23
- # that legitimately call Ruact.configure twice do not pollute stderr.
24
- # The "re-configuration warning" describe overrides this with its own assertions.
25
- before do
26
- Rails.singleton_class.send(:attr_accessor, :logger) unless Rails.respond_to?(:logger=)
27
- Rails.logger = instance_double(::Logger, warn: nil)
28
- end
29
-
30
- describe ".configure freezes the configuration" do
31
- it "is frozen the moment the configure block returns (AC1)" do
32
- Ruact.configure { |c| c.suspense_timeout = 7.0 }
33
- expect(Ruact.config).to be_frozen
34
- end
35
-
36
- it "preserves attribute reads after freeze (AC1)" do
37
- Ruact.configure do |c|
38
- c.suspense_timeout = 8.0
39
- c.strict_serialization = true
40
- c.vite_dev_server = "http://localhost:9999"
41
- c.manifest_path = "/tmp/manifest.json"
42
- end
43
-
44
- expect(Ruact.config.suspense_timeout).to eq(8.0)
45
- expect(Ruact.config.strict_serialization).to be(true)
46
- expect(Ruact.config.vite_dev_server).to eq("http://localhost:9999")
47
- expect(Ruact.config.manifest_path).to eq("/tmp/manifest.json")
48
- end
49
- end
50
-
51
- describe "post-boot mutation" do
52
- before { Ruact.configure { |c| c.suspense_timeout = 5.0 } }
53
-
54
- Configuration::ATTRIBUTES.each do |attr|
55
- it "raises Ruact::ConfigurationError when ##{attr} is assigned outside Ruact.configure (AC2)" do
56
- expect { Ruact.config.public_send("#{attr}=", "anything") }
57
- .to raise_error(Ruact::ConfigurationError, /Ruact::Configuration##{attr}/)
58
- end
59
- end
60
-
61
- it "error message includes the AC2 verbatim suggested-fix sentence (AC2.3)" do
62
- expect { Ruact.config.suspense_timeout = 12.0 }
63
- .to raise_error(
64
- Ruact::ConfigurationError,
65
- include("Wrap the change in Ruact.configure { |c| c.suspense_timeout = ... } " \
66
- "in config/initializers/ruact.rb and restart the process.")
67
- )
68
- end
69
-
70
- it "error message includes the design intent (AC2.4)" do
71
- expect { Ruact.config.suspense_timeout = 12.0 }
72
- .to raise_error(
73
- Ruact::ConfigurationError,
74
- /Story 7\.3.*runtime config drift/m
75
- )
76
- end
77
- end
78
-
79
- describe "deep-freeze of attribute values (AC2 — bypass guard)" do
80
- it "freezes mutable string values so in-place mutation cannot bypass the writer guard" do
81
- Ruact.configure { |c| c.manifest_path = +"/tmp/manifest.json" }
82
-
83
- expect(Ruact.config.manifest_path).to be_frozen
84
- expect { Ruact.config.manifest_path.replace("/tmp/elsewhere.json") }
85
- .to raise_error(FrozenError)
86
- end
87
-
88
- it "freezes vite_dev_server even when only defaults are used (AC5)" do
89
- # No configure block — first access publishes the default-frozen Configuration.
90
- expect(Ruact.config.vite_dev_server).to be_frozen
91
- end
92
-
93
- it "carries the deep-freeze invariant through atomic re-configuration" do
94
- Ruact.configure { |c| c.manifest_path = +"/tmp/first.json" }
95
- Ruact.configure { |c| c.suspense_timeout = 9.0 } # does NOT touch manifest_path
96
-
97
- # The second configure clones from the first; manifest_path retains the
98
- # frozen value, not a fresh mutable reference.
99
- expect(Ruact.config.manifest_path).to be_frozen
100
- end
101
-
102
- it "values are mutable inside the configure block; freeze happens only after the block returns (AC1)" do
103
- # AC1: "the DSL inside the block is unchanged. The freeze happens after
104
- # the block returns, not before." Deep-freeze of attribute values must
105
- # therefore happen at publication time, not at writer time — otherwise
106
- # a developer who in-place-mutates a value within the same configure
107
- # block (a documented Ruby idiom) would see a confusing FrozenError.
108
- captured_inside_block = nil
109
-
110
- Ruact.configure do |c|
111
- c.manifest_path = +"/tmp/a.json"
112
- captured_inside_block = c.manifest_path
113
- expect(captured_inside_block).not_to be_frozen
114
- c.manifest_path.replace("/tmp/b.json") # idiomatic in-place mutation, must work
115
- expect(c.manifest_path).to eq("/tmp/b.json")
116
- end
117
-
118
- # After the block returns, publication has deep-frozen the value.
119
- expect(Ruact.config.manifest_path).to eq("/tmp/b.json")
120
- expect(Ruact.config.manifest_path).to be_frozen
121
- expect { Ruact.config.manifest_path.replace("/tmp/c.json") }.to raise_error(FrozenError)
122
- end
123
-
124
- it "template-inherited values are mutable inside a second configure block (AC1, F9)" do
125
- # F9: After the first publication, the published config has deep-frozen
126
- # values. The second Ruact.configure call clones into a draft via
127
- # Configuration.new(template:); that draft must be unfrozen so the AC1
128
- # contract holds for re-configuration too — including idiomatic
129
- # in-place mutation of inherited values.
130
- Ruact.configure { |c| c.manifest_path = +"/tmp/a.json" }
131
- expect(Ruact.config.manifest_path).to be_frozen # baseline: first publication froze the value
132
-
133
- Ruact.configure do |c|
134
- expect(c.manifest_path).not_to be_frozen # draft cloned the value into an unfrozen dup
135
- c.manifest_path.replace("/tmp/b.json") # idiomatic in-place mutation on inherited value
136
- expect(c.manifest_path).to eq("/tmp/b.json")
137
- end
138
-
139
- # After the second block returns, publication freezes the draft's value.
140
- expect(Ruact.config.manifest_path).to eq("/tmp/b.json")
141
- expect(Ruact.config.manifest_path).to be_frozen
142
- end
143
- end
144
-
145
- describe "public API surface (AC1, AC7, AC9 — F10)" do
146
- it "does not expose seal! on Ruact::Configuration" do
147
- # seal! is a private implementation detail invoked by Ruact.configure /
148
- # Ruact.config via __send__. The public API surface is the four
149
- # readers + their writers (only callable inside a configure block) —
150
- # nothing else. External callers reaching into Ruact.config.seal!
151
- # must hit NoMethodError, not silently re-freeze.
152
- Ruact.configure { |c| c.suspense_timeout = 5.0 }
153
-
154
- expect(described_class.public_instance_methods(false)).not_to include(:seal!)
155
- expect(Ruact.config).not_to respond_to(:seal!)
156
- expect { Ruact.config.seal! }.to raise_error(NoMethodError, /private method/)
157
- end
158
-
159
- it "exposes only the documented readers + writers on the public surface" do
160
- Ruact.configure { |c| c.suspense_timeout = 5.0 }
161
-
162
- public_attrs = described_class.public_instance_methods(false)
163
- expected = Configuration::ATTRIBUTES + Configuration::ATTRIBUTES.map { |a| :"#{a}=" }
164
-
165
- expect(public_attrs.sort).to eq(expected.sort)
166
- end
167
- end
168
-
169
- describe "atomic re-configuration" do
170
- it "produces a frozen Configuration after both calls (AC3)" do
171
- Ruact.configure { |c| c.suspense_timeout = 5.0 }
172
- first_config = Ruact.config
173
-
174
- Ruact.configure { |c| c.suspense_timeout = 9.0 }
175
- second_config = Ruact.config
176
-
177
- expect(first_config).to be_frozen
178
- expect(second_config).to be_frozen
179
- expect(second_config).not_to equal(first_config)
180
- expect(second_config.suspense_timeout).to eq(9.0)
181
- end
182
-
183
- it "clones every attribute from the previous configuration (AC3)" do
184
- Ruact.configure do |c|
185
- c.suspense_timeout = 7.5
186
- c.strict_serialization = true
187
- end
188
-
189
- Ruact.configure { |c| c.vite_dev_server = "http://localhost:8000" }
190
-
191
- # Attributes set in the first call must survive the second call.
192
- expect(Ruact.config.suspense_timeout).to eq(7.5)
193
- expect(Ruact.config.strict_serialization).to be(true)
194
- expect(Ruact.config.vite_dev_server).to eq("http://localhost:8000")
195
- end
196
-
197
- it "intermediate readers inside the second configure block see the OLD frozen config (AC3)" do
198
- Ruact.configure { |c| c.suspense_timeout = 5.0 }
199
- old_config = Ruact.config
200
-
201
- observed_inside_block = nil
202
- observed_timeout = nil
203
-
204
- Ruact.configure do |draft|
205
- # While the block runs, Ruact.config still resolves to the OLD frozen
206
- # config; the draft is a separate object that is not yet swapped in.
207
- observed_inside_block = Ruact.config
208
- observed_timeout = Ruact.config.suspense_timeout
209
- draft.suspense_timeout = 99.0
210
- end
211
-
212
- expect(observed_inside_block).to equal(old_config)
213
- expect(observed_timeout).to eq(5.0)
214
- expect(Ruact.config).not_to equal(old_config)
215
- expect(Ruact.config.suspense_timeout).to eq(99.0)
216
- end
217
- end
218
-
219
- describe "re-configuration warning" do
220
- let(:fake_logger) { instance_double(::Logger, warn: nil) }
221
-
222
- # Override the parent before-hook with a per-example fake we can assert on.
223
- before { Rails.logger = fake_logger }
224
-
225
- it "does NOT warn on the first Ruact.configure call (AC3)" do
226
- Ruact.configure { |c| c.suspense_timeout = 5.0 }
227
- expect(fake_logger).not_to have_received(:warn)
228
- end
229
-
230
- it "warns on the second Ruact.configure call (AC3)" do
231
- Ruact.configure { |c| c.suspense_timeout = 5.0 }
232
- Ruact.configure { |c| c.suspense_timeout = 9.0 }
233
-
234
- expect(fake_logger).to have_received(:warn).with(
235
- a_string_matching(/\[ruact\] Ruact\.configure called after boot at .+:\d+/)
236
- )
237
- end
238
-
239
- it "warns on Ruact.configure that follows default Ruact.config first access (AC3, F2 fix)" do
240
- # Default first-access publishes the frozen-default Configuration; that
241
- # publication counts as boot, so the next configure call must warn.
242
- Ruact.config
243
- Ruact.configure { |c| c.suspense_timeout = 9.0 }
244
-
245
- expect(fake_logger).to have_received(:warn).with(
246
- a_string_matching(/\[ruact\] Ruact\.configure called after boot/)
247
- )
248
- end
249
- end
250
-
251
- describe "Ruact::ConfigurationError" do
252
- it "is a subclass of Ruact::Error (AC6)" do
253
- expect(Ruact::ConfigurationError.ancestors).to include(Ruact::Error)
254
- end
255
- end
256
-
257
- describe "boot-time defaults" do
258
- it "freezes the default Configuration on first access (AC5)" do
259
- # No Ruact.configure call — first access goes through Ruact.config directly.
260
- expect(Ruact.config).to be_frozen
261
- end
262
-
263
- it "applies documented defaults when no configure block runs (AC5)" do
264
- config = Ruact.config
265
-
266
- expect(config.manifest_path).to be_nil
267
- expect(config.strict_serialization).to be(false)
268
- expect(config.suspense_timeout).to eq(5.0)
269
- expect(config.vite_dev_server).to eq("http://localhost:5173")
270
- end
271
-
272
- it "rejects post-boot mutation against the default-frozen instance (AC5)" do
273
- expect { Ruact.config.suspense_timeout = 10.0 }
274
- .to raise_error(Ruact::ConfigurationError)
275
- end
276
- end
277
-
278
- describe "Story 8.5 — max_upload_bytes attribute", :story_8_5 do
279
- it "defaults to 10 MB (10 * 1024 * 1024 bytes)" do
280
- expect(Ruact.config.max_upload_bytes).to eq(10 * 1024 * 1024)
281
- end
282
-
283
- it "accepts an Integer inside Ruact.configure" do
284
- Ruact.configure { |c| c.max_upload_bytes = 25 * 1024 * 1024 }
285
- expect(Ruact.config.max_upload_bytes).to eq(25 * 1024 * 1024)
286
- end
287
-
288
- it "accepts nil to disable the gem-side guard" do
289
- Ruact.configure { |c| c.max_upload_bytes = nil }
290
- expect(Ruact.config.max_upload_bytes).to be_nil
291
- end
292
-
293
- it "is sealed by the standard freeze contract — direct mutation raises ConfigurationError" do
294
- Ruact.configure { |c| c.max_upload_bytes = 5 * 1024 * 1024 }
295
- expect { Ruact.config.max_upload_bytes = 9_000_000 }
296
- .to raise_error(Ruact::ConfigurationError, /Ruact::Configuration#max_upload_bytes/)
297
- end
298
-
299
- it "is carried across atomic re-configuration (template clone)" do
300
- Ruact.configure { |c| c.max_upload_bytes = 7 * 1024 * 1024 }
301
- Ruact.configure { |c| c.suspense_timeout = 6.0 }
302
- expect(Ruact.config.max_upload_bytes).to eq(7 * 1024 * 1024)
303
- end
304
-
305
- describe "writer-time validation (review patch)" do
306
- it "accepts 0 (a degenerate but legal cap that rejects every multipart/urlencoded request)" do
307
- expect { Ruact.configure { |c| c.max_upload_bytes = 0 } }.not_to raise_error
308
- expect(Ruact.config.max_upload_bytes).to eq(0)
309
- end
310
-
311
- it "rejects negative Integer with ConfigurationError" do
312
- expect { Ruact.configure { |c| c.max_upload_bytes = -1 } }
313
- .to raise_error(Ruact::ConfigurationError, /must be nil or a non-negative Integer/)
314
- end
315
-
316
- it "rejects String with ConfigurationError naming the offending value + class" do
317
- expect { Ruact.configure { |c| c.max_upload_bytes = "10485760" } }
318
- .to raise_error(Ruact::ConfigurationError, /got "10485760" \(String\)/)
319
- end
320
-
321
- it "rejects Float with ConfigurationError" do
322
- expect { Ruact.configure { |c| c.max_upload_bytes = 1024.0 } }
323
- .to raise_error(Ruact::ConfigurationError, /must be nil or a non-negative Integer/)
324
- end
325
-
326
- it "rejects Symbol with ConfigurationError" do
327
- expect { Ruact.configure { |c| c.max_upload_bytes = :unlimited } }
328
- .to raise_error(Ruact::ConfigurationError, /must be nil or a non-negative Integer/)
329
- end
330
-
331
- it "rejects true/false with ConfigurationError" do
332
- expect { Ruact.configure { |c| c.max_upload_bytes = true } }
333
- .to raise_error(Ruact::ConfigurationError, /must be nil or a non-negative Integer/)
334
- end
335
- end
336
- end
337
-
338
- describe "Story 8.4 — dev_error_payload_enabled attribute", :story_8_4 do
339
- it "defaults to nil so the endpoint controller can resolve to Rails env at request time" do
340
- expect(Ruact.config.dev_error_payload_enabled).to be_nil
341
- end
342
-
343
- it "accepts true inside Ruact.configure and exposes it after publication" do
344
- Ruact.configure { |c| c.dev_error_payload_enabled = true }
345
- expect(Ruact.config.dev_error_payload_enabled).to be(true)
346
- end
347
-
348
- it "accepts false inside Ruact.configure and exposes it after publication" do
349
- Ruact.configure { |c| c.dev_error_payload_enabled = false }
350
- expect(Ruact.config.dev_error_payload_enabled).to be(false)
351
- end
352
-
353
- it "is sealed by the standard freeze contract — direct mutation raises ConfigurationError" do
354
- Ruact.configure { |c| c.dev_error_payload_enabled = true }
355
- expect { Ruact.config.dev_error_payload_enabled = false }
356
- .to raise_error(Ruact::ConfigurationError, /Ruact::Configuration#dev_error_payload_enabled/)
357
- end
358
-
359
- it "is carried across atomic re-configuration (template clone)" do
360
- Ruact.configure { |c| c.dev_error_payload_enabled = false }
361
- Ruact.configure { |c| c.suspense_timeout = 6.0 }
362
- expect(Ruact.config.dev_error_payload_enabled).to be(false)
363
- end
364
- end
365
-
366
- describe "Story 9.4 — query_route_prefix attribute", :story_9_4 do
367
- it "defaults to \"/q\"" do
368
- expect(Ruact.config.query_route_prefix).to eq("/q")
369
- end
370
-
371
- it "accepts a custom prefix inside Ruact.configure" do
372
- Ruact.configure { |c| c.query_route_prefix = "/api/queries" }
373
- expect(Ruact.config.query_route_prefix).to eq("/api/queries")
374
- end
375
-
376
- it "is sealed by the standard freeze contract — direct mutation raises ConfigurationError" do
377
- Ruact.configure { |c| c.query_route_prefix = "/q" }
378
- expect { Ruact.config.query_route_prefix = "/other" }
379
- .to raise_error(Ruact::ConfigurationError, /Ruact::Configuration#query_route_prefix/)
380
- end
381
-
382
- it "is carried across atomic re-configuration (template clone)" do
383
- Ruact.configure { |c| c.query_route_prefix = "/internal/q" }
384
- Ruact.configure { |c| c.suspense_timeout = 6.0 }
385
- expect(Ruact.config.query_route_prefix).to eq("/internal/q")
386
- end
387
-
388
- describe "writer-time validation" do
389
- it "rejects a non-String with ConfigurationError naming the offending value + class" do
390
- expect { Ruact.configure { |c| c.query_route_prefix = :q } }
391
- .to raise_error(Ruact::ConfigurationError, /got :q \(Symbol\)/)
392
- end
393
-
394
- it "rejects a prefix that does not start with \"/\"" do
395
- expect { Ruact.configure { |c| c.query_route_prefix = "q" } }
396
- .to raise_error(Ruact::ConfigurationError, %r{must be a String starting with "/"})
397
- end
398
-
399
- it "rejects a prefix with a trailing slash (the macro joins with \"/\")" do
400
- expect { Ruact.configure { |c| c.query_route_prefix = "/q/" } }
401
- .to raise_error(Ruact::ConfigurationError, %r{must not end with "/"})
402
- end
403
-
404
- it "rejects nil" do
405
- expect { Ruact.configure { |c| c.query_route_prefix = nil } }
406
- .to raise_error(Ruact::ConfigurationError, %r{must be a String starting with "/"})
407
- end
408
- end
409
- end
410
-
411
- describe "Story 9.4 — query_parent_controller attribute", :story_9_4 do
412
- it "defaults to \"ApplicationController\"" do
413
- expect(Ruact.config.query_parent_controller).to eq("ApplicationController")
414
- end
415
-
416
- it "accepts a custom controller name inside Ruact.configure" do
417
- Ruact.configure { |c| c.query_parent_controller = "Api::BaseController" }
418
- expect(Ruact.config.query_parent_controller).to eq("Api::BaseController")
419
- end
420
-
421
- it "is sealed by the standard freeze contract — direct mutation raises ConfigurationError" do
422
- Ruact.configure { |c| c.query_parent_controller = "ApplicationController" }
423
- expect { Ruact.config.query_parent_controller = "Other" }
424
- .to raise_error(Ruact::ConfigurationError, /Ruact::Configuration#query_parent_controller/)
425
- end
426
-
427
- it "is carried across atomic re-configuration (template clone)" do
428
- Ruact.configure { |c| c.query_parent_controller = "Api::BaseController" }
429
- Ruact.configure { |c| c.suspense_timeout = 6.0 }
430
- expect(Ruact.config.query_parent_controller).to eq("Api::BaseController")
431
- end
432
-
433
- describe "writer-time validation" do
434
- it "rejects a Class with ConfigurationError (the name is constantized lazily at route-draw time)" do
435
- expect { Ruact.configure { |c| c.query_parent_controller = Class.new } }
436
- .to raise_error(Ruact::ConfigurationError, /must be a non-empty String/)
437
- end
438
-
439
- it "rejects an empty String" do
440
- expect { Ruact.configure { |c| c.query_parent_controller = "" } }
441
- .to raise_error(Ruact::ConfigurationError, /must be a non-empty String/)
442
- end
443
-
444
- it "rejects nil" do
445
- expect { Ruact.configure { |c| c.query_parent_controller = nil } }
446
- .to raise_error(Ruact::ConfigurationError, /must be a non-empty String/)
447
- end
448
- end
449
- end
450
-
451
- describe "Story 10.5 — shadcn_compatible_versions attribute", :story_10_5 do
452
- it "defaults to [1, 2]" do
453
- expect(Ruact.config.shadcn_compatible_versions).to eq([1, 2])
454
- end
455
-
456
- it "accepts a custom list inside Ruact.configure" do
457
- Ruact.configure { |c| c.shadcn_compatible_versions = [1, 2, 3] }
458
- expect(Ruact.config.shadcn_compatible_versions).to eq([1, 2, 3])
459
- end
460
-
461
- it "is sealed by the standard freeze contract — direct mutation raises ConfigurationError" do
462
- Ruact.configure { |c| c.shadcn_compatible_versions = [2] }
463
- expect { Ruact.config.shadcn_compatible_versions = [3] }
464
- .to raise_error(Ruact::ConfigurationError, /Ruact::Configuration#shadcn_compatible_versions/)
465
- end
466
-
467
- it "deep-freezes the Array value so in-place mutation cannot bypass the writer guard" do
468
- Ruact.configure { |c| c.shadcn_compatible_versions = [2] }
469
- expect(Ruact.config.shadcn_compatible_versions).to be_frozen
470
- expect { Ruact.config.shadcn_compatible_versions << 3 }.to raise_error(FrozenError)
471
- end
472
-
473
- it "is carried across atomic re-configuration (template clone)" do
474
- Ruact.configure { |c| c.shadcn_compatible_versions = [2, 3] }
475
- Ruact.configure { |c| c.suspense_timeout = 6.0 }
476
- expect(Ruact.config.shadcn_compatible_versions).to eq([2, 3])
477
- end
478
-
479
- describe "writer-time validation" do
480
- it "rejects a non-Array with ConfigurationError naming the offending value + class" do
481
- expect { Ruact.configure { |c| c.shadcn_compatible_versions = 2 } }
482
- .to raise_error(Ruact::ConfigurationError, /got 2 \(Integer\)/)
483
- end
484
-
485
- it "rejects an empty Array" do
486
- expect { Ruact.configure { |c| c.shadcn_compatible_versions = [] } }
487
- .to raise_error(Ruact::ConfigurationError, /non-empty Array/)
488
- end
489
-
490
- it "rejects an Array with non-Integer elements" do
491
- expect { Ruact.configure { |c| c.shadcn_compatible_versions = ["2"] } }
492
- .to raise_error(Ruact::ConfigurationError, /only Integer/)
493
- end
494
-
495
- it "rejects nil" do
496
- expect { Ruact.configure { |c| c.shadcn_compatible_versions = nil } }
497
- .to raise_error(Ruact::ConfigurationError, /non-empty Array/)
498
- end
499
- end
500
- end
501
-
502
- describe "error message includes caller location" do
503
- it "names the file:line of the offending mutation (AC2.2)" do
504
- Ruact.configure { |c| c.suspense_timeout = 5.0 }
505
-
506
- # Capture the file:line of the next line via __FILE__ / __LINE__.
507
- expected_path = __FILE__
508
- expected_line = __LINE__ + 2
509
- expect do
510
- Ruact.config.suspense_timeout = 99.0
511
- end.to raise_error(
512
- Ruact::ConfigurationError,
513
- a_string_matching(/Attempted at: #{Regexp.escape(expected_path)}:#{expected_line}/)
514
- )
515
- end
516
- end
517
- end
518
- end