@@ -6,6 +6,268 @@ import (
66 "github.com/stretchr/testify/require"
77)
88
9+ func TestNullPropagation (t * testing.T ) {
10+ live := Outline {Type : FixedIteratorType }
11+ null := Outline {Type : NullIteratorType }
12+
13+ // --- UnionIteratorType ---
14+
15+ t .Run ("union: all null → null" , func (t * testing.T ) {
16+ result := NullPropagation (Outline {Type : UnionIteratorType , SubOutlines : []Outline {null , null }})
17+ require .Equal (t , NullIteratorType , result .Type )
18+ })
19+
20+ t .Run ("union: all null (3 children) → null" , func (t * testing.T ) {
21+ result := NullPropagation (Outline {Type : UnionIteratorType , SubOutlines : []Outline {null , null , null }})
22+ require .Equal (t , NullIteratorType , result .Type )
23+ })
24+
25+ t .Run ("union: one live child → unchanged" , func (t * testing.T ) {
26+ result := NullPropagation (Outline {Type : UnionIteratorType , SubOutlines : []Outline {live , null }})
27+ require .Equal (t , UnionIteratorType , result .Type )
28+ })
29+
30+ t .Run ("union: mixed (3 children, 1 live) → unchanged" , func (t * testing.T ) {
31+ result := NullPropagation (Outline {Type : UnionIteratorType , SubOutlines : []Outline {null , live , null }})
32+ require .Equal (t , UnionIteratorType , result .Type )
33+ })
34+
35+ t .Run ("union: all live → unchanged" , func (t * testing.T ) {
36+ result := NullPropagation (Outline {Type : UnionIteratorType , SubOutlines : []Outline {live , live }})
37+ require .Equal (t , UnionIteratorType , result .Type )
38+ })
39+
40+ t .Run ("union: preserves ID when null" , func (t * testing.T ) {
41+ result := NullPropagation (Outline {Type : UnionIteratorType , ID : 7 , SubOutlines : []Outline {null , null }})
42+ require .Equal (t , NullIteratorType , result .Type )
43+ require .Equal (t , OutlineNodeID (7 ), result .ID )
44+ })
45+
46+ // --- IntersectionIteratorType ---
47+
48+ t .Run ("intersection: any null child → null" , func (t * testing.T ) {
49+ result := NullPropagation (Outline {Type : IntersectionIteratorType , SubOutlines : []Outline {live , null }})
50+ require .Equal (t , NullIteratorType , result .Type )
51+ })
52+
53+ t .Run ("intersection: null first child → null" , func (t * testing.T ) {
54+ result := NullPropagation (Outline {Type : IntersectionIteratorType , SubOutlines : []Outline {null , live }})
55+ require .Equal (t , NullIteratorType , result .Type )
56+ })
57+
58+ t .Run ("intersection: all null → null" , func (t * testing.T ) {
59+ result := NullPropagation (Outline {Type : IntersectionIteratorType , SubOutlines : []Outline {null , null }})
60+ require .Equal (t , NullIteratorType , result .Type )
61+ })
62+
63+ t .Run ("intersection: one null in 3 children → null" , func (t * testing.T ) {
64+ result := NullPropagation (Outline {Type : IntersectionIteratorType , SubOutlines : []Outline {live , null , live }})
65+ require .Equal (t , NullIteratorType , result .Type )
66+ })
67+
68+ t .Run ("intersection: all live → unchanged" , func (t * testing.T ) {
69+ result := NullPropagation (Outline {Type : IntersectionIteratorType , SubOutlines : []Outline {live , live }})
70+ require .Equal (t , IntersectionIteratorType , result .Type )
71+ })
72+
73+ t .Run ("intersection: preserves ID when null" , func (t * testing.T ) {
74+ result := NullPropagation (Outline {Type : IntersectionIteratorType , ID : 8 , SubOutlines : []Outline {live , null }})
75+ require .Equal (t , NullIteratorType , result .Type )
76+ require .Equal (t , OutlineNodeID (8 ), result .ID )
77+ })
78+
79+ // --- ArrowIteratorType ---
80+
81+ t .Run ("arrow: null left child → null" , func (t * testing.T ) {
82+ // Null → B has no sources to traverse, so result is empty.
83+ result := NullPropagation (Outline {Type : ArrowIteratorType , SubOutlines : []Outline {null , live }})
84+ require .Equal (t , NullIteratorType , result .Type )
85+ })
86+
87+ t .Run ("arrow: null right child → null" , func (t * testing.T ) {
88+ // A → Null has no target to reach.
89+ result := NullPropagation (Outline {Type : ArrowIteratorType , SubOutlines : []Outline {live , null }})
90+ require .Equal (t , NullIteratorType , result .Type )
91+ })
92+
93+ t .Run ("arrow: both children null → null" , func (t * testing.T ) {
94+ result := NullPropagation (Outline {Type : ArrowIteratorType , SubOutlines : []Outline {null , null }})
95+ require .Equal (t , NullIteratorType , result .Type )
96+ })
97+
98+ t .Run ("arrow: neither child null → unchanged" , func (t * testing.T ) {
99+ result := NullPropagation (Outline {Type : ArrowIteratorType , SubOutlines : []Outline {live , live }})
100+ require .Equal (t , ArrowIteratorType , result .Type )
101+ })
102+
103+ t .Run ("arrow: wrong child count (1) → unchanged (defensive guard)" , func (t * testing.T ) {
104+ result := NullPropagation (Outline {Type : ArrowIteratorType , SubOutlines : []Outline {null }})
105+ require .Equal (t , ArrowIteratorType , result .Type )
106+ })
107+
108+ t .Run ("arrow: preserves ID when null" , func (t * testing.T ) {
109+ result := NullPropagation (Outline {Type : ArrowIteratorType , ID : 42 , SubOutlines : []Outline {null , live }})
110+ require .Equal (t , NullIteratorType , result .Type )
111+ require .Equal (t , OutlineNodeID (42 ), result .ID )
112+ })
113+
114+ // --- IntersectionArrowIteratorType ---
115+
116+ t .Run ("intersection arrow: null left child → null" , func (t * testing.T ) {
117+ result := NullPropagation (Outline {Type : IntersectionArrowIteratorType , SubOutlines : []Outline {null , live }})
118+ require .Equal (t , NullIteratorType , result .Type )
119+ })
120+
121+ t .Run ("intersection arrow: null right child → null" , func (t * testing.T ) {
122+ result := NullPropagation (Outline {Type : IntersectionArrowIteratorType , SubOutlines : []Outline {live , null }})
123+ require .Equal (t , NullIteratorType , result .Type )
124+ })
125+
126+ t .Run ("intersection arrow: both children null → null" , func (t * testing.T ) {
127+ result := NullPropagation (Outline {Type : IntersectionArrowIteratorType , SubOutlines : []Outline {null , null }})
128+ require .Equal (t , NullIteratorType , result .Type )
129+ })
130+
131+ t .Run ("intersection arrow: neither child null → unchanged" , func (t * testing.T ) {
132+ result := NullPropagation (Outline {Type : IntersectionArrowIteratorType , SubOutlines : []Outline {live , live }})
133+ require .Equal (t , IntersectionArrowIteratorType , result .Type )
134+ })
135+
136+ t .Run ("intersection arrow: wrong child count (1) → unchanged (defensive guard)" , func (t * testing.T ) {
137+ result := NullPropagation (Outline {Type : IntersectionArrowIteratorType , SubOutlines : []Outline {null }})
138+ require .Equal (t , IntersectionArrowIteratorType , result .Type )
139+ })
140+
141+ t .Run ("intersection arrow: wrong child count (3) with null child → unchanged (defensive guard)" , func (t * testing.T ) {
142+ result := NullPropagation (Outline {Type : IntersectionArrowIteratorType , SubOutlines : []Outline {null , live , live }})
143+ require .Equal (t , IntersectionArrowIteratorType , result .Type )
144+ })
145+
146+ t .Run ("intersection arrow: preserves ID when null" , func (t * testing.T ) {
147+ result := NullPropagation (Outline {Type : IntersectionArrowIteratorType , ID : 9 , SubOutlines : []Outline {null , live }})
148+ require .Equal (t , NullIteratorType , result .Type )
149+ require .Equal (t , OutlineNodeID (9 ), result .ID )
150+ })
151+
152+ // --- ExclusionIteratorType ---
153+
154+ t .Run ("exclusion: null left child (main set) → null" , func (t * testing.T ) {
155+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {null , live }})
156+ require .Equal (t , NullIteratorType , result .Type )
157+ })
158+
159+ t .Run ("exclusion: null right child (excluded set) → unchanged (A − ∅ = A)" , func (t * testing.T ) {
160+ // Subtracting the empty set is a no-op; the node stays for a later pass.
161+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {live , null }})
162+ require .Equal (t , ExclusionIteratorType , result .Type )
163+ })
164+
165+ t .Run ("exclusion: both null → null (left child triggers)" , func (t * testing.T ) {
166+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {null , null }})
167+ require .Equal (t , NullIteratorType , result .Type )
168+ })
169+
170+ t .Run ("exclusion: neither null → unchanged" , func (t * testing.T ) {
171+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {live , live }})
172+ require .Equal (t , ExclusionIteratorType , result .Type )
173+ })
174+
175+ t .Run ("exclusion: wrong child count (1) → unchanged (defensive guard)" , func (t * testing.T ) {
176+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {null }})
177+ require .Equal (t , ExclusionIteratorType , result .Type )
178+ })
179+
180+ t .Run ("exclusion: preserves ID when null" , func (t * testing.T ) {
181+ result := NullPropagation (Outline {Type : ExclusionIteratorType , ID : 5 , SubOutlines : []Outline {null , live }})
182+ require .Equal (t , NullIteratorType , result .Type )
183+ require .Equal (t , OutlineNodeID (5 ), result .ID )
184+ })
185+
186+ // --- CaveatIteratorType ---
187+
188+ t .Run ("caveat: null child → null" , func (t * testing.T ) {
189+ result := NullPropagation (Outline {Type : CaveatIteratorType , SubOutlines : []Outline {null }})
190+ require .Equal (t , NullIteratorType , result .Type )
191+ })
192+
193+ t .Run ("caveat: live child → unchanged" , func (t * testing.T ) {
194+ result := NullPropagation (Outline {Type : CaveatIteratorType , SubOutlines : []Outline {live }})
195+ require .Equal (t , CaveatIteratorType , result .Type )
196+ })
197+
198+ t .Run ("caveat: wrong child count (2) → unchanged (defensive guard)" , func (t * testing.T ) {
199+ result := NullPropagation (Outline {Type : CaveatIteratorType , SubOutlines : []Outline {null , null }})
200+ require .Equal (t , CaveatIteratorType , result .Type )
201+ })
202+
203+ t .Run ("caveat: preserves ID when null" , func (t * testing.T ) {
204+ result := NullPropagation (Outline {Type : CaveatIteratorType , ID : 3 , SubOutlines : []Outline {null }})
205+ require .Equal (t , NullIteratorType , result .Type )
206+ require .Equal (t , OutlineNodeID (3 ), result .ID )
207+ })
208+
209+ // --- AliasIteratorType ---
210+
211+ t .Run ("alias: null child → null" , func (t * testing.T ) {
212+ result := NullPropagation (Outline {Type : AliasIteratorType , SubOutlines : []Outline {null }})
213+ require .Equal (t , NullIteratorType , result .Type )
214+ })
215+
216+ t .Run ("alias: live child → unchanged" , func (t * testing.T ) {
217+ result := NullPropagation (Outline {Type : AliasIteratorType , SubOutlines : []Outline {live }})
218+ require .Equal (t , AliasIteratorType , result .Type )
219+ })
220+
221+ t .Run ("alias: wrong child count (2) → unchanged (defensive guard)" , func (t * testing.T ) {
222+ result := NullPropagation (Outline {Type : AliasIteratorType , SubOutlines : []Outline {null , null }})
223+ require .Equal (t , AliasIteratorType , result .Type )
224+ })
225+
226+ // --- RecursiveIteratorType ---
227+
228+ t .Run ("recursive: null child → null" , func (t * testing.T ) {
229+ result := NullPropagation (Outline {Type : RecursiveIteratorType , SubOutlines : []Outline {null }})
230+ require .Equal (t , NullIteratorType , result .Type )
231+ })
232+
233+ t .Run ("recursive: live child → unchanged" , func (t * testing.T ) {
234+ result := NullPropagation (Outline {Type : RecursiveIteratorType , SubOutlines : []Outline {live }})
235+ require .Equal (t , RecursiveIteratorType , result .Type )
236+ })
237+
238+ t .Run ("recursive: wrong child count (2) → unchanged (defensive guard)" , func (t * testing.T ) {
239+ result := NullPropagation (Outline {Type : RecursiveIteratorType , SubOutlines : []Outline {null , null }})
240+ require .Equal (t , RecursiveIteratorType , result .Type )
241+ })
242+
243+ // --- Leaf / unhandled types → always unchanged ---
244+
245+ t .Run ("null node itself → unchanged" , func (t * testing.T ) {
246+ result := NullPropagation (Outline {Type : NullIteratorType })
247+ require .Equal (t , NullIteratorType , result .Type )
248+ })
249+
250+ t .Run ("datastore leaf → unchanged" , func (t * testing.T ) {
251+ result := NullPropagation (Outline {Type : DatastoreIteratorType })
252+ require .Equal (t , DatastoreIteratorType , result .Type )
253+ })
254+
255+ t .Run ("self leaf → unchanged" , func (t * testing.T ) {
256+ result := NullPropagation (Outline {Type : SelfIteratorType })
257+ require .Equal (t , SelfIteratorType , result .Type )
258+ })
259+
260+ t .Run ("fixed leaf → unchanged" , func (t * testing.T ) {
261+ result := NullPropagation (Outline {Type : FixedIteratorType })
262+ require .Equal (t , FixedIteratorType , result .Type )
263+ })
264+
265+ t .Run ("recursive sentinel leaf → unchanged" , func (t * testing.T ) {
266+ result := NullPropagation (Outline {Type : RecursiveSentinelIteratorType })
267+ require .Equal (t , RecursiveSentinelIteratorType , result .Type )
268+ })
269+ }
270+
9271func TestReorderMutation (t * testing.T ) {
10272 t .Run ("reorders children correctly" , func (t * testing.T ) {
11273 child0 := Outline {Type : FixedIteratorType , Args : & IteratorArgs {FixedPaths : []Path {* MustPathFromString ("document:doc0#viewer@user:alice" )}}}
0 commit comments