Skip to content

Make the spec suite runnable and trustworthy again - #8

Merged
IgorFroehner merged 9 commits into
jetrockets:masterfrom
IgorFroehner:chore/make-suite-runnable
Aug 13, 2026
Merged

Make the spec suite runnable and trustworthy again#8
IgorFroehner merged 9 commits into
jetrockets:masterfrom
IgorFroehner:chore/make-suite-runnable

Conversation

@IgorFroehner

@IgorFroehner IgorFroehner commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

First maintenance PR after a long gap. This one does no behavior changes to the gem, it just
makes the test suite run again, and makes it tell the truth when it does.

Why

bundle exec rspec loaded zero examples on any modern Ruby, and
bundle exec rubocop could not start at all. Both were dependency bit-rot, not
anything in our code. On top of that, the dummy app had no recorder_revisions
table and Security never actually called recorder, so nothing had ever
exercised the gem end to end — and the suite had no isolation, so any spec that
did would have been unreliable.

What's here

Seven commits, each one green on its own so the history stays bisectable:

Commit
20cd2d5 Make the spec suite runnable on a fresh clone
aba4333 Add recorder_revisions and users tables to the dummy app
8abf2c3 Enable recording on the dummy Security model
f4bbd01 Isolate global state between examples
561cf90 Add end-to-end specs for Security revisions
9fc905a Remove rails-dummy and its destructive dummy:app task
90752c5 Remove committed dummy app credentials

Getting it running

  • concurrent-ruby >= 1.3.5 dropped its implicit require 'logger', which
    Rails < 7.1 relies on — every spec file died at load. Pinned, along with a
    second pin for an unrelated conflict that stopped RuboCop starting. Both are
    in the Gemfile, not the gemspec: they are development constraints and
    should not reach consumers. Both come out with the Rails upgrade.
  • database.yml was gitignored, so a fresh clone had nothing to connect with.
    It is committed now and reads every value from ENV.
  • Ruby pinned to 3.3.11 — the newest Rails 6.1 can run on (3.4 drops
    mutex_m from default gems), and unlike 3.1 it is not already EOL.
  • bin/setup creates the database instead of only running Bundler.

Making it honest

State leaked three ways, all verified: rows survived not just examples but
entire rspec processes, Recorder::Config is a Singleton, and
Recorder::Store memoises a RequestStore that is never cleared outside a real
request cycle.

The store leak is the one that matters. A stray recorder_disabled! carries
into later examples, which then record nothing and pass anyway — precisely
the failure mode that lets a broken observer look healthy. spec/rails_helper.rb
now rolls back the database, resets the Config singleton and clears the
RequestStore before every example.

Two things found along the way

  • Security never called recorder. Including Recorder::Observer alone
    only adds the revisions association; the macro is what registers the
    callbacks. Nothing was ever recorded in the dummy app.
  • rake dummy:app deleted the fixtures. rails-dummy registers it, and its
    first step is FileUtils.rm_rf('spec/dummy') before regenerating from
    scratch — but our dummy app is hand-maintained and checked in. Gem removed.

Also dropped master.key, which was committed next to credentials.yml.enc.

Verification

From a clean checkout with the database dropped: ./bin/setup && bundle exec rake
40 examples, 0 failures. Repeated runs stay green with zero leftover rows.
RuboCop now runs and reports the same 5 pre-existing offences as before — this
PR adds none.

The suite has not been able to boot for some time. Three separate things
had to line up:

concurrent-ruby >= 1.3.5 dropped its implicit `require 'logger'`, which
Rails < 7.1 relies on, so every spec file died at load with
`NameError: uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger`.
rubocop could not start either, for an unrelated dependency conflict in
the stack jetrockets-standard pulls in. Both are pinned in the Gemfile
rather than the gemspec, since they are development constraints and
should not be inflicted on consumers. Both come out with the Rails
upgrade.

