@@ -605,3 +605,131 @@ func TestCompose_ContentConcatenatesOnMerge(t *testing.T) {
605605 assert .Equal (t , ContentKindCodeBlock , out .Sections [0 ].Content [0 ].Kind )
606606 assert .Equal (t , ContentKindTable , out .Sections [0 ].Content [1 ].Kind )
607607}
608+
609+ // TestMergeMatcher_BNil covers the `b == nil` arm: a same-heading
610+ // scope whose Matcher is nil in the later input keeps the earlier
611+ // input's matcher.
612+ func TestMergeMatcher_BNil (t * testing.T ) {
613+ a := & Matcher {Regex : "Meta" }
614+ got := mergeMatcher (a , nil )
615+ require .NotNil (t , got )
616+ assert .Equal (t , "Meta" , got .Regex )
617+ assert .NotSame (t , a , got , "result must be a clone, not the input pointer" )
618+ }
619+
620+ // TestMergeMatcher_ANil covers the symmetric `a == nil` arm.
621+ func TestMergeMatcher_ANil (t * testing.T ) {
622+ b := & Matcher {Regex : "Meta" }
623+ got := mergeMatcher (nil , b )
624+ require .NotNil (t , got )
625+ assert .Equal (t , "Meta" , got .Regex )
626+ assert .NotSame (t , b , got )
627+ }
628+
629+ // TestMergeMatcher_WiderMaxAndRepeatSet covers two branches: the
630+ // `bMax > max` widening (b's bounded max is larger than a's) and
631+ // the non-(1,1) `else` that sets an explicit Repeat. a allows
632+ // 1..2, b allows 1..5 -> merged is 1..5.
633+ func TestMergeMatcher_WiderMaxAndRepeatSet (t * testing.T ) {
634+ a := & Matcher {Regex : "Step" , Repeat : Repeat {Set : true , Min : 1 , Max : 2 }}
635+ b := & Matcher {Regex : "Step" , Repeat : Repeat {Set : true , Min : 1 , Max : 5 }}
636+ got := mergeMatcher (a , b )
637+ require .NotNil (t , got )
638+ gotMin , gotMax := got .Repeat .Bounds ()
639+ assert .Equal (t , 1 , gotMin )
640+ assert .Equal (t , 5 , gotMax , "the wider max must win" )
641+ assert .True (t , got .Repeat .Set , "non-(1,1) bounds must set Repeat explicitly" )
642+ }
643+
644+ // TestMergeMatcher_RequiredWinsOverOptional covers the min-widening
645+ // branch plus the explicit-Repeat else: an optional run (min 0)
646+ // merged with a required single (1,1) yields a required matcher.
647+ func TestMergeMatcher_RequiredWinsOverOptional (t * testing.T ) {
648+ optional := & Matcher {Regex : "Meta" , Repeat : Repeat {Set : true , Min : 0 , Max : 1 }}
649+ required := & Matcher {Regex : "Meta" } // exactly one
650+ got := mergeMatcher (optional , required )
651+ require .NotNil (t , got )
652+ gotMin , _ := got .Repeat .Bounds ()
653+ assert .Equal (t , 1 , gotMin , "required (min 1) must win over optional (min 0)" )
654+ }
655+
656+ // TestCloneContent_CopiesColumns covers the `e.Columns != nil`
657+ // deep-copy branch: a table ContentEntry's Columns slice must be
658+ // cloned, not aliased, when two same-heading scopes merge.
659+ func TestCloneContent_CopiesColumns (t * testing.T ) {
660+ cols := []string {"Name" , "Type" }
661+ a := & Schema {Sections : []Scope {{
662+ Heading : "Schema" , Matcher : & Matcher {Regex : "Schema" },
663+ Content : []ContentEntry {{Kind : ContentKindTable , Columns : cols }},
664+ }}}
665+ b := & Schema {Sections : []Scope {{
666+ Heading : "Schema" , Matcher : & Matcher {Regex : "Schema" },
667+ }}}
668+ out , err := Compose (a , b )
669+ require .NoError (t , err )
670+ require .Len (t , out .Sections , 1 )
671+ require .Len (t , out .Sections [0 ].Content , 1 )
672+ got := out .Sections [0 ].Content [0 ].Columns
673+ require .Equal (t , []string {"Name" , "Type" }, got )
674+ cols [0 ] = "MUTATED"
675+ assert .Equal (t , "Name" , out .Sections [0 ].Content [0 ].Columns [0 ],
676+ "cloneContent must deep-copy Columns, not alias the input slice" )
677+ }
678+
679+ // TestMergeMatcher_SequentialOredAndUnbounded covers the
680+ // `a.Sequential || b.Sequential` true arms and the unbounded-max
681+ // path (aMax/bMax == 0): merging an unbounded sequential run with
682+ // an unbounded plain run keeps Sequential and stays unbounded.
683+ func TestMergeMatcher_SequentialOredAndUnbounded (t * testing.T ) {
684+ a := & Matcher {
685+ Regex : "Step" ,
686+ Repeat : Repeat {Set : true , Min : 1 , Max : 0 }, // 1..unbounded
687+ Sequential : true ,
688+ }
689+ b := & Matcher {
690+ Regex : "Step" ,
691+ Repeat : Repeat {Set : true , Min : 1 , Max : 0 },
692+ }
693+ got := mergeMatcher (a , b )
694+ require .NotNil (t , got )
695+ assert .True (t , got .Sequential , "Sequential must OR across inputs" )
696+ gotMin , gotMax := got .Repeat .Bounds ()
697+ assert .Equal (t , 1 , gotMin )
698+ assert .Equal (t , 0 , gotMax , "unbounded max stays unbounded" )
699+ }
700+
701+ // TestMergeMatcher_SequentialFromBOnly covers the right operand of
702+ // the `||` (a.Sequential false, b.Sequential true).
703+ func TestMergeMatcher_SequentialFromBOnly (t * testing.T ) {
704+ a := & Matcher {Regex : "Step" , Repeat : Repeat {Set : true , Min : 1 , Max : 0 }}
705+ b := & Matcher {Regex : "Step" , Repeat : Repeat {Set : true , Min : 1 , Max : 0 }, Sequential : true }
706+ got := mergeMatcher (a , b )
707+ require .NotNil (t , got )
708+ assert .True (t , got .Sequential )
709+ }
710+
711+ // TestMergeMatcher_OptionalBothSides covers the `min == 1` false
712+ // branch: two optional (min 0) runs merge to an optional run, so
713+ // the explicit-Repeat else fires with min 0.
714+ func TestMergeMatcher_OptionalBothSides (t * testing.T ) {
715+ a := & Matcher {Regex : "Notes" , Repeat : Repeat {Set : true , Min : 0 , Max : 1 }}
716+ b := & Matcher {Regex : "Notes" , Repeat : Repeat {Set : true , Min : 0 , Max : 1 }}
717+ got := mergeMatcher (a , b )
718+ require .NotNil (t , got )
719+ gotMin , _ := got .Repeat .Bounds ()
720+ assert .Equal (t , 0 , gotMin , "two optional runs stay optional" )
721+ assert .True (t , got .Repeat .Optional ())
722+ }
723+
724+ // TestMergeMatcher_BoundedAThenUnboundedB covers the `bMax != 0`
725+ // false arm: a is bounded (max 3), b is unbounded (max 0). Either
726+ // side unbounded makes the merged run unbounded.
727+ func TestMergeMatcher_BoundedAThenUnboundedB (t * testing.T ) {
728+ a := & Matcher {Regex : "Step" , Repeat : Repeat {Set : true , Min : 1 , Max : 3 }}
729+ b := & Matcher {Regex : "Step" , Repeat : Repeat {Set : true , Min : 1 , Max : 0 }}
730+ got := mergeMatcher (a , b )
731+ require .NotNil (t , got )
732+ _ , gotMax := got .Repeat .Bounds ()
733+ assert .Equal (t , 0 , gotMax ,
734+ "a bounded max merged with an unbounded one yields unbounded" )
735+ }
0 commit comments