@@ -6,6 +6,263 @@ 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: preserves ID when null" , func (t * testing.T ) {
142+ result := NullPropagation (Outline {Type : IntersectionArrowIteratorType , ID : 9 , SubOutlines : []Outline {null , live }})
143+ require .Equal (t , NullIteratorType , result .Type )
144+ require .Equal (t , OutlineNodeID (9 ), result .ID )
145+ })
146+
147+ // --- ExclusionIteratorType ---
148+
149+ t .Run ("exclusion: null left child (minuend) → null" , func (t * testing.T ) {
150+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {null , live }})
151+ require .Equal (t , NullIteratorType , result .Type )
152+ })
153+
154+ t .Run ("exclusion: null right child (subtrahend) → unchanged (A − ∅ = A)" , func (t * testing.T ) {
155+ // Subtracting the empty set is a no-op; the node stays for a later pass.
156+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {live , null }})
157+ require .Equal (t , ExclusionIteratorType , result .Type )
158+ })
159+
160+ t .Run ("exclusion: both null → null (left child triggers)" , func (t * testing.T ) {
161+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {null , null }})
162+ require .Equal (t , NullIteratorType , result .Type )
163+ })
164+
165+ t .Run ("exclusion: neither null → unchanged" , func (t * testing.T ) {
166+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {live , live }})
167+ require .Equal (t , ExclusionIteratorType , result .Type )
168+ })
169+
170+ t .Run ("exclusion: wrong child count (1) → unchanged (defensive guard)" , func (t * testing.T ) {
171+ result := NullPropagation (Outline {Type : ExclusionIteratorType , SubOutlines : []Outline {null }})
172+ require .Equal (t , ExclusionIteratorType , result .Type )
173+ })
174+
175+ t .Run ("exclusion: preserves ID when null" , func (t * testing.T ) {
176+ result := NullPropagation (Outline {Type : ExclusionIteratorType , ID : 5 , SubOutlines : []Outline {null , live }})
177+ require .Equal (t , NullIteratorType , result .Type )
178+ require .Equal (t , OutlineNodeID (5 ), result .ID )
179+ })
180+
181+ // --- CaveatIteratorType ---
182+
183+ t .Run ("caveat: null child → null" , func (t * testing.T ) {
184+ result := NullPropagation (Outline {Type : CaveatIteratorType , SubOutlines : []Outline {null }})
185+ require .Equal (t , NullIteratorType , result .Type )
186+ })
187+
188+ t .Run ("caveat: live child → unchanged" , func (t * testing.T ) {
189+ result := NullPropagation (Outline {Type : CaveatIteratorType , SubOutlines : []Outline {live }})
190+ require .Equal (t , CaveatIteratorType , result .Type )
191+ })
192+
193+ t .Run ("caveat: wrong child count (2) → unchanged (defensive guard)" , func (t * testing.T ) {
194+ result := NullPropagation (Outline {Type : CaveatIteratorType , SubOutlines : []Outline {null , null }})
195+ require .Equal (t , CaveatIteratorType , result .Type )
196+ })
197+
198+ t .Run ("caveat: preserves ID when null" , func (t * testing.T ) {
199+ result := NullPropagation (Outline {Type : CaveatIteratorType , ID : 3 , SubOutlines : []Outline {null }})
200+ require .Equal (t , NullIteratorType , result .Type )
201+ require .Equal (t , OutlineNodeID (3 ), result .ID )
202+ })
203+
204+ // --- AliasIteratorType ---
205+
206+ t .Run ("alias: null child → null" , func (t * testing.T ) {
207+ result := NullPropagation (Outline {Type : AliasIteratorType , SubOutlines : []Outline {null }})
208+ require .Equal (t , NullIteratorType , result .Type )
209+ })
210+
211+ t .Run ("alias: live child → unchanged" , func (t * testing.T ) {
212+ result := NullPropagation (Outline {Type : AliasIteratorType , SubOutlines : []Outline {live }})
213+ require .Equal (t , AliasIteratorType , result .Type )
214+ })
215+
216+ t .Run ("alias: wrong child count (2) → unchanged (defensive guard)" , func (t * testing.T ) {
217+ result := NullPropagation (Outline {Type : AliasIteratorType , SubOutlines : []Outline {null , null }})
218+ require .Equal (t , AliasIteratorType , result .Type )
219+ })
220+
221+ // --- RecursiveIteratorType ---
222+
223+ t .Run ("recursive: null child → null" , func (t * testing.T ) {
224+ result := NullPropagation (Outline {Type : RecursiveIteratorType , SubOutlines : []Outline {null }})
225+ require .Equal (t , NullIteratorType , result .Type )
226+ })
227+
228+ t .Run ("recursive: live child → unchanged" , func (t * testing.T ) {
229+ result := NullPropagation (Outline {Type : RecursiveIteratorType , SubOutlines : []Outline {live }})
230+ require .Equal (t , RecursiveIteratorType , result .Type )
231+ })
232+
233+ t .Run ("recursive: wrong child count (2) → unchanged (defensive guard)" , func (t * testing.T ) {
234+ result := NullPropagation (Outline {Type : RecursiveIteratorType , SubOutlines : []Outline {null , null }})
235+ require .Equal (t , RecursiveIteratorType , result .Type )
236+ })
237+
238+ // --- Leaf / unhandled types → always unchanged ---
239+
240+ t .Run ("null node itself → unchanged" , func (t * testing.T ) {
241+ result := NullPropagation (Outline {Type : NullIteratorType })
242+ require .Equal (t , NullIteratorType , result .Type )
243+ })
244+
245+ t .Run ("datastore leaf → unchanged" , func (t * testing.T ) {
246+ result := NullPropagation (Outline {Type : DatastoreIteratorType })
247+ require .Equal (t , DatastoreIteratorType , result .Type )
248+ })
249+
250+ t .Run ("self leaf → unchanged" , func (t * testing.T ) {
251+ result := NullPropagation (Outline {Type : SelfIteratorType })
252+ require .Equal (t , SelfIteratorType , result .Type )
253+ })
254+
255+ t .Run ("fixed leaf → unchanged" , func (t * testing.T ) {
256+ result := NullPropagation (Outline {Type : FixedIteratorType })
257+ require .Equal (t , FixedIteratorType , result .Type )
258+ })
259+
260+ t .Run ("recursive sentinel leaf → unchanged" , func (t * testing.T ) {
261+ result := NullPropagation (Outline {Type : RecursiveSentinelIteratorType })
262+ require .Equal (t , RecursiveSentinelIteratorType , result .Type )
263+ })
264+ }
265+
9266func TestReorderMutation (t * testing.T ) {
10267 t .Run ("reorders children correctly" , func (t * testing.T ) {
11268 child0 := Outline {Type : FixedIteratorType , Args : & IteratorArgs {FixedPaths : []Path {* MustPathFromString ("document:doc0#viewer@user:alice" )}}}
0 commit comments