Skip to content

feat(myapp): upgrade to Rails 8.1 with Phlex + daisyUI and add locksmith tests#929

Merged
mhenrixon merged 15 commits intomainfrom
feature/myapp-upgrade-and-locksmith-testing
Mar 27, 2026
Merged

feat(myapp): upgrade to Rails 8.1 with Phlex + daisyUI and add locksmith tests#929
mhenrixon merged 15 commits intomainfrom
feature/myapp-upgrade-and-locksmith-testing

Conversation

@mhenrixon
Copy link
Copy Markdown
Owner

@mhenrixon mhenrixon commented Mar 27, 2026

Summary

  • Upgrade myapp from Rails 7.0.6 to Rails 8.1.3 with modern frontend stack
  • Phlex + daisyUI component-based views replacing ERB/Slim, following patterns from daisyui and cosmos reference projects
  • Lock testing dashboard for debugging sidekiq-unique-jobs before gem release
  • 108 new tests covering the queue_and_lock Lua script and lock_sync! fast path

myapp Upgrade

Before After
Rails 7.0 Rails 8.1
Tailwind 3 + postcss Tailwind 4 + @tailwindcss/cli
ERB/Slim views Phlex components
sprockets + cssbundling propshaft
Devise auth Removed (unnecessary for test app)
sinatra (Sidekiq Web) Native Sidekiq Web
yarn bun

Lock Testing Dashboard

  • Index page showing all 9 demo job types with lock metadata
  • One-click enqueue buttons (x1, x3, x5) per job type
  • Active locks table with digest, worker, type, queue
  • Detail view per job with full Redis lock state (queued/primed/locked/PTTL)
  • Flush all locks button for clean testing

Test Coverage for Performance Optimizations

  • queue_and_lock_spec.rb (33 tests): single-lock fast path, multi-lock path, re-lock idempotency, limit enforcement, TTL/PEXPIRE, all lock types, score handling
  • locksmith_spec.rb additions (11 tests): lock_sync! sync_locked state, lock_info, TTL, multi-limit
  • fast_path_spec.rb (38 tests): integration tests for fast path across all lock types (until_executed, until_executing, until_expired, while_executing, until_and_while_executing), blocking vs sync path verification

Test plan

  • bundle exec rspec spec/sidekiq_unique_jobs/lua/queue_and_lock_spec.rb — 33 pass
  • bundle exec rspec spec/sidekiq_unique_jobs/locksmith_spec.rb — 37 pass
  • bundle exec rspec spec/sidekiq_unique_jobs/locksmith/fast_path_spec.rb — 38 pass
  • bundle exec rubocop — no offenses on new files
  • Rails 8.1.3 boots and routes load correctly
  • CSS builds with Tailwind 4 + daisyUI 5

Summary by CodeRabbit

  • New Features

    • Lock Testing dashboard: per-job details, enqueue controls, flush-all action, navbar link to Sidekiq Web, and auto-dismissing flash messages.
  • Refactor

    • Moved to component/layout-based views with Tailwind + DaisyUI theming and Stimulus wiring; app root now shows the locks dashboard.
    • Upgraded Rails/Ruby baseline and simplified authentication surface (Devise UI removed).
  • Tests

    • Added extensive specs covering lock behavior, Lua scripts, and dashboard request/system tests.

…ith tests

## Summary
- Upgrade myapp from Rails 7.0 to Rails 8.1.3 with Phlex views and daisyUI components
- Replace ERB/Slim views with Phlex components following patterns from daisyui/cosmos repos
- Build a lock testing/debugging dashboard for sidekiq-unique-jobs
- Add comprehensive test coverage for queue_and_lock Lua script (33 tests)
- Add lock_sync! fast path tests to locksmith_spec (11 tests)
- Add fast path integration tests across all lock types (38 tests)

## myapp Changes
- Rails 7.0 -> 8.1, Tailwind 3 -> 4, sprockets -> propshaft
- Added phlex-rails and daisyui gem for component-based views
- New LocksController with dashboard showing all lock types
- Phlex components: ApplicationLayout, Navbar, IndexView, ShowView
- Removed Devise (unnecessary for test app), Slim, sinatra, cssbundling
- Lock dashboard: enqueue jobs, view active locks, inspect lock state

## Test Coverage
- spec/sidekiq_unique_jobs/lua/queue_and_lock_spec.rb: tests single-lock
  fast path, multi-lock path, TTL handling, all lock types, lock_score
- spec/sidekiq_unique_jobs/locksmith_spec.rb: lock_sync! method tests
  covering sync_locked state, idempotency, lock_info, TTL, multi-limit
- spec/sidekiq_unique_jobs/locksmith/fast_path_spec.rb: integration tests
  for all lock types via the combined queue_and_lock path
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Rails load defaults bumped to 8.1; Devise and its views/helpers/initializer removed; ERB layouts/partials replaced by Phlex components/layout; JS/CSS toolchain migrated to Bun + Tailwind v4 + DaisyUI; new Locks feature added to inspect/manage Sidekiq Unique Jobs (controller, Phlex views, routes); Sidekiq/lock-focused tests added.

Changes

