From 1c1c2010fb433b26dc978faeb140cd53faa05887 Mon Sep 17 00:00:00 2001 From: Erik Axel Nielsen Date: Thu, 3 Sep 2026 13:55:09 +0200 Subject: [PATCH 1/8] Report the errors swallowed while computing a cache digest Every rescue in the digest machinery returns a neutral value, so a misconfiguration, an autoload failure, or a raising `inherited` hook degrades to "no component dependencies" and the application serves stale HTML with nothing reported anywhere. - Route the four swallow sites through CacheDigest.handle_error - Log at `warn` through ActiveSupport's logger, preserving the production guarantee that a stale fragment beats a failed render - Raise instead in local environments, configurable with config.view_component.raise_on_cache_digest_errors --- docs/CHANGELOG.md | 4 + docs/api.md | 13 ++ docs/guide/caching.md | 16 +++ lib/view_component/cache_digest.rb | 33 ++++- .../cache_digest/dependency_tracking.rb | 8 +- lib/view_component/cache_digest/resolver.rb | 9 +- lib/view_component/config.rb | 21 +++- lib/view_component/engine.rb | 1 + test/sandbox/test/config_test.rb | 1 + .../test/experimentally_cacheable_test.rb | 113 ++++++++++++++++-- 10 files changed, 193 insertions(+), 26 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9834fc583..7e57c5e9c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,10 @@ nav_order: 6 ## main +* Report the errors swallowed while computing a component's cache digest, instead of degrading to an untracked component with no indication that anything went wrong. Digest errors are now raised in local environments and logged at `warn` elsewhere, configurable with `config.view_component.raise_on_cache_digest_errors`. + + *Erik Axel Nielsen* + ## 4.15.0 * Add experimental caching support, opt-in per component via `include ViewComponent::ExperimentallyCacheable`. diff --git a/docs/api.md b/docs/api.md index b615b2d54..859b75cf1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -294,6 +294,19 @@ A custom default layout used for the previews index page and individual previews config.view_component.previews.default_layout = "preview_layout" +### `.raise_on_cache_digest_errors` + +Whether to raise when computing a component's cache digest fails. + +Digest failures are otherwise swallowed, since a stale fragment is +preferable to a failed render, and reported to the log at `warn`. That +trade is wrong in development and test, where an untracked component +looks exactly like a component that was never cached. + +Defaults to `true` in local environments and `false` elsewhere: + + config.view_component.raise_on_cache_digest_errors = false + ## ViewComponent::TestHelpers ### `#render_in_view_context(...)` diff --git a/docs/guide/caching.md b/docs/guide/caching.md index 183de010d..cc46681f1 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -189,6 +189,22 @@ The same works in a template, where the branch is often the more natural place f Declared components must include `ViewComponent::ExperimentallyCacheable` themselves, since a component that hasn't opted in has no digest to depend on. +## When a digest can't be computed + +Computing a digest touches the autoloader, the filesystem, and Action View's dependency trackers, any of which can fail. In production those failures are swallowed: a component that can't be digested is left untracked, which is exactly the behavior it had before opting in, and a stale fragment beats a failed render. + +That trade is wrong while developing, where an untracked component is indistinguishable from a component that was never cached in the first place. Digest failures are therefore raised in local environments and logged at `warn` everywhere else: + +```console +[ViewComponent] Ignored an error while resolving PostComponent: NoMethodError: ... +``` + +To swallow them locally too, or to raise them in production: + +```ruby +config.view_component.raise_on_cache_digest_errors = false +``` + ## Caveats **Self-caching components can't take content from their callers.** Besides a block, this covers `with_content` and slots set by the caller: diff --git a/lib/view_component/cache_digest.rb b/lib/view_component/cache_digest.rb index 98694c8dc..cb58eaf0c 100644 --- a/lib/view_component/cache_digest.rb +++ b/lib/view_component/cache_digest.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/dependencies/autoload" +require "active_support/log_subscriber" require "action_view/digestor" require "action_view/render_parser" @@ -146,8 +147,8 @@ def partial_paths_in(source, name) RENDER_PARSER.new(name, source).render_calls.uniq.select do |path| source.include?(path) || source.include?(path.sub(%r{(\A|/)_}, '\1')) end - rescue - # Never let digest computation break rendering. + rescue => error + handle_error(error, "scanning #{name} for rendered partials") [] end @@ -221,6 +222,30 @@ def install! end end + # Report an exception the digest machinery swallowed. + # + # Every rescue here degrades to "this component has no dependencies", + # which is indistinguishable from a component that was never cached: the + # digest stops changing and the application serves stale HTML. A + # misconfiguration, an autoload failure, or a raising `inherited` hook is + # therefore completely invisible. + # + # In production a stale fragment beats a failed render, so the exception + # is only logged. Locally the trade goes the other way, so it's re-raised + # by default; see `config.view_component.raise_on_cache_digest_errors`. + # + # @private + def handle_error(error, context) + raise error if ViewComponent::Base.config.raise_on_cache_digest_errors + + logger&.warn { "[ViewComponent] Ignored an error while #{context}: #{error.class}: #{error.message}" } + end + + # @return [ActiveSupport::BroadcastLogger, Logger, nil] nil outside a Rails application. + def logger + ActiveSupport::LogSubscriber.logger + end + private # Resolve a constant name to a component that opted into caching. @@ -234,8 +259,8 @@ def constantize_component(constant_name) return unless component.respond_to?(:__vc_cacheable?) && component.__vc_cacheable? component - rescue - # Never let digest computation break rendering. + rescue => error + handle_error(error, "resolving #{constant_name}") nil end end diff --git a/lib/view_component/cache_digest/dependency_tracking.rb b/lib/view_component/cache_digest/dependency_tracking.rb index 737616dde..851efc8b2 100644 --- a/lib/view_component/cache_digest/dependency_tracking.rb +++ b/lib/view_component/cache_digest/dependency_tracking.rb @@ -27,10 +27,10 @@ def find_dependencies(name, template, view_paths = nil) end dependencies + CacheDigest.dependencies_in(template) - rescue - # A broken digest is preferable to a broken render. Falling back to the - # dependencies Rails found on its own means the component simply isn't - # tracked, which is the pre-existing behavior. + rescue => error + # Falling back to the dependencies Rails found on its own means the + # component simply isn't tracked, which is the pre-existing behavior. + CacheDigest.handle_error(error, "tracking component dependencies in #{name}") super end diff --git a/lib/view_component/cache_digest/resolver.rb b/lib/view_component/cache_digest/resolver.rb index eca6198bf..9e078aedb 100644 --- a/lib/view_component/cache_digest/resolver.rb +++ b/lib/view_component/cache_digest/resolver.rb @@ -30,10 +30,11 @@ def find_templates(name, prefix, partial, details, locals = []) return [] unless component [build_template(component, virtual_path, details)] - rescue - # Never let digest resolution break rendering. Returning no template - # makes the Digestor treat this as a missing node, which degrades to - # the behavior components have without this feature. + rescue => error + # Returning no template makes the Digestor treat this as a missing + # node, which degrades to the behavior components have without this + # feature. + CacheDigest.handle_error(error, "building the digest template for #{virtual_path || name}") [] end diff --git a/lib/view_component/config.rb b/lib/view_component/config.rb index 0ad336838..7b6bd453b 100644 --- a/lib/view_component/config.rb +++ b/lib/view_component/config.rb @@ -14,7 +14,8 @@ def defaults ActiveSupport::OrderedOptions.new.merge!({ generate: default_generate_options, previews: default_previews_options, - instrumentation_enabled: false + instrumentation_enabled: false, + raise_on_cache_digest_errors: default_raise_on_cache_digest_errors }) end @@ -131,6 +132,20 @@ def defaults # Whether ActiveSupport notifications are enabled. # Defaults to `false`. + # @!attribute raise_on_cache_digest_errors + # + # @return [Boolean] + # Whether to raise when computing a component's cache digest fails. + # + # Digest failures are otherwise swallowed, since a stale fragment is + # preferable to a failed render, and reported to the log at `warn`. That + # trade is wrong in development and test, where an untracked component + # looks exactly like a component that was never cached. + # + # Defaults to `true` in local environments and `false` elsewhere: + # + # config.view_component.raise_on_cache_digest_errors = false + def default_preview_paths (default_rails_preview_paths + default_rails_engines_preview_paths).uniq end @@ -155,6 +170,10 @@ def registered_rails_engines_with_previews end end + def default_raise_on_cache_digest_errors + defined?(Rails.env) && Rails.env.local? + end + def default_generate_options options = ActiveSupport::OrderedOptions.new(false) options.preview_path = "" diff --git a/lib/view_component/engine.rb b/lib/view_component/engine.rb index dc0a6e1c7..be33d4729 100644 --- a/lib/view_component/engine.rb +++ b/lib/view_component/engine.rb @@ -15,6 +15,7 @@ class Engine < Rails::Engine # :nodoc: options[config_option] ||= ViewComponent::Base.public_send(config_option) end options.instrumentation_enabled = false if options.instrumentation_enabled.nil? + options.raise_on_cache_digest_errors = Rails.env.local? if options.raise_on_cache_digest_errors.nil? options.previews.enabled = (Rails.env.development? || Rails.env.test?) if options.previews.enabled.nil? if options.previews.enabled diff --git a/test/sandbox/test/config_test.rb b/test/sandbox/test/config_test.rb index 9347a6ac1..d4380391e 100644 --- a/test/sandbox/test/config_test.rb +++ b/test/sandbox/test/config_test.rb @@ -13,6 +13,7 @@ def test_defaults_are_correct assert_equal @config.previews.controller, "ViewComponentsController" assert_equal @config.previews.route, "/rails/view_components" assert_equal @config.instrumentation_enabled, false + assert_equal @config.raise_on_cache_digest_errors, Rails.env.local? assert_equal @config.previews.paths, ["#{Rails.root}/test/components/previews"] end diff --git a/test/sandbox/test/experimentally_cacheable_test.rb b/test/sandbox/test/experimentally_cacheable_test.rb index bb0566163..4e09fae57 100644 --- a/test/sandbox/test/experimentally_cacheable_test.rb +++ b/test/sandbox/test/experimentally_cacheable_test.rb @@ -204,8 +204,22 @@ def test_partial_paths_are_not_extracted_from_sources_without_render end def test_partial_path_extraction_swallows_parser_errors + swallowing_digest_errors do |log| + ViewComponent::CacheDigest::RENDER_PARSER.stub(:new, ->(*) { raise "boom" }) do + assert_empty ViewComponent::CacheDigest.partial_paths_in("render \"a/b\"", "a/b") + end + + assert_match "Ignored an error while scanning a/b for rendered partials: RuntimeError: boom", log.string + end + end + + def test_partial_path_extraction_raises_parser_errors_locally ViewComponent::CacheDigest::RENDER_PARSER.stub(:new, ->(*) { raise "boom" }) do - assert_empty ViewComponent::CacheDigest.partial_paths_in("render \"a/b\"", "a/b") + error = assert_raises(RuntimeError) do + ViewComponent::CacheDigest.partial_paths_in("render \"a/b\"", "a/b") + end + + assert_equal "boom", error.message end end @@ -437,32 +451,74 @@ def test_resolver_is_identified_by_class def test_resolver_returns_no_template_when_synthesis_fails resolver = ViewComponent::CacheDigest::Resolver.instance + swallowing_digest_errors do |log| + ViewComponent::CacheDigest.stub(:component_for, ->(_) { raise "boom" }) do + assert_empty resolver.find_templates("cacheable_component", "view_component/cache_digest", true, {}) + end + + assert_match( + "Ignored an error while building the digest template for " \ + "view_component/cache_digest/cacheable_component: RuntimeError: boom", + log.string + ) + end + end + + def test_resolver_raises_when_synthesis_fails_locally + resolver = ViewComponent::CacheDigest::Resolver.instance + ViewComponent::CacheDigest.stub(:component_for, ->(_) { raise "boom" }) do - assert_empty resolver.find_templates("cacheable_component", "view_component/cache_digest", true, {}) + assert_raises(RuntimeError) do + resolver.find_templates("cacheable_component", "view_component/cache_digest", true, {}) + end end end def test_dependency_tracking_falls_back_when_scanning_fails template = build_template("<%= render CacheableComponent.new(title: 'a') %>") + swallowing_digest_errors do |log| + ViewComponent::CacheDigest.stub(:dependencies_in, ->(_) { raise "boom" }) do + refute_includes( + ActionView::DependencyTracker.find_dependencies("some/template", template, []), + "view_component/cache_digest/cacheable_component" + ) + end + + assert_match "Ignored an error while tracking component dependencies in some/template", log.string + end + end + + def test_dependency_tracking_raises_when_scanning_fails_locally + template = build_template("<%= render CacheableComponent.new(title: 'a') %>") + ViewComponent::CacheDigest.stub(:dependencies_in, ->(_) { raise "boom" }) do - refute_includes( - ActionView::DependencyTracker.find_dependencies("some/template", template, []), - "view_component/cache_digest/cacheable_component" - ) + assert_raises(RuntimeError) { ActionView::DependencyTracker.find_dependencies("some/template", template, []) } end end def test_constantizing_swallows_unexpected_errors - Object.const_set(:BoomComponent, Class.new do - def self.__vc_cacheable? - raise ArgumentError + with_boom_component do + swallowing_digest_errors do |log| + assert_nil ViewComponent::CacheDigest.send(:constantize_component, "BoomComponent") + + assert_match "Ignored an error while resolving BoomComponent: ArgumentError", log.string end - end) + end + end - assert_nil ViewComponent::CacheDigest.send(:constantize_component, "BoomComponent") - ensure - Object.send(:remove_const, :BoomComponent) + def test_constantizing_raises_unexpected_errors_locally + with_boom_component do + assert_raises(ArgumentError) { ViewComponent::CacheDigest.send(:constantize_component, "BoomComponent") } + end + end + + def test_digest_errors_are_swallowed_without_a_logger + without_raising_digest_errors do + ViewComponent::CacheDigest.stub(:logger, nil) do + assert_nil ViewComponent::CacheDigest.handle_error(RuntimeError.new("boom"), "digesting") + end + end end def test_install_is_idempotent @@ -487,6 +543,37 @@ def recompile(component) component.__vc_compile(force: true) end + # The digest machinery raises in local environments, and the sandbox runs as + # `test`, so the swallow-and-report path has to be opted into explicitly. + def without_raising_digest_errors + previous = ViewComponent::Base.config.raise_on_cache_digest_errors + ViewComponent::Base.config.raise_on_cache_digest_errors = false + + yield + ensure + ViewComponent::Base.config.raise_on_cache_digest_errors = previous + end + + def swallowing_digest_errors + log = StringIO.new + + without_raising_digest_errors do + ViewComponent::CacheDigest.stub(:logger, ActiveSupport::Logger.new(log)) { yield log } + end + end + + def with_boom_component + Object.const_set(:BoomComponent, Class.new do + def self.__vc_cacheable? + raise ArgumentError + end + end) + + yield + ensure + Object.send(:remove_const, :BoomComponent) + end + def build_template(source) ActionView::Template.new( source, From 8ff09fff75ef1aea1c1b2c31c5d9fa83480a3a32 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Tue, 22 Sep 2026 13:40:45 -0600 Subject: [PATCH 2/8] Raise cache digest errors Match Rails' digest behavior by allowing parser, autoload, dependency tracker, and resolver failures to propagate instead of silently leaving components untracked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/CHANGELOG.md | 2 +- docs/api.md | 13 --- docs/guide/caching.md | 14 +-- lib/view_component/cache_digest.rb | 31 ------- .../cache_digest/dependency_tracking.rb | 5 -- lib/view_component/cache_digest/resolver.rb | 6 -- lib/view_component/config.rb | 21 +---- lib/view_component/engine.rb | 1 - test/sandbox/test/config_test.rb | 1 - .../test/experimentally_cacheable_test.rb | 86 +------------------ 10 files changed, 7 insertions(+), 173 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 605244d0b..6deaf18eb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,7 +10,7 @@ nav_order: 6 ## main -* Report the errors swallowed while computing a component's cache digest, instead of degrading to an untracked component with no indication that anything went wrong. Digest errors are now raised in local environments and logged at `warn` elsewhere, configurable with `config.view_component.raise_on_cache_digest_errors`. +* Raise errors encountered while computing a component's cache digest, instead of silently degrading to an untracked component that can serve stale fragments. *Erik Axel Nielsen* diff --git a/docs/api.md b/docs/api.md index 859b75cf1..b615b2d54 100644 --- a/docs/api.md +++ b/docs/api.md @@ -294,19 +294,6 @@ A custom default layout used for the previews index page and individual previews config.view_component.previews.default_layout = "preview_layout" -### `.raise_on_cache_digest_errors` - -Whether to raise when computing a component's cache digest fails. - -Digest failures are otherwise swallowed, since a stale fragment is -preferable to a failed render, and reported to the log at `warn`. That -trade is wrong in development and test, where an untracked component -looks exactly like a component that was never cached. - -Defaults to `true` in local environments and `false` elsewhere: - - config.view_component.raise_on_cache_digest_errors = false - ## ViewComponent::TestHelpers ### `#render_in_view_context(...)` diff --git a/docs/guide/caching.md b/docs/guide/caching.md index edcd9b5b9..eef183fd8 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -206,19 +206,7 @@ Declared components must include `ViewComponent::ExperimentallyCacheable` themse ## When a digest can't be computed -Computing a digest touches the autoloader, the filesystem, and Action View's dependency trackers, any of which can fail. In production those failures are swallowed: a component that can't be digested is left untracked, which is exactly the behavior it had before opting in, and a stale fragment beats a failed render. - -That trade is wrong while developing, where an untracked component is indistinguishable from a component that was never cached in the first place. Digest failures are therefore raised in local environments and logged at `warn` everywhere else: - -```console -[ViewComponent] Ignored an error while resolving PostComponent: NoMethodError: ... -``` - -To swallow them locally too, or to raise them in production: - -```ruby -config.view_component.raise_on_cache_digest_errors = false -``` +Computing a digest touches the autoloader, the filesystem, and Action View's dependency trackers, any of which can fail. ViewComponent lets those errors raise, matching Rails' digest behavior. Otherwise a component could silently become untracked and serve stale fragments. ## Caveats diff --git a/lib/view_component/cache_digest.rb b/lib/view_component/cache_digest.rb index cb58eaf0c..87fa35ed0 100644 --- a/lib/view_component/cache_digest.rb +++ b/lib/view_component/cache_digest.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/dependencies/autoload" -require "active_support/log_subscriber" require "action_view/digestor" require "action_view/render_parser" @@ -147,9 +146,6 @@ def partial_paths_in(source, name) RENDER_PARSER.new(name, source).render_calls.uniq.select do |path| source.include?(path) || source.include?(path.sub(%r{(\A|/)_}, '\1')) end - rescue => error - handle_error(error, "scanning #{name} for rendered partials") - [] end # Action View has shipped its render parser as a class (Rails 7.1, and @@ -222,30 +218,6 @@ def install! end end - # Report an exception the digest machinery swallowed. - # - # Every rescue here degrades to "this component has no dependencies", - # which is indistinguishable from a component that was never cached: the - # digest stops changing and the application serves stale HTML. A - # misconfiguration, an autoload failure, or a raising `inherited` hook is - # therefore completely invisible. - # - # In production a stale fragment beats a failed render, so the exception - # is only logged. Locally the trade goes the other way, so it's re-raised - # by default; see `config.view_component.raise_on_cache_digest_errors`. - # - # @private - def handle_error(error, context) - raise error if ViewComponent::Base.config.raise_on_cache_digest_errors - - logger&.warn { "[ViewComponent] Ignored an error while #{context}: #{error.class}: #{error.message}" } - end - - # @return [ActiveSupport::BroadcastLogger, Logger, nil] nil outside a Rails application. - def logger - ActiveSupport::LogSubscriber.logger - end - private # Resolve a constant name to a component that opted into caching. @@ -259,9 +231,6 @@ def constantize_component(constant_name) return unless component.respond_to?(:__vc_cacheable?) && component.__vc_cacheable? component - rescue => error - handle_error(error, "resolving #{constant_name}") - nil end end diff --git a/lib/view_component/cache_digest/dependency_tracking.rb b/lib/view_component/cache_digest/dependency_tracking.rb index 851efc8b2..f6100c0bb 100644 --- a/lib/view_component/cache_digest/dependency_tracking.rb +++ b/lib/view_component/cache_digest/dependency_tracking.rb @@ -27,11 +27,6 @@ def find_dependencies(name, template, view_paths = nil) end dependencies + CacheDigest.dependencies_in(template) - rescue => error - # Falling back to the dependencies Rails found on its own means the - # component simply isn't tracked, which is the pre-existing behavior. - CacheDigest.handle_error(error, "tracking component dependencies in #{name}") - super end # @private diff --git a/lib/view_component/cache_digest/resolver.rb b/lib/view_component/cache_digest/resolver.rb index 9e078aedb..6ad9e9dc7 100644 --- a/lib/view_component/cache_digest/resolver.rb +++ b/lib/view_component/cache_digest/resolver.rb @@ -30,12 +30,6 @@ def find_templates(name, prefix, partial, details, locals = []) return [] unless component [build_template(component, virtual_path, details)] - rescue => error - # Returning no template makes the Digestor treat this as a missing - # node, which degrades to the behavior components have without this - # feature. - CacheDigest.handle_error(error, "building the digest template for #{virtual_path || name}") - [] end def to_s diff --git a/lib/view_component/config.rb b/lib/view_component/config.rb index 7b6bd453b..0ad336838 100644 --- a/lib/view_component/config.rb +++ b/lib/view_component/config.rb @@ -14,8 +14,7 @@ def defaults ActiveSupport::OrderedOptions.new.merge!({ generate: default_generate_options, previews: default_previews_options, - instrumentation_enabled: false, - raise_on_cache_digest_errors: default_raise_on_cache_digest_errors + instrumentation_enabled: false }) end @@ -132,20 +131,6 @@ def defaults # Whether ActiveSupport notifications are enabled. # Defaults to `false`. - # @!attribute raise_on_cache_digest_errors - # - # @return [Boolean] - # Whether to raise when computing a component's cache digest fails. - # - # Digest failures are otherwise swallowed, since a stale fragment is - # preferable to a failed render, and reported to the log at `warn`. That - # trade is wrong in development and test, where an untracked component - # looks exactly like a component that was never cached. - # - # Defaults to `true` in local environments and `false` elsewhere: - # - # config.view_component.raise_on_cache_digest_errors = false - def default_preview_paths (default_rails_preview_paths + default_rails_engines_preview_paths).uniq end @@ -170,10 +155,6 @@ def registered_rails_engines_with_previews end end - def default_raise_on_cache_digest_errors - defined?(Rails.env) && Rails.env.local? - end - def default_generate_options options = ActiveSupport::OrderedOptions.new(false) options.preview_path = "" diff --git a/lib/view_component/engine.rb b/lib/view_component/engine.rb index 013bddee3..c6d31b13c 100644 --- a/lib/view_component/engine.rb +++ b/lib/view_component/engine.rb @@ -15,7 +15,6 @@ class Engine < Rails::Engine # :nodoc: options[config_option] ||= ViewComponent::Base.public_send(config_option) end options.instrumentation_enabled = false if options.instrumentation_enabled.nil? - options.raise_on_cache_digest_errors = Rails.env.local? if options.raise_on_cache_digest_errors.nil? options.previews.enabled = (Rails.env.development? || Rails.env.test?) if options.previews.enabled.nil? if options.previews.enabled diff --git a/test/sandbox/test/config_test.rb b/test/sandbox/test/config_test.rb index d4380391e..9347a6ac1 100644 --- a/test/sandbox/test/config_test.rb +++ b/test/sandbox/test/config_test.rb @@ -13,7 +13,6 @@ def test_defaults_are_correct assert_equal @config.previews.controller, "ViewComponentsController" assert_equal @config.previews.route, "/rails/view_components" assert_equal @config.instrumentation_enabled, false - assert_equal @config.raise_on_cache_digest_errors, Rails.env.local? assert_equal @config.previews.paths, ["#{Rails.root}/test/components/previews"] end diff --git a/test/sandbox/test/experimentally_cacheable_test.rb b/test/sandbox/test/experimentally_cacheable_test.rb index 8bd96bc4f..877db7952 100644 --- a/test/sandbox/test/experimentally_cacheable_test.rb +++ b/test/sandbox/test/experimentally_cacheable_test.rb @@ -203,17 +203,7 @@ def test_partial_paths_are_not_extracted_from_sources_without_render assert_empty ViewComponent::CacheDigest.partial_paths_in("def call; end", "a/b") end - def test_partial_path_extraction_swallows_parser_errors - swallowing_digest_errors do |log| - ViewComponent::CacheDigest::RENDER_PARSER.stub(:new, ->(*) { raise "boom" }) do - assert_empty ViewComponent::CacheDigest.partial_paths_in("render \"a/b\"", "a/b") - end - - assert_match "Ignored an error while scanning a/b for rendered partials: RuntimeError: boom", log.string - end - end - - def test_partial_path_extraction_raises_parser_errors_locally + def test_partial_path_extraction_raises_parser_errors ViewComponent::CacheDigest::RENDER_PARSER.stub(:new, ->(*) { raise "boom" }) do error = assert_raises(RuntimeError) do ViewComponent::CacheDigest.partial_paths_in("render \"a/b\"", "a/b") @@ -493,23 +483,7 @@ def test_resolver_is_identified_by_class assert_equal resolver, ViewComponent::CacheDigest::Resolver.new end - def test_resolver_returns_no_template_when_synthesis_fails - resolver = ViewComponent::CacheDigest::Resolver.instance - - swallowing_digest_errors do |log| - ViewComponent::CacheDigest.stub(:component_for, ->(_) { raise "boom" }) do - assert_empty resolver.find_templates("cacheable_component", "view_component/cache_digest", true, {}) - end - - assert_match( - "Ignored an error while building the digest template for " \ - "view_component/cache_digest/cacheable_component: RuntimeError: boom", - log.string - ) - end - end - - def test_resolver_raises_when_synthesis_fails_locally + def test_resolver_raises_when_synthesis_fails resolver = ViewComponent::CacheDigest::Resolver.instance ViewComponent::CacheDigest.stub(:component_for, ->(_) { raise "boom" }) do @@ -519,22 +493,7 @@ def test_resolver_raises_when_synthesis_fails_locally end end - def test_dependency_tracking_falls_back_when_scanning_fails - template = build_template("<%= render CacheableComponent.new(title: 'a') %>") - - swallowing_digest_errors do |log| - ViewComponent::CacheDigest.stub(:dependencies_in, ->(_) { raise "boom" }) do - refute_includes( - ActionView::DependencyTracker.find_dependencies("some/template", template, []), - "view_component/cache_digest/cacheable_component" - ) - end - - assert_match "Ignored an error while tracking component dependencies in some/template", log.string - end - end - - def test_dependency_tracking_raises_when_scanning_fails_locally + def test_dependency_tracking_raises_when_scanning_fails template = build_template("<%= render CacheableComponent.new(title: 'a') %>") ViewComponent::CacheDigest.stub(:dependencies_in, ->(_) { raise "boom" }) do @@ -542,30 +501,12 @@ def test_dependency_tracking_raises_when_scanning_fails_locally end end - def test_constantizing_swallows_unexpected_errors - with_boom_component do - swallowing_digest_errors do |log| - assert_nil ViewComponent::CacheDigest.send(:constantize_component, "BoomComponent") - - assert_match "Ignored an error while resolving BoomComponent: ArgumentError", log.string - end - end - end - - def test_constantizing_raises_unexpected_errors_locally + def test_constantizing_raises_unexpected_errors with_boom_component do assert_raises(ArgumentError) { ViewComponent::CacheDigest.send(:constantize_component, "BoomComponent") } end end - def test_digest_errors_are_swallowed_without_a_logger - without_raising_digest_errors do - ViewComponent::CacheDigest.stub(:logger, nil) do - assert_nil ViewComponent::CacheDigest.handle_error(RuntimeError.new("boom"), "digesting") - end - end - end - def test_install_is_idempotent resolver_count = ActionController::Base.view_paths.count { |path| path.is_a?(ViewComponent::CacheDigest::Resolver) } @@ -588,25 +529,6 @@ def recompile(component) component.__vc_compile(force: true) end - # The digest machinery raises in local environments, and the sandbox runs as - # `test`, so the swallow-and-report path has to be opted into explicitly. - def without_raising_digest_errors - previous = ViewComponent::Base.config.raise_on_cache_digest_errors - ViewComponent::Base.config.raise_on_cache_digest_errors = false - - yield - ensure - ViewComponent::Base.config.raise_on_cache_digest_errors = previous - end - - def swallowing_digest_errors - log = StringIO.new - - without_raising_digest_errors do - ViewComponent::CacheDigest.stub(:logger, ActiveSupport::Logger.new(log)) { yield log } - end - end - def with_boom_component Object.const_set(:BoomComponent, Class.new do def self.__vc_cacheable? From 0e6ff7efc8ddddef06ab5d15c4399a53acab6fbe Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Tue, 22 Sep 2026 13:53:04 -0600 Subject: [PATCH 3/8] Fix cache digest prose lint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/CHANGELOG.md | 2 +- docs/guide/caching.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6deaf18eb..bdf1055df 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,7 +10,7 @@ nav_order: 6 ## main -* Raise errors encountered while computing a component's cache digest, instead of silently degrading to an untracked component that can serve stale fragments. +* Raise errors encountered while computing a component's cache digest, instead of degrading to an untracked component that can serve stale fragments. *Erik Axel Nielsen* diff --git a/docs/guide/caching.md b/docs/guide/caching.md index eef183fd8..5cbcfe3f0 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -206,7 +206,7 @@ Declared components must include `ViewComponent::ExperimentallyCacheable` themse ## When a digest can't be computed -Computing a digest touches the autoloader, the filesystem, and Action View's dependency trackers, any of which can fail. ViewComponent lets those errors raise, matching Rails' digest behavior. Otherwise a component could silently become untracked and serve stale fragments. +Computing a digest touches the autoloader, the filesystem, and Action View's dependency trackers, any of which can fail. ViewComponent lets those errors raise, matching Rails' digest behavior. Otherwise a component could become untracked and serve stale fragments without warning. ## Caveats From 572073b7d49f4bf16d926b75ba16e47e31eeb231 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Tue, 22 Sep 2026 13:57:41 -0600 Subject: [PATCH 4/8] Test digest errors through a component Replace CacheDigest stubs and private constantization calls with a cacheable component exercised through the public cache_digest API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../raising_cache_digest_component.rb | 25 +++++++++ .../test/experimentally_cacheable_test.rb | 55 +++---------------- 2 files changed, 34 insertions(+), 46 deletions(-) create mode 100644 test/sandbox/app/components/raising_cache_digest_component.rb diff --git a/test/sandbox/app/components/raising_cache_digest_component.rb b/test/sandbox/app/components/raising_cache_digest_component.rb new file mode 100644 index 000000000..a5f3b106e --- /dev/null +++ b/test/sandbox/app/components/raising_cache_digest_component.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class RaisingCacheDigestComponent < ViewComponent::Base + include ViewComponent::ExperimentallyCacheable + + cache_on :identity + + class_attribute :raise_on_digest, default: false + + def self.sidecar_files(*) + raise "boom" if raise_on_digest + + super + end + + def call + "raising cache digest" + end + + private + + def identity + "raising-cache-digest" + end +end diff --git a/test/sandbox/test/experimentally_cacheable_test.rb b/test/sandbox/test/experimentally_cacheable_test.rb index 877db7952..607f5ccce 100644 --- a/test/sandbox/test/experimentally_cacheable_test.rb +++ b/test/sandbox/test/experimentally_cacheable_test.rb @@ -41,6 +41,15 @@ def test_cache_digest_is_computable_outside_a_request refute_empty digest end + def test_cache_digest_errors_are_raised + RaisingCacheDigestComponent.raise_on_digest = true + error = assert_raises(RuntimeError) { RaisingCacheDigestComponent.cache_digest } + + assert_equal "boom", error.message + ensure + RaisingCacheDigestComponent.raise_on_digest = false + end + def test_cache_digest_changes_when_the_template_changes assert_digest_changes( "app/components/cacheable_component.html.erb", @@ -203,16 +212,6 @@ def test_partial_paths_are_not_extracted_from_sources_without_render assert_empty ViewComponent::CacheDigest.partial_paths_in("def call; end", "a/b") end - def test_partial_path_extraction_raises_parser_errors - ViewComponent::CacheDigest::RENDER_PARSER.stub(:new, ->(*) { raise "boom" }) do - error = assert_raises(RuntimeError) do - ViewComponent::CacheDigest.partial_paths_in("render \"a/b\"", "a/b") - end - - assert_equal "boom", error.message - end - end - # Action View has shipped the parser as a class (7.1, main) and as a module # with a `Default` implementation (7.2 through 8.1). Exercised with doubles so # both shapes are covered whichever version is running. @@ -483,30 +482,6 @@ def test_resolver_is_identified_by_class assert_equal resolver, ViewComponent::CacheDigest::Resolver.new end - def test_resolver_raises_when_synthesis_fails - resolver = ViewComponent::CacheDigest::Resolver.instance - - ViewComponent::CacheDigest.stub(:component_for, ->(_) { raise "boom" }) do - assert_raises(RuntimeError) do - resolver.find_templates("cacheable_component", "view_component/cache_digest", true, {}) - end - end - end - - def test_dependency_tracking_raises_when_scanning_fails - template = build_template("<%= render CacheableComponent.new(title: 'a') %>") - - ViewComponent::CacheDigest.stub(:dependencies_in, ->(_) { raise "boom" }) do - assert_raises(RuntimeError) { ActionView::DependencyTracker.find_dependencies("some/template", template, []) } - end - end - - def test_constantizing_raises_unexpected_errors - with_boom_component do - assert_raises(ArgumentError) { ViewComponent::CacheDigest.send(:constantize_component, "BoomComponent") } - end - end - def test_install_is_idempotent resolver_count = ActionController::Base.view_paths.count { |path| path.is_a?(ViewComponent::CacheDigest::Resolver) } @@ -529,18 +504,6 @@ def recompile(component) component.__vc_compile(force: true) end - def with_boom_component - Object.const_set(:BoomComponent, Class.new do - def self.__vc_cacheable? - raise ArgumentError - end - end) - - yield - ensure - Object.send(:remove_const, :BoomComponent) - end - def build_template(source, virtual_path: "test/template") ActionView::Template.new( source, From 9380c1b459ba53cd75d51206297b286e3ca590a6 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Tue, 22 Sep 2026 14:00:45 -0600 Subject: [PATCH 5/8] Exercise errors through cache_on Use a real cache key method that raises during a normal cached render instead of overriding component internals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../raising_cache_digest_component.rb | 25 ------------------- .../components/raising_cache_key_component.rb | 17 +++++++++++++ .../test/experimentally_cacheable_test.rb | 9 +++---- 3 files changed, 21 insertions(+), 30 deletions(-) delete mode 100644 test/sandbox/app/components/raising_cache_digest_component.rb create mode 100644 test/sandbox/app/components/raising_cache_key_component.rb diff --git a/test/sandbox/app/components/raising_cache_digest_component.rb b/test/sandbox/app/components/raising_cache_digest_component.rb deleted file mode 100644 index a5f3b106e..000000000 --- a/test/sandbox/app/components/raising_cache_digest_component.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -class RaisingCacheDigestComponent < ViewComponent::Base - include ViewComponent::ExperimentallyCacheable - - cache_on :identity - - class_attribute :raise_on_digest, default: false - - def self.sidecar_files(*) - raise "boom" if raise_on_digest - - super - end - - def call - "raising cache digest" - end - - private - - def identity - "raising-cache-digest" - end -end diff --git a/test/sandbox/app/components/raising_cache_key_component.rb b/test/sandbox/app/components/raising_cache_key_component.rb new file mode 100644 index 000000000..b7225803b --- /dev/null +++ b/test/sandbox/app/components/raising_cache_key_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class RaisingCacheKeyComponent < ViewComponent::Base + include ViewComponent::ExperimentallyCacheable + + cache_on :identity + + def call + "raising cache key" + end + + private + + def identity + raise "boom" + end +end diff --git a/test/sandbox/test/experimentally_cacheable_test.rb b/test/sandbox/test/experimentally_cacheable_test.rb index 607f5ccce..c58ecb46d 100644 --- a/test/sandbox/test/experimentally_cacheable_test.rb +++ b/test/sandbox/test/experimentally_cacheable_test.rb @@ -41,13 +41,12 @@ def test_cache_digest_is_computable_outside_a_request refute_empty digest end - def test_cache_digest_errors_are_raised - RaisingCacheDigestComponent.raise_on_digest = true - error = assert_raises(RuntimeError) { RaisingCacheDigestComponent.cache_digest } + def test_cache_on_method_errors_are_raised + error = with_caching do + assert_raises(RuntimeError) { render_inline(RaisingCacheKeyComponent.new) } + end assert_equal "boom", error.message - ensure - RaisingCacheDigestComponent.raise_on_digest = false end def test_cache_digest_changes_when_the_template_changes From e6630597abc416e52fd97e583f7c78c1ddb81eea Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Tue, 22 Sep 2026 14:07:23 -0600 Subject: [PATCH 6/8] Test digest errors through public API Exercise autoload, template dependency tracking, and digest source failures through real components and cache_digest without stubbing ViewComponent internals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...eable_raising_ruby_dependency_component.rb | 13 ++++++++++++ ...ing_template_dependency_component.html.erb | 1 + ...e_raising_template_dependency_component.rb | 9 +++++++++ ...able_unreadable_digest_source_component.rb | 9 +++++++++ .../placeholder | 1 + .../components/raising_cache_key_component.rb | 17 ---------------- .../test/experimentally_cacheable_test.rb | 20 ++++++++++++++----- .../cache_digest/raising_ruby_dependency.rb | 3 +++ .../raising_template_dependency.rb | 3 +++ 9 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 test/sandbox/app/components/cacheable_raising_ruby_dependency_component.rb create mode 100644 test/sandbox/app/components/cacheable_raising_template_dependency_component.html.erb create mode 100644 test/sandbox/app/components/cacheable_raising_template_dependency_component.rb create mode 100644 test/sandbox/app/components/cacheable_unreadable_digest_source_component.rb create mode 100644 test/sandbox/app/components/cacheable_unreadable_digest_source_component.yml/placeholder delete mode 100644 test/sandbox/app/components/raising_cache_key_component.rb create mode 100644 test/sandbox/test/fixtures/cache_digest/raising_ruby_dependency.rb create mode 100644 test/sandbox/test/fixtures/cache_digest/raising_template_dependency.rb diff --git a/test/sandbox/app/components/cacheable_raising_ruby_dependency_component.rb b/test/sandbox/app/components/cacheable_raising_ruby_dependency_component.rb new file mode 100644 index 000000000..0b5299df6 --- /dev/null +++ b/test/sandbox/app/components/cacheable_raising_ruby_dependency_component.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module CacheDigestFixtures + autoload :RaisingRubyDependency, Rails.root.join("test/fixtures/cache_digest/raising_ruby_dependency.rb") +end + +class CacheableRaisingRubyDependencyComponent < ViewComponent::Base + include ViewComponent::ExperimentallyCacheable + + def call + render CacheDigestFixtures::RaisingRubyDependency.new + end +end diff --git a/test/sandbox/app/components/cacheable_raising_template_dependency_component.html.erb b/test/sandbox/app/components/cacheable_raising_template_dependency_component.html.erb new file mode 100644 index 000000000..8cd72058b --- /dev/null +++ b/test/sandbox/app/components/cacheable_raising_template_dependency_component.html.erb @@ -0,0 +1 @@ +<%= render CacheDigestFixtures::RaisingTemplateDependency.new %> diff --git a/test/sandbox/app/components/cacheable_raising_template_dependency_component.rb b/test/sandbox/app/components/cacheable_raising_template_dependency_component.rb new file mode 100644 index 000000000..20a092cfb --- /dev/null +++ b/test/sandbox/app/components/cacheable_raising_template_dependency_component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module CacheDigestFixtures + autoload :RaisingTemplateDependency, Rails.root.join("test/fixtures/cache_digest/raising_template_dependency.rb") +end + +class CacheableRaisingTemplateDependencyComponent < ViewComponent::Base + include ViewComponent::ExperimentallyCacheable +end diff --git a/test/sandbox/app/components/cacheable_unreadable_digest_source_component.rb b/test/sandbox/app/components/cacheable_unreadable_digest_source_component.rb new file mode 100644 index 000000000..19db7d5dd --- /dev/null +++ b/test/sandbox/app/components/cacheable_unreadable_digest_source_component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class CacheableUnreadableDigestSourceComponent < ViewComponent::Base + include ViewComponent::ExperimentallyCacheable + + def call + "unreadable digest source" + end +end diff --git a/test/sandbox/app/components/cacheable_unreadable_digest_source_component.yml/placeholder b/test/sandbox/app/components/cacheable_unreadable_digest_source_component.yml/placeholder new file mode 100644 index 000000000..2e61bb449 --- /dev/null +++ b/test/sandbox/app/components/cacheable_unreadable_digest_source_component.yml/placeholder @@ -0,0 +1 @@ +This directory is intentionally unreadable as a digest source. diff --git a/test/sandbox/app/components/raising_cache_key_component.rb b/test/sandbox/app/components/raising_cache_key_component.rb deleted file mode 100644 index b7225803b..000000000 --- a/test/sandbox/app/components/raising_cache_key_component.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class RaisingCacheKeyComponent < ViewComponent::Base - include ViewComponent::ExperimentallyCacheable - - cache_on :identity - - def call - "raising cache key" - end - - private - - def identity - raise "boom" - end -end diff --git a/test/sandbox/test/experimentally_cacheable_test.rb b/test/sandbox/test/experimentally_cacheable_test.rb index c58ecb46d..2db444fad 100644 --- a/test/sandbox/test/experimentally_cacheable_test.rb +++ b/test/sandbox/test/experimentally_cacheable_test.rb @@ -41,12 +41,22 @@ def test_cache_digest_is_computable_outside_a_request refute_empty digest end - def test_cache_on_method_errors_are_raised - error = with_caching do - assert_raises(RuntimeError) { render_inline(RaisingCacheKeyComponent.new) } - end + def test_cache_digest_raises_when_a_ruby_dependency_fails_to_load + error = assert_raises(RuntimeError) { CacheableRaisingRubyDependencyComponent.cache_digest } + + assert_equal "raising Ruby dependency", error.message + end + + def test_cache_digest_raises_when_a_template_dependency_fails_to_load + error = assert_raises(RuntimeError) { CacheableRaisingTemplateDependencyComponent.cache_digest } + + assert_equal "raising template dependency", error.message + end + + def test_cache_digest_raises_when_a_digest_source_cannot_be_read + error = assert_raises(Errno::EISDIR) { CacheableUnreadableDigestSourceComponent.cache_digest } - assert_equal "boom", error.message + assert_includes error.message, "cacheable_unreadable_digest_source_component.yml" end def test_cache_digest_changes_when_the_template_changes diff --git a/test/sandbox/test/fixtures/cache_digest/raising_ruby_dependency.rb b/test/sandbox/test/fixtures/cache_digest/raising_ruby_dependency.rb new file mode 100644 index 000000000..cbdc86b12 --- /dev/null +++ b/test/sandbox/test/fixtures/cache_digest/raising_ruby_dependency.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +raise "raising Ruby dependency" diff --git a/test/sandbox/test/fixtures/cache_digest/raising_template_dependency.rb b/test/sandbox/test/fixtures/cache_digest/raising_template_dependency.rb new file mode 100644 index 000000000..3a53bd73f --- /dev/null +++ b/test/sandbox/test/fixtures/cache_digest/raising_template_dependency.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +raise "raising template dependency" From f5b1d9b557ef6f846b6aa1825c4f59226fc58361 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Tue, 22 Sep 2026 14:09:58 -0600 Subject: [PATCH 7/8] Autoload digest fixtures with Zeitwerk Use a dedicated Rails autoload path for failing dependency fixtures instead of inline Ruby autoload declarations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/cacheable_raising_ruby_dependency_component.rb | 4 ---- .../cacheable_raising_template_dependency_component.rb | 4 ---- test/sandbox/config/application.rb | 1 + .../raising_ruby_dependency.rb | 0 .../raising_template_dependency.rb | 0 5 files changed, 1 insertion(+), 8 deletions(-) rename test/sandbox/test/fixtures/{cache_digest => cache_digest_fixtures}/raising_ruby_dependency.rb (100%) rename test/sandbox/test/fixtures/{cache_digest => cache_digest_fixtures}/raising_template_dependency.rb (100%) diff --git a/test/sandbox/app/components/cacheable_raising_ruby_dependency_component.rb b/test/sandbox/app/components/cacheable_raising_ruby_dependency_component.rb index 0b5299df6..bf5445e59 100644 --- a/test/sandbox/app/components/cacheable_raising_ruby_dependency_component.rb +++ b/test/sandbox/app/components/cacheable_raising_ruby_dependency_component.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -module CacheDigestFixtures - autoload :RaisingRubyDependency, Rails.root.join("test/fixtures/cache_digest/raising_ruby_dependency.rb") -end - class CacheableRaisingRubyDependencyComponent < ViewComponent::Base include ViewComponent::ExperimentallyCacheable diff --git a/test/sandbox/app/components/cacheable_raising_template_dependency_component.rb b/test/sandbox/app/components/cacheable_raising_template_dependency_component.rb index 20a092cfb..29e3f2b08 100644 --- a/test/sandbox/app/components/cacheable_raising_template_dependency_component.rb +++ b/test/sandbox/app/components/cacheable_raising_template_dependency_component.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -module CacheDigestFixtures - autoload :RaisingTemplateDependency, Rails.root.join("test/fixtures/cache_digest/raising_template_dependency.rb") -end - class CacheableRaisingTemplateDependencyComponent < ViewComponent::Base include ViewComponent::ExperimentallyCacheable end diff --git a/test/sandbox/config/application.rb b/test/sandbox/config/application.rb index a8310d2c3..65c9082fd 100644 --- a/test/sandbox/config/application.rb +++ b/test/sandbox/config/application.rb @@ -44,6 +44,7 @@ class Application < Rails::Application # Prepare test_set_no_duplicate_autoload_paths config.autoload_paths.push("#{config.root}/my/components/previews") + config.autoload_paths.push("#{config.root}/test/fixtures") config.view_component.previews.paths << "#{config.root}/my/components/previews" config.view_component.previews.paths << "#{Rails.root}/lib/component_previews" diff --git a/test/sandbox/test/fixtures/cache_digest/raising_ruby_dependency.rb b/test/sandbox/test/fixtures/cache_digest_fixtures/raising_ruby_dependency.rb similarity index 100% rename from test/sandbox/test/fixtures/cache_digest/raising_ruby_dependency.rb rename to test/sandbox/test/fixtures/cache_digest_fixtures/raising_ruby_dependency.rb diff --git a/test/sandbox/test/fixtures/cache_digest/raising_template_dependency.rb b/test/sandbox/test/fixtures/cache_digest_fixtures/raising_template_dependency.rb similarity index 100% rename from test/sandbox/test/fixtures/cache_digest/raising_template_dependency.rb rename to test/sandbox/test/fixtures/cache_digest_fixtures/raising_template_dependency.rb From 70e8601083474abce81c7bf8dfb2c50e6650dc33 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Tue, 22 Sep 2026 14:11:27 -0600 Subject: [PATCH 8/8] Update docs/CHANGELOG.md --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bdf1055df..a6153f043 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,7 +10,7 @@ nav_order: 6 ## main -* Raise errors encountered while computing a component's cache digest, instead of degrading to an untracked component that can serve stale fragments. +* Raise errors encountered while computing a component's cache digest instead of degrading to an untracked component that can serve stale fragments. *Erik Axel Nielsen*