ruact 0.0.10 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +123 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +16 -6
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -1
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -92
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
data/spec/ruact/doctor_spec.rb
DELETED
|
@@ -1,769 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "tmpdir"
|
|
5
|
-
require "fileutils"
|
|
6
|
-
require "socket"
|
|
7
|
-
|
|
8
|
-
# The Rails stub (including Rails.root) is provided by spec/support/rails_stub.rb.
|
|
9
|
-
require "ruact/doctor"
|
|
10
|
-
|
|
11
|
-
RSpec.describe Ruact::Doctor do
|
|
12
|
-
let(:tmpdir) { Pathname.new(Dir.mktmpdir) }
|
|
13
|
-
|
|
14
|
-
before { Rails.root = tmpdir }
|
|
15
|
-
after { FileUtils.rm_rf(tmpdir) }
|
|
16
|
-
|
|
17
|
-
# --- helpers ---
|
|
18
|
-
|
|
19
|
-
def make_controller(with_include: true)
|
|
20
|
-
dir = tmpdir.join("app", "controllers")
|
|
21
|
-
FileUtils.mkdir_p(dir)
|
|
22
|
-
content = with_include ? "include Ruact::Controller\n" : "class ApplicationController\nend\n"
|
|
23
|
-
File.write(dir.join("application_controller.rb"), content)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# `with_assets` is the second half of a migrated layout: the React root alone
|
|
27
|
-
# leaves ruact on its built-in (CSS-less) shell, so the two markers are
|
|
28
|
-
# independent inputs, not one sentinel.
|
|
29
|
-
def make_layout(with_sentinel: true, with_assets: true)
|
|
30
|
-
dir = tmpdir.join("app", "views", "layouts")
|
|
31
|
-
FileUtils.mkdir_p(dir)
|
|
32
|
-
content =
|
|
33
|
-
if !with_sentinel
|
|
34
|
-
"<body></body>\n"
|
|
35
|
-
elsif with_assets
|
|
36
|
-
"<%# ruact: root %>\n<div id=\"root\"></div>\n<%= ruact_js_assets %>\n"
|
|
37
|
-
else
|
|
38
|
-
"<%# ruact: root %>\n<div id=\"root\"></div>\n"
|
|
39
|
-
end
|
|
40
|
-
File.write(dir.join("application.html.erb"), content)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def make_manifest
|
|
44
|
-
dir = tmpdir.join("public")
|
|
45
|
-
FileUtils.mkdir_p(dir)
|
|
46
|
-
File.write(dir.join("react-client-manifest.json"), "{}")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# --- check_manifest ---
|
|
50
|
-
|
|
51
|
-
describe "#check_manifest (AC#1, #2)" do
|
|
52
|
-
subject(:doctor) { described_class.new }
|
|
53
|
-
|
|
54
|
-
context "when manifest file exists" do
|
|
55
|
-
before { make_manifest }
|
|
56
|
-
|
|
57
|
-
it "returns :pass" do
|
|
58
|
-
status, = doctor.send(:check_manifest)
|
|
59
|
-
expect(status).to eq(:pass)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it "message includes 'Manifest found at'" do
|
|
63
|
-
_, msg = doctor.send(:check_manifest)
|
|
64
|
-
expect(msg).to include("Manifest found at")
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
context "when manifest file is missing" do
|
|
69
|
-
it "returns :fail" do
|
|
70
|
-
status, = doctor.send(:check_manifest)
|
|
71
|
-
expect(status).to eq(:fail)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
it "message is 'Manifest not found — run vite build'" do
|
|
75
|
-
_, msg = doctor.send(:check_manifest)
|
|
76
|
-
expect(msg).to eq("Manifest not found — run vite build")
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# --- check_vite ---
|
|
82
|
-
|
|
83
|
-
describe "#check_vite (AC#1, #3)" do
|
|
84
|
-
subject(:doctor) { described_class.new }
|
|
85
|
-
|
|
86
|
-
context "when Vite is accessible" do
|
|
87
|
-
before { allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil)) }
|
|
88
|
-
|
|
89
|
-
it "returns :pass" do
|
|
90
|
-
status, = doctor.send(:check_vite)
|
|
91
|
-
expect(status).to eq(:pass)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
context "when Vite is not accessible" do
|
|
96
|
-
before { allow(TCPSocket).to receive(:new).and_raise(Errno::ECONNREFUSED) }
|
|
97
|
-
|
|
98
|
-
it "returns :fail" do
|
|
99
|
-
status, = doctor.send(:check_vite)
|
|
100
|
-
expect(status).to eq(:fail)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
it "message is 'Vite not accessible at localhost:5173 — run npm run dev'" do
|
|
104
|
-
_, msg = doctor.send(:check_vite)
|
|
105
|
-
expect(msg).to eq("Vite not accessible at localhost:5173 — run npm run dev")
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# --- check_controller ---
|
|
111
|
-
|
|
112
|
-
describe "#check_controller (AC#1, #4)" do
|
|
113
|
-
subject(:doctor) { described_class.new }
|
|
114
|
-
|
|
115
|
-
context "when ApplicationController includes Ruact::Controller" do
|
|
116
|
-
before { make_controller(with_include: true) }
|
|
117
|
-
|
|
118
|
-
it "returns :pass" do
|
|
119
|
-
status, = doctor.send(:check_controller)
|
|
120
|
-
expect(status).to eq(:pass)
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
context "when Ruact::Controller is not included" do
|
|
125
|
-
before { make_controller(with_include: false) }
|
|
126
|
-
|
|
127
|
-
it "returns :fail" do
|
|
128
|
-
status, = doctor.send(:check_controller)
|
|
129
|
-
expect(status).to eq(:fail)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
it "message is 'Ruact::Controller not included in ApplicationController'" do
|
|
133
|
-
_, msg = doctor.send(:check_controller)
|
|
134
|
-
expect(msg).to eq("Ruact::Controller not included in ApplicationController")
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# --- check_layout ---
|
|
140
|
-
|
|
141
|
-
describe "#check_layout (AC#1, #5)" do
|
|
142
|
-
subject(:doctor) { described_class.new }
|
|
143
|
-
|
|
144
|
-
context "when the layout is wired and the app opted in" do
|
|
145
|
-
before do
|
|
146
|
-
make_layout(with_sentinel: true, with_assets: true)
|
|
147
|
-
Ruact.configure { |c| c.layout = true }
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
it "returns :pass" do
|
|
151
|
-
status, = doctor.send(:check_layout)
|
|
152
|
-
expect(status).to eq(:pass)
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Both halves are silent when wrong, and they are DIFFERENT fixes — "add one
|
|
157
|
-
# line to the layout" versus "flip one setting" — so they report separately.
|
|
158
|
-
context "when the layout is wired but Ruact.config.layout is false" do
|
|
159
|
-
before do
|
|
160
|
-
make_layout(with_sentinel: true, with_assets: true)
|
|
161
|
-
Ruact.configure { |c| c.layout = false }
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
it "returns :warn naming the setting, not the layout" do
|
|
165
|
-
status, message, remediation = doctor.send(:check_layout)
|
|
166
|
-
|
|
167
|
-
expect(status).to eq(:warn)
|
|
168
|
-
expect(message).to include("config.layout is false")
|
|
169
|
-
expect(remediation).to include("config.layout = true")
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
it "does not fail the doctor run" do
|
|
173
|
-
expect(described_class::SUCCESS_STATUSES).to include(:warn)
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
context "when the layout has the React root but never calls ruact_js_assets" do
|
|
178
|
-
before do
|
|
179
|
-
make_layout(with_sentinel: true, with_assets: false)
|
|
180
|
-
Ruact.configure { |c| c.layout = true }
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
it "fails and names the helper to add" do
|
|
184
|
-
status, _message, remediation = doctor.send(:check_layout)
|
|
185
|
-
|
|
186
|
-
expect(status).to eq(:fail)
|
|
187
|
-
expect(remediation).to include("ruact_js_assets")
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# The drift this shares with the runtime and the generator: a MENTION of the
|
|
192
|
-
# helper in a comment is not a call, and reporting :pass on one sent a
|
|
193
|
-
# developer looking anywhere but at the real problem.
|
|
194
|
-
context "when the layout only mentions the helper in a comment" do
|
|
195
|
-
before do
|
|
196
|
-
dir = tmpdir.join("app", "views", "layouts")
|
|
197
|
-
FileUtils.mkdir_p(dir)
|
|
198
|
-
File.write(dir.join("application.html.erb"),
|
|
199
|
-
"<%# ruact: root %>\n<div id=\"root\"></div>\n<%# TODO: add ruact_js_assets %>\n")
|
|
200
|
-
Ruact.configure { |c| c.layout = true }
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
it "does not report it as wired" do
|
|
204
|
-
status, = doctor.send(:check_layout)
|
|
205
|
-
expect(status).to eq(:fail)
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
context "when the layout has neither the root nor the helper" do
|
|
210
|
-
before { make_layout(with_sentinel: false) }
|
|
211
|
-
|
|
212
|
-
it "returns :fail" do
|
|
213
|
-
status, = doctor.send(:check_layout)
|
|
214
|
-
expect(status).to eq(:fail)
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
it "names both halves, since either could be the missing one" do
|
|
218
|
-
_, msg = doctor.send(:check_layout)
|
|
219
|
-
expect(msg).to include("React root").and include("ruact_js_assets")
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
context "when there is no application layout file at all" do
|
|
224
|
-
it "reports the file, not its contents" do
|
|
225
|
-
status, msg = doctor.send(:check_layout)
|
|
226
|
-
|
|
227
|
-
expect(status).to eq(:fail)
|
|
228
|
-
expect(msg).to eq("React shell missing from application.html.erb")
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# --- check_streaming ---
|
|
234
|
-
|
|
235
|
-
describe "#check_streaming (AC#5)" do
|
|
236
|
-
subject(:doctor) { described_class.new }
|
|
237
|
-
|
|
238
|
-
after { Ruact.streaming_mode = nil }
|
|
239
|
-
|
|
240
|
-
it "always returns :pass" do
|
|
241
|
-
status, = doctor.send(:check_streaming)
|
|
242
|
-
expect(status).to eq(:pass)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
context "when streaming_mode is :enabled (AC#1, #5)" do
|
|
246
|
-
before do
|
|
247
|
-
Ruact.streaming_mode = :enabled
|
|
248
|
-
stub_const("Puma", Module.new)
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
it "message includes 'enabled' and 'Puma'" do
|
|
252
|
-
_, msg = doctor.send(:check_streaming)
|
|
253
|
-
expect(msg).to include("enabled").and include("Puma")
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
context "when streaming_mode is :buffered with no known server (AC#3, #5)" do
|
|
258
|
-
before { Ruact.streaming_mode = :buffered }
|
|
259
|
-
|
|
260
|
-
it "message includes 'buffered'" do
|
|
261
|
-
_, msg = doctor.send(:check_streaming)
|
|
262
|
-
expect(msg).to include("buffered")
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
context "when streaming_mode is nil (not yet detected)" do
|
|
267
|
-
before { Ruact.streaming_mode = nil }
|
|
268
|
-
|
|
269
|
-
it "defaults to buffered in the message" do
|
|
270
|
-
_, msg = doctor.send(:check_streaming)
|
|
271
|
-
expect(msg).to include("buffered")
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# --- check_legacy_constant ---
|
|
277
|
-
|
|
278
|
-
describe "#check_legacy_constant (Story 5.1)" do
|
|
279
|
-
subject(:doctor) { described_class.new }
|
|
280
|
-
|
|
281
|
-
# Built via Array#join so this spec file passes the gem-CI
|
|
282
|
-
# `name-propagation` guard without an exclusion (Story 5.1 review F4).
|
|
283
|
-
let(:legacy_const) { %w[Rails Rsc].join }
|
|
284
|
-
let(:legacy_gem) { %w[rails rsc].join("_") }
|
|
285
|
-
|
|
286
|
-
def make_initializer(content, filename: "ruact.rb")
|
|
287
|
-
dir = tmpdir.join("config", "initializers")
|
|
288
|
-
FileUtils.mkdir_p(dir)
|
|
289
|
-
File.write(dir.join(filename), content)
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def make_app_file(content, path:)
|
|
293
|
-
dir = tmpdir.join("app", File.dirname(path))
|
|
294
|
-
FileUtils.mkdir_p(dir)
|
|
295
|
-
File.write(tmpdir.join("app", path), content)
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
context "when no legacy references exist" do
|
|
299
|
-
it "returns :pass" do
|
|
300
|
-
status, = doctor.send(:check_legacy_constant)
|
|
301
|
-
expect(status).to eq(:pass)
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
context "when an initializer references the legacy constant" do
|
|
306
|
-
before { make_initializer("#{legacy_const}.configure do |c|\n c.foo = 1\nend\n") }
|
|
307
|
-
|
|
308
|
-
it "returns :fail" do
|
|
309
|
-
status, = doctor.send(:check_legacy_constant)
|
|
310
|
-
expect(status).to eq(:fail)
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
it "message names the file:line and instructs the rename" do
|
|
314
|
-
_, msg = doctor.send(:check_legacy_constant)
|
|
315
|
-
expect(msg).to include("ruact.rb:1")
|
|
316
|
-
expect(msg).to include("Replace `#{legacy_const}` with `Ruact`")
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
it "message includes the rename documentation link (AC5)" do
|
|
320
|
-
_, msg = doctor.send(:check_legacy_constant)
|
|
321
|
-
expect(msg).to include("https://github.com/luizcg/ruact/blob/main/CHANGELOG.md#renamed")
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
context "when an app file requires the legacy gem name" do
|
|
326
|
-
before { make_app_file("require \"#{legacy_gem}\"\n", path: "models/foo.rb") }
|
|
327
|
-
|
|
328
|
-
it "returns :fail" do
|
|
329
|
-
status, = doctor.send(:check_legacy_constant)
|
|
330
|
-
expect(status).to eq(:fail)
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
context "when only the modern Ruact constant is referenced" do
|
|
335
|
-
before { make_initializer("Ruact.configure { |c| c.foo = 1 }\n") }
|
|
336
|
-
|
|
337
|
-
it "returns :pass" do
|
|
338
|
-
status, = doctor.send(:check_legacy_constant)
|
|
339
|
-
expect(status).to eq(:pass)
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
context "when a word coincidentally contains the legacy substring" do
|
|
344
|
-
# e.g. "TrailsRsce" should not trigger the regex (boundary check).
|
|
345
|
-
before { make_initializer("# documenting TrailsRsce_engine here\n") }
|
|
346
|
-
|
|
347
|
-
it "returns :pass" do
|
|
348
|
-
status, = doctor.send(:check_legacy_constant)
|
|
349
|
-
expect(status).to eq(:pass)
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
# --- check_serialize_only (Story 13.1, AC2 + AC4) ---
|
|
355
|
-
|
|
356
|
-
describe "#check_serialize_only", :story_13_1 do
|
|
357
|
-
subject(:doctor) { described_class.new(serialize_only_root: scan_root.to_s) }
|
|
358
|
-
|
|
359
|
-
# Injectable scan root → point the tripwire at a fixture tree.
|
|
360
|
-
let(:scan_root) { Pathname.new(Dir.mktmpdir) }
|
|
361
|
-
|
|
362
|
-
after { FileUtils.rm_rf(scan_root) }
|
|
363
|
-
|
|
364
|
-
def write_source(name, content)
|
|
365
|
-
path = scan_root.join(name)
|
|
366
|
-
FileUtils.mkdir_p(path.dirname)
|
|
367
|
-
File.write(path, content)
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
context "with a clean tree (no inbound deserializer)" do
|
|
371
|
-
before { write_source("clean.rb", "class Foo\n def bar = 42\nend\n") }
|
|
372
|
-
|
|
373
|
-
it "returns :pass silently" do
|
|
374
|
-
status, msg = doctor.send(:check_serialize_only)
|
|
375
|
-
expect(status).to eq(:pass)
|
|
376
|
-
expect(msg).to include("Serialize-only invariant holds")
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
context "with an unguarded inbound Flight deserializer" do
|
|
381
|
-
before do
|
|
382
|
-
write_source("evil.rb", "class FlightDeserializer\n def call(body)\n parse_flight(body)\n end\nend\n")
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
it "returns :fail" do
|
|
386
|
-
status, = doctor.send(:check_serialize_only)
|
|
387
|
-
expect(status).to eq(:fail)
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
it "names the offending file:line and points to the ADR invariant" do
|
|
391
|
-
_, msg = doctor.send(:check_serialize_only)
|
|
392
|
-
expect(msg).to include("evil.rb:1") # first offense = the *Deserializer constant
|
|
393
|
-
expect(msg).to include("CVE-2025-55182")
|
|
394
|
-
expect(msg).to include("server-functions-api.md")
|
|
395
|
-
end
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
context "with a deserializer carrying the allow annotation" do
|
|
399
|
-
before do
|
|
400
|
-
annotation = ["# ruact:allow", "flight", "deserialization"].join("-")
|
|
401
|
-
write_source("guarded.rb", "def parse_flight(body) #{annotation} reviewed legacy bridge\n body\nend\n")
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
it "returns :pass (the escape hatch makes it a guard, not a ban)" do
|
|
405
|
-
status, = doctor.send(:check_serialize_only)
|
|
406
|
-
expect(status).to eq(:pass)
|
|
407
|
-
end
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
context "with a signal that lives in an excluded location" do
|
|
411
|
-
before do
|
|
412
|
-
# generators' client-side templates are out of scope (browser RSC)
|
|
413
|
-
write_source("lib/generators/ruact/install/templates/app.rb", "def parse_flight(b) = b\n")
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
it "returns :pass (templates are excluded from the scan)" do
|
|
417
|
-
status, = doctor.send(:check_serialize_only)
|
|
418
|
-
expect(status).to eq(:pass)
|
|
419
|
-
end
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
context "with a `decode_flight` inbound entry point (review finding R3)" do
|
|
423
|
-
before { write_source("decoder.rb", "def decode_flight(body)\n body\nend\n") }
|
|
424
|
-
|
|
425
|
-
it "returns :fail (decode_flight is an inbound deserialization signal)" do
|
|
426
|
-
status, = doctor.send(:check_serialize_only)
|
|
427
|
-
expect(status).to eq(:fail)
|
|
428
|
-
end
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
context "with a deserializer in a differently-located file ALSO named doctor.rb (review finding R1)" do
|
|
432
|
-
# Only the gem's own lib/ruact/doctor.rb is excluded (by exact path), not
|
|
433
|
-
# every basename `doctor.rb` — a nested namesake must still be scanned.
|
|
434
|
-
before { write_source("ruact/flight/doctor.rb", "class FlightDeserializer; end\n") }
|
|
435
|
-
|
|
436
|
-
it "returns :fail (basename collision is not a free pass)" do
|
|
437
|
-
status, = doctor.send(:check_serialize_only)
|
|
438
|
-
expect(status).to eq(:fail)
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
# --- check_flight_middleware (Story 13.1, AC3 + AC4) ---
|
|
444
|
-
|
|
445
|
-
describe "#check_flight_middleware", :story_13_1 do
|
|
446
|
-
subject(:doctor) { described_class.new }
|
|
447
|
-
|
|
448
|
-
# Plain value objects (not doubles) modelling the iterable middleware stack
|
|
449
|
-
# the check reads — each entry exposes a `.name`, mirroring a real
|
|
450
|
-
# ActionDispatch::MiddlewareStack::Middleware.
|
|
451
|
-
def middleware_entry(name)
|
|
452
|
-
Struct.new(:name).new(name)
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
def stub_app(stack)
|
|
456
|
-
app = Struct.new(:middleware).new(stack)
|
|
457
|
-
allow(Rails).to receive(:application).and_return(app)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
context "when a response-transforming middleware (Rack::Deflater) is mounted" do
|
|
461
|
-
before { stub_app([middleware_entry("Rack::Deflater")]) }
|
|
462
|
-
|
|
463
|
-
it "returns :warn (never :fail)" do
|
|
464
|
-
status, msg = doctor.send(:check_flight_middleware)
|
|
465
|
-
expect(status).to eq(:warn)
|
|
466
|
-
expect(msg).to include("Rack::Deflater").and include("text/x-component")
|
|
467
|
-
end
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
context "when no response-transforming middleware is mounted" do
|
|
471
|
-
before { stub_app([middleware_entry("Rack::Runtime")]) }
|
|
472
|
-
|
|
473
|
-
it "returns :pass" do
|
|
474
|
-
status, = doctor.send(:check_flight_middleware)
|
|
475
|
-
expect(status).to eq(:pass)
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
context "when no Rails application is present" do
|
|
480
|
-
before { allow(Rails).to receive(:application).and_return(nil) }
|
|
481
|
-
|
|
482
|
-
it "returns :pass (guarded edge context)" do
|
|
483
|
-
status, = doctor.send(:check_flight_middleware)
|
|
484
|
-
expect(status).to eq(:pass)
|
|
485
|
-
end
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
context "when the app is not yet booted (middleware is a non-enumerable proxy)" do
|
|
489
|
-
# Mirrors a pre-`initialize!` Rails::Configuration::MiddlewareStackProxy,
|
|
490
|
-
# which does NOT respond to :each — must be skipped, not crash on filter_map.
|
|
491
|
-
before do
|
|
492
|
-
proxy = Object.new # responds to neither :each nor :filter_map
|
|
493
|
-
app = Struct.new(:middleware).new(proxy)
|
|
494
|
-
allow(Rails).to receive(:application).and_return(app)
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
it "returns :pass without raising" do
|
|
498
|
-
expect { doctor.send(:check_flight_middleware) }.not_to raise_error
|
|
499
|
-
status, = doctor.send(:check_flight_middleware)
|
|
500
|
-
expect(status).to eq(:pass)
|
|
501
|
-
end
|
|
502
|
-
end
|
|
503
|
-
end
|
|
504
|
-
|
|
505
|
-
# --- :warn status semantics (Story 13.1, AC3) ---
|
|
506
|
-
|
|
507
|
-
describe "#format_result with :warn", :story_13_1 do
|
|
508
|
-
subject(:doctor) { described_class.new }
|
|
509
|
-
|
|
510
|
-
it "renders :warn with the ⚠ glyph (not ✗)" do
|
|
511
|
-
expect(doctor.send(:format_result, :warn, "heads up")).to eq("⚠ heads up")
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
describe "#run with an unexpected status (review finding R1)", :story_13_1 do
|
|
516
|
-
before do
|
|
517
|
-
make_manifest
|
|
518
|
-
make_controller(with_include: true)
|
|
519
|
-
make_layout(with_sentinel: true)
|
|
520
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
it "fails the run when a check returns a status that is neither :pass nor :warn" do
|
|
524
|
-
doctor = described_class.new
|
|
525
|
-
# All other checks pass; a malformed status (rendered ✗) must NOT be
|
|
526
|
-
# silently treated as a pass — only :pass / :warn are success.
|
|
527
|
-
allow(doctor).to receive(:check_streaming).and_return([:error, "broken status"])
|
|
528
|
-
expect(doctor.run).to be false
|
|
529
|
-
end
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
describe ".run with a :warn present (Story 13.1, AC3)", :story_13_1 do
|
|
533
|
-
before do
|
|
534
|
-
make_manifest
|
|
535
|
-
make_controller(with_include: true)
|
|
536
|
-
make_layout(with_sentinel: true)
|
|
537
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
538
|
-
app = Struct.new(:middleware).new([Struct.new(:name).new("Rack::Deflater")])
|
|
539
|
-
allow(Rails).to receive(:application).and_return(app)
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
it "still returns true — a :warn does not fail the run" do
|
|
543
|
-
expect(described_class.run).to be true
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
it "prints the warning glyph" do
|
|
547
|
-
expect { described_class.run }.to output(/⚠.*Rack::Deflater/).to_stdout
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
it "does not print the fix hint" do
|
|
551
|
-
expect { described_class.run }.not_to output(/rails generate/).to_stdout
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
|
|
555
|
-
# --- run / .run ---
|
|
556
|
-
|
|
557
|
-
describe ".run / #run (AC#1, #7)" do
|
|
558
|
-
before do
|
|
559
|
-
make_manifest
|
|
560
|
-
make_controller(with_include: true)
|
|
561
|
-
make_layout(with_sentinel: true)
|
|
562
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
563
|
-
end
|
|
564
|
-
|
|
565
|
-
context "when all checks pass" do
|
|
566
|
-
it "returns true" do
|
|
567
|
-
expect(described_class.run).to be true
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
it "does not print the fix hint" do
|
|
571
|
-
expect { described_class.run }.not_to output(/rails generate/).to_stdout
|
|
572
|
-
end
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
context "when any check fails" do
|
|
576
|
-
before { allow(TCPSocket).to receive(:new).and_raise(Errno::ECONNREFUSED) }
|
|
577
|
-
|
|
578
|
-
it "returns false" do
|
|
579
|
-
expect(described_class.run).to be false
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
it "prints the fix hint" do
|
|
583
|
-
expect { described_class.run }
|
|
584
|
-
.to output(/Run rails generate ruact:install to fix configuration issues/).to_stdout
|
|
585
|
-
end
|
|
586
|
-
end
|
|
587
|
-
end
|
|
588
|
-
|
|
589
|
-
# --- JSON introspection (Story 15.3, FR107, AC1 + AC3 + AC4) ---
|
|
590
|
-
|
|
591
|
-
describe "#as_json (machine-readable report)", :story_15_3 do
|
|
592
|
-
subject(:doctor) { described_class.new }
|
|
593
|
-
|
|
594
|
-
context "when all checks pass" do
|
|
595
|
-
before do
|
|
596
|
-
make_manifest
|
|
597
|
-
make_controller(with_include: true)
|
|
598
|
-
make_layout(with_sentinel: true)
|
|
599
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
it "returns a document that round-trips through JSON.parse", :aggregate_failures do
|
|
603
|
-
report = doctor.as_json
|
|
604
|
-
reparsed = JSON.parse(JSON.generate(report))
|
|
605
|
-
expect(reparsed).to eq(report)
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
it "gates the document with the EXPERIMENTAL schema_version (0)" do
|
|
609
|
-
expect(doctor.as_json["schema_version"]).to eq(0)
|
|
610
|
-
expect(doctor.as_json["schema_version"]).to eq(Ruact::Doctor::SCHEMA_VERSION)
|
|
611
|
-
end
|
|
612
|
-
|
|
613
|
-
it "carries every check with name/status/message/remediation keys", :aggregate_failures do
|
|
614
|
-
checks = doctor.as_json["checks"]
|
|
615
|
-
expect(checks.map { |c| c["name"] }).to eq(described_class::CHECKS.map(&:to_s))
|
|
616
|
-
checks.each do |check|
|
|
617
|
-
expect(check.keys).to contain_exactly("name", "status", "message", "remediation")
|
|
618
|
-
expect(check["status"]).to be_a(String)
|
|
619
|
-
expect(check["message"]).to be_a(String)
|
|
620
|
-
end
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
it "reports top-level status 'pass' and exits-zero semantics" do
|
|
624
|
-
expect(doctor.as_json["status"]).to eq("pass")
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
it "emits NO prose to stdout (JSON mode is the document only)" do
|
|
628
|
-
expect { doctor.as_json }.not_to output.to_stdout
|
|
629
|
-
end
|
|
630
|
-
end
|
|
631
|
-
|
|
632
|
-
context "when a check fails" do
|
|
633
|
-
before do
|
|
634
|
-
make_controller(with_include: true)
|
|
635
|
-
make_layout(with_sentinel: true)
|
|
636
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
637
|
-
# manifest is missing → check_manifest fails
|
|
638
|
-
end
|
|
639
|
-
|
|
640
|
-
it "reports top-level status 'fail' (non-zero exit semantics)" do
|
|
641
|
-
expect(doctor.as_json["status"]).to eq("fail")
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
it "carries the separate machine-readable remediation for the failing check" do
|
|
645
|
-
manifest = doctor.as_json["checks"].find { |c| c["name"] == "manifest" }
|
|
646
|
-
expect(manifest["status"]).to eq("fail")
|
|
647
|
-
expect(manifest["remediation"]).to eq("Run vite build (or bin/dev) to generate the client manifest.")
|
|
648
|
-
end
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
context "when a check warns (Rack::Deflater mounted)" do
|
|
652
|
-
before do
|
|
653
|
-
make_manifest
|
|
654
|
-
make_controller(with_include: true)
|
|
655
|
-
make_layout(with_sentinel: true)
|
|
656
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
657
|
-
app = Struct.new(:middleware).new([Struct.new(:name).new("Rack::Deflater")])
|
|
658
|
-
allow(Rails).to receive(:application).and_return(app)
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
it "keeps status 'pass' — a :warn does not fail — but exposes the warn check" do
|
|
662
|
-
report = doctor.as_json
|
|
663
|
-
expect(report["status"]).to eq("pass")
|
|
664
|
-
flight = report["checks"].find { |c| c["name"] == "flight_middleware" }
|
|
665
|
-
expect(flight["status"]).to eq("warn")
|
|
666
|
-
expect(flight["remediation"]).to include("Exclude text/x-component from compression")
|
|
667
|
-
end
|
|
668
|
-
end
|
|
669
|
-
|
|
670
|
-
it "keeps the message byte-identical to the human tuple (remediation is separate)" do
|
|
671
|
-
_status, human_message = doctor.send(:check_manifest)
|
|
672
|
-
json_message = doctor.as_json["checks"].find { |c| c["name"] == "manifest" }["message"]
|
|
673
|
-
expect(json_message).to eq(human_message)
|
|
674
|
-
end
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
describe "#results / #passed? (shared compute, Story 15.3)", :story_15_3 do
|
|
678
|
-
subject(:doctor) { described_class.new }
|
|
679
|
-
|
|
680
|
-
before do
|
|
681
|
-
make_manifest
|
|
682
|
-
make_controller(with_include: true)
|
|
683
|
-
make_layout(with_sentinel: true)
|
|
684
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
685
|
-
end
|
|
686
|
-
|
|
687
|
-
it "returns one tuple per check, index-aligned with CHECKS" do
|
|
688
|
-
expect(doctor.results.length).to eq(described_class::CHECKS.length)
|
|
689
|
-
end
|
|
690
|
-
|
|
691
|
-
it "passed? is true when all checks pass/warn" do
|
|
692
|
-
expect(doctor.passed?).to be true
|
|
693
|
-
end
|
|
694
|
-
|
|
695
|
-
it "runs each check exactly once (as_json does not double-run check_vite's socket)" do
|
|
696
|
-
# check_vite opens a socket via TCPSocket.new; a single as_json must open it
|
|
697
|
-
# exactly once — proving #results is computed once, not per consumer.
|
|
698
|
-
doctor.as_json
|
|
699
|
-
expect(TCPSocket).to have_received(:new).once
|
|
700
|
-
end
|
|
701
|
-
end
|
|
702
|
-
|
|
703
|
-
describe "#run stays byte-identical after the #results refactor (Story 15.3, AC4a)", :story_15_3 do
|
|
704
|
-
subject(:doctor) { described_class.new }
|
|
705
|
-
|
|
706
|
-
before do
|
|
707
|
-
make_manifest
|
|
708
|
-
make_controller(with_include: true)
|
|
709
|
-
make_layout(with_sentinel: true)
|
|
710
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
711
|
-
end
|
|
712
|
-
|
|
713
|
-
it "prints exactly the header + one format_result line per check (no JSON/prose leak), returns true" do
|
|
714
|
-
# Reconstruct the expected output from the SAME format_result the human
|
|
715
|
-
# path uses — proves #run still emits header + one glyph line per check
|
|
716
|
-
# (the optional 3rd remediation element is ignored) and nothing else.
|
|
717
|
-
lines = doctor.results.map { |status, message| doctor.send(:format_result, status, message) }.join("\n")
|
|
718
|
-
expected = "[ruact] Health check\n#{lines}\n"
|
|
719
|
-
|
|
720
|
-
expect { expect(doctor.run).to be true }.to output(expected).to_stdout
|
|
721
|
-
end
|
|
722
|
-
end
|
|
723
|
-
|
|
724
|
-
describe "Rake task definition (Story 5.12)", :story_5_12 do
|
|
725
|
-
# Loads gem/lib/tasks/ruact.rake into a fresh Rake::Application so the
|
|
726
|
-
# task table is isolated from any other spec that may have loaded tasks.
|
|
727
|
-
# Asserts the new namespace is discoverable AND the legacy namespace is
|
|
728
|
-
# gone — guards against a partial rename (e.g. file renamed but the
|
|
729
|
-
# namespace inside left as :rsc, or vice-versa).
|
|
730
|
-
around do |example|
|
|
731
|
-
require "rake"
|
|
732
|
-
prev = Rake.application
|
|
733
|
-
Rake.application = Rake::Application.new
|
|
734
|
-
Rake.application.add_loader("rake", Rake::DefaultLoader.new)
|
|
735
|
-
Rake.application.add_loader("rb", Rake::DefaultLoader.new)
|
|
736
|
-
load File.expand_path("../../lib/tasks/ruact.rake", __dir__)
|
|
737
|
-
example.run
|
|
738
|
-
ensure
|
|
739
|
-
Rake.application = prev
|
|
740
|
-
end
|
|
741
|
-
|
|
742
|
-
let(:rake_app) { Rake.application }
|
|
743
|
-
|
|
744
|
-
it "defines Rake::Task['ruact:doctor']" do
|
|
745
|
-
expect(rake_app.lookup("ruact:doctor")).not_to be_nil
|
|
746
|
-
end
|
|
747
|
-
|
|
748
|
-
it "does NOT define the legacy Rake task under the pre-Story-5.12 namespace" do
|
|
749
|
-
# Build the legacy task name from fragments so this spec file stays
|
|
750
|
-
# grep-clean under the Story 5.12 CI guard (which rejects the literal
|
|
751
|
-
# `<legacy>:doctor` substring anywhere in tracked files).
|
|
752
|
-
legacy_task = %w[rsc doctor].join(":")
|
|
753
|
-
expect(rake_app.lookup(legacy_task)).to be_nil
|
|
754
|
-
end
|
|
755
|
-
|
|
756
|
-
it "the ruact:doctor task action invokes Ruact::Doctor.run" do
|
|
757
|
-
task = rake_app.lookup("ruact:doctor")
|
|
758
|
-
expect(task).not_to be_nil
|
|
759
|
-
# actions is a list of Procs; just assert at least one is attached
|
|
760
|
-
expect(task.actions).not_to be_empty
|
|
761
|
-
end
|
|
762
|
-
|
|
763
|
-
it "defines Rake::Task['ruact:routes'] with an action (Story 15.3)", :story_15_3 do
|
|
764
|
-
task = rake_app.lookup("ruact:routes")
|
|
765
|
-
expect(task).not_to be_nil
|
|
766
|
-
expect(task.actions).not_to be_empty
|
|
767
|
-
end
|
|
768
|
-
end
|
|
769
|
-
end
|