Cohort / File(s) Summary
Linter & tooling configs
myapp/.erb-lint.yml, myapp/.eslintignore, myapp/.eslintrc.js, myapp/postcss.config.js, myapp/stylelint.config.js, myapp/tailwind.config.js
Removed legacy linting/PostCSS/Tailwind configuration files, deleting previously enforced lint rules and PostCSS/Tailwind runtime config.
Package & tool versions
myapp/package.json, myapp/.bun-version, myapp/.tool-versions, myapp/.ruby-version, myapp/Procfile.dev
Switched to Bun (pinned), updated package scripts/devDependencies for bunx/Tailwind v4/DaisyUI, adjusted tool-version pins, and changed Procfile.dev CSS watcher to Bun.
Gemfile & Ruby/Rails
myapp/Gemfile
Updated Ruby/Rails constraints (>= 3.3, >= 8.1.0), added/removed gems (propshaft, phlex-rails, daisyui via GitHub, web-console), and adjusted bootsnap and tzinfo-data settings.
Rails core config & schema
myapp/config/application.rb, myapp/config/environments/*, myapp/config/initializers/assets.rb, myapp/db/schema.rb
Load defaults moved to 8.1; added autoload paths for app/views/layouts/components; replaced cache_classes with enable_reloading variants; assets path updated to include app/assets/builds; schema updated to ActiveRecord::Schema[8.1] and pg_catalog.plpgsql.
Devise removal
myapp/config/initializers/devise.rb, myapp/app/helpers/devise_helper.rb, myapp/app/views/devise/**, myapp/app/models/user.rb
Removed Devise initializer, helper module, all Devise views/mailers/partials, and removed devise call from User model—authentication configuration and views deleted.
View layer → Phlex
myapp/app/views/layouts/application.html.erb, myapp/app/views/layouts/application_layout.rb, myapp/app/views/components/*
Replaced ERB layout/partials with Phlex components and layout classes (ApplicationComponent/ApplicationView, ApplicationLayout, Navbar, Footer, Locks views).
Home & Issues cleanup
myapp/app/controllers/home_controller.rb, myapp/app/helpers/home_helper.rb, myapp/app/views/home/index.html.slim, myapp/app/controllers/issues_controller.rb
Removed Home and Issues controllers, helper and view templates; related specs/factories removed or retargeted; root changed to Locks dashboard.
Locks feature
myapp/app/controllers/locks_controller.rb, myapp/app/views/locks/index_view.rb, myapp/app/views/locks/show_view.rb
Added LocksController (index, show, enqueue, flush) and Phlex views to inspect Sidekiq Unique Jobs digests, compute lock metadata (queued/primed/locked/pttl), enqueue demo jobs, and flush digests.
Sidekiq / Redis changes
myapp/config/initializers/sidekiq.rb, myapp/app/jobs/issue_432_job.rb, myapp/app/jobs/until_executing_job.rb
Removed top-level REDIS constant; enabled SidekiqUniqueJobs lock_info; Issue432Job now uses RedisClient + redis.call; job renamed/configured to UntilExecutingJob with lock: :until_executing and lock_timeout: 0.
JS / Stimulus updates
myapp/app/javascript/application.js, myapp/app/javascript/controllers/*, myapp/config/importmap.rb
Removed Turbo global exposure and mrujs/request.js imports; added Stimulus application entry, eager controller loader and flash_controller; importmap pinned local Hotwire minified files and pin_all_from "app/javascript/controllers".
CSS / Tailwind
myapp/app/assets/stylesheets/application.tailwind.css
Switched Tailwind entry to @import "tailwindcss", added DaisyUI plugin/themes and Tailwind plugins, content source globs (including gem files), theme customizations, and small component style overrides.
Routes & Sidekiq mounting
myapp/config/routes.rb
Removed Devise routes and prior admin-protected Sidekiq mount; mounted Sidekiq Web unconditionally; added locks resource with enqueue (POST) and flush (DELETE) collection actions; root set to locks#index.
Controller/layout wiring
myapp/app/controllers/application_controller.rb
Removed protect_from_forgery with: :exception; set layout via layout -> { ApplicationLayout }.
Tests
spec/**/*, spec/sidekiq_unique_jobs/**/*, myapp/spec/**/*
Removed Devise/home-related factories/specs; updated request/system specs to target Locks dashboard; added comprehensive Locksmith and Lua script specs validating lock acquisition/release and fast-path behaviors.
Other scripts/seeds
myapp/bin/dev, myapp/db/seeds.rb
bin/dev rewritten to prefer hivemind/foreman with safer shell handling; seeds file stripped of admin user creation and documented as unnecessary for demo jobs.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser
  participant Rails as Rails App
  participant Sidekiq as Sidekiq
  participant Redis as Redis

  Browser->>Rails: GET /locks or GET /locks/:id
  Rails->>Redis: read digests / digest keys
  Redis-->>Rails: digests + lock metadata
  Rails-->>Browser: render Phlex Locks::IndexView/ShowView

  Browser->>Rails: POST /locks/enqueue (job_name,count)
  Rails->>Sidekiq: enqueue perform_async(job_class, args) x N
  Sidekiq->>Redis: push job + SidekiqUniqueJobs updates
  Redis-->>Sidekiq: ACK
  Rails-->>Browser: redirect with notice

  Browser->>Rails: DELETE /locks/flush
  Rails->>Redis: delete digests by pattern ("*")
  Redis-->>Rails: deletion result
  Rails-->>Browser: redirect with notice
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰
I nibbled old ERB, then hopped to Phlex light,
Tailwind petals dancing, Bun whiskers bright,
Locks and digests shimmer under Redis night,
I queued a tiny job and watched it take flight,
A rabbit claps — the dashboard looks just right.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main changes: Rails upgrade to 8.1, adoption of Phlex + daisyUI for views, and addition of locksmith tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread myapp/app/controllers/locks_controller.rb Dismissed
Comment thread myapp/app/controllers/locks_controller.rb Fixed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (3)
spec/sidekiq_unique_jobs/locksmith/fast_path_spec.rb (1)

