From 38635dace5cd4b10d132c0020da5337710128134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 18 May 2026 13:45:58 -0600 Subject: [PATCH 1/7] Add archived puzzle seeds with deterministic success rates Adds 7 archived puzzles: 5 with random low/mixed answer rates and 2 with 90% correct rate to support manual testing of the low success rate filter without needing production data. --- db/seeds.rb | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index b2e25fa..95e3a8e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -36,6 +36,71 @@ end end +# ====== Create Archived (already sent) Puzzle records ====== +archived_puzzles = [ + { + question: "Ruby or Rails provided this method? before_action :authenticate_user!", + answer: :rails, + explanation: "`before_action` is a Rails callback defined in `ActionController::Callbacks`. It runs specified methods before controller actions.", + sent_at: 7.days.ago, + high_success: false + }, + { + question: "Ruby or Rails provided this method? 42.times { puts 'hello' }", + answer: :ruby, + explanation: "`Integer#times` is a core Ruby method that iterates a block a specified number of times.", + sent_at: 6.days.ago, + high_success: false + }, + { + question: "Ruby or Rails provided this method? User.where(active: true).order(:name)", + answer: :rails, + explanation: "`where` and `order` are ActiveRecord query methods provided by Rails to build SQL queries.", + sent_at: 5.days.ago, + high_success: false + }, + { + question: "Ruby or Rails provided this method? 'hello world'.split(' ')", + answer: :ruby, + explanation: "`String#split` is a core Ruby method that divides a string into an array based on a delimiter.", + sent_at: 4.days.ago, + high_success: false + }, + { + question: "Ruby or Rails provided this method? flash[:notice] = 'Saved!'", + answer: :rails, + explanation: "`flash` is a Rails feature provided by `ActionDispatch::Flash` for passing messages between requests.", + sent_at: 3.days.ago, + high_success: false + }, + # high success rate puzzles (9/10 correct = 90%) -- should NOT appear in low success rate filter + { + question: "Ruby or Rails provided this method? [1, 2, 3].reduce(:+)", + answer: :ruby, + explanation: "`Enumerable#reduce` (also `inject`) is a core Ruby method that combines elements using a binary operation.", + sent_at: 2.days.ago, + high_success: true + }, + { + question: "Ruby or Rails provided this method? validates :email, presence: true, uniqueness: true", + answer: :rails, + explanation: "`validates` is an ActiveModel/ActiveRecord method from Rails that adds validation rules to models.", + sent_at: 1.day.ago, + high_success: true + } +] + +high_success_questions = archived_puzzles.select { |p| p[:high_success] }.map { |p| p[:question] } + +archived_puzzles.each do |p| + Puzzle.find_or_create_by!(question: p[:question]) do |puzzle| + puzzle.answer = p[:answer] + puzzle.explanation = p[:explanation] + puzzle.state = :archived + puzzle.sent_at = p[:sent_at] + end +end + # ====== Create the Server ====== server = Server.find_or_create_by!(server_id: 1179555097060061245) do |s| s.name = "OmbuTest" @@ -55,7 +120,7 @@ { user_id: 110, username: "user10", role: "member" } ] -users.each do |user_data| +users.each_with_index do |user_data, user_idx| user = User.find_or_create_by!(user_id: user_data[:user_id]) do |u| u.username = user_data[:username] u.role = user_data[:role] @@ -64,10 +129,12 @@ # Associate user with the server if not already linked user.servers << server unless user.servers.include?(server) - # Seed random answers for this user if they have none + # Seed random answers for approved puzzles if user has none if user.answers.where(server_id: server.id).empty? 3.times do - puzzle = Puzzle.all.sample + puzzle = Puzzle.approved.sample + next unless puzzle + Answer.find_or_create_by!( user_id: user.id, puzzle_id: puzzle.id, @@ -78,4 +145,18 @@ end end end + + # Seed answers for archived puzzles so correct_answer_percentage has data. + # High-success puzzles get 9/10 correct; others get random low/mixed results. + Puzzle.archived.each do |puzzle| + is_high_success = high_success_questions.include?(puzzle.question) + Answer.find_or_create_by!( + user_id: user.id, + puzzle_id: puzzle.id, + server_id: server.id + ) do |answer| + answer.choice = [ "ruby", "rails" ].sample + answer.is_correct = is_high_success ? user_idx < 9 : [ true, false ].sample + end + end end From 7cf727b50c89c172ca7efce1ec6e8032f42e94e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 19 May 2026 09:46:51 -0600 Subject: [PATCH 2/7] Address review feedback and add cloned puzzle seeds - Remove random answers for approved puzzles. Real users only answer puzzles after they've been sent (archived state). - Add two cloned puzzles so the 'hide cloned puzzles' filter from #78 has data to filter. --- db/seeds.rb | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 95e3a8e..b9d17c5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -101,6 +101,28 @@ end end +# ====== Clone a couple archived puzzles so the "hide cloned" filter has data ====== +cloned_source_questions = [ + "Ruby or Rails provided this method? before_action :authenticate_user!", + "Ruby or Rails provided this method? User.where(active: true).order(:name)" +] + +cloned_source_questions.each do |question| + parent = Puzzle.find_by(question: question) + next unless parent + next if Puzzle.where(original_puzzle_id: parent.id).exists? + + Puzzle.create!( + question: parent.question, + answer: parent.answer, + explanation: parent.explanation, + link: parent.link, + suggested_by: parent.suggested_by, + state: :pending, + original_puzzle: parent + ) +end + # ====== Create the Server ====== server = Server.find_or_create_by!(server_id: 1179555097060061245) do |s| s.name = "OmbuTest" @@ -129,24 +151,8 @@ # Associate user with the server if not already linked user.servers << server unless user.servers.include?(server) - # Seed random answers for approved puzzles if user has none - if user.answers.where(server_id: server.id).empty? - 3.times do - puzzle = Puzzle.approved.sample - next unless puzzle - - Answer.find_or_create_by!( - user_id: user.id, - puzzle_id: puzzle.id, - server_id: server.id - ) do |answer| - answer.choice = [ "ruby", "rails" ].sample - answer.is_correct = [ true, false ].sample - end - end - end - # Seed answers for archived puzzles so correct_answer_percentage has data. + # Real users only answer puzzles after they've been sent (archived state). # High-success puzzles get 9/10 correct; others get random low/mixed results. Puzzle.archived.each do |puzzle| is_high_success = high_success_questions.include?(puzzle.question) From a5e21b535f7ed88a206e930b97df8806df406da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 19 May 2026 09:49:22 -0600 Subject: [PATCH 3/7] Add archived-state cloned puzzles to seeds Mix two pending clones with two archived clones so the 'hide cloned' filter can be exercised across both states. --- db/seeds.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index b9d17c5..c680e9b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -101,14 +101,17 @@ end end -# ====== Clone a couple archived puzzles so the "hide cloned" filter has data ====== -cloned_source_questions = [ - "Ruby or Rails provided this method? before_action :authenticate_user!", - "Ruby or Rails provided this method? User.where(active: true).order(:name)" +# ====== Clone a few archived puzzles so the "hide cloned" filter has data ====== +# Mix of pending and archived clones to exercise both states. +cloned_sources = [ + { question: "Ruby or Rails provided this method? before_action :authenticate_user!", state: :pending }, + { question: "Ruby or Rails provided this method? User.where(active: true).order(:name)", state: :pending }, + { question: "Ruby or Rails provided this method? 42.times { puts 'hello' }", state: :archived, sent_at: 12.hours.ago }, + { question: "Ruby or Rails provided this method? flash[:notice] = 'Saved!'", state: :archived, sent_at: 6.hours.ago } ] -cloned_source_questions.each do |question| - parent = Puzzle.find_by(question: question) +cloned_sources.each do |source| + parent = Puzzle.find_by(question: source[:question]) next unless parent next if Puzzle.where(original_puzzle_id: parent.id).exists? @@ -118,7 +121,8 @@ explanation: parent.explanation, link: parent.link, suggested_by: parent.suggested_by, - state: :pending, + state: source[:state], + sent_at: source[:sent_at], original_puzzle: parent ) end From 36deac3423c4fe22cc2f38364a459377ce1de5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 19 May 2026 10:00:06 -0600 Subject: [PATCH 4/7] Isolate DailyPuzzleJob archived test from fixture interference The :one fixture is approved with no sent_at, so it's eligible for DailyPuzzleJob. The job picks one approved+unsent puzzle at random, which made this test flaky depending on which puzzle was picked. Clear the eligible pool before creating the puzzle under test, mirroring the pattern in 'does nothing when no approved unsent puzzles exist'. --- test/jobs/daily_puzzle_job_test.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/jobs/daily_puzzle_job_test.rb b/test/jobs/daily_puzzle_job_test.rb index c470ed0..129d60e 100644 --- a/test/jobs/daily_puzzle_job_test.rb +++ b/test/jobs/daily_puzzle_job_test.rb @@ -41,6 +41,8 @@ class DailyPuzzleJobTest < ActiveJob::TestCase end test "marks the selected puzzle as archived and sets sent_at" do + Puzzle.approved.where(sent_at: nil).delete_all + puzzle = Puzzle.create!( question: "Approved unsent puzzle question", answer: "ruby", From 275da56af67d5ac365d740f1dfc3181301848111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 19 May 2026 10:46:42 -0600 Subject: [PATCH 5/7] Make seed success rates deterministic and label clones - Replace high_success boolean with per-puzzle success_rate (20, 40, 50, 70, 80 for low; 90, 100 for high). user_idx < rate/10 makes the seeded correct/incorrect distribution exact across runs. - Append '(clone)' to cloned puzzle questions so they are easy to spot in the archived table during manual testing. - Clones inherit their parent's success_rate via original_puzzle lookup. --- db/seeds.rb | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index c680e9b..20758a4 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -37,60 +37,62 @@ end # ====== Create Archived (already sent) Puzzle records ====== +# Each puzzle has a fixed success_rate (% of 10 users who answer correctly). +# Rates <= 80 appear in the "low success rate" filter; rates > 80 do not. archived_puzzles = [ { question: "Ruby or Rails provided this method? before_action :authenticate_user!", answer: :rails, explanation: "`before_action` is a Rails callback defined in `ActionController::Callbacks`. It runs specified methods before controller actions.", sent_at: 7.days.ago, - high_success: false + success_rate: 20 }, { question: "Ruby or Rails provided this method? 42.times { puts 'hello' }", answer: :ruby, explanation: "`Integer#times` is a core Ruby method that iterates a block a specified number of times.", sent_at: 6.days.ago, - high_success: false + success_rate: 40 }, { question: "Ruby or Rails provided this method? User.where(active: true).order(:name)", answer: :rails, explanation: "`where` and `order` are ActiveRecord query methods provided by Rails to build SQL queries.", sent_at: 5.days.ago, - high_success: false + success_rate: 50 }, { question: "Ruby or Rails provided this method? 'hello world'.split(' ')", answer: :ruby, explanation: "`String#split` is a core Ruby method that divides a string into an array based on a delimiter.", sent_at: 4.days.ago, - high_success: false + success_rate: 70 }, { question: "Ruby or Rails provided this method? flash[:notice] = 'Saved!'", answer: :rails, explanation: "`flash` is a Rails feature provided by `ActionDispatch::Flash` for passing messages between requests.", sent_at: 3.days.ago, - high_success: false + success_rate: 80 }, - # high success rate puzzles (9/10 correct = 90%) -- should NOT appear in low success rate filter + # high success rate puzzles -- should NOT appear in the low success rate filter (rate > 80) { question: "Ruby or Rails provided this method? [1, 2, 3].reduce(:+)", answer: :ruby, explanation: "`Enumerable#reduce` (also `inject`) is a core Ruby method that combines elements using a binary operation.", sent_at: 2.days.ago, - high_success: true + success_rate: 90 }, { question: "Ruby or Rails provided this method? validates :email, presence: true, uniqueness: true", answer: :rails, explanation: "`validates` is an ActiveModel/ActiveRecord method from Rails that adds validation rules to models.", sent_at: 1.day.ago, - high_success: true + success_rate: 100 } ] -high_success_questions = archived_puzzles.select { |p| p[:high_success] }.map { |p| p[:question] } +success_rate_by_question = archived_puzzles.to_h { |p| [ p[:question], p[:success_rate] ] } archived_puzzles.each do |p| Puzzle.find_or_create_by!(question: p[:question]) do |puzzle| @@ -116,7 +118,7 @@ next if Puzzle.where(original_puzzle_id: parent.id).exists? Puzzle.create!( - question: parent.question, + question: "#{parent.question} (clone)", answer: parent.answer, explanation: parent.explanation, link: parent.link, @@ -157,16 +159,19 @@ # Seed answers for archived puzzles so correct_answer_percentage has data. # Real users only answer puzzles after they've been sent (archived state). - # High-success puzzles get 9/10 correct; others get random low/mixed results. + # Each puzzle's success_rate is deterministic: user_idx < rate/10 means correct, + # so a rate of 20 yields exactly 2/10 correct, 80 yields 8/10, etc. Puzzle.archived.each do |puzzle| - is_high_success = high_success_questions.include?(puzzle.question) + base_question = puzzle.original_puzzle&.question || puzzle.question + rate = success_rate_by_question.fetch(base_question, 50) + Answer.find_or_create_by!( user_id: user.id, puzzle_id: puzzle.id, server_id: server.id ) do |answer| answer.choice = [ "ruby", "rails" ].sample - answer.is_correct = is_high_success ? user_idx < 9 : [ true, false ].sample + answer.is_correct = user_idx < (rate / 10) end end end From 8ae3f5d9c5104128898ebaa5d486d22581acfe8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 19 May 2026 10:51:04 -0600 Subject: [PATCH 6/7] Extract answer seeding into its own per-puzzle loop Now that each archived puzzle carries an explicit success_rate, the correct-answer distribution no longer needs to ride along inside the user loop. Iterating per puzzle and picking the first N server users per puzzle reads more directly: 'for each puzzle, mark rate/10 users correct'. The user loop is now purely about creating users and linking them to the server. --- db/seeds.rb | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 20758a4..06a9ef9 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -148,7 +148,7 @@ { user_id: 110, username: "user10", role: "member" } ] -users.each_with_index do |user_data, user_idx| +users.each do |user_data| user = User.find_or_create_by!(user_id: user_data[:user_id]) do |u| u.username = user_data[:username] u.role = user_data[:role] @@ -156,22 +156,26 @@ # Associate user with the server if not already linked user.servers << server unless user.servers.include?(server) +end + +# ====== Seed answers for archived puzzles ====== +# Real users only answer puzzles after they've been sent (archived state). +# For each archived puzzle, mark the first `success_rate / 10` users correct +# and the rest incorrect, so each puzzle hits its target rate exactly. +server_users = server.users.order(:id) - # Seed answers for archived puzzles so correct_answer_percentage has data. - # Real users only answer puzzles after they've been sent (archived state). - # Each puzzle's success_rate is deterministic: user_idx < rate/10 means correct, - # so a rate of 20 yields exactly 2/10 correct, 80 yields 8/10, etc. - Puzzle.archived.each do |puzzle| - base_question = puzzle.original_puzzle&.question || puzzle.question - rate = success_rate_by_question.fetch(base_question, 50) +Puzzle.archived.each do |puzzle| + base_question = puzzle.original_puzzle&.question || puzzle.question + correct_count = success_rate_by_question.fetch(base_question, 50) / 10 + server_users.each_with_index do |user, idx| Answer.find_or_create_by!( user_id: user.id, puzzle_id: puzzle.id, server_id: server.id ) do |answer| answer.choice = [ "ruby", "rails" ].sample - answer.is_correct = user_idx < (rate / 10) + answer.is_correct = idx < correct_count end end end From 8b4a570d22dc302d319bc507310527c39bdb87c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 19 May 2026 10:52:33 -0600 Subject: [PATCH 7/7] Merge regular and archived puzzles into a single seed collection Both groups already had the same shape; archived just carried extra fields (state, sent_at, success_rate). Folding them into one array removes the duplicated find_or_create_by! loop and gives a single place to add new puzzles. Entries without an explicit state fall back to the column default (:pending), preserving existing behavior. --- db/seeds.rb | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 06a9ef9..1df51e8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,6 +1,9 @@ # db/seeds.rb # ====== Create Puzzle records ====== +# Archived entries carry `sent_at` and a fixed `success_rate` (% of 10 users +# who answer correctly). Rates <= 80 appear in the "low success rate" filter; +# rates > 80 do not. Entries without `state` default to :pending. puzzles = [ { question: "Ruby or Rails provided this method? Array.new(5) { |i| i * 2 }", @@ -26,24 +29,12 @@ question: "Ruby or Rails provided this method? params[:id]", answer: :rails, explanation: "`params[:id]` is used in Rails to fetch query parameters or URL parameters in controller actions." - } -] - -puzzles.each do |p| - Puzzle.find_or_create_by!(question: p[:question]) do |puzzle| - puzzle.answer = p[:answer] - puzzle.explanation = p[:explanation] - end -end - -# ====== Create Archived (already sent) Puzzle records ====== -# Each puzzle has a fixed success_rate (% of 10 users who answer correctly). -# Rates <= 80 appear in the "low success rate" filter; rates > 80 do not. -archived_puzzles = [ + }, { question: "Ruby or Rails provided this method? before_action :authenticate_user!", answer: :rails, explanation: "`before_action` is a Rails callback defined in `ActionController::Callbacks`. It runs specified methods before controller actions.", + state: :archived, sent_at: 7.days.ago, success_rate: 20 }, @@ -51,6 +42,7 @@ question: "Ruby or Rails provided this method? 42.times { puts 'hello' }", answer: :ruby, explanation: "`Integer#times` is a core Ruby method that iterates a block a specified number of times.", + state: :archived, sent_at: 6.days.ago, success_rate: 40 }, @@ -58,6 +50,7 @@ question: "Ruby or Rails provided this method? User.where(active: true).order(:name)", answer: :rails, explanation: "`where` and `order` are ActiveRecord query methods provided by Rails to build SQL queries.", + state: :archived, sent_at: 5.days.ago, success_rate: 50 }, @@ -65,6 +58,7 @@ question: "Ruby or Rails provided this method? 'hello world'.split(' ')", answer: :ruby, explanation: "`String#split` is a core Ruby method that divides a string into an array based on a delimiter.", + state: :archived, sent_at: 4.days.ago, success_rate: 70 }, @@ -72,14 +66,15 @@ question: "Ruby or Rails provided this method? flash[:notice] = 'Saved!'", answer: :rails, explanation: "`flash` is a Rails feature provided by `ActionDispatch::Flash` for passing messages between requests.", + state: :archived, sent_at: 3.days.ago, success_rate: 80 }, - # high success rate puzzles -- should NOT appear in the low success rate filter (rate > 80) { question: "Ruby or Rails provided this method? [1, 2, 3].reduce(:+)", answer: :ruby, explanation: "`Enumerable#reduce` (also `inject`) is a core Ruby method that combines elements using a binary operation.", + state: :archived, sent_at: 2.days.ago, success_rate: 90 }, @@ -87,18 +82,21 @@ question: "Ruby or Rails provided this method? validates :email, presence: true, uniqueness: true", answer: :rails, explanation: "`validates` is an ActiveModel/ActiveRecord method from Rails that adds validation rules to models.", + state: :archived, sent_at: 1.day.ago, success_rate: 100 } ] -success_rate_by_question = archived_puzzles.to_h { |p| [ p[:question], p[:success_rate] ] } +success_rate_by_question = puzzles.each_with_object({}) do |p, h| + h[p[:question]] = p[:success_rate] if p[:success_rate] +end -archived_puzzles.each do |p| +puzzles.each do |p| Puzzle.find_or_create_by!(question: p[:question]) do |puzzle| puzzle.answer = p[:answer] puzzle.explanation = p[:explanation] - puzzle.state = :archived + puzzle.state = p[:state] if p[:state] puzzle.sent_at = p[:sent_at] end end