ruact 0.0.7 → 0.0.8

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/docs/internal/decisions/server-functions-api.md +55 -0
  4. data/lib/generators/ruact/install/install_generator.rb +114 -1
  5. data/lib/generators/ruact/install/templates/AGENTS.md.tt +159 -0
  6. data/lib/ruact/controller.rb +9 -0
  7. data/lib/ruact/doctor.rb +67 -9
  8. data/lib/ruact/erb_preprocessor.rb +113 -0
  9. data/lib/ruact/errors.rb +16 -0
  10. data/lib/ruact/manifest_resolver.rb +2 -2
  11. data/lib/ruact/serializable.rb +98 -6
  12. data/lib/ruact/server.rb +163 -0
  13. data/lib/ruact/server_functions/introspection.rb +81 -0
  14. data/lib/ruact/server_functions.rb +26 -4
  15. data/lib/ruact/testing/component_query.rb +113 -0
  16. data/lib/ruact/testing/flight_extractor.rb +221 -0
  17. data/lib/ruact/testing/flight_structure_diff.rb +267 -0
  18. data/lib/ruact/testing/flight_wire_parser.rb +138 -0
  19. data/lib/ruact/testing.rb +90 -0
  20. data/lib/ruact/version.rb +1 -1
  21. data/lib/tasks/ruact.rake +55 -2
  22. data/spec/ruact/doctor_spec.rb +141 -0
  23. data/spec/ruact/erb_preprocessor_spec.rb +145 -0
  24. data/spec/ruact/install_generator_spec.rb +285 -0
  25. data/spec/ruact/manifest_resolver_spec.rb +15 -0
  26. data/spec/ruact/serializable_spec.rb +126 -0
  27. data/spec/ruact/server_bucket_request_spec.rb +291 -0
  28. data/spec/ruact/server_functions/introspection_spec.rb +135 -0
  29. data/spec/ruact/tasks_json_introspection_spec.rb +141 -0
  30. data/spec/ruact/testing/have_ruact_component_spec.rb +170 -0
  31. data/spec/ruact/testing/no_production_load_spec.rb +41 -0
  32. data/spec/support/flight_wire_parser.rb +12 -126
  33. data/spec/support/matchers/flight_fixture_matcher.rb +7 -258
  34. data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +25 -0
  35. data/vendor/javascript/ruact-server-functions-runtime/index.js +104 -7
  36. data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +173 -0
  37. data/vendor/javascript/vite-plugin-ruact/type-tests/auto-revalidate.test-d.ts +25 -0
  38. metadata +14 -2
@@ -359,6 +359,7 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
359
359
  gen.create_server_functions_directory
360
360
  gen.append_gitignore_entries
361
361
  gen.create_vite_config
362
+ gen.create_agents_md
362
363
  end.not_to raise_error
363
364
  end
364
365
 
@@ -371,6 +372,7 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
371
372
  expect(File.read(File.join(app_root, ".gitignore")))
372
373
  .to include("app/javascript/.ruact/server-functions.ts")
373
374
  expect(File).to exist(File.join(app_root, "vite.config.js"))
375
+ expect(File).to exist(File.join(app_root, "AGENTS.md"))
374
376
  end
375
377
  end
376
378
  end
@@ -889,4 +891,287 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
889
891
  end
890
892
  end
891
893
  end