44-48: Consider clarifying the conditional expectation.

The conditional if lock_ttl.nil? on line 47 makes the test behavior context-dependent. Consider splitting this into separate examples or adding a comment explaining why until_expired locks remain locked after unlock.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/sidekiq_unique_jobs/locksmith/fast_path_spec.rb` around lines 44 - 48,
The conditional expectation makes the test ambiguous; split into two clear
examples (or add an explanatory comment) around locksmith_one.lock /
locksmith_one.unlock: create one example for the case when lock_ttl is nil that
asserts expect(locksmith_one).not_to be_locked, and another example for the
"until_expired" case that asserts the lock remains locked after unlock
(expect(locksmith_one).to be_locked) or add a short comment above the current
expectation explaining why locks with lock_ttl (until_expired) remain locked
after unlock; reference the locksmith_one.lock, locksmith_one.unlock, and
lock_ttl/ until_expired symbols to locate the logic to change.
spec/sidekiq_unique_jobs/lua/queue_and_lock_spec.rb (1)

262-274: Test title is misleading — only verifies combined path, not comparison.

The test is titled "produces the same locked state as queue -> lmove -> lock" but only exercises the combined queue_and_lock script without actually testing the separate scripts for comparison. Consider either:

  1. Renaming to reflect what it actually tests (e.g., "correctly sets locked state")
  2. Adding the actual comparison with separate script calls
♻️ Proposed fix to clarify test intent
-  context "when compared with separate queue+lock scripts" do
-    it "produces the same locked state as queue -> lmove -> lock" do
-      # Combined path
-      result_combined = call_script(:queue_and_lock, key.to_a, argv_one)
-      combined_locked = hget(key.locked, job_id_one)
-      combined_digest = get(key.digest)
-
-      # Verify the combined script locked correctly
-      expect(result_combined).to eq(job_id_one)
-      expect(combined_locked).not_to be_nil
-      expect(combined_digest).to eq(job_id_one)
-    end
-  end
+  context "with combined script verification" do
+    it "correctly sets locked state" do
+      result = call_script(:queue_and_lock, key.to_a, argv_one)
+
+      expect(result).to eq(job_id_one)
+      expect(hget(key.locked, job_id_one)).not_to be_nil
+      expect(get(key.digest)).to eq(job_id_one)
+    end
+  end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/sidekiq_unique_jobs/lua/queue_and_lock_spec.rb` around lines 262 - 274,
The test "produces the same locked state as queue -> lmove -> lock" only calls
the combined script (queue_and_lock) and asserts its effects (result_combined,
combined_locked via hget(key.locked, job_id_one), combined_digest via
get(key.digest)), so either rename the example to reflect it only verifies the
combined path (e.g., "correctly sets locked state") or extend the test to
actually perform the separate steps (call_script for queue, then simulate lmove,
then call_script for lock) and compare their resulting states to the combined
run (compare the locked hash and digest values and the return value job_id_one).
Make the change around the existing call_script(:queue_and_lock, key.to_a,
argv_one) and assertions for result_combined, combined_locked, and
combined_digest.
myapp/app/controllers/locks_controller.rb (1)

101-104: Destructive action: consider adding confirmation or scoping.

The flush action deletes ALL lock digests across the entire system using a wildcard pattern "*". While appropriate for a testing dashboard, consider:

  1. Adding a confirmation step in the UI
  2. Logging who triggered the flush for audit purposes
📝 Proposed enhancement to add logging
 def flush
+  Rails.logger.info("Lock flush triggered from #{request.remote_ip}")
   SidekiqUniqueJobs::Digests.new.delete_by_pattern("*")
   redirect_to locks_path, notice: "All lock digests flushed"
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@myapp/app/controllers/locks_controller.rb` around lines 101 - 104, The flush
action currently calls SidekiqUniqueJobs::Digests.new.delete_by_pattern("*") in
the flush method which indiscriminately removes all digests; change this to
require explicit confirmation and scope the deletion (e.g., accept a scoped
pattern or specific lock IDs via params and validate a confirmation param) and
record an audit entry (e.g., log current_user.id/email, timestamp and the
pattern used) before performing deletions; update the flush controller method
(flush) to check for confirmation/admin permission, derive a safe pattern from
params (not raw "*"), call delete_by_pattern only with that safe pattern, and
write an audit log entry via Rails.logger or an Audit model prior to redirect_to
locks_path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@myapp/app/assets/stylesheets/application.tailwind.css`:
- Around line 4-21: Stylelint is flagging valid Tailwind v4 at-rules like
`@plugin`, `@source`, and `@theme` because the inherited at-rule-no-unknown rule from
stylelint-config-standard-scss doesn't recognize them; update your linter config
(the .stylelintrc.json rule "at-rule-no-unknown") to either set it to null or
configure it to ignore these at-rules (add "plugin", "source", "theme", and
optionally "import" to ignoreAtRules) so Tailwind directives pass validation.

