Skip to content

test: pin config default-eval timing across entry points (ADR-005 step 1 guard) - #223

Merged
pftg merged 1 commit into
masterfrom
test/adr5-config-timing-guard
Aug 22, 2026
Merged

test: pin config default-eval timing across entry points (ADR-005 step 1 guard)#223
pftg merged 1 commit into
masterfrom
test/adr5-config-timing-guard

Conversation

@pftg

@pftg pftg commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

First commit of accepted ADR-005 (config storage inversion): before SnapDiff::Config becomes the single config store with config_legacy.rb flipped to delegating writers, the timing of default evaluation needs a guard net. mattr_accessor class-body block defaults are evaluated once at require time with whatever ENV/Rails/pwd state exists at that moment — the #222 load-order guard pins the require graph, but nothing pinned this. This PR only adds the guard; no behavior changes.

What it pins (current master behavior, per entry point)

12 subprocess probes — 3 per documented entry point (capybara_screenshot_diff, capybara_screenshot_diff/minitest, snap_diff, capybara/screenshot/diff):

Default Input Current timing (pinned)
fail_if_new ENV["CI"] require-time, frozen — ENV mutation after require (even before first read) not seen
root Rails.root, else pwd require-time, frozen — post-require Rails.root reassignment / chdir not seen
default_options[:wait] Capybara.default_max_wait_time call-time, live — follows changes made after require
all 27 MAPPING settings value snapshot, read through both surfaces (SnapDiff.config.x == legacy mattr == expected default)

Write-surface bidirectionality is already covered by snap_diff_config_test.rb and is not duplicated here.

Gate evidence (the guard bites)

Temporarily hoisted the fail_if_new ENV read from class-body eval to a lazy read-time default (mattr_writer + memoizing def self.fail_if_new):

  • RED: 8 of 12 probes failed (Capybara::Screenshot::Diff.fail_if_new: expected true, got false on the CI-set probes; expected-false failures on the snapshot probes, which mutate ENV["CI"] post-require pre-first-read exactly to distinguish lazy from eager)
  • Revert → GREEN: 12/12

Probes mutate inputs after require but before first read, so both memoized and non-memoized lazy variants are caught.

Test numbers

  • baseline rake test:unit on master: 492 runs, 0 failures
  • with this PR: rake test:unit 504 runs / 0 failures; full rake test 537 runs, 1484 assertions, 0 failures, 0 errors, 6 skips (pre-existing); standardrb clean (143 files, no offenses)

🤖 Generated with Claude Code

Summary by Sourcery

Pin configuration default evaluation timing and cross-surface values as a guard for the ADR-005 storage inversion.

Enhancements:

  • Add subprocess coverage that locks configuration default evaluation timing across all documented gem entry points, including require-time snapshots for environment- and path-derived values and call-time evaluation for the Capybara wait setting.
  • Verify that all mapped configuration values remain consistent between the SnapDiff and legacy configuration surfaces.

Tests:

  • Add 12 subprocess-based configuration timing probes covering default values, ENV and Rails.root changes, working-directory changes, and live Capybara wait updates.

…p 1 guard)

Subprocess probes that pin, for each documented entry point
(capybara_screenshot_diff, capybara_screenshot_diff/minitest, snap_diff,
capybara/screenshot/diff):

- every SnapDiff::Config::MAPPING setting reads identically through both
  surfaces (SnapDiff.config.x and the legacy mattr_accessor) and matches
  the expected default
- fail_if_new's ENV["CI"] read happens at require time and is frozen:
  mutating ENV after the require (even before the first read) is not seen
- root's Rails.root / pwd fallback is likewise evaluated at require time
  and frozen against post-require Rails.root reassignment or chdir
- Diff.default_options[:wait] is the opposite: it reads
  Capybara.default_max_wait_time live, at call time

These are green on current master by design: they pin CURRENT behavior so
the coming ADR-005 storage inversion (SnapDiff::Config becoming the single
store, config_legacy flipped to delegating writers) goes red if it shifts
when any default is evaluated or what it evaluates to.

Gate-checked: temporarily hoisting the fail_if_new ENV read from
class-body eval to a lazy read-time default turned 8 of the 12 probes red;
reverting restored green.
@sourcery-ai

sourcery-ai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a new unit test suite that probes and pins the timing and value snapshots of configuration defaults across all documented entry points, ensuring require-time vs call-time behavior stays stable through upcoming config storage refactors.

File-Level Changes

Change Details Files
Add subprocess-based tests that pin config default evaluation timing and value snapshots across all documented entry points.
  • Introduce ConfigDefaultTimingTest using Open3 to run ruby subprocess probes per entry point so require-time behavior can be observed independently of test_helper preloads.
  • Define shared CHECK_HELPER helpers to compare expected vs actual config values through both SnapDiff.config and legacy mattr surfaces.
  • Add SNAPSHOT_SCRIPT probe to assert default values, require-time freezing of ENV/pwd-derived defaults, and call-time liveness of Capybara-coupled wait.
  • Add CI_SET_SCRIPT probe to ensure ENV['CI'] being set at require time permanently flips fail_if_new on, regardless of post-require ENV mutations.
  • Add RAILS_ROOT_SCRIPT probe to ensure Rails.root at require time is used as the root default and is not affected by post-require reassignment.
  • Iterate tests over all documented entry points to guarantee consistent behavior across capybara_screenshot_diff, capybara_screenshot_diff/minitest, snap_diff, and capybara/screenshot/diff.