894
+
895
+ # Story 15.1 (FR105) — `ruact:install` emits an AGENTS.md teaching coding
896
+ # agents the ruact conventions, traps, and verification commands. The ruact
897
+ # content is delimited by explicit markers so re-runs are idempotent, a
898
+ # user-authored AGENTS.md is appended to (never clobbered), and --force
899
+ # refreshes ONLY the marked section. Content assertions grep stable tokens
900
+ # (commands, file paths, API names) — never full-file snapshots, since later
901
+ # stories (15.2/15.3/15.4) deliberately evolve the prose.
902
+ describe "install generator — AGENTS.md emission (Story 15.1 — FR105)", :story_15_1 do
903
+ require "stringio"
904
+ require "generators/ruact/install/install_generator"
905
+
906
+ let(:app_root) { Dir.mktmpdir("ruact_install_1501") }
907
+ let(:agents_md_path) { File.join(app_root, "AGENTS.md") }
908
+ let(:begin_marker) { "<!-- ruact:begin -->" }
909
+ let(:end_marker) { "<!-- ruact:end -->" }
910
+ let(:template_path) do
911
+ File.expand_path("../../lib/generators/ruact/install/templates/AGENTS.md.tt", __dir__)
912
+ end
913
+
914
+ after { FileUtils.rm_rf(app_root) }
915
+
916
+ def build_generator(root, opts = {})
917
+ Ruact::Generators::InstallGenerator.new([], opts, destination_root: root)
918
+ end
919
+
920
+ def silently
921
+ original = $stdout
922
+ $stdout = StringIO.new
923
+ yield
924
+ ensure
925
+ $stdout = original
926
+ end
927
+
928
+ # Captures and returns the generator's say/say_status stdout for text asserts.
929
+ def capture_stdout
930
+ original = $stdout
931
+ $stdout = StringIO.new
932
+ yield
933
+ $stdout.string
934
+ ensure
935
+ $stdout = original
936
+ end
937
+
938
+ describe "fresh emission (AC#1)" do
939
+ it "creates AGENTS.md delimited by the ruact markers", :aggregate_failures do
940
+ silently { build_generator(app_root).create_agents_md }
941
+
942
+ expect(File).to exist(agents_md_path)
943
+ content = File.read(agents_md_path)
944
+ expect(content).to start_with(begin_marker)
945
+ expect(content.rstrip).to end_with(end_marker)
946
+ end
947
+
948
+ it "covers the model, codegen, queries and verification areas", :aggregate_failures do
949
+ silently { build_generator(app_root).create_agents_md }
950
+ content = File.read(agents_md_path)
951
+
952
+ # The verb rule (mutations = non-GET routed actions on Ruact::Server)
953
+ expect(content).to include("Ruact::Server")
954
+ expect(content).to include("non-GET")
955
+ # Route-driven codegen: ground-truth file + regenerate command
956
+ expect(content).to include("app/javascript/.ruact/server-functions.ts")
957
+ expect(content).to include("bin/rails ruact:server_functions:generate")
958
+ # Queries (reads)
959
+ expect(content).to include("Ruact::Query")
960
+ expect(content).to include("app/queries/")
961
+ expect(content).to include("ruact_queries")
962
+ expect(content).to include("useQuery")
963
+ # Verification commands (the only two that exist today)
964
+ expect(content).to include("bin/rails ruact:doctor")
965
+ # Docs pointers
966
+ expect(content).to include("https://ruact.dev")
967
+ expect(content).to include("llms.txt")
968
+ end
969
+
970
+ it "covers the five traps and the safety areas", :aggregate_failures do
971
+ silently { build_generator(app_root).create_agents_md }
972
+ content = File.read(agents_md_path)
973
+
974
+ # Trap 1 — children unsupported / self-closing only
975
+ expect(content).to include("self-closing")
976
+ expect(content).to include("children")
977
+ # Trap 2 — Ruby (not JS) inside {} props
978
+ expect(content).to include("Ruby, not JavaScript")
979
+ # Trap 3 — Accept-header dual shape (exact application/json)
980
+ expect(content).to include("Accept")
981
+ expect(content).to include("application/json")
982
+ # Trap 4 — ruact_errors fall-through
983
+ expect(content).to include("ruact_errors")
984
+ expect(content).to include("fall-through")
985
+ # Trap 5 — name derivation from routes
986
+ expect(content).to include("createPost")
987
+ expect(content).to include("ruact_function_name")
988
+ # Serialization allowlist
989
+ expect(content).to include("ruact_props")
990
+ expect(content).to include("strict_serialization")
991
+ # SGID helpers (purpose + expiry required grain)
992
+ expect(content).to include("Ruact.signed_global_id")
993
+ expect(content).to include("Ruact.locate_signed")
994
+ end
995
+
996
+ it "makes no claim before its artifact (AC#4 — content honesty)", :aggregate_failures do
997
+ silently { build_generator(app_root).create_agents_md }
998
+ content = File.read(agents_md_path)
999
+
1000
+ # (The loud children `PreprocessorError` is Story 15.2's shipped
1001
+ # artifact, and the `--json` introspection commands are Story 15.3's —
1002
+ # both may now be claimed; see the `:story_15_2` / `:story_15_3` guards
1003
+ # below that PIN each claim's presence. Story 15.4's public render
1004
+ # helpers are now shipped too — see the `:story_15_4` guard below.)
1005
+ #
1006
+ # tsc does not run in a fresh install (no typescript devDep / tsconfig) —
1007
+ # it may only appear conditionally phrased (D1).
1008
+ expect(content).to match(/if your app has typescript tooling/i) if content.include?("tsc")
1009
+ # Volatile strings must not be quoted: no gem version pins.
1010
+ expect(content).not_to match(/\b0\.0\.\d+\b/)
1011
+ end
1012
+
1013
+ # Story 15.2 (D2) — now that the loud children error is a shipped artifact,
1014
+ # trap #1 truthfully claims it fails LOUDLY (was "fails silently" in 15.1).
1015
+ it "trap #1 claims the loud PreprocessorError (Story 15.2 D2)", :aggregate_failures, :story_15_2 do
1016
+ silently { build_generator(app_root).create_agents_md }
1017
+ content = File.read(agents_md_path)
1018
+
1019
+ expect(content).to include("PreprocessorError")
1020
+ expect(content).to match(/fail LOUDLY/i)
1021
+ expect(content).not_to match(/fail silently/i)
1022
+ end
1023
+
1024
+ # Story 15.3 (D2) — now that the `--json` introspection surface is a shipped
1025
+ # artifact, the "Verify your work" section references it (relaxes the 15.1
1026
+ # honesty guard above). This PINS the claim AND its EXPERIMENTAL framing so
1027
+ # the template never presents the JSON shape as a stable contract.
1028
+ it "references --json introspection as EXPERIMENTAL (15.3 D2)", :aggregate_failures, :story_15_3 do
1029
+ silently { build_generator(app_root).create_agents_md }
1030
+ content = File.read(agents_md_path)
1031
+
1032
+ expect(content).to include("ruact:routes -- --json")
1033
+ expect(content).to include("ruact:doctor")
1034
+ expect(content).to include("Append `-- --json`")
1035
+ expect(content).to match(/EXPERIMENTAL/)
1036
+ expect(content).to include("schema_version")
1037
+ end
1038
+
1039
+ # Story 15.4 (D6) — now that the public render-assertion helpers are a
1040
+ # shipped artifact (FR108), the "Verify your work" section references them.
1041
+ # This PINS the claim (name + explicit require + the JSON-vs-Flight rule)
1042
+ # so it stays consistent with `website/public/llms.txt`.
1043
+ it "references the ruact/testing render helpers (15.4 D6)", :aggregate_failures, :story_15_4 do
1044
+ silently { build_generator(app_root).create_agents_md }
1045
+ content = File.read(agents_md_path)
1046
+
1047
+ expect(content).to include(%(require "ruact/testing"))
1048
+ expect(content).to include("have_ruact_component")
1049
+ expect(content).to include("JSON.parse(response.body)")
1050
+ end
1051
+ end
1052
+
1053
+ describe "idempotency (AC#2 — double-run zero diff)" do
1054
+ it "leaves the file byte-identical on a second run and reports a skip", :aggregate_failures do
1055
+ silently { build_generator(app_root).create_agents_md }
1056
+ first = File.read(agents_md_path)
1057
+
1058
+ output = capture_stdout { build_generator(app_root).create_agents_md }
1059
+
1060
+ expect(File.read(agents_md_path)).to eq(first)
1061
+ expect(output).to include("skip")
1062
+ end
1063
+ end
1064
+
1065
+ describe "append mode (AC#2 — user-authored AGENTS.md)" do
1066
+ let(:user_content) { "# My app\n\nUse pnpm here. Never touch config/secrets.yml.\n" }
1067
+
1068
+ it "appends the marked section preserving every pre-existing user byte", :aggregate_failures do
1069
+ File.write(agents_md_path, user_content)
1070
+
1071
+ silently { build_generator(app_root).create_agents_md }
1072
+ content = File.read(agents_md_path)
1073
+
1074
+ expect(content).to start_with(user_content)
1075
+ expect(content.scan(begin_marker).count).to eq(1)
1076
+ expect(content.scan(end_marker).count).to eq(1)
1077
+ expect(content).to include("\n\n#{begin_marker}")
1078
+ end
1079
+
1080
+ it "blank-line separates the section when the user file lacks a trailing newline" do
1081
+ File.write(agents_md_path, "# My app — no trailing newline")
1082
+
1083
+ silently { build_generator(app_root).create_agents_md }
1084
+
1085
+ expect(File.read(agents_md_path))
1086
+ .to include("# My app — no trailing newline\n\n#{begin_marker}")
1087
+ end
1088
+
1089
+ it "appending then re-running is idempotent (skip, zero diff)" do
1090
+ File.write(agents_md_path, user_content)
1091
+ silently { build_generator(app_root).create_agents_md }
1092
+ appended = File.read(agents_md_path)
1093
+
1094
+ silently { build_generator(app_root).create_agents_md }
1095
+
1096
+ expect(File.read(agents_md_path)).to eq(appended)
1097
+ end
1098
+ end
1099
+
1100
+ describe "--force refresh (AC#2 — marked-block-only)" do
1101
+ it "replaces ONLY the between-marker content, never user bytes outside", :aggregate_failures do
1102
+ silently { build_generator(app_root).create_agents_md }
1103
+ section = File.read(agents_md_path)
1104
+ stale = section.sub("## Five traps", "## STALE CONTENT FROM AN OLDER GEM")
1105
+ File.write(agents_md_path, "# mine, above\n\n#{stale}\n# mine, below\n")
1106
+
1107
+ silently { build_generator(app_root, { force: true }).create_agents_md }
1108
+ content = File.read(agents_md_path)
1109
+
1110
+ expect(content).to start_with("# mine, above\n")
1111
+ expect(content).to end_with("# mine, below\n")
1112
+ expect(content).to include("## Five traps")
1113
+ expect(content).not_to include("STALE CONTENT FROM AN OLDER GEM")
1114
+ expect(content.scan(begin_marker).count).to eq(1)
1115
+ end
1116
+
1117
+ it "creates the file under --force when none exists" do
1118
+ silently { build_generator(app_root, { force: true }).create_agents_md }
1119
+ expect(File.read(agents_md_path)).to include(begin_marker)
1120
+ end
1121
+ end
1122
+
1123
+ # Codex R1 — an incomplete marker state is ambiguous in BOTH directions
1124
+ # (appending would duplicate content next to a stray marker; replacing
1125
+ # would guess the range), so the only byte-safe move is warn + no-op.
1126
+ describe "broken marker pair (byte-safety)" do
1127
+ [
1128
+ ["a begin marker without its end marker",
1129
+ "# mine\n\n<!-- ruact:begin -->\ntruncated ruact section, no end marker\n"],
1130
+ ["an end marker without its begin marker",
1131
+ "# mine\n\nstray tail of a ruact section\n<!-- ruact:end -->\n"],
1132
+ ["an end marker preceding the begin marker",
1133
+ "# mine\n\n<!-- ruact:end -->\nreversed\n<!-- ruact:begin -->\n"],
1134
+ # Codex R2 — a VALID pair plus stray extra markers is still ambiguous:
1135
+ # skip/refresh would leave (or eat past) the unmatched marker.
1136
+ ["a stray end marker before a valid pair",
1137
+ "<!-- ruact:end -->\nstray\n\n<!-- ruact:begin -->\nsection\n<!-- ruact:end -->\n"],
1138
+ ["a stray begin marker after a valid pair",
1139
+ "<!-- ruact:begin -->\nsection\n<!-- ruact:end -->\n\nstray\n<!-- ruact:begin -->\n"],
1140
+ ["two complete marker pairs",
1141
+ "<!-- ruact:begin -->\none\n<!-- ruact:end -->\n\n<!-- ruact:begin -->\ntwo\n<!-- ruact:end -->\n"]
1142
+ ].each do |(label, broken)|
1143
+ it "warns and leaves the file untouched with #{label} (no --force)", :aggregate_failures do
1144
+ File.write(agents_md_path, broken)
1145
+
1146
+ output = capture_stdout { build_generator(app_root).create_agents_md }
1147
+
1148
+ expect(File.read(agents_md_path)).to eq(broken)
1149
+ expect(output).to include("warn")
1150
+ end
1151
+
1152
+ it "warns and leaves the file untouched with #{label} (--force)", :aggregate_failures do
1153
+ File.write(agents_md_path, broken)
1154
+
1155
+ output = capture_stdout { build_generator(app_root, { force: true }).create_agents_md }
1156
+
1157
+ expect(File.read(agents_md_path)).to eq(broken)
1158
+ expect(output).to include("warn")
1159
+ end
1160
+ end
1161
+ end
1162
+
1163
+ describe "action ordering + template budget" do
1164
+ it "defines create_agents_md BEFORE install_javascript_dependencies" do
1165
+ klass = Ruact::Generators::InstallGenerator
1166
+ line = ->(name) { klass.instance_method(name).source_location.last }
1167
+ expect(line.call(:create_agents_md)).to be < line.call(:install_javascript_dependencies)
1168
+ end
1169
+
1170
+ # AC#3 tripwire — the ~150-line budget is the epic's scope probe; the
1171
+ # tripwire's tolerance is 160 (the tripwire's, not the product contract's).
1172
+ it "keeps the template body at or under 160 lines" do
1173
+ expect(File.read(template_path).lines.count).to be <= 160
1174
+ end
1175
+ end
1176
+ end
892
1177
  end