In `@myapp/app/controllers/locks_controller.rb`:
- Line 4: Remove the unsafe CSRF bypass by deleting the skip_before_action
:verify_authenticity_token, only: [:enqueue, :flush] line and re-enable Rails'
CSRF protection for the enqueue and flush actions; ensure the frontend includes
and sends the Rails CSRF token for AJAX/Turbo requests so the controller methods
enqueue and flush continue to work normally.
- Around line 74-82: The enqueue action currently calls safe_constantize on
params[:job_name] without checking an allowlist; validate job_name against the
DEMO_JOBS allowlist first (e.g., ensure DEMO_JOBS.include?(job_name)) and
reject/redirect if not allowed, then call safe_constantize to obtain job_class;
update the conditional that handles unknown jobs to cover both not-in-allowlist
and safe_constantize returning nil so arbitrary classes in initialize are never
resolved for unapproved job names.

In `@myapp/app/jobs/issue_432_job.rb`:
- Around line 38-41: The log method currently builds the diagnostic suffix
string (using Time.now.to_i and redis.call('GET', 'start') / redis.call('GET',
'counter')) and returns it, but callers ignore the return value; change log to
Rails.logger.debug the timing/counter string instead of returning it (e.g.,
compose the suffix into a local variable and call Rails.logger.debug with it)
and then return nil or the original intended value so the diagnostic info is
actually written to logs; update the method named log to use Rails.logger.debug
for the suffix rather than relying on its return.

In `@myapp/app/views/locks/index_view.rb`:
- Around line 100-105: The enqueue_button helper renders unlabeled buttons;
update the enqueue_button method to provide an accessible name by adding an
aria-label (or hidden descriptive text) that includes both the job_name and
count (e.g. "Enqueue 3 jobs for <job_name>") so screen readers can distinguish
controls; modify the call that builds the button_to (the enqueue_button helper
referencing enqueue_locks_path) to include aria: { label: "..."} or insert
visually-hidden text inside the block, and handle singular/plural wording if
desired.

In `@myapp/config/application.rb`:
- Around line 28-30: The autoload entries for Phlex roots (config.autoload_paths
<< "#{root}/app/views", "#{root}/app/views/layouts",
"#{root}/app/views/components") must also be added to config.eager_load_paths so
view classes like IndexView, ShowView, ApplicationLayout, Navbar, etc. are
present when eager loading in production; update the same file to append those
three paths to config.eager_load_paths (using the same "#{root}/app/..."
strings) so they are eagerly loaded.

In `@myapp/config/initializers/assets.rb`:
- Around line 3-4: Update the misleading comment above the assets path addition:
replace the statement that Propshaft "automatically" serves assets with a clear
note that the path is being added manually because Propshaft does not include
app/assets/builds by default (we're adding Rails.application.config.assets.paths
<< Rails.root.join("app", "assets", "builds") manually, e.g., because
cssbundling-rails is not used and `@tailwindcss/cli` is used directly).

In `@myapp/config/routes.rb`:
- Around line 4-6: The Sidekiq::Web route is exposed without authentication;
wrap the Sidekiq::Web mount in basic HTTP auth to protect /sidekiq (modify the
mount in routes.rb where Sidekiq::Web is referenced). Implement
Rack::Auth::Basic (or a Rails routing constraint) that checks credentials
sourced from environment vars (e.g. SIDEKIQ_USERNAME and SIDEKIQ_PASSWORD) and
returns 401 on failure, ensuring the authentication is applied before calling
mount Sidekiq::Web so only authorized users can access the web UI.

In `@myapp/Gemfile`:
- Around line 7-26: Your Gemfile removed Devise but the User model still invokes
the Devise DSL (the call starting with "devise :database_authenticatable,
:registerable, ...") so the app will crash on boot; open the User model (class
User) and either re-add the devise gem to the Gemfile or remove the Devise DSL
invocation and any Devise-specific callbacks/concerns (e.g., the "devise" call
and related methods), and update/replace any Devise-dependent logic
(validations, scopes, modules) to use your new authentication approach so the
model no longer references Devise symbols.

In `@myapp/spec/requests/home_spec.rb`:
- Around line 5-33: Wrap the "Locks Dashboard" request spec group in a
SidekiqUniqueJobs.use_config block and set SidekiqUniqueJobs.config.enabled =
false for the duration of the group so global uniqueness and Redis state are
disabled during these examples; locate the RSpec.describe "Locks Dashboard"
block and modify it to run inside SidekiqUniqueJobs.use_config {
SidekiqUniqueJobs.config.enabled = false; ... } ensuring the config change is
scoped to the block (restored automatically) so the GET "/" and GET "/locks/:id"
examples run with uniqueness disabled.

In `@myapp/spec/system/static_pages_spec.rb`:
- Around line 5-9: Wrap the smoke spec so uniqueness is temporarily disabled
using SidekiqUniqueJobs.use_config and setting SidekiqUniqueJobs.config.enabled
= false for the duration of the test; locate the "Lock Testing Dashboard"
example (the RSpec.describe block and the it "can browse the dashboard" example)
and update the test body to run the visit "/" and expectation inside
SidekiqUniqueJobs.use_config { SidekiqUniqueJobs.config.enabled = false; ... }
so uniqueness state does not leak into this non-uniqueness spec.

In `@spec/sidekiq_unique_jobs/locksmith_spec.rb`:
- Around line 428-435: The test currently only checks the block ran but doesn't
fail if execute falls back to the synchronous path; update the spec to spy/stub
the sync-only method (lock_sync!) on the locksmith instance (locksmith_one)
before calling execute, then assert that lock_sync! was not called while still
asserting the block executed; reference the execute method and the lock_sync!
method on locksmith_one so the test will fail if execute regresses to the sync
path.

---

