@@ -18,11 +18,13 @@ import (
1818var flowBuilderTestDBSeq atomic.Int64
1919
2020type mockStore struct {
21- nodes map [uint ]* model.Node
22- edges map [uint ][]model.Edge
21+ nodes map [uint ]* model.Node
22+ edges map [uint ][]model.Edge
23+ fetchedNodeIDs []uint
2324}
2425
2526func (m * mockStore ) GetEdgesFrom (_ context.Context , nodeID uint ) ([]model.Edge , error ) {
27+ m .fetchedNodeIDs = append (m .fetchedNodeIDs , nodeID )
2628 return m .edges [nodeID ], nil
2729}
2830
@@ -42,6 +44,26 @@ func callEdge(from, to uint, idx int) model.Edge {
4244 return model.Edge {ID : uint (idx ), FromNodeID : from , ToNodeID : to , Kind : model .EdgeKindCalls , Fingerprint : fmt .Sprintf ("e%d" , idx )}
4345}
4446
47+ func flowMemberIDs (flow * model.Flow ) []uint {
48+ ids := make ([]uint , 0 , len (flow .Members ))
49+ for _ , member := range flow .Members {
50+ ids = append (ids , member .NodeID )
51+ }
52+ return ids
53+ }
54+
55+ func assertUintSliceEqual (t * testing.T , got , want []uint ) {
56+ t .Helper ()
57+ if len (got ) != len (want ) {
58+ t .Fatalf ("expected IDs %v, got %v" , want , got )
59+ }
60+ for i := range want {
61+ if got [i ] != want [i ] {
62+ t .Fatalf ("expected IDs %v, got %v" , want , got )
63+ }
64+ }
65+ }
66+
4567func setupFlowBuilderTestDB (t * testing.T ) (* gorm.DB , * gormstore.Store ) {
4668 t .Helper ()
4769 dsn := fmt .Sprintf ("file:flows-builder-%s-%d?mode=memory&cache=shared" , strings .ReplaceAll (t .Name (), "/" , "-" ), flowBuilderTestDBSeq .Add (1 ))
@@ -155,6 +177,82 @@ func TestTraceFlow_PropagatesNamespace(t *testing.T) {
155177 }
156178}
157179
180+ func TestTraceFlowBounded_ExactMaxNodesReturnsCapAndSignalsTruncation (t * testing.T ) {
181+ ms := & mockStore {
182+ nodes : map [uint ]* model.Node {
183+ 1 : newNode (1 , "A" ),
184+ 2 : newNode (2 , "B" ),
185+ 3 : newNode (3 , "C" ),
186+ 4 : newNode (4 , "D" ),
187+ },
188+ edges : map [uint ][]model.Edge {1 : {callEdge (1 , 2 , 1 ), callEdge (1 , 3 , 2 ), callEdge (1 , 4 , 3 )}},
189+ }
190+ tracer := New (ms )
191+
192+ result , err := tracer .TraceFlowBounded (context .Background (), 1 , TraceOptions {MaxNodes : 3 })
193+ if err != nil {
194+ t .Fatal (err )
195+ }
196+
197+ assertUintSliceEqual (t , flowMemberIDs (result .Flow ), []uint {1 , 2 , 3 })
198+ if ! result .Truncated {
199+ t .Fatalf ("expected truncated result when an additional call was reachable beyond MaxNodes" )
200+ }
201+ if result .ReturnedNodes != 3 {
202+ t .Fatalf ("expected ReturnedNodes 3, got %d" , result .ReturnedNodes )
203+ }
204+ }
205+
206+ func TestTraceFlowBounded_DoesNotEnqueueBeyondMaxNodesInHighFanout (t * testing.T ) {
207+ nodes := map [uint ]* model.Node {1 : newNode (1 , "Root" )}
208+ edges := map [uint ][]model.Edge {}
209+ for id := uint (2 ); id <= 101 ; id ++ {
210+ nodes [id ] = newNode (id , fmt .Sprintf ("N%d" , id ))
211+ edges [1 ] = append (edges [1 ], callEdge (1 , id , int (id )))
212+ }
213+ ms := & mockStore {nodes : nodes , edges : edges }
214+ tracer := New (ms )
215+
216+ result , err := tracer .TraceFlowBounded (context .Background (), 1 , TraceOptions {MaxNodes : 3 })
217+ if err != nil {
218+ t .Fatal (err )
219+ }
220+
221+ assertUintSliceEqual (t , flowMemberIDs (result .Flow ), []uint {1 , 2 , 3 })
222+ assertUintSliceEqual (t , ms .fetchedNodeIDs , []uint {1 , 2 , 3 })
223+ if ! result .Truncated {
224+ t .Fatalf ("expected high fan-out traversal to report truncation" )
225+ }
226+ }
227+
228+ func TestTraceFlowBounded_PreservesBFSMemberOrderWhenBounded (t * testing.T ) {
229+ ms := & mockStore {
230+ nodes : map [uint ]* model.Node {
231+ 1 : newNode (1 , "A" ),
232+ 2 : newNode (2 , "B" ),
233+ 3 : newNode (3 , "C" ),
234+ 4 : newNode (4 , "D" ),
235+ 5 : newNode (5 , "E" ),
236+ },
237+ edges : map [uint ][]model.Edge {
238+ 1 : {callEdge (1 , 2 , 1 ), callEdge (1 , 3 , 2 )},
239+ 2 : {callEdge (2 , 4 , 3 )},
240+ 3 : {callEdge (3 , 5 , 4 )},
241+ },
242+ }
243+ tracer := New (ms )
244+
245+ result , err := tracer .TraceFlowBounded (context .Background (), 1 , TraceOptions {MaxNodes : 4 })
246+ if err != nil {
247+ t .Fatal (err )
248+ }
249+
250+ assertUintSliceEqual (t , flowMemberIDs (result .Flow ), []uint {1 , 2 , 3 , 4 })
251+ if ! result .Truncated {
252+ t .Fatalf ("expected truncated result when node 5 could not be enqueued" )
253+ }
254+ }
255+
158256func TestFlowBuilder_Rebuild_PersistsFlowPerEntrypoint (t * testing.T ) {
159257 db , st := setupFlowBuilderTestDB (t )
160258
0 commit comments