@@ -107,6 +107,21 @@ module Ruact
107
107
  expect { described_class.resolve }.to raise_error(ManifestError, %r{Vite dev server.*bin/dev}m)
108
108
  end
109
109
 
110
+ # Story 15.0 (F8) — an error message is a prompt: agents (and humans)
111
+ # regex-match error text, so the ENGLISH wording and its stable tokens
112
+ # (`Vite dev server`, `react-client-manifest.json`, `bin/dev`) are pinned.
113
+ it "raises the ENGLISH message naming the dev-server URL, the manifest path and the bin/dev fix " \
114
+ "(Story 15.0)", :story_15_0 do
115
+ allow(Net::HTTP).to receive(:start).and_raise(Errno::ECONNREFUSED)
116
+ allow(described_class).to receive(:file_path).and_return("/no/such/manifest.json")
117
+
118
+ expect { described_class.resolve }.to raise_error(ManifestError) do |error|
119
+ expect(error.message).to include("[ruact] Vite dev server unreachable at http://localhost:5173")
120
+ expect(error.message).to include("no react-client-manifest.json found at /no/such/manifest.json")
121
+ expect(error.message).to include("run `bin/dev`")
122
+ end
123
+ end
124
+
110
125
  it "ignores a non-2xx dev-server response and falls back" do
