From ea2f3e74c5a2a66cb24a20b8e6a093792569e19a Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 15:09:56 +0300 Subject: [PATCH 01/13] Refactor hook applier to handle multi-level inheritance - Enhance `HookApplier` with three distinct modes: no hooks, entries in ancestors, and interleaved entries with hooks. - Add `include_hooks_only` and `entries_in_ancestors?` methods for better inheritance handling. - Reverse hook iteration to enforce correct method resolution order. - Adjust DSL generator to call `included` directly for entry extensions. --- lib/stroma/dsl/generator.rb | 2 +- lib/stroma/hooks/applier.rb | 46 +++++++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/lib/stroma/dsl/generator.rb b/lib/stroma/dsl/generator.rb index 8bfa489..0730490 100644 --- a/lib/stroma/dsl/generator.rb +++ b/lib/stroma/dsl/generator.rb @@ -75,7 +75,7 @@ def included(base) base.instance_variable_set(:@stroma_matrix, mtx) base.instance_variable_set(:@stroma, State.new) - mtx.entries.each { |entry| base.include(entry.extension) } + mtx.entries.each { |entry| entry.extension.send(:included, base) } end end diff --git a/lib/stroma/hooks/applier.rb b/lib/stroma/hooks/applier.rb index 33e3418..c62bbc4 100644 --- a/lib/stroma/hooks/applier.rb +++ b/lib/stroma/hooks/applier.rb @@ -50,16 +50,54 @@ def initialize(target_class, hooks, matrix) # Applies all registered hooks to the target class. # - # For each registry entry, includes before hooks first, - # then after hooks. Does nothing if hooks collection is empty. + # Three modes based on current state: + # - No hooks: return immediately (defer entry inclusion) + # - Entries already in ancestors: include only hooks + # - Entries not in ancestors: interleave entries with hooks # # @return [void] def apply! return if @hooks.empty? + if entries_in_ancestors? + include_hooks_only + else + include_entries_with_hooks + end + end + + private + + # Checks whether any entry extension is already in the target class ancestors. + # + # @return [Boolean] + def entries_in_ancestors? + @matrix.entries.any? { |e| @target_class.ancestors.include?(e.extension) } + end + + # Includes entries interleaved with their hooks. + # + # For each entry: after hooks first (reversed), then entry, then before hooks (reversed). + # reverse_each ensures first registered = outermost in MRO. + # + # @return [void] + def include_entries_with_hooks + @matrix.entries.each do |entry| + @hooks.after(entry.key).reverse_each { |hook| @target_class.include(hook.extension) } + @target_class.include(entry.extension) + @hooks.before(entry.key).reverse_each { |hook| @target_class.include(hook.extension) } + end + end + + # Includes only hook extensions without entries. + # + # Used when entries are already in ancestors (multi-level inheritance). + # + # @return [void] + def include_hooks_only @matrix.entries.each do |entry| - @hooks.before(entry.key).each { |hook| @target_class.include(hook.extension) } - @hooks.after(entry.key).each { |hook| @target_class.include(hook.extension) } + @hooks.before(entry.key).reverse_each { |hook| @target_class.include(hook.extension) } + @hooks.after(entry.key).reverse_each { |hook| @target_class.include(hook.extension) } end end end From 80d0c46aa1086409156ecf0dcad0f634b75ad5b7 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 15:12:41 +0300 Subject: [PATCH 02/13] Add private methods for improved hook applier flexibility - Introduce `entries_in_ancestors?`, `include_entries_with_hooks`, and `include_hooks_only` as private methods. - Enhance `HookApplier` class with additional handling for entries and hooks. - Mark methods as private to encapsulate internal logic. --- sig/lib/stroma/hooks/applier.rbs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sig/lib/stroma/hooks/applier.rbs b/sig/lib/stroma/hooks/applier.rbs index 628e090..e9ca196 100644 --- a/sig/lib/stroma/hooks/applier.rbs +++ b/sig/lib/stroma/hooks/applier.rbs @@ -10,6 +10,14 @@ module Stroma def initialize: (Class target_class, Collection hooks, Matrix matrix) -> void def apply!: () -> void + + private + + def entries_in_ancestors?: () -> bool + + def include_entries_with_hooks: () -> void + + def include_hooks_only: () -> void end end end From c365cfab0089ba04f0ce30cb0a3433a94f88c290 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 15:15:25 +0300 Subject: [PATCH 03/13] Refactor DSL generator and HookApplier for deferred modules and interleaving - Update `DSL::Generator` spec to verify deferred entry inclusion for base class and interleaved inclusion with hooks for child classes. - Add tests for multi-level inheritance, ensuring hooks propagate correctly to descendants. - Enhance `HookApplier` spec with comprehensive scenarios for hook positioning (before and after) relative to entries, multi-hook applications, and deferred execution. - Introduce edge case tests for existing entries in ancestors and private methods like `entries_in_ancestors?`. --- spec/stroma/dsl/generator_spec.rb | 64 +++++++++++- spec/stroma/hooks/applier_spec.rb | 167 ++++++++++++++++++++++++------ 2 files changed, 199 insertions(+), 32 deletions(-) diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index c2f5b2c..3a52eac 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -56,9 +56,9 @@ expect(base_class).to respond_to(:inherited) end - it "includes all registered DSL modules", :aggregate_failures do - expect(base_class.ancestors).to include(inputs_dsl) - expect(base_class.ancestors).to include(outputs_dsl) + it "does not include entry modules in base ancestors (deferred)", :aggregate_failures do + expect(base_class.ancestors).not_to include(inputs_dsl) + expect(base_class.ancestors).not_to include(outputs_dsl) end it "creates stroma state" do @@ -102,6 +102,16 @@ def self.included(base) expect(child_class.extension_method).to eq(:extension_result) end + it "includes entry modules in child via interleaving", :aggregate_failures do + expect(child_class.ancestors).to include(inputs_dsl) + expect(child_class.ancestors).to include(outputs_dsl) + end + + it "positions hook adjacent to its target entry in child" do + ancestors = child_class.ancestors + expect(ancestors.index(extension_module)).to be < ancestors.index(inputs_dsl) + end + it "copies stroma state to child", :aggregate_failures do expect(child_class.stroma).not_to eq(base_class.stroma) expect(child_class.stroma).to be_a(Stroma::State) @@ -112,6 +122,54 @@ def self.included(base) end end + describe "multi-level inheritance" do + let(:auth_module) do + Module.new do + def self.included(base) + base.define_singleton_method(:auth_configured) { true } + end + end + end + + let(:base_class) do + mtx = matrix + Class.new { include mtx.dsl } + end + + let(:app_base) do + base = base_class + auth = auth_module + Class.new(base) do + extensions do + before :inputs, auth + end + end + end + + let(:leaf_class) { Class.new(app_base) } + + it "defers entries in base (no hooks)", :aggregate_failures do + expect(base_class.ancestors).not_to include(inputs_dsl) + expect(base_class.ancestors).not_to include(outputs_dsl) + end + + it "defers entries in app_base (hooks registered but not applied yet)", :aggregate_failures do + expect(app_base.ancestors).not_to include(inputs_dsl) + expect(app_base.ancestors).not_to include(outputs_dsl) + end + + it "interleaves entries with hooks in leaf class", :aggregate_failures do + ancestors = leaf_class.ancestors + + expect(ancestors).to include(inputs_dsl, outputs_dsl, auth_module) + expect(ancestors.index(auth_module)).to be < ancestors.index(inputs_dsl) + end + + it "propagates hook class methods to leaf via interleaving" do + expect(leaf_class).to respond_to(:auth_configured) + end + end + describe "inheritance isolation" do let(:extension_module) { Module.new } diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index 93b98d6..166049d 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -30,54 +30,163 @@ end describe "#apply!" do - it "does nothing when hooks empty" do - applier.apply! - expect(target_class.ancestors).not_to include(inputs_dsl) + context "when hooks are empty (defer)" do + it "does not modify target class ancestors" do + ancestors_before = target_class.ancestors.dup + applier.apply! + expect(target_class.ancestors).to eq(ancestors_before) + end + + it "does not include entry extensions", :aggregate_failures do + applier.apply! + expect(target_class.ancestors).not_to include(inputs_dsl) + expect(target_class.ancestors).not_to include(outputs_dsl) + end end - context "with before hooks" do - let(:before_extension) { Module.new } + context "when entries are NOT in ancestors (interleave)" do + context "with a before hook" do + let(:before_ext) { Module.new } - before do - hooks.add(:before, :inputs, before_extension) + before { hooks.add(:before, :inputs, before_ext) } + + it "positions before hook above entry in MRO" do + applier.apply! + ancestors = target_class.ancestors + + expect(ancestors.index(before_ext)).to be < ancestors.index(inputs_dsl) + end + + it "includes all entries" do + applier.apply! + ancestors = target_class.ancestors + + expect(ancestors).to include(inputs_dsl, outputs_dsl) + end end - it "includes before hook extension" do - applier.apply! - expect(target_class.ancestors).to include(before_extension) + context "with an after hook" do + let(:after_ext) { Module.new } + + before { hooks.add(:after, :inputs, after_ext) } + + it "positions after hook below entry in MRO" do + applier.apply! + ancestors = target_class.ancestors + + expect(ancestors.index(after_ext)).to be > ancestors.index(inputs_dsl) + end end - end - context "with after hooks" do - let(:after_extension) { Module.new } + context "with both before and after hooks" do + let(:before_ext) { Module.new } + let(:after_ext) { Module.new } - before do - hooks.add(:after, :outputs, after_extension) + before do + hooks.add(:before, :inputs, before_ext) + hooks.add(:after, :inputs, after_ext) + end + + it "positions before above and after below entry", :aggregate_failures do + applier.apply! + ancestors = target_class.ancestors + + before_idx = ancestors.index(before_ext) + entry_idx = ancestors.index(inputs_dsl) + after_idx = ancestors.index(after_ext) + + expect(before_idx).to be < entry_idx + expect(after_idx).to be > entry_idx + end end - it "includes after hook extension" do - applier.apply! - expect(target_class.ancestors).to include(after_extension) + context "with multiple before hooks" do + let(:first_before) { Module.new } + let(:second_before) { Module.new } + + before do + hooks.add(:before, :inputs, first_before) + hooks.add(:before, :inputs, second_before) + end + + it "first registered is outermost in MRO", :aggregate_failures do + applier.apply! + ancestors = target_class.ancestors + + expect(ancestors.index(first_before)).to be < ancestors.index(second_before) + expect(ancestors.index(second_before)).to be < ancestors.index(inputs_dsl) + end + end + + context "with multiple after hooks" do + let(:first_after) { Module.new } + let(:second_after) { Module.new } + + before do + hooks.add(:after, :inputs, first_after) + hooks.add(:after, :inputs, second_after) + end + + it "first registered is closest to entry", :aggregate_failures do + applier.apply! + ancestors = target_class.ancestors + + expect(ancestors.index(inputs_dsl)).to be < ancestors.index(first_after) + expect(ancestors.index(first_after)).to be < ancestors.index(second_after) + end + end + + context "with hooks for different entries" do + let(:before_inputs_ext) { Module.new } + let(:after_outputs_ext) { Module.new } + + before do + hooks.add(:before, :inputs, before_inputs_ext) + hooks.add(:after, :outputs, after_outputs_ext) + end + + it "each hook is adjacent to its target entry", :aggregate_failures do + applier.apply! + ancestors = target_class.ancestors + + expect(ancestors.index(before_inputs_ext)).to be < ancestors.index(inputs_dsl) + expect(ancestors.index(after_outputs_ext)).to be > ancestors.index(outputs_dsl) + end end end - context "with multiple hooks" do # rubocop:disable RSpec/MultipleMemoizedHelpers - let(:before_inputs) { Module.new } - let(:after_inputs) { Module.new } - let(:before_outputs) { Module.new } + context "when entries are already in ancestors (hooks only)" do + let(:before_ext) { Module.new } before do - hooks.add(:before, :inputs, before_inputs) - hooks.add(:after, :inputs, after_inputs) - hooks.add(:before, :outputs, before_outputs) + target_class.include(outputs_dsl) + target_class.include(inputs_dsl) + hooks.add(:before, :inputs, before_ext) end - it "applies all hooks", :aggregate_failures do + it "includes hook extensions" do applier.apply! - expect(target_class.ancestors).to include(before_inputs) - expect(target_class.ancestors).to include(after_inputs) - expect(target_class.ancestors).to include(before_outputs) + expect(target_class.ancestors).to include(before_ext) + end + + it "does not duplicate entries in ancestors" do + count_before = target_class.ancestors.count { |a| a == inputs_dsl } + applier.apply! + count_after = target_class.ancestors.count { |a| a == inputs_dsl } + + expect(count_after).to eq(count_before) end end end + + describe "entries_in_ancestors? (private)" do + it "returns false when entries not in ancestors" do + expect(applier.send(:entries_in_ancestors?)).to be false + end + + it "returns true when an entry is in ancestors" do + target_class.include(inputs_dsl) + expect(applier.send(:entries_in_ancestors?)).to be true + end + end end From 2fecdf106c619861281d611472bb7076a4e8354f Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 15:22:15 +0300 Subject: [PATCH 04/13] Refactor DSL generator and HookApplier specs for consistency - Remove "(deferred)", "(interleave)", and similar labels from test titles for clarity and uniformity. - Reorder and group related tests in `DSL::Generator` spec to improve readability. - Replace "parent_class" with "base_class" in `inheritance isolation` tests to align with consistent terminology. - Remove unused `entries_in_ancestors?` tests from `HookApplier` spec to streamline focus on public behavior. --- spec/stroma/dsl/generator_spec.rb | 34 +++++++++++++++---------------- spec/stroma/hooks/applier_spec.rb | 17 +++------------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index 3a52eac..6fe22a9 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -56,7 +56,7 @@ expect(base_class).to respond_to(:inherited) end - it "does not include entry modules in base ancestors (deferred)", :aggregate_failures do + it "does not include entry modules in base ancestors", :aggregate_failures do expect(base_class.ancestors).not_to include(inputs_dsl) expect(base_class.ancestors).not_to include(outputs_dsl) end @@ -93,25 +93,25 @@ def self.included(base) let(:child_class) { Class.new(base_class) } - it "applies hooks to child class" do - expect(child_class.ancestors).to include(extension_module) - end - - it "child has extension method", :aggregate_failures do - expect(child_class).to respond_to(:extension_method) - expect(child_class.extension_method).to eq(:extension_result) - end - it "includes entry modules in child via interleaving", :aggregate_failures do expect(child_class.ancestors).to include(inputs_dsl) expect(child_class.ancestors).to include(outputs_dsl) end + it "applies hooks to child class" do + expect(child_class.ancestors).to include(extension_module) + end + it "positions hook adjacent to its target entry in child" do ancestors = child_class.ancestors expect(ancestors.index(extension_module)).to be < ancestors.index(inputs_dsl) end + it "child has extension method", :aggregate_failures do + expect(child_class).to respond_to(:extension_method) + expect(child_class.extension_method).to eq(:extension_result) + end + it "copies stroma state to child", :aggregate_failures do expect(child_class.stroma).not_to eq(base_class.stroma) expect(child_class.stroma).to be_a(Stroma::State) @@ -173,7 +173,7 @@ def self.included(base) describe "inheritance isolation" do let(:extension_module) { Module.new } - let(:parent_class) do + let(:base_class) do mtx = matrix ext = extension_module Class.new do @@ -185,9 +185,9 @@ def self.included(base) end end - let(:child_class) { Class.new(parent_class) } + let(:child_class) { Class.new(base_class) } - it "child modifications do not affect parent", :aggregate_failures do + it "child modifications do not affect base", :aggregate_failures do child_extension = Module.new child_class.class_eval do @@ -196,20 +196,20 @@ def self.included(base) end end - expect(parent_class.stroma.hooks.after(:outputs)).to be_empty + expect(base_class.stroma.hooks.after(:outputs)).to be_empty expect(child_class.stroma.hooks.after(:outputs)).not_to be_empty end - it "child inherits parent hooks", :aggregate_failures do + it "child inherits base hooks", :aggregate_failures do expect(child_class.stroma.hooks.before(:inputs).size).to eq(1) expect(child_class.ancestors).to include(extension_module) end - it "parent modifications after child creation do not affect child" do + it "base modifications after child creation do not affect child" do child_before_count = child_class.stroma.hooks.before(:outputs).size new_extension = Module.new - parent_class.class_eval do + base_class.class_eval do extensions do before :outputs, new_extension end diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index 166049d..345428c 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -30,7 +30,7 @@ end describe "#apply!" do - context "when hooks are empty (defer)" do + context "when hooks are empty" do it "does not modify target class ancestors" do ancestors_before = target_class.ancestors.dup applier.apply! @@ -44,7 +44,7 @@ end end - context "when entries are NOT in ancestors (interleave)" do + context "when entries are not in ancestors" do context "with a before hook" do let(:before_ext) { Module.new } @@ -155,7 +155,7 @@ end end - context "when entries are already in ancestors (hooks only)" do + context "when entries are already in ancestors" do let(:before_ext) { Module.new } before do @@ -178,15 +178,4 @@ end end end - - describe "entries_in_ancestors? (private)" do - it "returns false when entries not in ancestors" do - expect(applier.send(:entries_in_ancestors?)).to be false - end - - it "returns true when an entry is in ancestors" do - target_class.include(inputs_dsl) - expect(applier.send(:entries_in_ancestors?)).to be true - end - end end From 1283796c17de0bcf741b8f726b97f10d13912db1 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 17:26:57 +0300 Subject: [PATCH 05/13] Handle deferred inclusion in DSL generator for entry extensions - Introduce deferred inclusion workflow, triggering `included` callback for entry extensions without immediately appending features to ancestors. - Document idempotency requirement for `self.included` callbacks in entry extensions. - Ensure proper interleaving of module inclusion and hooks by deferring actual module insertion until processed by Applier. --- lib/stroma/dsl/generator.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/stroma/dsl/generator.rb b/lib/stroma/dsl/generator.rb index 0730490..b0bd08d 100644 --- a/lib/stroma/dsl/generator.rb +++ b/lib/stroma/dsl/generator.rb @@ -75,6 +75,12 @@ def included(base) base.instance_variable_set(:@stroma_matrix, mtx) base.instance_variable_set(:@stroma, State.new) + # Deferred inclusion: triggers `included` callback without `append_features`. + # The callback runs ClassMethods/Workspace setup on base. + # `append_features` (actual module insertion into ancestors) is deferred + # until Applier interleaves entries with hooks in child classes. + # NOTE: `included` will fire again when Applier calls `include` on child, + # so entry extensions must design `self.included` as idempotent. mtx.entries.each { |entry| entry.extension.send(:included, base) } end end From 719c9e95270341d7f11a915e0cd7fdcb7bd2366d Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 17:27:06 +0300 Subject: [PATCH 06/13] Handle deferred entries and hooks interleaving in HookApplier - Update `HookApplier` to manage deferred entry inclusion with hooks, ensuring correct method resolution order (MRO). - Refactor `entries_in_ancestors?` to check for all entries instead of any, enabling precise handling of partially included entries. - Improve documentation for hook inclusion logic and state-based operational modes. - Enhance method idempotency to allow seamless reprocessing of included entries and hooks. --- lib/stroma/hooks/applier.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/stroma/hooks/applier.rb b/lib/stroma/hooks/applier.rb index c62bbc4..2c83992 100644 --- a/lib/stroma/hooks/applier.rb +++ b/lib/stroma/hooks/applier.rb @@ -2,13 +2,15 @@ module Stroma module Hooks - # Applies registered hooks to a target class. + # Applies registered hooks to a target class with deferred entry inclusion. # # ## Purpose # - # Includes hook extension modules into target class. - # Maintains order based on matrix registry entries. - # For each entry: before hooks first, then after hooks. + # Manages hook and entry module inclusion into the target class. + # Operates in three modes depending on current state: + # - No hooks: returns immediately (entries stay deferred) + # - Entries already in ancestors: includes only new hooks + # - Entries not in ancestors: interleaves entries with hooks for correct MRO # # ## Usage # @@ -68,11 +70,16 @@ def apply! private - # Checks whether any entry extension is already in the target class ancestors. + # Checks whether all entry extensions are already in the target class ancestors. + # + # Uses all? so that partial inclusion (some entries present, some not) + # falls through to include_entries_with_hooks where Ruby skips + # already-included modules (idempotent) and interleaves the rest. # # @return [Boolean] def entries_in_ancestors? - @matrix.entries.any? { |e| @target_class.ancestors.include?(e.extension) } + ancestors = @target_class.ancestors + @matrix.entries.all? { |e| ancestors.include?(e.extension) } end # Includes entries interleaved with their hooks. From ecac13c77b388ed14c3eae625dfe9a92a5c2cdb8 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 17:27:12 +0300 Subject: [PATCH 07/13] Add tests for multi-level inheritance and hook propagation - Enhance `DSL::Generator` spec with new test cases for hooks and entries in multi-level inheritance scenarios. - Add tests for propagating hooks through all ancestor levels. - Verify inheritance isolation for grandchild classes without explicit hooks. - Update `HookApplier` spec with expanded scenarios for before and after hooks. - Cover cases for individual hooks, combined hooks (both before and after), and duplication prevention in ancestors. - Ensure improved coverage for hook inclusion and inheritance behaviors across the specs. --- spec/stroma/dsl/generator_spec.rb | 68 +++++++++++++++++++++++++++++++ spec/stroma/hooks/applier_spec.rb | 52 ++++++++++++++++++----- 2 files changed, 109 insertions(+), 11 deletions(-) diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index 6fe22a9..d805f39 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -170,6 +170,74 @@ def self.included(base) end end + describe "multi-level with hooks at every level" do + let(:auth_module) { Module.new } + let(:logging_module) { Module.new } + + let(:base_class) do + mtx = matrix + Class.new { include mtx.dsl } + end + + let(:mid_class) do + base = base_class + auth = auth_module + Class.new(base) do + extensions do + before :inputs, auth + end + end + end + + let(:leaf_class) do + mid = mid_class + logging = logging_module + Class.new(mid) do + extensions do + after :inputs, logging + end + end + end + + it "leaf inherits hooks from all levels", :aggregate_failures do + grandchild = Class.new(leaf_class) + ancestors = grandchild.ancestors + + expect(ancestors).to include(auth_module) + expect(ancestors).to include(logging_module) + expect(ancestors).to include(inputs_dsl) + expect(ancestors).to include(outputs_dsl) + end + end + + describe "grandchild without own hooks" do + let(:auth_module) { Module.new } + + let(:base_class) do + mtx = matrix + Class.new { include mtx.dsl } + end + + let(:mid_class) do + base = base_class + auth = auth_module + Class.new(base) do + extensions do + before :inputs, auth + end + end + end + + it "entries propagate to grandchild via ancestors", :aggregate_failures do + child = Class.new(mid_class) + grandchild = Class.new(child) + + expect(grandchild.ancestors).to include(inputs_dsl) + expect(grandchild.ancestors).to include(outputs_dsl) + expect(grandchild.ancestors).to include(auth_module) + end + end + describe "inheritance isolation" do let(:extension_module) { Module.new } diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index 345428c..3bc558d 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -156,25 +156,55 @@ end context "when entries are already in ancestors" do - let(:before_ext) { Module.new } - before do target_class.include(outputs_dsl) target_class.include(inputs_dsl) - hooks.add(:before, :inputs, before_ext) end - it "includes hook extensions" do - applier.apply! - expect(target_class.ancestors).to include(before_ext) + context "with a before hook" do + let(:before_ext) { Module.new } + + before { hooks.add(:before, :inputs, before_ext) } + + it "includes hook extensions" do + applier.apply! + expect(target_class.ancestors).to include(before_ext) + end + + it "does not duplicate entries in ancestors" do + count_before = target_class.ancestors.count { |a| a == inputs_dsl } + applier.apply! + count_after = target_class.ancestors.count { |a| a == inputs_dsl } + + expect(count_after).to eq(count_before) + end end - it "does not duplicate entries in ancestors" do - count_before = target_class.ancestors.count { |a| a == inputs_dsl } - applier.apply! - count_after = target_class.ancestors.count { |a| a == inputs_dsl } + context "with an after hook" do + let(:after_ext) { Module.new } - expect(count_after).to eq(count_before) + before { hooks.add(:after, :inputs, after_ext) } + + it "includes after hook extensions" do + applier.apply! + expect(target_class.ancestors).to include(after_ext) + end + end + + context "with both before and after hooks" do + let(:before_ext) { Module.new } + let(:after_ext) { Module.new } + + before do + hooks.add(:before, :inputs, before_ext) + hooks.add(:after, :inputs, after_ext) + end + + it "includes both hook types", :aggregate_failures do + applier.apply! + expect(target_class.ancestors).to include(before_ext) + expect(target_class.ancestors).to include(after_ext) + end end end end From c716be718ce096abd07d9e0ef55168fbbf57a4ff Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 17:39:10 +0300 Subject: [PATCH 08/13] Refactor specs and resolve naming inconsistencies for hooks - Simplify and unify naming in `DSL::Generator` and `HookApplier` specs, replacing redundant and inconsistent variable names (e.g., renaming `before_ext` to `before_extension`). - Restructure tests in `DSL::Generator` spec for clarity: - Group scenarios by hook levels (one level, every level, no hooks). - Rename test contexts for consistency (e.g., "multi-level with hooks at every level"). - Enhance `HookApplier` tests to ensure proper MRO (method resolution order) with hooks relative to entries: - Add scenarios for combined hooks, multi-hook order, and hook isolation. - Ensure hooks and entries propagate correctly across classes and descendants. - Improve readability and maintainability of test cases through better grouping and naming. --- spec/stroma/dsl/generator_spec.rb | 155 ++++++++++++++---------------- spec/stroma/hooks/applier_spec.rb | 115 ++++++++++------------ 2 files changed, 126 insertions(+), 144 deletions(-) diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index d805f39..beef747 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -123,118 +123,109 @@ def self.included(base) end describe "multi-level inheritance" do - let(:auth_module) do - Module.new do - def self.included(base) - base.define_singleton_method(:auth_configured) { true } - end - end - end - let(:base_class) do mtx = matrix Class.new { include mtx.dsl } end - let(:app_base) do - base = base_class - auth = auth_module - Class.new(base) do - extensions do - before :inputs, auth + context "with hooks at one level" do + let(:auth_module) do + Module.new do + def self.included(base) + base.define_singleton_method(:auth_configured) { true } + end end end - end - let(:leaf_class) { Class.new(app_base) } - - it "defers entries in base (no hooks)", :aggregate_failures do - expect(base_class.ancestors).not_to include(inputs_dsl) - expect(base_class.ancestors).not_to include(outputs_dsl) - end + let(:mid_class) do + base = base_class + auth = auth_module + Class.new(base) do + extensions do + before :inputs, auth + end + end + end - it "defers entries in app_base (hooks registered but not applied yet)", :aggregate_failures do - expect(app_base.ancestors).not_to include(inputs_dsl) - expect(app_base.ancestors).not_to include(outputs_dsl) - end + let(:leaf_class) { Class.new(mid_class) } - it "interleaves entries with hooks in leaf class", :aggregate_failures do - ancestors = leaf_class.ancestors + it "defers entries in base (no hooks)", :aggregate_failures do + expect(base_class.ancestors).not_to include(inputs_dsl) + expect(base_class.ancestors).not_to include(outputs_dsl) + end - expect(ancestors).to include(inputs_dsl, outputs_dsl, auth_module) - expect(ancestors.index(auth_module)).to be < ancestors.index(inputs_dsl) - end + it "defers entries in mid class (hooks registered but not applied yet)", :aggregate_failures do + expect(mid_class.ancestors).not_to include(inputs_dsl) + expect(mid_class.ancestors).not_to include(outputs_dsl) + end - it "propagates hook class methods to leaf via interleaving" do - expect(leaf_class).to respond_to(:auth_configured) - end - end + it "interleaves entries with hooks in leaf class", :aggregate_failures do + ancestors = leaf_class.ancestors - describe "multi-level with hooks at every level" do - let(:auth_module) { Module.new } - let(:logging_module) { Module.new } + expect(ancestors).to include(inputs_dsl, outputs_dsl, auth_module) + expect(ancestors.index(auth_module)).to be < ancestors.index(inputs_dsl) + end - let(:base_class) do - mtx = matrix - Class.new { include mtx.dsl } + it "propagates hook class methods to leaf via interleaving" do + expect(leaf_class).to respond_to(:auth_configured) + end end - let(:mid_class) do - base = base_class - auth = auth_module - Class.new(base) do - extensions do - before :inputs, auth + context "with hooks at every level" do + let(:auth_module) { Module.new } + let(:logging_module) { Module.new } + + let(:mid_class) do + base = base_class + auth = auth_module + Class.new(base) do + extensions do + before :inputs, auth + end end end - end - let(:leaf_class) do - mid = mid_class - logging = logging_module - Class.new(mid) do - extensions do - after :inputs, logging + let(:leaf_class) do + mid = mid_class + logging = logging_module + Class.new(mid) do + extensions do + after :inputs, logging + end end end - end - it "leaf inherits hooks from all levels", :aggregate_failures do - grandchild = Class.new(leaf_class) - ancestors = grandchild.ancestors + it "grandchild inherits hooks from all levels", :aggregate_failures do + grandchild = Class.new(leaf_class) - expect(ancestors).to include(auth_module) - expect(ancestors).to include(logging_module) - expect(ancestors).to include(inputs_dsl) - expect(ancestors).to include(outputs_dsl) + expect(grandchild.ancestors).to include(auth_module) + expect(grandchild.ancestors).to include(logging_module) + expect(grandchild.ancestors).to include(inputs_dsl) + expect(grandchild.ancestors).to include(outputs_dsl) + end end - end - - describe "grandchild without own hooks" do - let(:auth_module) { Module.new } - let(:base_class) do - mtx = matrix - Class.new { include mtx.dsl } - end + context "when grandchild has no own hooks" do + let(:auth_module) { Module.new } - let(:mid_class) do - base = base_class - auth = auth_module - Class.new(base) do - extensions do - before :inputs, auth + let(:mid_class) do + base = base_class + auth = auth_module + Class.new(base) do + extensions do + before :inputs, auth + end end end - end - it "entries propagate to grandchild via ancestors", :aggregate_failures do - child = Class.new(mid_class) - grandchild = Class.new(child) + let(:child_class) { Class.new(mid_class) } + let(:grandchild) { Class.new(child_class) } - expect(grandchild.ancestors).to include(inputs_dsl) - expect(grandchild.ancestors).to include(outputs_dsl) - expect(grandchild.ancestors).to include(auth_module) + it "entries propagate to grandchild via ancestors", :aggregate_failures do + expect(grandchild.ancestors).to include(inputs_dsl) + expect(grandchild.ancestors).to include(outputs_dsl) + expect(grandchild.ancestors).to include(auth_module) + end end end diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index 3bc558d..7694219 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -17,15 +17,15 @@ let(:applier) { described_class.new(target_class, hooks, matrix) } describe ".apply!" do - let(:before_extension) { Module.new } + let(:before_extensionension) { Module.new } before do - hooks.add(:before, :inputs, before_extension) + hooks.add(:before, :inputs, before_extensionension) + described_class.apply!(target_class, hooks, matrix) end it "applies hooks via class method" do - described_class.apply!(target_class, hooks, matrix) - expect(target_class.ancestors).to include(before_extension) + expect(target_class.ancestors).to include(before_extensionension) end end @@ -46,57 +46,51 @@ context "when entries are not in ancestors" do context "with a before hook" do - let(:before_ext) { Module.new } + let(:before_extension) { Module.new } - before { hooks.add(:before, :inputs, before_ext) } + before do + hooks.add(:before, :inputs, before_extension) + applier.apply! + end it "positions before hook above entry in MRO" do - applier.apply! ancestors = target_class.ancestors - - expect(ancestors.index(before_ext)).to be < ancestors.index(inputs_dsl) + expect(ancestors.index(before_extension)).to be < ancestors.index(inputs_dsl) end it "includes all entries" do - applier.apply! - ancestors = target_class.ancestors - - expect(ancestors).to include(inputs_dsl, outputs_dsl) + expect(target_class.ancestors).to include(inputs_dsl, outputs_dsl) end end context "with an after hook" do - let(:after_ext) { Module.new } + let(:after_extension) { Module.new } - before { hooks.add(:after, :inputs, after_ext) } + before do + hooks.add(:after, :inputs, after_extension) + applier.apply! + end it "positions after hook below entry in MRO" do - applier.apply! ancestors = target_class.ancestors - - expect(ancestors.index(after_ext)).to be > ancestors.index(inputs_dsl) + expect(ancestors.index(after_extension)).to be > ancestors.index(inputs_dsl) end end context "with both before and after hooks" do - let(:before_ext) { Module.new } - let(:after_ext) { Module.new } + let(:before_extension) { Module.new } + let(:after_extension) { Module.new } before do - hooks.add(:before, :inputs, before_ext) - hooks.add(:after, :inputs, after_ext) + hooks.add(:before, :inputs, before_extension) + hooks.add(:after, :inputs, after_extension) + applier.apply! end it "positions before above and after below entry", :aggregate_failures do - applier.apply! ancestors = target_class.ancestors - - before_idx = ancestors.index(before_ext) - entry_idx = ancestors.index(inputs_dsl) - after_idx = ancestors.index(after_ext) - - expect(before_idx).to be < entry_idx - expect(after_idx).to be > entry_idx + expect(ancestors.index(before_extension)).to be < ancestors.index(inputs_dsl) + expect(ancestors.index(after_extension)).to be > ancestors.index(inputs_dsl) end end @@ -107,12 +101,11 @@ before do hooks.add(:before, :inputs, first_before) hooks.add(:before, :inputs, second_before) + applier.apply! end it "first registered is outermost in MRO", :aggregate_failures do - applier.apply! ancestors = target_class.ancestors - expect(ancestors.index(first_before)).to be < ancestors.index(second_before) expect(ancestors.index(second_before)).to be < ancestors.index(inputs_dsl) end @@ -125,32 +118,30 @@ before do hooks.add(:after, :inputs, first_after) hooks.add(:after, :inputs, second_after) + applier.apply! end it "first registered is closest to entry", :aggregate_failures do - applier.apply! ancestors = target_class.ancestors - expect(ancestors.index(inputs_dsl)).to be < ancestors.index(first_after) expect(ancestors.index(first_after)).to be < ancestors.index(second_after) end end context "with hooks for different entries" do - let(:before_inputs_ext) { Module.new } - let(:after_outputs_ext) { Module.new } + let(:before_inputs_extension) { Module.new } + let(:after_outputs_extension) { Module.new } before do - hooks.add(:before, :inputs, before_inputs_ext) - hooks.add(:after, :outputs, after_outputs_ext) + hooks.add(:before, :inputs, before_inputs_extension) + hooks.add(:after, :outputs, after_outputs_extension) + applier.apply! end it "each hook is adjacent to its target entry", :aggregate_failures do - applier.apply! ancestors = target_class.ancestors - - expect(ancestors.index(before_inputs_ext)).to be < ancestors.index(inputs_dsl) - expect(ancestors.index(after_outputs_ext)).to be > ancestors.index(outputs_dsl) + expect(ancestors.index(before_inputs_extension)).to be < ancestors.index(inputs_dsl) + expect(ancestors.index(after_outputs_extension)).to be > ancestors.index(outputs_dsl) end end end @@ -162,48 +153,48 @@ end context "with a before hook" do - let(:before_ext) { Module.new } + let(:before_extension) { Module.new } - before { hooks.add(:before, :inputs, before_ext) } + before do + hooks.add(:before, :inputs, before_extension) + applier.apply! + end it "includes hook extensions" do - applier.apply! - expect(target_class.ancestors).to include(before_ext) + expect(target_class.ancestors).to include(before_extension) end it "does not duplicate entries in ancestors" do - count_before = target_class.ancestors.count { |a| a == inputs_dsl } - applier.apply! - count_after = target_class.ancestors.count { |a| a == inputs_dsl } - - expect(count_after).to eq(count_before) + expect(target_class.ancestors.count { |a| a == inputs_dsl }).to eq(1) end end context "with an after hook" do - let(:after_ext) { Module.new } + let(:after_extension) { Module.new } - before { hooks.add(:after, :inputs, after_ext) } + before do + hooks.add(:after, :inputs, after_extension) + applier.apply! + end it "includes after hook extensions" do - applier.apply! - expect(target_class.ancestors).to include(after_ext) + expect(target_class.ancestors).to include(after_extension) end end context "with both before and after hooks" do - let(:before_ext) { Module.new } - let(:after_ext) { Module.new } + let(:before_extension) { Module.new } + let(:after_extension) { Module.new } before do - hooks.add(:before, :inputs, before_ext) - hooks.add(:after, :inputs, after_ext) + hooks.add(:before, :inputs, before_extension) + hooks.add(:after, :inputs, after_extension) + applier.apply! end it "includes both hook types", :aggregate_failures do - applier.apply! - expect(target_class.ancestors).to include(before_ext) - expect(target_class.ancestors).to include(after_ext) + expect(target_class.ancestors).to include(before_extension) + expect(target_class.ancestors).to include(after_extension) end end end From 76c8252d8f1d0db3099d7cbdec7408014c6e54ad Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 17:41:11 +0300 Subject: [PATCH 09/13] Fix typo in `HookApplier` spec for improved consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Correct typo in variable name: `before_extensionension` → `before_extension`. - Update expectations and hook addition logic to align with the corrected name. - Ensure accurate naming improves readability and reduces potential confusion in the spec. --- spec/stroma/hooks/applier_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/stroma/hooks/applier_spec.rb b/spec/stroma/hooks/applier_spec.rb index 7694219..2911ace 100644 --- a/spec/stroma/hooks/applier_spec.rb +++ b/spec/stroma/hooks/applier_spec.rb @@ -17,15 +17,15 @@ let(:applier) { described_class.new(target_class, hooks, matrix) } describe ".apply!" do - let(:before_extensionension) { Module.new } + let(:before_extension) { Module.new } before do - hooks.add(:before, :inputs, before_extensionension) + hooks.add(:before, :inputs, before_extension) described_class.apply!(target_class, hooks, matrix) end it "applies hooks via class method" do - expect(target_class.ancestors).to include(before_extensionension) + expect(target_class.ancestors).to include(before_extension) end end From b4a3497603bfdddab5ff1fe7097c748a61044b85 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 18:37:00 +0300 Subject: [PATCH 10/13] Document deferred entry inclusion logic for DSL Generator - Add detailed explanation about deferred `Module#include` behavior and interleaving with hooks in child classes. - Specify the contract for idempotent `self.included` implementation in entry extensions. - Improve documentation for lifecycle stages of entry inclusion across the class hierarchy. --- lib/stroma/dsl/generator.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/stroma/dsl/generator.rb b/lib/stroma/dsl/generator.rb index b0bd08d..4ce23bb 100644 --- a/lib/stroma/dsl/generator.rb +++ b/lib/stroma/dsl/generator.rb @@ -16,6 +16,19 @@ module DSL # - ServiceClass gets @stroma_matrix (same reference) # - ServiceClass gets @stroma (unique State per class) # + # ## Deferred Entry Inclusion + # + # Entry extensions are NOT included via `Module#include` at the base class level. + # Instead, only the `self.included` callback is fired (via `send(:included, base)`) + # to set up ClassMethods, constants, etc. The actual module insertion into the + # ancestor chain (`append_features`) is deferred until {Hooks::Applier} interleaves + # entries with hooks in child classes. + # + # **Contract:** Entry extensions MUST implement `self.included` as idempotent. + # The callback fires twice per entry per class hierarchy: + # 1. At base class creation (deferred, via `send(:included, base)`) + # 2. At child class creation (real, via `include` in {Hooks::Applier}) + # # ## Usage # # ```ruby From b005b60e779ac5dd9d58f97dedd63809976608e1 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 18:37:05 +0300 Subject: [PATCH 11/13] Add tests for hook positioning and MRO in multi-level inheritance - Extend `DSL::Generator` spec with tests to validate hook positioning relative to entries in grandchild classes. - Verify `before_hook` maintains correct ordering relative to entries across inherited modules. - Confirm `after_hook` appears above inherited entries in method resolution order (MRO). - Include explanatory comments on Ruby's `Module#include` limitations and their impact on MRO behavior. --- spec/stroma/dsl/generator_spec.rb | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index beef747..fb1b4db 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -197,11 +197,27 @@ def self.included(base) it "grandchild inherits hooks from all levels", :aggregate_failures do grandchild = Class.new(leaf_class) + ancestors = grandchild.ancestors - expect(grandchild.ancestors).to include(auth_module) - expect(grandchild.ancestors).to include(logging_module) - expect(grandchild.ancestors).to include(inputs_dsl) - expect(grandchild.ancestors).to include(outputs_dsl) + expect(ancestors).to include(auth_module, logging_module, inputs_dsl, outputs_dsl) + end + + it "preserves before hook position relative to entry in grandchild" do + grandchild = Class.new(leaf_class) + ancestors = grandchild.ancestors + + expect(ancestors.index(auth_module)).to be < ancestors.index(inputs_dsl) + end + + # Ruby's Module#include cannot position a new module below an inherited module. + # After hooks registered at parent level are placed above inherited entries in + # grandchild MRO. This does not affect phase execution order (controlled by + # the orchestrator), only the super call chain. + it "after hook from parent level is above inherited entry in grandchild MRO" do + grandchild = Class.new(leaf_class) + ancestors = grandchild.ancestors + + expect(ancestors.index(logging_module)).to be < ancestors.index(inputs_dsl) end end From 7b166f595142ddc0ff2e2f607e543a6cfc1d6bf3 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 18:43:51 +0300 Subject: [PATCH 12/13] Refactor test descriptions for clarity and precision - Update test descriptions in `DSL::Generator` spec to improve accuracy and readability. - Replace ambiguous terms like "defers entries" with clearer phrasing (e.g., "does not include entries"). - Consolidate overlapping test cases by grouping related assertions, simplifying structure. - Ensure test titles align with multi-level inheritance and hook propagation logic. --- spec/stroma/dsl/generator_spec.rb | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index fb1b4db..1444ac8 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -107,7 +107,7 @@ def self.included(base) expect(ancestors.index(extension_module)).to be < ancestors.index(inputs_dsl) end - it "child has extension method", :aggregate_failures do + it "propagates extension callback to child", :aggregate_failures do expect(child_class).to respond_to(:extension_method) expect(child_class.extension_method).to eq(:extension_result) end @@ -149,12 +149,12 @@ def self.included(base) let(:leaf_class) { Class.new(mid_class) } - it "defers entries in base (no hooks)", :aggregate_failures do + it "does not include entries in base", :aggregate_failures do expect(base_class.ancestors).not_to include(inputs_dsl) expect(base_class.ancestors).not_to include(outputs_dsl) end - it "defers entries in mid class (hooks registered but not applied yet)", :aggregate_failures do + it "does not include entries in mid class", :aggregate_failures do expect(mid_class.ancestors).not_to include(inputs_dsl) expect(mid_class.ancestors).not_to include(outputs_dsl) end @@ -166,7 +166,7 @@ def self.included(base) expect(ancestors.index(auth_module)).to be < ancestors.index(inputs_dsl) end - it "propagates hook class methods to leaf via interleaving" do + it "propagates hook callbacks to leaf" do expect(leaf_class).to respond_to(:auth_configured) end end @@ -195,17 +195,14 @@ def self.included(base) end end - it "grandchild inherits hooks from all levels", :aggregate_failures do - grandchild = Class.new(leaf_class) - ancestors = grandchild.ancestors + let(:grandchild) { Class.new(leaf_class) } - expect(ancestors).to include(auth_module, logging_module, inputs_dsl, outputs_dsl) + it "includes hooks and entries from all levels", :aggregate_failures do + expect(grandchild.ancestors).to include(auth_module, logging_module, inputs_dsl, outputs_dsl) end - it "preserves before hook position relative to entry in grandchild" do - grandchild = Class.new(leaf_class) + it "positions before hook above entry in MRO" do ancestors = grandchild.ancestors - expect(ancestors.index(auth_module)).to be < ancestors.index(inputs_dsl) end @@ -213,10 +210,8 @@ def self.included(base) # After hooks registered at parent level are placed above inherited entries in # grandchild MRO. This does not affect phase execution order (controlled by # the orchestrator), only the super call chain. - it "after hook from parent level is above inherited entry in grandchild MRO" do - grandchild = Class.new(leaf_class) + it "positions cross-level after hook above entry in MRO" do ancestors = grandchild.ancestors - expect(ancestors.index(logging_module)).to be < ancestors.index(inputs_dsl) end end @@ -237,7 +232,7 @@ def self.included(base) let(:child_class) { Class.new(mid_class) } let(:grandchild) { Class.new(child_class) } - it "entries propagate to grandchild via ancestors", :aggregate_failures do + it "propagates entries and hooks to grandchild", :aggregate_failures do expect(grandchild.ancestors).to include(inputs_dsl) expect(grandchild.ancestors).to include(outputs_dsl) expect(grandchild.ancestors).to include(auth_module) From 34b3f10bf1bc9f00fa01cc5df7d2926fbc5546b2 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 15 Feb 2026 18:48:54 +0300 Subject: [PATCH 13/13] Refactor tests to avoid unused variable and improve consistency - Remove `grandchild` variable from multi-level inheritance tests to reduce redundancy. - Update tests to dynamically create grandchild classes within assertions. - Simplify test expectations for MRO validation with improved clarity and consistency. - Ensure alignment with prior improvements to the `DSL::Generator` spec. --- spec/stroma/dsl/generator_spec.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/spec/stroma/dsl/generator_spec.rb b/spec/stroma/dsl/generator_spec.rb index 1444ac8..4e23467 100644 --- a/spec/stroma/dsl/generator_spec.rb +++ b/spec/stroma/dsl/generator_spec.rb @@ -195,14 +195,13 @@ def self.included(base) end end - let(:grandchild) { Class.new(leaf_class) } - it "includes hooks and entries from all levels", :aggregate_failures do - expect(grandchild.ancestors).to include(auth_module, logging_module, inputs_dsl, outputs_dsl) + ancestors = Class.new(leaf_class).ancestors + expect(ancestors).to include(auth_module, logging_module, inputs_dsl, outputs_dsl) end it "positions before hook above entry in MRO" do - ancestors = grandchild.ancestors + ancestors = Class.new(leaf_class).ancestors expect(ancestors.index(auth_module)).to be < ancestors.index(inputs_dsl) end @@ -211,7 +210,7 @@ def self.included(base) # grandchild MRO. This does not affect phase execution order (controlled by # the orchestrator), only the super call chain. it "positions cross-level after hook above entry in MRO" do - ancestors = grandchild.ancestors + ancestors = Class.new(leaf_class).ancestors expect(ancestors.index(logging_module)).to be < ancestors.index(inputs_dsl) end end