@@ -101,21 +101,71 @@ describe('resolveWorkflowReferences', () => {
101101 } )
102102
103103 it ( 'bounds converging paths (diamond) instead of re-expanding' , ( ) => {
104- // A → B, A → C, B → D, C → D. D reconverges; it must appear under both B and C
105- // but only expand once (here D is a leaf anyway; the guard prevents blow-up).
104+ // A → B, A → C, B → D, C → D, D → E. D reconverges; it must appear under both
105+ // B and C but expand its subtree (E) only under the first-visited branch.
106+ const workflowsWithE = [ ...workflows , { id : 'e' , name : 'E' } ]
106107 const blocks = [
107108 workflowBlock ( 'a' , 'b' ) ,
108109 workflowBlock ( 'a' , 'c' ) ,
109110 workflowBlock ( 'b' , 'd' ) ,
110111 workflowBlock ( 'c' , 'd' ) ,
112+ workflowBlock ( 'd' , 'e' ) ,
111113 ]
112- const { callees } = resolveWorkflowReferences ( 'a' , workflows , blocks , [ ] )
114+ const { callees } = resolveWorkflowReferences ( 'a' , workflowsWithE , blocks , [ ] )
113115 const b = callees . find ( ( n ) => n . id === 'b' )
114116 const c = callees . find ( ( n ) => n . id === 'c' )
115- // D expands under the first-visited branch (B) and is a plain leaf under C.
116- expect ( b ?. children . map ( ( n ) => n . id ) ) . toEqual ( [ 'd' ] )
117- expect ( c ?. children . map ( ( n ) => n . id ) ) . toEqual ( [ 'd' ] )
118- expect ( c ?. children [ 0 ] ) . toMatchObject ( { id : 'd' , cycle : false , children : [ ] } )
117+ // D expands under the first-visited branch (B) and is a collapsed leaf under C.
118+ expect ( b ?. children ) . toEqual ( [
119+ {
120+ id : 'd' ,
121+ name : 'D' ,
122+ cycle : false ,
123+ children : [ { id : 'e' , name : 'E' , cycle : false , children : [ ] } ] ,
124+ } ,
125+ ] )
126+ expect ( c ?. children ) . toEqual ( [ { id : 'd' , name : 'D' , cycle : false , children : [ ] } ] )
127+ } )
128+
129+ it ( 'truncates expansion at the depth ceiling' , ( ) => {
130+ // A linear chain longer than MAX_REFERENCE_DEPTH (25): w0 → w1 → … → w29.
131+ const chain = Array . from ( { length : 30 } , ( _ , i ) => ( { id : `w${ i } ` , name : `W${ i } ` } ) )
132+ const blocks = Array . from ( { length : 29 } , ( _ , i ) => workflowBlock ( `w${ i } ` , `w${ i + 1 } ` ) )
133+ const { callees } = resolveWorkflowReferences ( 'w0' , chain , blocks , [ ] )
134+ let depth = 0
135+ let node = callees [ 0 ]
136+ while ( node ) {
137+ depth += 1
138+ node = node . children [ 0 ]
139+ }
140+ expect ( depth ) . toBe ( 25 )
141+ } )
142+
143+ it ( 're-expands a depth-truncated node when a shallower path reaches it' , ( ) => {
144+ // Root fans out to a 25-deep chain (visited first by name sort: "A…") whose
145+ // tail X gets truncated at the ceiling, and a direct edge (via "Z") to X.
146+ // The shallow path must still show X's child Y instead of a collapsed leaf.
147+ const nodes = [
148+ { id : 'root' , name : 'Root' } ,
149+ ...Array . from ( { length : 24 } , ( _ , i ) => ( {
150+ id : `a${ i } ` ,
151+ name : `A${ String ( i ) . padStart ( 2 , '0' ) } ` ,
152+ } ) ) ,
153+ { id : 'x' , name : 'X' } ,
154+ { id : 'y' , name : 'Y' } ,
155+ { id : 'z' , name : 'Z' } ,
156+ ]
157+ const blocks = [
158+ workflowBlock ( 'root' , 'a0' ) ,
159+ ...Array . from ( { length : 23 } , ( _ , i ) => workflowBlock ( `a${ i } ` , `a${ i + 1 } ` ) ) ,
160+ workflowBlock ( 'a23' , 'x' ) ,
161+ workflowBlock ( 'root' , 'z' ) ,
162+ workflowBlock ( 'z' , 'x' ) ,
163+ workflowBlock ( 'x' , 'y' ) ,
164+ ]
165+ const { callees } = resolveWorkflowReferences ( 'root' , nodes , blocks , [ ] )
166+ const z = callees . find ( ( n ) => n . id === 'z' )
167+ const xUnderZ = z ?. children . find ( ( n ) => n . id === 'x' )
168+ expect ( xUnderZ ?. children . map ( ( n ) => n . id ) ) . toEqual ( [ 'y' ] )
119169 } )
120170
121171 it ( 'drops dangling / out-of-workspace child ids' , ( ) => {
@@ -217,6 +267,29 @@ describe('resolveWorkflowReferences', () => {
217267 expect ( callees . map ( ( n ) => n . id ) ) . toEqual ( [ 'c' ] )
218268 } )
219269
270+ it ( 'resolves legacy workflow-typed tools and isolates index-scoped modes per tool' , ( ) => {
271+ // Tool 0 is a legacy `workflow`-typed entry (still rendered/executed by the
272+ // editor); tool 1 is advanced-mode via its own index-scoped key. Tool 0 must
273+ // stay basic (`b`) — tool 1's override must not bleed into it.
274+ const blocks : ReferenceBlockRow [ ] = [
275+ {
276+ parentId : 'a' ,
277+ type : 'agent' ,
278+ childFromSelector : null ,
279+ childFromManual : null ,
280+ canonicalModes : { '1:workflowId' : 'advanced' } ,
281+ toolInputValues : [
282+ [
283+ { type : 'workflow' , params : { workflowId : 'b' } } ,
284+ { type : 'workflow_input' , params : { workflowId : 'c' , manualWorkflowId : 'd' } } ,
285+ ] ,
286+ ] ,
287+ } ,
288+ ]
289+ const { callees } = resolveWorkflowReferences ( 'a' , workflows , blocks , [ ] )
290+ expect ( callees . map ( ( n ) => n . id ) ) . toEqual ( [ 'b' , 'd' ] )
291+ } )
292+
220293 it ( 'resolves workflow tools from a JSON-stringified tool-input value' , ( ) => {
221294 const blocks : ReferenceBlockRow [ ] = [
222295 {
0 commit comments