test/unit/config_default_timing_test.rb

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@pftg, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2099661b-1302-4286-acfb-1350e2a398fe

📥 Commits

Reviewing files that changed from the base of the PR and between 40bbf96 and 827678b.

📒 Files selected for processing (1)
  • test/unit/config_default_timing_test.rb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@pftg
pftg merged commit 40204de into master Aug 22, 2026
6 checks passed
@pftg
pftg deleted the test/adr5-config-timing-guard branch August 22, 2026 22:14

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="test/unit/config_default_timing_test.rb" line_range="65-68" />
<code_context>
+    require "tmpdir"
+    Dir.chdir(Dir.tmpdir)
+
+    # 1) Every mapped setting reads the same through both surfaces.
+    SnapDiff::Config::MAPPING.each do |name, (mod, mattr)|
+      check("SnapDiff.config.#{name} vs #{mod}.#{mattr}", mod.public_send(mattr), SnapDiff.config.public_send(name))
+    end
+
+    # 2) Expected default values (CI unset, no Rails, at require time).
</code_context>
<issue_to_address>
**issue (testing):** The probe does not assert that every key in `SnapDiff::Config::MAPPING` has an expected default. It only checks that mapped values agree between the two surfaces, while the separate hard-coded expectations can silently omit a newly added mapping and allow its incorrect default to pass.

**Triggers:** When a setting is added to `MAPPING` without also being added to the hard-coded expectation hash.

**Suggested fix:** Build the expected values from a complete key set and assert `SnapDiff::Config::MAPPING.keys.sort == expected.keys.sort` before checking the values.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: test/unit/config_default_timing_test.rb:68


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +65 to +68
# 1) Every mapped setting reads the same through both surfaces.
SnapDiff::Config::MAPPING.each do |name, (mod, mattr)|
check("SnapDiff.config.#{name} vs #{mod}.#{mattr}", mod.public_send(mattr), SnapDiff.config.public_send(name))
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (testing): The probe does not assert that every key in SnapDiff::Config::MAPPING has an expected default. It only checks that mapped values agree between the two surfaces, while the separate hard-coded expectations can silently omit a newly added mapping and allow its incorrect default to pass.

Triggers: When a setting is added to MAPPING without also being added to the hard-coded expectation hash.

Suggested fix: Build the expected values from a complete key set and assert SnapDiff::Config::MAPPING.keys.sort == expected.keys.sort before checking the values.

pftg added a commit that referenced this pull request Aug 22, 2026
…p 7b) (#230)

config_legacy.rb was the last file in the v1 trees holding real logic.
Step 1 moved config STORAGE to SnapDiff::Config but left the DERIVED
values behind, so deleting lib/capybara/ at 3.0 would have lost
behaviour rather than being a `git rm`.

Moved into SnapDiff::Config as instance methods:

- #active?            (was Capybara::Screenshot.active?)
- #screenshot_area    (was Capybara::Screenshot.screenshot_area)
- #screenshot_area_abs
- #default_options    (was Diff.default_options, incl. the vips
                       tolerance 0.001 literal)

Inverted so the canonical names hold the bodies:

- SnapDiff.compare now builds the Comparison; Diff.compare forwards.
- SnapDiff.start now does the two-arg yield; Diff.configure forwards.

The Config::MAPPING accessor generator moved to snap_diff/config.rb
alongside the storage it delegates to -- same reason legacy_shims.rb
generates the legacy constants from the snap_diff side: the generator
is code, and the v1 trees must stay alias-only.

Net effect: the alias-only gate's ALLOWED_WITH_CODE allowlist is now
EMPTY, and config_legacy.rb is checked by the general rule like every
other legacy file. The pinned method inventory that narrowed its
exemption goes with the exemption.

default_options[:wait] stays a call-time read of
Capybara.default_max_wait_time -- freezing it into Config#initialize
reds all four #223 timing guards.

Found while moving: the active? precedence rule had no test. Replacing
the whole expression with a bare `enabled` kept all 529 unit tests
green. Its truth table is now pinned through both the canonical method
and the legacy forwarder.

AVAILABLE_DRIVERS stays in config_legacy.rb (it is a bare constant
assignment, so alias-shaped and never a gate blocker). Moving the
storage to SnapDiff::Drivers was tried and reverted: eagerly it needs
Utils, which requires Drivers back; lazily it breaks
image_compare_test's published stubbing point, because Utils reads
Drivers.available and would no longer see a stubbed legacy constant.

Legacy public surface verified byte-identical: singleton_methods +
parameters for Capybara::Screenshot and Capybara::Screenshot::Diff are
unchanged. On the SnapDiff side, .compare gains an explicit signature
(was a `(...)` pass-through) and .start loses its &block capture (now a
bare yield) -- both a consequence of the bodies landing there, and both
call-compatible.

rake test:unit 530 runs / 0 failures; rake test 563 runs / 0 failures.
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