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,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../flight_wire_parser"
4
- require "ruact/testing/flight_structure_diff"
5
-
6
- module Ruact
7
- module Spec
8
- # Story 15.4 (FR108) — the structural diff engine was PROMOTED onto the
9
- # gem's `lib/` load path as `Ruact::Testing::FlightStructureDiff` (shared,
10
- # single implementation — wrap, not fork). This test-only `Ruact::Spec`
11
- # alias keeps the internal matcher wrappers below and their self-tests
12
- # working unchanged.
13
- FlightStructureDiff = Ruact::Testing::FlightStructureDiff
14
- end
15
- end
16
-
17
- # `match_flight_fixture(name)` — Phase 1 byte-exact snapshot matcher.
18
- #
19
- # Reads `spec/fixtures/flight/<name>.txt` and compares the actual wire bytes
20
- # to the file contents via `==`. Used when the wire format itself is the
21
- # contract (escape rules, ordering invariants, payload framing).
22
- #
23
- # @example
24
- # expect(serializer.serialize_value("$danger")).to match_flight_fixture("string_dollar_escape")
25
- RSpec::Matchers.define :match_flight_fixture do |name|
26
- match do |actual|
27
- fixtures_dir = File.expand_path("../../fixtures/flight", __dir__)
28
- fixture_path = File.join(fixtures_dir, "#{name}.txt")
29
- @fixture_path = fixture_path
30
- @expected = File.read(fixture_path)
31
- actual == @expected
32
- end
33
-
34
- failure_message do |actual|
35
- "Expected output to match fixture at #{@fixture_path}.\n\n" \
36
- "Expected:\n#{@expected.inspect}\n\n" \
37
- "Got:\n#{actual.inspect}"
38
- end
39
-
40
- failure_message_when_negated do |_actual|
41
- "Expected output NOT to match fixture at #{@fixture_path}, but it did."
42
- end
43
-
44
- description do
45
- "match Flight wire fixture '#{name}'"
46
- end
47
- end
48
-
49
- # `match_flight_structure(expected)` — Phase 2 structural matcher (Story 7.5).
50
- #
51
- # Parses the actual Flight wire output via `Ruact::Spec::FlightWireParser`,
52
- # then compares the resulting array of row records against `expected`. Hash
53
- # payloads are compared structurally (key insertion order is ignored), so
54
- # cosmetic JSON re-ordering does not break specs that assert on semantics.
55
- # Import rows are matched as a multiset; non-import rows are compared
56
- # positionally because Flight semantics depend on their order.
57
- #
58
- # Failure messages name the row index and the differing field — bytes are
59
- # only printed when no narrower diff is available.
60
- #
61
- # @example
62
- # expect(wire).to match_flight_structure([
63
- # { id: 1, class: :import, payload: ["/L.jsx", "L", ["/L.jsx"]] },
64
- # { id: 0, class: :model, payload: ["$", "$L1", nil, {}] }
65
- # ])
66
- RSpec::Matchers.define :match_flight_structure do |expected|
67
- match do |actual|
68
- @parsed = Ruact::Spec::FlightWireParser.parse(actual)
69
- @expected_rows = expected
70
- @diffs = Ruact::Spec::FlightStructureDiff.compute(@parsed, expected)
71
- @diffs.empty?
72
- end
73
-
74
- failure_message do |_actual|
75
- if @diffs.length == 1
76
- Ruact::Spec::FlightStructureDiff.format_single(@diffs.first)
77
- else
78
- Ruact::Spec::FlightStructureDiff.format_multi(@diffs, @parsed, @expected_rows)
79
- end
80
- end
81
-
82
- failure_message_when_negated do |_actual|
83
- "Expected output NOT to match the given Flight wire structure, but it did."
84
- end
85
-
86
- description do
87
- "match Flight wire structure (#{expected.length} row(s))"
88
- end
89
- end
90
-
91
- # `include_flight_row(predicate)` — Phase 2 ordering-independent presence matcher (Story 7.5).
92
- #
93
- # Parses the actual Flight wire output, then asserts at least one parsed row
94
- # satisfies the predicate hash. Subset semantics: only the keys present in
95
- # `predicate` are compared. Predicate keys must be one of `:id`, `:class`,
96
- # `:payload`, `:raw` — unknown keys raise upfront so typos don't silently
97
- # match every row. Values may be plain Ruby values (compared with `==`) or
98
- # RSpec argument matchers like `hash_including(...)` (compared with `===`).
99
- # Negation (`not_to`) is supported.
100
- #
101
- # @example
102
- # expect(wire).to include_flight_row(class: :model, payload: hash_including("postId" => 42))
103
- RSpec::Matchers.define :include_flight_row do |predicate|
104
- match do |actual|
105
- Ruact::Spec::FlightStructureDiff.validate_predicate!(predicate)
106
- @parsed = Ruact::Spec::FlightWireParser.parse(actual)
107
- @predicate = predicate
108
- @matched_idx = @parsed.find_index { |row| Ruact::Spec::FlightStructureDiff.row_matches?(row, predicate) }
109
- !@matched_idx.nil?
110
- end
111
-
112
- failure_message do |_actual|
113
- summary = @parsed.each_with_index.map do |row, i|
114
- payload_snippet = row[:payload].inspect
115
- payload_snippet = "#{payload_snippet[0, 80]}…" if payload_snippet.length > 80
116
- " [#{i}] id=#{row[:id].inspect}, class=#{row[:class]}, payload=#{payload_snippet}"
117
- end.join("\n")
118
- summary = " (no rows parsed)" if @parsed.empty?
119
-
120
- "Expected Flight output to include a row matching: #{predicate.inspect}.\nParsed rows:\n#{summary}"
121
- end
122
-
123
- failure_message_when_negated do |_actual|
124
- "Expected Flight output NOT to include a row matching: #{predicate.inspect}, but row #{@matched_idx} matched."
125
- end
126
-
127
- description do
128
- "include a Flight row matching #{predicate.inspect}"
129
- end
130
- end
@@ -1,250 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- # Specs for the three Flight wire matcher modes added in Story 7.5
6
- # (`match_flight_fixture`, `match_flight_structure`, `include_flight_row`).
7
- RSpec.describe "Flight wire matchers" do
8
- # Captures an `ExpectationNotMetError` raised by the inner block so the
9
- # spec can make multiple assertions on its message without resorting to a
10
- # multi-line block chained off `raise_error`.
11
- def capture_failure
12
- yield
13
- nil
14
- rescue RSpec::Expectations::ExpectationNotMetError => e
15
- e
16
- end
17
-
18
- describe "match_flight_fixture (existing snapshot mode)" do
19
- let(:nil_wire) { "0:null\n" }
20
-
21
- it "passes against canonical fixture content (regression check)" do
22
- expect(nil_wire).to match_flight_fixture("nil")
23
- end
24
- end
25
-
26
- describe "match_flight_structure" do
27
- let(:simple_wire) { %(0:{"className":"box"}\n) }
28
- let(:two_row_wire) { %(1:I["/L.jsx","L",["/L.jsx"]]\n0:["$","$L1",null,{}]\n) }
29
-
30
- it "passes when the actual wire matches a single-row expected structure" do
31
- expect(simple_wire).to match_flight_structure([
32
- { id: 0, class: :model, payload: { "className" => "box" } }
33
- ])
34
- end
35
-
36
- it "passes for a two-row mixed import + model sequence" do
37
- expect(two_row_wire).to match_flight_structure([
38
- { id: 1, class: :import, payload: ["/L.jsx", "L", ["/L.jsx"]] },
39
- { id: 0, class: :model, payload: ["$", "$L1", nil, {}] }
40
- ])
41
- end
42
-
43
- it "fails with a missing-row message when actual has fewer rows than expected" do
44
- err = capture_failure do
45
- expect(simple_wire).to match_flight_structure([
46
- { id: 0, class: :model, payload: { "className" => "box" } },
47
- { id: 1, class: :import, payload: [] }
48
- ])
49
- end
50
-
51
- expect(err.message).to include("Expected row 1 (import) was not produced.")
52
- expect(err.message).to include("expected: {")
53
- end
54
-
55
- it "produces the AC3 verbatim row-indexed diff for a single-field semantic regression" do
56
- broken_wire = %(0:["$X","div",null,{"className":"box","children":"hi"}]\n)
57
- expected_payload = ["$", "div", nil, { "className" => "box", "children" => "hi" }]
58
- got_payload = ["$X", "div", nil, { "className" => "box", "children" => "hi" }]
59
- expected_structure = [{ id: 0, class: :model, payload: expected_payload }]
60
-
61
- # Hash#inspect changed between Ruby 3.3 (`{"a"=>"b"}`) and Ruby 3.4
62
- # (`{"a" => "b"}`). The AC3 contract is "values shown via .inspect" —
63
- # so we render the expected message with the same .inspect the matcher
64
- # uses at runtime, keeping the spec stable across the CI Ruby matrix.
65
- expected_message = <<~MSG.strip
66
- Expected Flight output to match structure.
67
-
68
- Row 0 (model) differs at .payload[0]:
69
- expected: "$"
70
- got: "$X"
71
-
72
- Row 0 (model) full diff:
73
- expected: #{expected_payload.inspect}
74
- got: #{got_payload.inspect}
75
- MSG
76
-
77
- expect do
78
- expect(broken_wire).to match_flight_structure(expected_structure)
79
- end.to raise_error(RSpec::Expectations::ExpectationNotMetError, expected_message)
80
- end
81
-
82
- it "fails with an unexpected-row message when actual has extra rows" do
83
- err = capture_failure do
84
- expect(two_row_wire).to match_flight_structure([
85
- { id: 1, class: :import,
86
- payload: ["/L.jsx", "L", ["/L.jsx"]] }
87
- ])
88
- end
89
-
90
- expect(err.message).to include("Got unexpected row 1 (model)")
91
- end
92
-
93
- it "tolerates cosmetic JSON key reordering in payload hashes (AC4)" do
94
- canonical = %(0:{"a":1,"b":2}\n)
95
- perturbed = %(0:{"b":2,"a":1}\n)
96
- expected_structure = [{ id: 0, class: :model, payload: { "a" => 1, "b" => 2 } }]
97
-
98
- expect(canonical).to match_flight_structure(expected_structure)
99
- expect(perturbed).to match_flight_structure(expected_structure)
100
- end
101
-
102
- it "passes negation when the structure does not match" do
103
- expect(simple_wire).not_to match_flight_structure([
104
- { id: 0, class: :model, payload: { "className" => "circle" } }
105
- ])
106
- end
107
-
108
- # AC1: "multiple I rows are an unordered set". The expected list below
109
- # reverses the import order vs the wire — the structural matcher must
110
- # still consider this a match because import-row ordering is not
111
- # protocol-significant within the import class.
112
- it "treats import rows as an unordered set (AC1)" do
113
- wire = %(1:I["/A.jsx","A",["/A.jsx"]]\n2:I["/B.jsx","B",["/B.jsx"]]\n0:["$","$L1",null,{}]\n)
114
-
115
- expect(wire).to match_flight_structure([
116
- { id: 2, class: :import, payload: ["/B.jsx", "B", ["/B.jsx"]] },
117
- { id: 1, class: :import, payload: ["/A.jsx", "A", ["/A.jsx"]] },
118
- { id: 0, class: :model, payload: ["$", "$L1", nil, {}] }
119
- ])
120
- end
121
-
122
- # Defends against incomplete expected rows silently passing when actual
123
- # payload happens to be nil (e.g. `{ id: 0, class: :model }` without
124
- # `:payload` would otherwise satisfy any row whose payload is nil).
125
- it "raises ArgumentError when an expected row is missing :payload" do
126
- expect do
127
- expect(simple_wire).to match_flight_structure([{ id: 0, class: :model }])
128
- end.to raise_error(ArgumentError, /missing required keys.*:payload/)
129
- end
130
-
131
- it "raises ArgumentError when an expected row is missing :class" do
132
- expect do
133
- expect(simple_wire).to match_flight_structure([{ id: 0, payload: {} }])
134
- end.to raise_error(ArgumentError, /missing required keys.*:class/)
135
- end
136
-
137
- # Multi-row failure message: the count, AC3 wording for missing/extra,
138
- # plus a "✓" line for every matching row so the reader can confirm
139
- # which rows passed (AC3 — "Other rows that match are summarized as
140
- # `Row N (<class>): ✓`").
141
- it "shows matching-row checkmarks alongside multi-row diffs" do
142
- wire = %(1:I["/A.jsx","A",["/A.jsx"]]\n0:["$X","div",null,{}]\n)
143
-
144
- err = capture_failure do
145
- expect(wire).to match_flight_structure([
146
- { id: 1, class: :import, payload: ["/A.jsx", "A", ["/A.jsx"]] },
147
- { id: 0, class: :model, payload: ["$", "div", nil, {}] },
148
- { id: 2, class: :model, payload: ["$", "span", nil, {}] }
149
- ])
150
- end
151
-
152
- expect(err.message).to include("Expected Flight output to match structure. 2 rows differ:")
153
- expect(err.message).to include("Row 0 (import): ✓")
154
- expect(err.message).to include("Row 1 (model) differs at .payload[0]:")
155
- expect(err.message).to include("Expected row 2 (model) was not produced.")
156
- end
157
- end
158
-
159
- describe "include_flight_row" do
160
- let(:wire_with_post_id) do
161
- %(1:I["/L.jsx","L",["/L.jsx"]]\n0:["$","$L1",null,{"postId":42}]\n)
162
- end
163
-
164
- it "matches when at least one row satisfies a hash_including payload predicate" do
165
- expect(wire_with_post_id).to include_flight_row(
166
- class: :model,
167
- payload: include("postId" => 42)
168
- )
169
- end
170
-
171
- it "fails listing parsed rows when no row matches the predicate" do
172
- err = capture_failure do
173
- expect(wire_with_post_id).to include_flight_row(
174
- class: :model,
175
- payload: include("postId" => 999)
176
- )
177
- end
178
-
179
- expect(err.message).to include("Expected Flight output to include a row matching")
180
- expect(err.message).to include("[0] id=1, class=import")
181
- expect(err.message).to include("[1] id=0, class=model")
182
- end
183
-
184
- it "supports negation with not_to" do
185
- expect(wire_with_post_id).not_to include_flight_row(class: :error)
186
- end
187
-
188
- it "fails negation when a row matches, naming the offending row index" do
189
- expect do
190
- expect(wire_with_post_id).not_to include_flight_row(class: :import)
191
- end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /but row 0 matched/)
192
- end
193
-
194
- it "supports array_including for the payload key" do
195
- expect(wire_with_post_id).to include_flight_row(
196
- class: :import,
197
- payload: include("/L.jsx")
198
- )
199
- end
200
-
201
- # AC4 fixture-mode failure proof. The structural matcher tolerates the
202
- # cosmetic perturbation; the fixture matcher fails *positively* with the
203
- # expected-vs-got diff visible — confirming fixture mode is the wire-
204
- # format contract guard. This spec verifies the failure message rather
205
- # than relying on a `not_to` shortcut (which would prove only that the
206
- # matcher returned false, not that the failure is loud and informative).
207
- it "demonstrates cosmetic-vs-fixture asymmetry — structure tolerates re-ordering, fixture fails loudly (AC4)" do
208
- canonical_wire = %(0:{"debug":true,"count":5,"label":"x"}\n)
209
- perturbed_wire = %(0:{"label":"x","count":5,"debug":true}\n)
210
- expected_structure = [
211
- { id: 0, class: :model, payload: { "debug" => true, "count" => 5, "label" => "x" } }
212
- ]
213
-
214
- err = capture_failure do
215
- expect(perturbed_wire).to match_flight_fixture("hash")
216
- end
217
-
218
- aggregate_failures do
219
- # Structural mode: both pass — JSON key reordering is cosmetic.
220
- expect(canonical_wire).to match_flight_structure(expected_structure)
221
- expect(perturbed_wire).to match_flight_structure(expected_structure)
222
-
223
- # Fixture mode: canonical passes — the fixture file is the canonical
224
- # wire bytes.
225
- expect(canonical_wire).to match_flight_fixture("hash")
226
-
227
- # Fixture mode against the perturbed wire fails *loudly* with the
228
- # bytes-for-bytes diff so a human reviewer can see the cosmetic drift.
229
- expect(err).to be_a(RSpec::Expectations::ExpectationNotMetError)
230
- expect(err.message).to include("Expected output to match fixture at", "hash.txt", "Expected:", "Got:")
231
- expect(err.message).to include(perturbed_wire.inspect)
232
- end
233
- end
234
-
235
- # Predicate validation: an unknown key (typo) must raise immediately.
236
- # Otherwise `row[:payloed]` returns nil and `nil == nil` would silently
237
- # match every row, hiding broken specs.
238
- it "raises ArgumentError when the predicate has an unknown key" do
239
- expect do
240
- expect(wire_with_post_id).to include_flight_row(payloed: { "postId" => 42 })
241
- end.to raise_error(ArgumentError, /unknown keys.*:payloed/)
242
- end
243
-
244
- it "raises ArgumentError when given an empty predicate" do
245
- expect do
246
- expect(wire_with_post_id).to include_flight_row({})
247
- end.to raise_error(ArgumentError, /predicate cannot be empty/)
248
- end
249
- end
250
- end
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Spec-only Rails bootstrap for tests that need a `Rails` constant. Two modes:
4
- #
5
- # 1. **Augment** (default since Story 7.9): when the `rails` gem is in the
6
- # bundle, load its core (`rails.rb` — just `Rails::VERSION`,
7
- # `Rails::Railtie`, `ActiveSupport::StringInquirer`, etc., *not*
8
- # `action_controller` / `action_view`) and add test-only writers
9
- # (`Rails.root=`, `Rails.env=`, `Rails.logger=`) on top. Specs that need
10
- # the full Rails request cycle (e.g. controller_request_spec.rb) require
11
- # `action_controller/railtie` and `action_view/railtie` themselves — the
12
- # rest of the suite never pays that cost.
13
- #
14
- # 2. **Full stub** (fallback): when `rails` is not in the bundle (e.g. a
15
- # matrix run that pruned it), provide a minimal `Rails` module + a
16
- # `Rails::Railtie` class with no-op class methods so `gem/lib/ruact/railtie.rb`
17
- # can `class Railtie < Rails::Railtie` without crashing. `$LOADED_FEATURES`
18
- # is patched so `require "rails"` inside loaded files no-ops.
19
- #
20
- # Loaded automatically by spec_helper.
21
- #
22
- # Augmentation is idempotent (each `Rails.define_singleton_method` is guarded
23
- # by `unless Rails.singleton_class.method_defined?(...)`), so we run through
24
- # this file every time it loads — even when Rails was already required by
25
- # another spec file (e.g. controller_request_spec.rb pre-loads
26
- # `action_controller/railtie`). Skipping with `return if defined?(Rails)`
27
- # would leave doctor_spec / railtie_spec without the test-only writers when
28
- # the request spec runs first.
29
-
30
- unless defined?(Rails)
31
- begin
32
- # Loads Rails core only — not the request-cycle subsystem. Cheap.
33
- require "rails"
34
- rescue LoadError
35
- # Rails not available; fall through to the full stub below.
36
- end
37
- end
38
-
39
- if defined?(Rails) && Rails.respond_to?(:application)
40
- # Augment-mode: real Rails is present. Add test-only writers that the
41
- # existing suite (doctor_spec, railtie_spec) relies on, without clobbering
42
- # real Rails's behaviour outside the test override.
43
- unless Rails.singleton_class.method_defined?(:root=)
44
- original_root = Rails.method(:root) if Rails.respond_to?(:root)
45
-
46
- Rails.define_singleton_method(:root=) { |v| @_test_root = v }
47
- Rails.define_singleton_method(:root) do
48
- return @_test_root if defined?(@_test_root) && @_test_root
49
-
50
- original_root&.call
51
- end
52
- end
53
-
54
- unless Rails.singleton_class.method_defined?(:env=)
55
- Rails.define_singleton_method(:env=) { |v| @_test_env = v }
56
- original_env = Rails.method(:env) if Rails.respond_to?(:env)
57
- Rails.define_singleton_method(:env) do
58
- return @_test_env if defined?(@_test_env) && @_test_env
59
-
60
- original_env ? original_env.call : ActiveSupport::StringInquirer.new("test")
61
- end
62
- end
63
-
64
- unless Rails.singleton_class.method_defined?(:logger=)
65
- Rails.define_singleton_method(:logger=) { |v| @_test_logger = v }
66
- original_logger = Rails.method(:logger) if Rails.respond_to?(:logger)
67
- Rails.define_singleton_method(:logger) do
68
- return @_test_logger if defined?(@_test_logger) && @_test_logger
69
-
70
- original_logger&.call
71
- end
72
- end
73
-
74
- return
75
- end
76
-
77
- # Full-stub fallback: rails gem is not in the bundle.
78
- $LOADED_FEATURES << "rails.rb"
79
-
80
- module Rails
81
- class Railtie
82
- def self.initializer(*, **); end
83
- def self.rake_tasks(&); end
84
-
85
- def self.config
86
- @config ||= Class.new do
87
- def method_missing(name, *, **, &); end
88
-
89
- def respond_to_missing?(*, **)
90
- true
91
- end
92
- end.new
93
- end
94
- end
95
-
96
- class << self
97
- attr_accessor :env, :logger, :root
98
- end
99
- end
100
-
101
- module ActiveSupport # rubocop:disable Style/OneClassPerFile
102
- class StringInquirer < String
103
- def method_missing(method_name, *args)
104
- if method_name.to_s.end_with?("?")
105
- self == method_name.to_s.chomp("?")
106
- else
107
- super
108
- end
109
- end
110
-
111
- def respond_to_missing?(*, **)
112
- true
113
- end
114
- end
115
- end