From 2576776b57016ef6283cd5a9966b973330a17fb4 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 4 Jun 2026 11:33:15 -1000 Subject: [PATCH 1/4] fix(benchmarks): seed Pro dummy DB so /posts_page stops 500ing (#3602) The Pro benchmark suite reported `/posts_page` at a 100% failure rate (5xx on every request) while every other route was green. Root cause: the benchmark workflow never creates/migrates/seeds the production database, so the `posts` table does not exist and the route's first query raises ActiveRecord::StatementInvalid (no such table: posts). `/posts_page` is the only synchronous benchmark route that reads the database directly. The streaming RSC posts routes hit the same broken DB but return HTTP 200 because the status line is flushed before the body errors, so the failure only surfaced here. Changes: - benchmark.yml: add a "Prepare and seed benchmark database" step (RAILS_ENV=production `rails db:prepare`) before the server starts, gated to the Pro Rails suite. db:prepare creates the DB, loads the schema, and runs the seeds. - db/seeds.rb: rewrite to be deterministic and free of the faker gem. faker is a development/test-only dependency and is not loaded under RAILS_ENV=production, so the previous seeds raised NameError there. Deterministic data also keeps benchmark runs reproducible. - spec/requests/posts_page_spec.rb: new integration test that seeds records and asserts /posts_page server-renders the posts and returns 200 (and 200, not 500, with an empty table), guarding against a regression. The spec self-loads the schema since the suite leaves maintain_test_schema! disabled. Fixes #3602 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/benchmark.yml | 18 +++++ react_on_rails_pro/spec/dummy/db/seeds.rb | 68 ++++++++++++------- .../dummy/spec/requests/posts_page_spec.rb | 65 ++++++++++++++++++ 3 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 154121e670..7727ed4bd6 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -387,6 +387,24 @@ jobs: echo "✅ Production assets built successfully" + - name: Prepare and seed benchmark database + # The Pro Rails suite serves DB-backed routes (e.g. /posts_page). Without + # a migrated + seeded database those routes 500 on every request (the + # `posts` table does not exist), so the benchmark must create and seed it + # before the server starts. `db:prepare` creates the database, loads the + # schema, and runs db/seeds.rb (which is deterministic and faker-free so + # it works under RAILS_ENV=production). + if: matrix.server_kind == 'rails' && matrix.pro_env == 'true' + working-directory: ${{ matrix.app_directory }} + env: + RAILS_ENV: production + NODE_ENV: production + run: | + set -e + echo "🌱 Preparing and seeding ${{ matrix.suite_name }} benchmark database..." + bundle exec rails db:prepare + echo "✅ Database prepared and seeded" + - name: Start Pro node renderer if: matrix.server_kind == 'node-renderer' working-directory: ${{ matrix.app_directory }} diff --git a/react_on_rails_pro/spec/dummy/db/seeds.rb b/react_on_rails_pro/spec/dummy/db/seeds.rb index 980f5288df..57bae3391f 100644 --- a/react_on_rails_pro/spec/dummy/db/seeds.rb +++ b/react_on_rails_pro/spec/dummy/db/seeds.rb @@ -1,48 +1,66 @@ # frozen_string_literal: true -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# Seed data for the dummy app. The benchmark suite's `/posts_page` route renders +# these records, and they are handy for local development too. # -# Examples: -# -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Emanuel', city: cities.first) +# This file is intentionally deterministic and free of the `faker` gem: it must +# run under `RAILS_ENV=production` in CI (via `rails db:prepare`), where the +# development/test-only `faker` gem is not loaded. Deterministic data also keeps +# benchmark runs reproducible from one CI run to the next. + +USER_COUNT = 10 +POSTS_PER_USER = 3..7 +COMMENTS_PER_POST = 2..5 + +LOREM = %w[ + lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud + exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute + irure reprehenderit voluptate velit esse cillum fugiat nulla pariatur +].freeze + +lorem_words = lambda do |count, offset| + Array.new(count) { |i| LOREM[(offset + i) % LOREM.size] } +end +lorem_sentence = ->(word_count, offset) { "#{lorem_words.call(word_count, offset).join(' ').capitalize}." } +lorem_paragraph = lambda do |sentence_count, offset| + Array.new(sentence_count) { |i| lorem_sentence.call(6 + ((offset + i) % 6), offset + (i * 7)) }.join(" ") +end -# Clear existing data puts "Clearing existing data..." Comment.delete_all Post.delete_all User.delete_all -# Create Users puts "Creating users..." -10.times do - User.create!( - name: Faker::Name.name, - email: Faker::Internet.unique.email - ) +users = Array.new(USER_COUNT) do |i| + User.create!(name: "User #{i + 1}", email: "user-#{i + 1}@example.com") end -# Create Posts puts "Creating posts..." -User.all.each do |user| - rand(3..7).times do - user.posts.create!( - title: Faker::Lorem.sentence(word_count: 3), - body: Faker::Lorem.paragraphs(number: 3).join("\n\n") +posts = [] +users.each_with_index do |user, user_index| + post_count = POSTS_PER_USER.first + (user_index % POSTS_PER_USER.size) + post_count.times do |post_index| + seed = (user_index * 11) + post_index + posts << user.posts.create!( + title: lorem_sentence.call(3, seed), + body: Array.new(3) { |p| lorem_paragraph.call(4, seed + p) }.join("\n\n") ) end end -# Create Comments puts "Creating comments..." -Post.all.each do |post| - rand(2..5).times do +posts.each_with_index do |post, post_index| + comment_count = COMMENTS_PER_POST.first + (post_index % COMMENTS_PER_POST.size) + comment_count.times do |comment_index| + seed = (post_index * 7) + comment_index post.comments.create!( - user: User.all.sample, - body: Faker::Lorem.paragraph + user: users[seed % users.size], + body: lorem_paragraph.call(2, seed) ) end end -puts "Seed data created successfully!" +puts "Seed data created successfully! " \ + "(#{User.count} users, #{Post.count} posts, #{Comment.count} comments)" diff --git a/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb b/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb new file mode 100644 index 0000000000..1a1f7d0b7c --- /dev/null +++ b/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "rails_helper" + +# Integration coverage for the `/posts_page` benchmark route (issue #3602). +# +# `/posts_page` is the only synchronous (non-streaming) benchmark route that +# reads from the database, so a missing `posts` table surfaces as a hard 500 on +# every request — which is exactly how it regressed in the Pro benchmark suite. +# (The streaming RSC posts routes mask the same failure because their HTTP 200 +# status line is sent before the body errors.) These examples render the page +# with seeded records and assert it returns 200 with the post content, guarding +# against the route silently breaking again. +# +# Requires the Pro node renderer to be running, like the other server-rendering +# request specs: the page is rendered with `prerender: true`. +RSpec.describe "Posts page", :server_rendering do + before do + # The suite intentionally leaves `maintain_test_schema!` disabled, so make + # this DB-backed spec self-sufficient: load the schema if it is not present + # yet. The table check is cheap and the load only runs on the first example. + unless ActiveRecord::Base.connection.table_exists?(:posts) + ActiveRecord::Schema.verbose = false + load Rails.root.join("db/schema.rb") + end + + Comment.delete_all + Post.delete_all + User.delete_all + + 2.times do |i| + user = User.create!(name: "User #{i + 1}", email: "user-#{i + 1}@example.com") + post = user.posts.create!(title: "Sentinel Post #{i + 1}", body: "Body of sentinel post #{i + 1}.") + post.comments.create!(user: user, body: "Comment on sentinel post #{i + 1}.") + end + end + + after do + Comment.delete_all + Post.delete_all + User.delete_all + end + + it "server-renders the seeded posts and returns 200" do + get "/posts_page" + + expect(response).to have_http_status(:ok) + + html = Nokogiri::HTML(response.body) + expect(html.css("h1").map(&:text)).to include("Posts Page") + expect(response.body).to include("Sentinel Post 1") + expect(response.body).to include("Sentinel Post 2") + end + + it "returns 200 (not 500) when there are no posts to render" do + Comment.delete_all + Post.delete_all + User.delete_all + + get "/posts_page" + + expect(response).to have_http_status(:ok) + expect(response.body).to include("No posts found") + end +end From 0ed8ebd2d04a55e5d66b4eff851637a75e87ad84 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 4 Jun 2026 16:08:50 -1000 Subject: [PATCH 2/4] fix(benchmarks): verify seeding and correct review comments Address PR review findings on #3615: - benchmark.yml: db:prepare only seeds on first DB creation, so an already-present-but-empty DB would silently skip seeding and serve a 200 "No posts found" payload that the benchmark scores as healthy. Replace the unconditional success echo with a `rails runner` row-count check that fails the job loudly if zero posts were seeded, and correct the step comment to state the seed-on-creation semantics. - posts_page_spec.rb: the before block always loads the schema, so the spec never reproduces the missing-table 500 its header claimed to guard. Reword the header to accurately describe what it covers (the render + empty-data paths, with the missing-table 500 prevented upstream by seeding the benchmark DB). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/benchmark.yml | 12 ++++++++---- .../dummy/spec/requests/posts_page_spec.rb | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 7727ed4bd6..ec9f83c614 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -391,9 +391,13 @@ jobs: # The Pro Rails suite serves DB-backed routes (e.g. /posts_page). Without # a migrated + seeded database those routes 500 on every request (the # `posts` table does not exist), so the benchmark must create and seed it - # before the server starts. `db:prepare` creates the database, loads the - # schema, and runs db/seeds.rb (which is deterministic and faker-free so - # it works under RAILS_ENV=production). + # before the server starts. On a fresh CI runner `db:prepare` creates the + # database, loads the schema, and — because the database is newly created — + # runs db/seeds.rb (deterministic and faker-free so it works under + # RAILS_ENV=production). Note: `db:prepare` only seeds on first creation, so + # the row-count check below fails the job loudly if seeding was skipped (an + # empty table would otherwise serve a 200 "No posts found" payload that the + # benchmark scores as healthy). if: matrix.server_kind == 'rails' && matrix.pro_env == 'true' working-directory: ${{ matrix.app_directory }} env: @@ -403,7 +407,7 @@ jobs: set -e echo "🌱 Preparing and seeding ${{ matrix.suite_name }} benchmark database..." bundle exec rails db:prepare - echo "✅ Database prepared and seeded" + bundle exec rails runner 'count = Post.count; abort("Benchmark DB has 0 posts after db:prepare; seeds did not run") if count.zero?; puts "✅ Database seeded (#{count} posts)"' - name: Start Pro node renderer if: matrix.server_kind == 'node-renderer' diff --git a/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb b/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb index 1a1f7d0b7c..26ef6a9c7b 100644 --- a/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb +++ b/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb @@ -4,13 +4,18 @@ # Integration coverage for the `/posts_page` benchmark route (issue #3602). # -# `/posts_page` is the only synchronous (non-streaming) benchmark route that -# reads from the database, so a missing `posts` table surfaces as a hard 500 on -# every request — which is exactly how it regressed in the Pro benchmark suite. -# (The streaming RSC posts routes mask the same failure because their HTTP 200 -# status line is sent before the body errors.) These examples render the page -# with seeded records and assert it returns 200 with the post content, guarding -# against the route silently breaking again. +# `/posts_page` renders DB records synchronously (non-streaming), so when the +# `posts` table was missing in the Pro benchmark suite it surfaced as a hard 500 +# on every request. (The streaming RSC posts routes mask that class of failure +# because their HTTP 200 status line is flushed before the body errors.) +# +# These examples do not reproduce the missing-table case itself — the `before` +# block loads the schema (see below), so the table is always present here. The +# missing-table 500 is prevented upstream by seeding the benchmark DB before the +# server starts (.github/workflows/benchmark.yml). What these examples guard is +# the layer above that: with the table in place the route must server-render +# seeded posts and return 200, and an empty table must return 200 "No posts +# found" rather than 500. # # Requires the Pro node renderer to be running, like the other server-rendering # request specs: the page is rendered with `prerender: true`. From 498045164b4deb4ca18ab8ff702c6fbc099c2475 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 4 Jun 2026 16:34:25 -1000 Subject: [PATCH 3/4] fix(benchmarks): address review polish on seed/spec/workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply non-blocking review feedback from PR #3615 (claude, greptile, coderabbit). No behavior change — seeded data is byte-for-byte identical. - seeds.rb: replace top-level constants with local variables so re-running `rails db:seed` in an already-loaded process no longer emits `warning: already initialized constant`; cycle per-record counts through precomputed arrays so the modulo stays correct if a range is ever made exclusive or non-unit-step. - benchmark.yml: route `matrix.suite_name` through an env var in the "Prepare and seed" step (injection hardening), matching the adjacent "Execute benchmark suite" step. - posts_page_spec.rb: broaden the self-loading schema guard to check all three tables the examples touch (users/posts/comments), and document why the H1 assertion parses with Nokogiri while the title checks use substrings. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/benchmark.yml | 7 ++++++- react_on_rails_pro/spec/dummy/db/seeds.rb | 21 ++++++++++++------- .../dummy/spec/requests/posts_page_spec.rb | 9 ++++++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ec9f83c614..ec79f34ef4 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -400,12 +400,17 @@ jobs: # benchmark scores as healthy). if: matrix.server_kind == 'rails' && matrix.pro_env == 'true' working-directory: ${{ matrix.app_directory }} + # Route the matrix value through env so the shell treats it as data, not + # code (GitHub Actions injection hardening), matching the "Execute + # benchmark suite" step below — even though it comes from our own + # generate_matrix.rb. env: RAILS_ENV: production NODE_ENV: production + SUITE_NAME: ${{ matrix.suite_name }} run: | set -e - echo "🌱 Preparing and seeding ${{ matrix.suite_name }} benchmark database..." + echo "🌱 Preparing and seeding $SUITE_NAME benchmark database..." bundle exec rails db:prepare bundle exec rails runner 'count = Post.count; abort("Benchmark DB has 0 posts after db:prepare; seeds did not run") if count.zero?; puts "✅ Database seeded (#{count} posts)"' diff --git a/react_on_rails_pro/spec/dummy/db/seeds.rb b/react_on_rails_pro/spec/dummy/db/seeds.rb index 57bae3391f..4de1b03132 100644 --- a/react_on_rails_pro/spec/dummy/db/seeds.rb +++ b/react_on_rails_pro/spec/dummy/db/seeds.rb @@ -8,11 +8,16 @@ # development/test-only `faker` gem is not loaded. Deterministic data also keeps # benchmark runs reproducible from one CI run to the next. -USER_COUNT = 10 -POSTS_PER_USER = 3..7 -COMMENTS_PER_POST = 2..5 +# Local variables (not top-level constants) so re-running `rails db:seed` in an +# already-loaded process — seeds.rb is `load`ed, not `require`d — does not emit +# `warning: already initialized constant`. The per-record counts cycle through +# these arrays, which stays correct even if a range is later made exclusive or +# non-unit-step (unlike `range.first + (idx % range.size)`). +user_count = 10 +post_counts = (3..7).to_a +comment_counts = (2..5).to_a -LOREM = %w[ +lorem = %w[ lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute @@ -20,7 +25,7 @@ ].freeze lorem_words = lambda do |count, offset| - Array.new(count) { |i| LOREM[(offset + i) % LOREM.size] } + Array.new(count) { |i| lorem[(offset + i) % lorem.size] } end lorem_sentence = ->(word_count, offset) { "#{lorem_words.call(word_count, offset).join(' ').capitalize}." } lorem_paragraph = lambda do |sentence_count, offset| @@ -33,14 +38,14 @@ User.delete_all puts "Creating users..." -users = Array.new(USER_COUNT) do |i| +users = Array.new(user_count) do |i| User.create!(name: "User #{i + 1}", email: "user-#{i + 1}@example.com") end puts "Creating posts..." posts = [] users.each_with_index do |user, user_index| - post_count = POSTS_PER_USER.first + (user_index % POSTS_PER_USER.size) + post_count = post_counts[user_index % post_counts.size] post_count.times do |post_index| seed = (user_index * 11) + post_index posts << user.posts.create!( @@ -52,7 +57,7 @@ puts "Creating comments..." posts.each_with_index do |post, post_index| - comment_count = COMMENTS_PER_POST.first + (post_index % COMMENTS_PER_POST.size) + comment_count = comment_counts[post_index % comment_counts.size] comment_count.times do |comment_index| seed = (post_index * 7) + comment_index post.comments.create!( diff --git a/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb b/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb index 26ef6a9c7b..fa872315e6 100644 --- a/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb +++ b/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb @@ -23,8 +23,10 @@ before do # The suite intentionally leaves `maintain_test_schema!` disabled, so make # this DB-backed spec self-sufficient: load the schema if it is not present - # yet. The table check is cheap and the load only runs on the first example. - unless ActiveRecord::Base.connection.table_exists?(:posts) + # yet. Check every table the examples touch (not just `posts`) so a partial + # schema can't slip past the guard. The check is cheap and the load only runs + # on the first example. + unless %i[users posts comments].all? { |t| ActiveRecord::Base.connection.table_exists?(t) } ActiveRecord::Schema.verbose = false load Rails.root.join("db/schema.rb") end @@ -51,6 +53,9 @@ expect(response).to have_http_status(:ok) + # Parse for the heading so we assert "Posts Page" renders specifically in an + #

(the page chrome), not just anywhere in the body. The seeded titles + # below are plain-text content, so a raw substring match is sufficient there. html = Nokogiri::HTML(response.body) expect(html.css("h1").map(&:text)).to include("Posts Page") expect(response.body).to include("Sentinel Post 1") From 7e4519bb5a139151bb07db3c4829e22929ab421c Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 4 Jun 2026 17:04:14 -1000 Subject: [PATCH 4/4] fix(benchmarks): harden seed guard and add seeds unit spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on PR #3615: - benchmark.yml: use `set -euo pipefail` and guard every table /posts_page reads (users and comments, not just posts) so a partial seed can no longer pass the benchmark job green. - Add spec/dummy/spec/db/seeds_spec.rb: loads db/seeds.rb with the Faker constant hidden (reproducing RAILS_ENV=production) and asserts the tables populate deterministically. This catches a Faker reintroduction or empty seed on every Pro dummy spec run — the layer that actually broke in #3602 previously had no automated coverage. - Trim over-verbose comments in benchmark.yml, seeds.rb, and posts_page_spec.rb. The CI-gating gap that lets Pro-dummy-only changes skip these specs is pre-existing and tracked separately in #3633. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/benchmark.yml | 15 +++-- react_on_rails_pro/spec/dummy/db/seeds.rb | 5 +- .../spec/dummy/spec/db/seeds_spec.rb | 66 +++++++++++++++++++ .../dummy/spec/requests/posts_page_spec.rb | 3 +- 4 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 react_on_rails_pro/spec/dummy/spec/db/seeds_spec.rb diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ec79f34ef4..312fe701a0 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -400,19 +400,22 @@ jobs: # benchmark scores as healthy). if: matrix.server_kind == 'rails' && matrix.pro_env == 'true' working-directory: ${{ matrix.app_directory }} - # Route the matrix value through env so the shell treats it as data, not - # code (GitHub Actions injection hardening), matching the "Execute - # benchmark suite" step below — even though it comes from our own - # generate_matrix.rb. + # Pass the matrix value via env (treated as data, not interpolated into + # the shell), matching the "Execute benchmark suite" step below. env: RAILS_ENV: production NODE_ENV: production SUITE_NAME: ${{ matrix.suite_name }} run: | - set -e + set -euo pipefail echo "🌱 Preparing and seeding $SUITE_NAME benchmark database..." bundle exec rails db:prepare - bundle exec rails runner 'count = Post.count; abort("Benchmark DB has 0 posts after db:prepare; seeds did not run") if count.zero?; puts "✅ Database seeded (#{count} posts)"' + # Guard every table `/posts_page` reads (posts, plus the users and + # comments it joins), not just posts: a partial seed would otherwise + # let the route 500 (or render an empty page) while the job stayed + # green. `set -o pipefail` keeps this abort fatal even if the command + # is ever piped. + bundle exec rails runner 'counts = { posts: Post.count, users: User.count, comments: Comment.count }; empty = counts.select { |_, c| c.zero? }.keys; abort("Benchmark DB seeding incomplete after db:prepare (empty: #{empty.join(", ")}); seeds did not run") if empty.any?; puts "✅ Database seeded (#{counts.map { |k, v| "#{v} #{k}" }.join(", ")})"' - name: Start Pro node renderer if: matrix.server_kind == 'node-renderer' diff --git a/react_on_rails_pro/spec/dummy/db/seeds.rb b/react_on_rails_pro/spec/dummy/db/seeds.rb index 4de1b03132..017660c43b 100644 --- a/react_on_rails_pro/spec/dummy/db/seeds.rb +++ b/react_on_rails_pro/spec/dummy/db/seeds.rb @@ -10,9 +10,8 @@ # Local variables (not top-level constants) so re-running `rails db:seed` in an # already-loaded process — seeds.rb is `load`ed, not `require`d — does not emit -# `warning: already initialized constant`. The per-record counts cycle through -# these arrays, which stays correct even if a range is later made exclusive or -# non-unit-step (unlike `range.first + (idx % range.size)`). +# `warning: already initialized constant`. Per-record counts index into these +# arrays, so cycling stays correct regardless of each range's bounds or step. user_count = 10 post_counts = (3..7).to_a comment_counts = (2..5).to_a diff --git a/react_on_rails_pro/spec/dummy/spec/db/seeds_spec.rb b/react_on_rails_pro/spec/dummy/spec/db/seeds_spec.rb new file mode 100644 index 0000000000..2cbc419110 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/spec/db/seeds_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "rails_helper" + +# Unit coverage for `db/seeds.rb`, the layer that actually broke in issue #3602. +# +# The Pro benchmark workflow runs `rails db:prepare` under RAILS_ENV=production, +# where the development/test-only `faker` gem is NOT in the bundle. If seeds.rb +# reintroduces a `Faker::...` call (or otherwise stops populating the tables), +# the production CI run leaves `/posts_page` with no data — exactly the #3602 +# failure. These examples reproduce that environment by hiding the `Faker` +# constant while loading the seed file, so such a regression fails here (on every +# Pro dummy spec run) instead of silently in the benchmark suite. +# +# Unlike posts_page_spec.rb this needs no node renderer — it only touches the DB. +RSpec.describe "db/seeds.rb" do + before do + # maintain_test_schema! is disabled in this suite (see posts_page_spec.rb), so + # load the schema if the tables the seeds touch are missing. + unless %i[users posts comments].all? { |t| ActiveRecord::Base.connection.table_exists?(t) } + ActiveRecord::Schema.verbose = false + load Rails.root.join("db/schema.rb") + end + end + + after do + Comment.delete_all + Post.delete_all + User.delete_all + end + + # seeds.rb is `load`ed (not `require`d) in production via db:prepare; do the + # same here. It clears the tables itself, so each load starts from a clean slate. + # Its progress `puts` output is captured to keep spec output quiet. + def load_seeds + original_stdout = $stdout + $stdout = StringIO.new + load Rails.root.join("db/seeds.rb") + ensure + $stdout = original_stdout + end + + it "populates users, posts, and comments without the faker gem" do + hide_const("Faker") + + expect { load_seeds }.not_to raise_error + + expect(User.count).to be_positive + expect(Post.count).to be_positive + expect(Comment.count).to be_positive + end + + it "produces identical data on every load (deterministic, reproducible benchmarks)" do + hide_const("Faker") + + load_seeds + first = { users: User.count, posts: Post.count, comments: Comment.count, + titles: Post.order(:id).pluck(:title) } + + load_seeds + second = { users: User.count, posts: Post.count, comments: Comment.count, + titles: Post.order(:id).pluck(:title) } + + expect(second).to eq(first) + end +end diff --git a/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb b/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb index fa872315e6..29d0cd9a34 100644 --- a/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb +++ b/react_on_rails_pro/spec/dummy/spec/requests/posts_page_spec.rb @@ -24,8 +24,7 @@ # The suite intentionally leaves `maintain_test_schema!` disabled, so make # this DB-backed spec self-sufficient: load the schema if it is not present # yet. Check every table the examples touch (not just `posts`) so a partial - # schema can't slip past the guard. The check is cheap and the load only runs - # on the first example. + # schema can't slip past the guard. unless %i[users posts comments].all? { |t| ActiveRecord::Base.connection.table_exists?(t) } ActiveRecord::Schema.verbose = false load Rails.root.join("db/schema.rb")