Nitpick comments:
In `@myapp/app/controllers/locks_controller.rb`:
- Around line 101-104: The flush action currently calls
SidekiqUniqueJobs::Digests.new.delete_by_pattern("*") in the flush method which
indiscriminately removes all digests; change this to require explicit
confirmation and scope the deletion (e.g., accept a scoped pattern or specific
lock IDs via params and validate a confirmation param) and record an audit entry
(e.g., log current_user.id/email, timestamp and the pattern used) before
performing deletions; update the flush controller method (flush) to check for
confirmation/admin permission, derive a safe pattern from params (not raw "*"),
call delete_by_pattern only with that safe pattern, and write an audit log entry
via Rails.logger or an Audit model prior to redirect_to locks_path.

In `@spec/sidekiq_unique_jobs/locksmith/fast_path_spec.rb`:
- Around line 44-48: The conditional expectation makes the test ambiguous; split
into two clear examples (or add an explanatory comment) around
locksmith_one.lock / locksmith_one.unlock: create one example for the case when
lock_ttl is nil that asserts expect(locksmith_one).not_to be_locked, and another
example for the "until_expired" case that asserts the lock remains locked after
unlock (expect(locksmith_one).to be_locked) or add a short comment above the
current expectation explaining why locks with lock_ttl (until_expired) remain
locked after unlock; reference the locksmith_one.lock, locksmith_one.unlock, and
lock_ttl/ until_expired symbols to locate the logic to change.

In `@spec/sidekiq_unique_jobs/lua/queue_and_lock_spec.rb`:
- Around line 262-274: The test "produces the same locked state as queue ->
lmove -> lock" only calls the combined script (queue_and_lock) and asserts its
effects (result_combined, combined_locked via hget(key.locked, job_id_one),
combined_digest via get(key.digest)), so either rename the example to reflect it
only verifies the combined path (e.g., "correctly sets locked state") or extend
the test to actually perform the separate steps (call_script for queue, then
simulate lmove, then call_script for lock) and compare their resulting states to
the combined run (compare the locked hash and digest values and the return value
job_id_one). Make the change around the existing call_script(:queue_and_lock,
key.to_a, argv_one) and assertions for result_combined, combined_locked, and
combined_digest.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d70b4ab3-db55-4ac0-b427-0b6d5f503982

📥 Commits

Reviewing files that changed from the base of the PR and between 5818b81 and 2a2fefc.

⛔ Files ignored due to path filters (3)
  • myapp/bun.lock is excluded by !**/*.lock
  • myapp/package-lock.json is excluded by !**/package-lock.json
  • myapp/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (63)
  • myapp/.erb-lint.yml
  • myapp/.eslintignore
  • myapp/.eslintrc.js
  • myapp/Gemfile
  • myapp/Procfile.dev
  • myapp/app/assets/stylesheets/application.tailwind.css
  • myapp/app/controllers/application_controller.rb
  • myapp/app/controllers/home_controller.rb
  • myapp/app/controllers/issues_controller.rb
  • myapp/app/controllers/locks_controller.rb
  • myapp/app/helpers/devise_helper.rb
  • myapp/app/helpers/home_helper.rb
  • myapp/app/javascript/application.js
  • myapp/app/javascript/controllers/application.js
  • myapp/app/javascript/controllers/flash_controller.js
  • myapp/app/javascript/controllers/index.js
  • myapp/app/jobs/issue_432_job.rb
  • myapp/app/jobs/until_executing_job.rb
  • myapp/app/views/application/_header.html.erb
  • myapp/app/views/components/application_component.rb
  • myapp/app/views/components/application_view.rb
  • myapp/app/views/components/navbar.rb
  • myapp/app/views/devise/confirmations/new.html.erb
  • myapp/app/views/devise/mailer/confirmation_instructions.html.erb
  • myapp/app/views/devise/mailer/password_change.html.erb
  • myapp/app/views/devise/mailer/reset_password_instructions.html.erb
  • myapp/app/views/devise/mailer/unlock_instructions.html.erb
  • myapp/app/views/devise/passwords/edit.html.erb
  • myapp/app/views/devise/passwords/new.html.erb
  • myapp/app/views/devise/registrations/edit.html.erb
  • myapp/app/views/devise/registrations/new.html.erb
  • myapp/app/views/devise/sessions/new.html.erb
  • myapp/app/views/devise/shared/_form_footer.html.erb
  • myapp/app/views/devise/shared/_links.html.erb
  • myapp/app/views/devise/unlocks/new.html.erb
  • myapp/app/views/home/index.html.slim
  • myapp/app/views/issues/index.html.slim
  • myapp/app/views/layouts/application.html.erb
  • myapp/app/views/layouts/application_layout.rb
  • myapp/app/views/locks/index_view.rb
  • myapp/app/views/locks/show_view.rb
  • myapp/config/application.rb
  • myapp/config/environments/development.rb
  • myapp/config/environments/production.rb
  • myapp/config/environments/test.rb
  • myapp/config/importmap.rb
  • myapp/config/initializers/assets.rb
  • myapp/config/initializers/devise.rb
  • myapp/config/initializers/sidekiq.rb
  • myapp/config/routes.rb
  • myapp/db/schema.rb
  • myapp/package.json
  • myapp/postcss.config.js
  • myapp/spec/factories/users.rb
  • myapp/spec/helpers/home_helper_spec.rb
  • myapp/spec/requests/home_spec.rb
  • myapp/spec/system/static_pages_spec.rb
  • myapp/spec/views/home/index.html.slim_spec.rb
  • myapp/stylelint.config.js
  • myapp/tailwind.config.js
  • spec/sidekiq_unique_jobs/locksmith/fast_path_spec.rb
  • spec/sidekiq_unique_jobs/locksmith_spec.rb
  • spec/sidekiq_unique_jobs/lua/queue_and_lock_spec.rb