111
126
  allow(Net::HTTP).to receive(:start).and_return(Net::HTTPNotFound.new("1.1", "404", "Not Found"))
112
127
  allow(described_class).to receive(:file_path).and_return("/no/such/manifest.json")
@@ -32,6 +32,132 @@ module Ruact
32
32
  end
33
33
  end
34
34
 
35
+ # Story 13.7 — `ruact_props` on ActiveRecord models.
36
+ #
37
+ # ActiveRecord defines its attribute reader methods LAZILY (on first
38
+ # instance access), so at the moment the `ruact_props :title` macro runs,
39
+ # `method_defined?(:title)` is `false` even for a real column. The eager
40
+ # boot check used to raise there, so an AR model that included
41
+ # `Ruact::Serializable` crashed at class-load. Story 13.7 makes the check
42
+ # HYBRID: POROs are still checked eagerly at class-load (byte-identical, see
43
+ # the block above), while for a lazy-attribute (AR) class the loud check is
44
+ # DEFERRED to the first `ruact_serialize` — where the DB is up and the reader
45
+ # exists. Loudness is preserved: a bogus prop still raises a clean
46
+ # `ArgumentError`, just at first render of that model instead of at boot.
47
+ #
48
+ # Zero suite-boot DB footprint: the connection + table are built INSIDE the
49
+ # example (direct connection DDL, NOT `Schema.define`, so we touch nothing
50
+ # global — no `schema_migrations` / `ar_internal_metadata`), and only a
51
+ # connection we ourselves opened is torn down.
52
+ describe "ActiveRecord models (Story 13.7)", :story_13_7 do
53
+ before do
54
+ require "active_record"
55
+ @already_connected =
56
+ ActiveRecord::Base.respond_to?(:connected?) && ActiveRecord::Base.connected?
57
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") unless @already_connected
58
+ ActiveRecord::Base.connection.create_table(:ruact_props_probe_posts, force: true) do |t|
59
+ t.string :title
60
+ t.string :secret
61
+ end
62
+ end
63
+
64
+ after do
65
+ if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
66
+ ActiveRecord::Base.connection.drop_table(:ruact_props_probe_posts, if_exists: true)
67
+ end
68
+ ActiveRecord::Base.remove_connection unless @already_connected
69
+ end
70
+
71
+ let(:ar_model) do
72
+ Class.new(ActiveRecord::Base) do
73
+ self.table_name = "ruact_props_probe_posts"
74
+ include Ruact::Serializable
75
+
76
+ ruact_props :id, :title
77
+ end
78
+ end
79
+
80
+ it "loads without raising even though readers are lazy at class-load (AC#1)" do
81
+ expect { ar_model }.not_to raise_error
82
+ end
83
+
84
+ it "ruact_serialize returns only the declared allowlist (AC#1)" do
85
+ row = ar_model.create!(title: "Hello", secret: "top-secret")
86
+
87
+ expect(row.ruact_serialize).to eq({ "id" => row.id, "title" => "Hello" })
88
+ expect(row.ruact_serialize.keys).not_to include("secret")
89
+ end
90
+
91
+ it "defers the loud check to first serialize for a bogus prop (AC#2)" do
92
+ model = nil
93
+
94
+ # Declaring a bogus prop on an AR model does NOT raise at class-load —
95
+ # the reader might still be defined lazily, so the check is deferred.
96
+ expect do
97
+ model = Class.new(ActiveRecord::Base) do
98
+ self.table_name = "ruact_props_probe_posts"
99
+ include Ruact::Serializable
100
+
101
+ ruact_props :ttile # typo — neither a column nor a method
102
+ end
103
+ end.not_to raise_error
104
+
105
+ # …but it fails LOUDLY (same clean ArgumentError) on first serialize.
106
+ expect { model.new.ruact_serialize }
107
+ .to raise_error(ArgumentError, /ttile.*not defined/)
108
+ end
109
+
110
+ it "re-validates loudly when props are re-declared after a first serialize (AC#2)" do
111
+ model = Class.new(ActiveRecord::Base) do
112
+ self.table_name = "ruact_props_probe_posts"
113
+ include Ruact::Serializable
114
+
115
+ ruact_props :id, :title
116
+ end
117
+
118
+ # First serialize succeeds and memoizes "validated"…
119
+ model.create!(title: "Hello").ruact_serialize
120
+
121
+ # …a re-declaration with a bogus prop must NOT be masked by the stale
122
+ # memo: the next serialize still raises the clean ArgumentError.
123
+ model.ruact_props :id, :ttile
124
+ expect { model.new.ruact_serialize }
125
+ .to raise_error(ArgumentError, /ttile.*not defined/)
126
+ end
127
+
128
+ it "re-validates an inherited declaration re-declared on the parent after a subclass serialize (AC#2)" do
129
+ parent = Class.new(ActiveRecord::Base) do
130
+ self.table_name = "ruact_props_probe_posts"
131
+ include Ruact::Serializable
132
+
133
+ ruact_props :id, :title
134
+ end
135
+ child = Class.new(parent)
136
+
137
+ # The subclass serializes first, memoizing the inherited (valid) list…
138
+ parent.create!(title: "Hello")
139
+ child.first.ruact_serialize
140
+
141
+ # …then the PARENT re-declares with a bogus prop. The subclass must not
142
+ # keep serving its stale memo: its next serialize re-validates loudly.
143
+ parent.ruact_props :id, :ttile
144
+ expect { child.first.ruact_serialize }
145
+ .to raise_error(ArgumentError, /ttile.*not defined/)
146
+ end
147
+
148
+ it "dispatches through serialize_serializable under strict true AND false (AC#4)" do
149
+ row = ar_model.create!(title: "Hello", secret: "top-secret")
150
+
151
+ [true, false].each do |strict|
152
+ output = Ruact::Flight::Renderer.render(
153
+ row, Ruact::ClientManifest.from_hash({}), strict_serialization: strict
154
+ )
155
+ expect(output).to include("Hello")
156
+ expect(output).not_to include("top-secret")
157
+ end
158
+ end
159
+ end
160
+
35
161
  describe "ruact_serialize (AC#1)" do
36
162
  it "returns only declared props" do
37
163
  obj = serializable_class.new