From 82adc1468d0022804f3af85e2000ec0c927cf89c Mon Sep 17 00:00:00 2001 From: "Kit (OpenClaw)" Date: Tue, 3 Mar 2026 14:02:26 -0500 Subject: [PATCH] WA-CI-005: Fix MountPoint.unwrap_app to stop at Class objects The unwrap_app helper introduced in PR #739 was recursively calling .app on Rails engine classes. Engine classes respond to .app (returning their routes), causing unwrap_app to traverse past the engine class and never return it. Result: MountPoint.find returned nil for all engines. Fix: add `return app if app.is_a?(Class)` guard before the .app delegation check. Engine classes are Class objects; the Constraints wrappers we want to peel are instances. This is compatible with Rails 6.1 and Rails 7. Fixes next-branch CI regression (broken since 2026-03-02 15:54 ET). --- core/lib/workarea/mount_point.rb | 20 +++++++++----------- core/test/lib/workarea/mount_point_test.rb | 7 +++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/core/lib/workarea/mount_point.rb b/core/lib/workarea/mount_point.rb index 8d38ffbb1..c17693645 100644 --- a/core/lib/workarea/mount_point.rb +++ b/core/lib/workarea/mount_point.rb @@ -2,20 +2,18 @@ module Workarea module MountPoint mattr_accessor :cache - # Traverse the app-delegation chain to find the underlying app class. - # In Rails 7, mounted engines are wrapped in one or more - # +ActionDispatch::Routing::Mapper::Constraints+ layers. Plain routes use - # +ActionDispatch::Routing::RouteSet::Dispatcher+, which does not respond to - # +#app+, so we must guard before each step. + # Traverse Rack app-delegation wrappers (Constraints layers added by Rails router) + # stopping as soon as we reach a Class (engine classes are Classes) or something + # that does not respond to :app. A depth ceiling prevents infinite loops. # - # A depth ceiling prevents infinite loops in degenerate cases (e.g., a - # Rack app whose +#app+ method returns +self+). - # - # @param app [Object] the route app (or a wrapper around it) - # @param depth [Integer] recursion depth guard (stops at 10) - # @return [Object] the innermost app object + # @param app [Object] current app object to inspect + # @param depth [Integer] recursion depth guard + # @return [Object] the innermost non-wrapper app def self.unwrap_app(app, depth = 0) return app if depth > 10 + # Stop when we reach a Class — Rails engines ARE classes and respond to .app, + # but we should not traverse into them. + return app if app.is_a?(Class) app.respond_to?(:app) ? unwrap_app(app.app, depth + 1) : app end diff --git a/core/test/lib/workarea/mount_point_test.rb b/core/test/lib/workarea/mount_point_test.rb index ec572a0f4..850036922 100644 --- a/core/test/lib/workarea/mount_point_test.rb +++ b/core/test/lib/workarea/mount_point_test.rb @@ -72,5 +72,12 @@ def test_find_memoizes_result assert_equal first, second end end + + def test_unwrap_app_stops_at_class + # Engine classes are Classes — unwrap_app must NOT call .app on them + # (engines respond to .app but we must treat them as the final node) + assert_equal Workarea::Storefront::Engine, + Workarea::MountPoint.unwrap_app(Workarea::Storefront::Engine) + end end end