💤 Files with no reviewable changes (30)
  • myapp/app/controllers/home_controller.rb
  • myapp/spec/factories/users.rb
  • myapp/app/views/devise/mailer/reset_password_instructions.html.erb
  • myapp/app/helpers/home_helper.rb
  • myapp/app/views/layouts/application.html.erb
  • myapp/app/views/application/_header.html.erb
  • myapp/app/views/devise/shared/_form_footer.html.erb
  • myapp/app/views/devise/mailer/confirmation_instructions.html.erb
  • myapp/app/views/devise/mailer/password_change.html.erb
  • myapp/spec/views/home/index.html.slim_spec.rb
  • myapp/app/views/devise/registrations/new.html.erb
  • myapp/app/views/home/index.html.slim
  • myapp/app/views/devise/mailer/unlock_instructions.html.erb
  • myapp/spec/helpers/home_helper_spec.rb
  • myapp/.erb-lint.yml
  • myapp/postcss.config.js
  • myapp/.eslintignore
  • myapp/app/views/devise/passwords/edit.html.erb
  • myapp/app/views/devise/passwords/new.html.erb
  • myapp/app/views/devise/confirmations/new.html.erb
  • myapp/app/views/devise/unlocks/new.html.erb
  • myapp/app/views/devise/sessions/new.html.erb
  • myapp/tailwind.config.js
  • myapp/.eslintrc.js
  • myapp/app/views/devise/shared/_links.html.erb
  • myapp/app/controllers/issues_controller.rb
  • myapp/stylelint.config.js
  • myapp/app/helpers/devise_helper.rb
  • myapp/config/initializers/devise.rb
  • myapp/app/views/devise/registrations/edit.html.erb

Comment thread myapp/app/assets/stylesheets/application.tailwind.css
Comment thread myapp/app/controllers/locks_controller.rb
Comment thread myapp/app/controllers/locks_controller.rb
Comment thread myapp/app/jobs/issue_432_job.rb Outdated
Comment thread myapp/app/views/locks/index_view.rb
Comment thread myapp/config/routes.rb
Comment thread myapp/Gemfile
Comment thread myapp/spec/requests/home_spec.rb
Comment thread myapp/spec/system/static_pages_spec.rb
Comment thread spec/sidekiq_unique_jobs/locksmith_spec.rb
- Validate job_name against DEMO_JOBS allowlist before constantize (RCE fix)
- Remove Devise DSL from User model (gem was removed from Gemfile)
- Fix Issue432Job log method to include diagnostic info in log output
- Add aria-label to enqueue buttons for accessibility
- Fix misleading comment in assets initializer
- Improve locksmith execute test to verify async path via spy
- Upgrade to Ruby 4.0.2, Bun 1.3.11, Bundler 4.0.9
- Add .ruby-version, .bun-version, update .tool-versions
Comment thread myapp/app/controllers/locks_controller.rb Dismissed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
myapp/app/jobs/issue_432_job.rb (1)

34-36: Consider using a connection pool for RedisClient.

