Skip to content

Require an id when exposing a record, not persistence - #79

Closed
ngan wants to merge 1 commit into
mainfrom
np-expose-requires-id
Closed

Require an id when exposing a record, not persistence#79
ngan wants to merge 1 commit into
mainfrom
np-expose-requires-id

Conversation

@ngan

@ngan ngan commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes a false positive in the guard added by #77, found by running against zenpayroll on CI (zenpayroll#363546, build 1660069).

The failure

cannot expose :company_address_2: the Addresses::Db::Address is not persisted.

From spec/fixture_kit/reporting_toolkit/fully_populated_payroll_custom_report.rb:

expose company:,
       employee_1:,
       company_address_2: Addresses.const_get(:Db)::Address.new(id: company_address_2.id)

That is not a mistake. company_address_2 is created with FactoryBot.create a few lines earlier — the bare Address.new(id:) is a deliberate reference stub, used to expose that row under a different model class than the factory returns. Since expose only ever stores the class and the id, it worked fine before the guard.

Fix

Check record.id.nil? rather than record.persisted?.

That still catches what the guard was added for — an unsaved record captured with a nil id and silently resolving to nil in tests — while accepting a reference that points at a real row. It's also consistent with how the value is used: Repository#load_record does unscoped.find_by(id: …), so an id is the only thing a lookup needs.

The tradeoff: a hard-destroyed record (id present, row gone) now slips through and resolves to nil instead of raising. That's the narrower case, and distinguishing it from the soft-deleted records unscoped exists to support isn't reliable, so I'd rather not guess.

Blast radius

One line across 159 fixture files in zenpayroll. I checked every failed job in the build — company_address_2 is the only UnpersistedRecordError, and .new(id: appears in exactly one fixture.

Testing

  • bundle exec rspec — 197 examples, 0 failures
  • FIXTURE_KIT_INTEGRATION_FRAMEWORK=minitest bundle exec rspec — 197 examples, 0 failures
  • Reproduced the zp pattern in the dummy app first (it raised), confirmed it resolves to the real row after the change, and added it as a spec
  • Dropped the destroyed-record spec, which no longer applies; kept the no-id specs for a single record and for a record inside a collection

Note

The Address.new(id: …) stub is working around a missing feature — there's no first-class way to say "expose this row, looked up as class X." A FixtureKit.reference(Klass, id) helper would express that intent directly and stop the pattern looking like a bug. Out of scope here; worth considering separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PQjkiuuGX2t3TSZKpzqpPv

The guard added alongside class/id capture used `persisted?`, which rejects
an unsaved instance built solely to carry the id of a real row:

    expose(address: Addresses::Db::Address.new(id: company_address.id))

Only the class and the id are ever stored, so that is a legitimate way to
expose a row under a different model class than the one that created it --
and it worked before the guard existed. A zenpayroll fixture relies on it.

Check for a nil id instead. That still catches the case the guard was for,
an unsaved record silently resolving to nil in tests, and it no longer
rejects a reference that points at a real row. Records looked up through
`Repository` use `unscoped`, so an id is the only thing a lookup needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQjkiuuGX2t3TSZKpzqpPv
@ngan

ngan commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favour of fixing this fixture-side. persisted? is the more accurate check, and an id that can't be looked up as a real record is exactly what it should reject. The zenpayroll fixture will fetch the underlying ActiveRecord record instead of hand-rolling a stub — matching the precedent already in spec/fixture_kit/payroll_canada/with_pay_schedule.rb.

@ngan ngan closed this Aug 4, 2026
@ngan
ngan deleted the np-expose-requires-id branch August 4, 2026 03:23
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.

1 participant