Skip to content

Bullet loads on its own now that it knows Active Record 8.2 - #1409

Merged
dschmura merged 2 commits into
mainfrom
bullet-knows-active-record-82
Oct 16, 2026
Merged

dschmura merged 2 commits into
mainfrom
bullet-knows-active-record-82

Conversation

@dschmura

Copy link
Copy Markdown
Owner

Bullet 8.2.0 raised at load on Active Record 8.2, so since the move to Rails main it has loaded through config/bullet_on_rails_main.rb, a shim that hands it its 8.1 patches, with require: false on the Gemfile line so Bundler would not load it first (#1337). A canary spec — "is still needed: Bullet does not know this Active Record by itself" — was written to fail the day Bullet supported 8.2 itself, and its failure message listed the removal steps.

Bullet 8.3.0 does. Dependabot's grouped bundler pull request (#1408) carried it, and the canary went red there (test_shard (3)) exactly as designed.

This bumps bullet alone and follows the canary's list: the shim and its require in config/application.rb are gone, and the Gemfile line loads bullet normally. The canary example is deleted; the spec's two behaviour examples stay, renamed to spec/config/bullet_spec.rb, because after the shim goes they are what proves Bullet still runs and still catches an N+1 on this Active Record.

Bumping bullet here rather than on #1408's branch keeps Dependabot's pull request untouched: once this merges, Dependabot rebases the group and drops bullet from it.

Closes #1337

Verification

  • With bullet 8.3.0 locked and the shim still in place, the canary failed locally exactly as in chore(deps): bump the bundler group with 6 updates #1408's CI ("Bullet now supports Active Record 8.2: …").
  • The removal had its own red: shim deleted, require: false still set → the suite cannot boot (NameError: uninitialized constant Bullet). Dropping require: false → spec/config/bullet_spec.rb 2 examples, 0 failures.
  • No tracked file outside history still names the shim (bullet_on_rails_main), with a positive control on bullet_spec.
  • Full suite: 6023 examples, 0 failures, parity 6023/6023, merged coverage 96.75%.

Bullet 8.2.0 raised at load on Active Record 8.2, so since the move to
Rails main it has loaded through config/bullet_on_rails_main.rb, a shim
that hands it its 8.1 patches, with `require: false` on the Gemfile line
so Bundler would not load it first (#1337). A canary spec,
"is still needed: Bullet does not know this Active Record by itself",
was written to fail the day Bullet supported 8.2 itself, and its failure
message listed the removal steps.

Bullet 8.3.0 does. Dependabot's grouped bundler pull request (#1408)
carried it, and the canary went red there exactly as designed.

This commit bumps bullet alone and follows the canary's list: the shim
and its require in config/application.rb are gone, and the Gemfile line
loads bullet normally. The canary example is deleted; the spec's two
behaviour examples stay, renamed to spec/config/bullet_spec.rb, because
after the shim goes they are what proves Bullet still runs and still
catches an N+1 on this Active Record. The removal had its own red: with
the shim deleted and `require: false` still set, the suite cannot boot
(`uninitialized constant Bullet`), and dropping it is the green.

Bumping bullet here rather than on #1408's branch keeps Dependabot's pull
request untouched: once this merges, Dependabot rebases the group and
drops bullet from it.
@dschmura
dschmura merged commit b30497b into main Oct 16, 2026
20 checks passed
@dschmura
dschmura deleted the bullet-knows-active-record-82 branch October 16, 2026 10:07
dschmura added a commit that referenced this pull request Oct 16, 2026
)

#1402 shipped because nothing about the broken template looked broken.
The identity picker's crop footer sent `DELETE` to a URL it was handed;
one partial up, the workspace Profile handed it
`workspace_path(workspace)`, an ordinary route helper whose DELETE is
`workspaces#destroy`. **Remove photo** discarded the workspace, with no
confirmation, and every file involved read as correct on its own.

A scan after the fix (#1412) enumerated all 29 DELETE sites in the
templates and found no second instance — but a hand scan proves one tree
on one day. This makes the scan a code-smell spec,
`spec/code_smells/destroy_requests_name_their_target_spec.rb`.

## What it checks

The census reads every request a template makes — `button_to`,
`link_to`, `form_with`, `form_tag`, under `app/views` and
`app/components` — and when the URL or the method is one of the
template's declared locals, it follows the local to every `render` that
passes it, then up again until it reaches the template that names a
route helper. Declared defaults are read too, which is how the confirm
dialog's `confirm_method: :delete` default resolves per caller. Each
DELETE that comes out is checked against the real route table:

1. it lands on a named route with a DELETE action;
2. if that action soft-deletes — a controller `destroy` that calls
`discard!`, a set **derived from `app/controllers`**, not written down —
the chain passes through `shared/confirm_dialog`.

Today's tree passes: **33 DELETE entry points** — the 29 sites, with the
archived banner's one site fanning out to its five callers and the crop
footer's to its two, and the confirm dialog's declaration replaced by
its seven DELETE callers. The workspace's destroy is reached only from
the danger zone's dialog. The floor is 33.

## Red, two ways

- **Negative control** (committed): rewrites the Profile page's
`remove_url` to `workspace_path(workspace)` and must find the resulting
chain reaching `workspaces#destroy` outside the dialog.
- **The real bug** (one-off, not committed — CI's shallow clones can't
read history): the census run over the four pre-#1412 view files from
`78fbb4cd^` failed on exactly the chain an owner's click took:

  ```text
app/views/workspaces/_profile_section.html.erb >
app/views/shared/_identity_picker.html.erb >
app/views/shared/identity_picker/_crop_footer.html.erb button_to
workspace_path(workspace)
  ```

Positive controls: the census follows the identity picker through both
partials to `workspaces/logos#destroy` and `settings/avatars#destroy`,
and the derived soft-deleting set includes `workspaces#destroy` and
`workspaces/projects#destroy`.

## Blind spots

It reads template text, and prints these whenever a rule fails:
collection renders (`render partial:`, four in the tree, all passing
records), URLs built in Ruby, hand-written HTML method overrides, and
partials nothing renders. `/docs/developer/testing` has a section on
reading its failures.

## Changelog

The seven pull requests merged since 2026-10-14 (#1401, #1405, #1406,
#1407, #1409, #1410, #1412) had entries written before their numbers
existed; each now cites its number. The five Dependabot PRs in the same
window are left out, matching how routine bumps have been treated: none
of the recent ones (#1385–#1389, #1396) is cited.

## Verification

- The guard: 7 examples, 0 failures; with the docs-citation spec, 10
examples.
- The census's 33 entries reconciled line by line against the 29-site
hand inventory.
- `bin/comment-block-check` (staged), rubocop and `rake markdown:check`
exit 0.
- Full suite: see the commit's gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drop the Bullet patch once Bullet supports Active Record 8.2

1 participant