Each job instance creates its own RedisClient connection. While acceptable for this dev/test app, in a production context you'd typically use RedisClient.config(...).new_pool or leverage Sidekiq's existing Redis connection pool to avoid connection churn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@myapp/app/jobs/issue_432_job.rb` around lines 34 - 36, The redis method
currently builds a new RedisClient per job instance (`@redis` ||=
RedisClient.new(...)), which can create many connections; change it to use a
shared connection pool (e.g., call RedisClient.config(url:
ENV.fetch("REDIS_URL", nil)).new_pool or use Sidekiq's existing pool) and update
callers in Issue432Job to check out a connection from that pool (use pool.with
or equivalent) instead of referencing `@redis` directly; ensure the pool is
memoized (e.g., `@redis_pool`) and created once for the process.
myapp/app/controllers/locks_controller.rb (1)

74-99: Static analysis RCE warning is a false positive; optional defense-in-depth improvement available.

The Brakeman/CodeQL warnings about constantize on line 83 are false positives because the allowlist check (DEMO_JOBS.key?(job_name)) at lines 78-81 ensures only the 9 predefined job names can reach constantize.

As a minor defense-in-depth measure, consider using safe_constantize to gracefully handle the edge case where a job class in DEMO_JOBS doesn't exist at runtime (e.g., file deleted but constant not removed):

🛡️ Optional: use safe_constantize for defense-in-depth
-    job_class = job_name.constantize
+    job_class = job_name.safe_constantize
+    unless job_class
+      redirect_to locks_path, alert: "Job class not found: #{job_name}"
+      return
+    end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@myapp/app/controllers/locks_controller.rb` around lines 74 - 99, The enqueue
action currently calls job_name.constantize after checking DEMO_JOBS, which is
sufficient but can fail if the constant is unexpectedly missing; change
job_name.constantize to job_name.safe_constantize and handle a nil job_class
(e.g., redirect_to locks_path with an alert like "Job class not loaded" or treat
as failed enqueue) so enqueue gracefully handles the rare runtime-missing-class
case; reference the enqueue method, DEMO_JOBS allowlist, and the job_class
variable when making this defensive change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@myapp/app/controllers/locks_controller.rb`:
- Around line 74-99: The enqueue action currently calls job_name.constantize
after checking DEMO_JOBS, which is sufficient but can fail if the constant is
unexpectedly missing; change job_name.constantize to job_name.safe_constantize
and handle a nil job_class (e.g., redirect_to locks_path with an alert like "Job
class not loaded" or treat as failed enqueue) so enqueue gracefully handles the
rare runtime-missing-class case; reference the enqueue method, DEMO_JOBS
allowlist, and the job_class variable when making this defensive change.

In `@myapp/app/jobs/issue_432_job.rb`:
- Around line 34-36: The redis method currently builds a new RedisClient per job
instance (`@redis` ||= RedisClient.new(...)), which can create many connections;
change it to use a shared connection pool (e.g., call RedisClient.config(url:
ENV.fetch("REDIS_URL", nil)).new_pool or use Sidekiq's existing pool) and update
callers in Issue432Job to check out a connection from that pool (use pool.with
or equivalent) instead of referencing `@redis` directly; ensure the pool is
memoized (e.g., `@redis_pool`) and created once for the process.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bcaf4412-2cdb-4e05-9a12-94c6a8764b2b

📥 Commits

Reviewing files that changed from the base of the PR and between 2a2fefc and dee6874.

⛔ Files ignored due to path filters (1)
  • myapp/Gemfile.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • myapp/.bun-version
  • myapp/.ruby-version
  • myapp/.tool-versions
  • myapp/Gemfile
  • myapp/app/controllers/locks_controller.rb
  • myapp/app/jobs/issue_432_job.rb
  • myapp/app/models/user.rb
  • myapp/app/views/locks/index_view.rb
  • myapp/config/initializers/assets.rb
  • myapp/db/schema.rb
  • myapp/package.json
  • spec/sidekiq_unique_jobs/locksmith_spec.rb
💤 Files with no reviewable changes (1)
  • myapp/app/models/user.rb
✅ Files skipped from review due to trivial changes (4)
  • myapp/.ruby-version
  • myapp/.bun-version
  • myapp/.tool-versions
  • myapp/app/views/locks/index_view.rb
🚧 Files skipped from review as they are similar to previous changes (4)
  • myapp/config/initializers/assets.rb
  • myapp/Gemfile
  • spec/sidekiq_unique_jobs/locksmith_spec.rb
  • myapp/db/schema.rb

The seeds file referenced Devise methods (password, confirm) that no
longer exist after removing the gem. The lock testing dashboard doesn't
need any database records.
overmind is not installed. Use the same process manager fallback chain
as cosmos: hivemind -> foreman -> install foreman.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@myapp/bin/dev`:
- Line 5: The Procfile.dev is unsetting the PORT environment so the export in
bin/dev (and overrides like PORT=4000 bin/dev) never reach Puma; edit
Procfile.dev to remove the "unset PORT" from the command that currently runs
"unset PORT && bin/rails server" so the process uses the exported PORT, ensuring
commands like PORT=4000 bin/dev and the bin/dev export take effect for Puma and
the rails server.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 46a33c37-4980-4fce-87c7-1be9ffceda18

📥 Commits

Reviewing files that changed from the base of the PR and between 7937a5e and b5c49d4.

📒 Files selected for processing (1)
  • myapp/bin/dev

Comment thread myapp/bin/dev
- Switch to dracula theme for dark mode look
- Color-coded lock type badges per lock strategy
- Hero section header with centered layout
- Individual stat cards with SVG icons instead of stats bar
- Cards with icons, dividers, hover effects, and mockup-code for args
- Empty state illustrations for no-locks
- Sticky navbar with lock icon and mobile dropdown
- Footer showing gem/Rails/Ruby versions
- Improved show view with grid config display and collapsible JSON
- Add rails_icons gem with heroicons library for proper SVG icons
- Replace all inline SVG paths with hero() helper calls
- Create IconHelper module included in ApplicationComponent
- Fix SystemStackError in enqueue action - rescue infinite recursion
  caused by on_conflict: :reschedule re-enqueueing immediately
- Show rejected job count in flash message
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
myapp/app/views/locks/show_view.rb (1)

87-105: The constantize call is safe here given the controller's whitelist.

The controller validates job_name against DEMO_JOBS before passing it to this view (see locks_controller.rb:62-72), so only known job class names can reach this code path. Since this is a localhost-only dev tool, this is acceptable.

Consider adding a brief comment for future maintainers:

 def job_config_card
   div(class: "card bg-base-100 shadow-sm") do
     div(class: "card-body") do
       h2(class: "card-title text-lg mb-2") { "Sidekiq Options" }