database.yml was gitignored, so a fresh clone had nothing to connect
with. It is committed now and reads every value from the environment,
so local setups and CI can point elsewhere without editing it.

Ruby is pinned to 3.3.11: it is the newest version Rails 6.1 can run on,
since 3.4 drops mutex_m from the default gems, and unlike 3.1 it is not
already end of life.

bin/setup now creates the database instead of only running bundler.
The dummy app only ever had a securities table, so Recorder::Revision
had no table behind it and nothing could exercise the gem end to end.

The revisions migration mirrors the generator template so that the specs
test what users actually install; a comment on each side asks for them to
be kept in sync. The one intentional difference is the versioned
`ActiveRecord::Migration[6.1]` — the template still subclasses the bare
class, which has been unsupported since Rails 5, and fixing that belongs
in its own change.

users exists because Recorder::Revision belongs_to :user and the gem
expects host apps to supply that constant.

schema.rb is regenerated by db:migrate rather than edited by hand, which
reformats it, so it is excluded from rubocop.
Security included Recorder::Observer but never called `recorder`, and
including the module alone only adds the `revisions` association — it is
the macro that registers the after_create/update/destroy callbacks. So
no revision was ever recorded in the dummy app, whatever the specs did.
Recorder keeps state in three places that all outlived a single example,
and all three leaked: rows stayed in the database, Config is a Singleton,
and Store memoises a RequestStore that is never cleared outside a real
request cycle. Rows survived across whole rspec processes too.

The store leak is the dangerous one. A stray `recorder_disabled!` carries
into later examples, which then record nothing and pass anyway — the exact
failure mode that lets a broken observer look healthy.

RailsExampleGroup is included explicitly rather than leaning on
rspec-rails' global FixtureSupport include, which is deprecated
(rspec/rspec-rails#1355) and due to be removed in rspec-rails 7. Without
one of the two, plain RSpec.describe groups get no transaction at all,
since nothing here lives in a Rails-typed directory to infer it from.

isolation_spec guards the lot. Both examples assert a clean slate and
then dirty it, so whichever runs second catches a leak and the guard
holds under `config.order = :random` without pinning an order.
security_spec.rb was a require line with no examples. Everything the
suite covered until now stubbed the pieces in isolation, so nothing
proved that including Recorder::Observer records anything at all.

Covers create, update, a no-op update, destroy, and the disabled path,
against real callbacks and real rows. Deliberately a smoke test rather
than exhaustive observer coverage.
The Rakefile required rails/dummy/tasks, which registers `rake dummy:app`.
Its first step is FileUtils.rm_rf('spec/dummy') before regenerating the
app from scratch. This dummy app is hand-maintained and checked in — it
has models, migrations and a database.yml the specs depend on — so
running that task destroyed the fixtures.

Nothing else used the gem.
master.key was checked in alongside credentials.yml.enc, which defeats
the point of encrypting them. Nothing in the dummy app reads credentials.
Several comments narrated what the code replaced or justified choices by
contrast with a previous arrangement, which belongs in commit messages
rather than in the files. Dropped those, and shortened the rest to what a
reader needs at the point of use.
It asserted the behaviour of spec/rails_helper.rb rather than of the gem,
which is not something worth carrying in the suite.
@IgorFroehner
IgorFroehner force-pushed the chore/make-suite-runnable branch from fbe78f6 to 553ab52 Compare August 12, 2026 15:29
@IgorFroehner
IgorFroehner marked this pull request as ready for review August 12, 2026 17:35
@igor-alexandrov

Copy link
Copy Markdown
Member

@IgorFroehner before I will be able to merge this, we need to do one very important thing – automate tests running in CI. Please do this in a separate PR.

@IgorFroehner

IgorFroehner commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

@IgorFroehner before I will be able to merge this, we need to do one very important thing – automate tests running in CI. Please do this in a separate PR.

@igor-alexandrov the idea is to merge this and have the tests working and passing, and then create the CI pull request with the CI green. As you can see in this PR we have the CI green #9.

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.

2 participants