+      # Safe: job_name validated against DEMO_JOBS whitelist in controller
       job_class = `@job_name.constantize`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@myapp/app/views/locks/show_view.rb` around lines 87 - 105, Add a short inline
comment above the job_config_card's use of `@job_name.constantize` explaining that
this constantize is safe because LocksController validates job_name against the
DEMO_JOBS whitelist (see LocksController#... where job_name is checked) and that
this view is only used in a localhost/dev-only tool; reference the
job_config_card method, the `@job_name` variable, and the DEMO_JOBS whitelist so
future maintainers understand the safety rationale.
myapp/app/views/locks/index_view.rb (2)

53-65: Remove redundant button text argument.

When button_to is given both a name argument and a block, the block content is rendered and the name is ignored. Line 53's "Flush All Locks" is dead code since the block (lines 57-64) provides the button content.

♻️ Suggested fix
-            button_to "Flush All Locks", flush_locks_path,
+            button_to flush_locks_path,
               method: :delete,
               class: "btn btn-error btn-outline btn-sm gap-2",
               data: { turbo_confirm: "This will delete all lock digests. Continue?" } do
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@myapp/app/views/locks/index_view.rb` around lines 53 - 65, The button_to call
is passing a name argument ("Flush All Locks") which is ignored when a block is
provided; remove the redundant first argument so the call becomes button_to
flush_locks_path, method: :delete, class: "btn btn-error btn-outline btn-sm
gap-2", data: { turbo_confirm: "This will delete all lock digests. Continue?" }
do ... end and keep the existing block content (the svg and plain "Flush All
Locks") as the rendered button content; update the invocation around the
button_to call in the view to eliminate the dead string argument.

25-29: Consider making queue_stats explicitly optional.

The view_template checks if @queue_stats`` (line 34), implying it can be nil. Making the parameter explicitly optional improves API clarity:

-    def initialize(jobs:, digests:, queue_stats:)
+    def initialize(jobs:, digests:, queue_stats: nil)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@myapp/app/views/locks/index_view.rb` around lines 25 - 29, The initialize
method currently requires queue_stats but the template checks if `@queue_stats`,
so make queue_stats explicitly optional by changing the initialize signature to
accept queue_stats: nil and keep assigning `@queue_stats` = queue_stats; update
any callers that pass queue_stats (or rely on default nil) and add a short
comment on initialize or param docs to reflect that queue_stats may be nil;
ensure view_template continues to guard with if `@queue_stats`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@myapp/app/views/locks/show_view.rb`:
- Around line 189-194: The dynamic class interpolation in state_stat (the
"text-#{color}" usage) can be stripped by Tailwind JIT; replace it with a
deterministic lookup map that maps permitted color keys (e.g., "info",
"warning", "success", "neutral") to concrete class strings and use that lookup
when building the div class, or alternatively add those classes to the Tailwind
safelist in tailwind.config.js; update the state_stat method to read from the
lookup (e.g., color_class = COLOR_MAP[color.to_s] || 'text-neutral') and use
that variable instead of interpolating to ensure classes are present in
production.

---

Nitpick comments:
In `@myapp/app/views/locks/index_view.rb`:
- Around line 53-65: The button_to call is passing a name argument ("Flush All
Locks") which is ignored when a block is provided; remove the redundant first
argument so the call becomes button_to flush_locks_path, method: :delete, class:
"btn btn-error btn-outline btn-sm gap-2", data: { turbo_confirm: "This will
delete all lock digests. Continue?" } do ... end and keep the existing block
content (the svg and plain "Flush All Locks") as the rendered button content;
update the invocation around the button_to call in the view to eliminate the
dead string argument.
- Around line 25-29: The initialize method currently requires queue_stats but
the template checks if `@queue_stats`, so make queue_stats explicitly optional by
changing the initialize signature to accept queue_stats: nil and keep assigning
`@queue_stats` = queue_stats; update any callers that pass queue_stats (or rely on
default nil) and add a short comment on initialize or param docs to reflect that
queue_stats may be nil; ensure view_template continues to guard with if
`@queue_stats`.

In `@myapp/app/views/locks/show_view.rb`:
- Around line 87-105: Add a short inline comment above the job_config_card's use
of `@job_name.constantize` explaining that this constantize is safe because
LocksController validates job_name against the DEMO_JOBS whitelist (see
LocksController#... where job_name is checked) and that this view is only used
in a localhost/dev-only tool; reference the job_config_card method, the
`@job_name` variable, and the DEMO_JOBS whitelist so future maintainers understand
the safety rationale.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 88d7f951-737b-4e8b-810b-40774fef4dbc

📥 Commits

Reviewing files that changed from the base of the PR and between b5c49d4 and d685375.

📒 Files selected for processing (5)
  • myapp/app/views/components/footer.rb
  • myapp/app/views/components/navbar.rb
  • myapp/app/views/layouts/application_layout.rb
  • myapp/app/views/locks/index_view.rb
  • myapp/app/views/locks/show_view.rb
✅ Files skipped from review due to trivial changes (1)
  • myapp/app/views/components/footer.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • myapp/app/views/layouts/application_layout.rb
  • myapp/app/views/components/navbar.rb

Comment thread myapp/app/views/locks/show_view.rb
Dynamic "text-#{color}" gets purged by Tailwind's JIT compiler.
Use STAT_COLORS constant with complete class names.
@mhenrixon mhenrixon enabled auto-merge (squash) March 27, 2026 19:23
@mhenrixon mhenrixon self-assigned this Mar 27, 2026
- Use plugins syntax instead of require (rubocop 1.86+)
- Enforce double_quotes for string literals
- Add rubocop-thread_safety plugin (inherited from parent config)
- Remove myapp/**/* exclusion from parent .rubocop.yml
- Set SuggestExtensions: false
Remove rubocop-thread_safety from both parent gem and myapp. Update
myapp .rubocop.yml with inherit_from: [] to prevent parent config
inheritance, enforce double_quotes for string literals.
…ting!

Sidekiq 8 deprecated `require "sidekiq/testing"` in favor of the
explicit `Sidekiq.testing!(:fake)` API. Updated the gem's own
testing.rb module which is the canonical entry point.
@mhenrixon mhenrixon merged commit 04abf8a into main Mar 27, 2026
32 checks passed
@mhenrixon mhenrixon deleted the feature/myapp-upgrade-and-locksmith-testing branch March 27, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants