Skip to content

Commit 085e0e5

Browse files
committed
fix(executor): name the outermost container and catch parallel children by workflow id
1 parent 999e6f6 commit 085e0e5

2 files changed

Lines changed: 77 additions & 6 deletions

File tree

‎apps/sim/executor/utils/run-from-block.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,53 @@ describe('validateRunFromBlock', () => {
333333
expect(result.error).toContain(outerParallelId)
334334
})
335335

336+
it('names the outermost container for a block in a nested loop, since the inner loop is refused too', () => {
337+
const dag = createDAG([
338+
createNode('B', [], { isLoopNode: true, subflowId: 'inner-loop', subflowType: 'loop' }),
339+
])
340+
dag.loopConfigs.set('outer-loop', {
341+
id: 'outer-loop',
342+
nodes: ['inner-loop'],
343+
iterations: 2,
344+
loopType: 'for',
345+
} as any)
346+
dag.loopConfigs.set('inner-loop', {
347+
id: 'inner-loop',
348+
nodes: ['B'],
349+
iterations: 2,
350+
loopType: 'for',
351+
} as any)
352+
353+
const result = validateRunFromBlock('B', dag, new Set())
354+
355+
expect(result.valid).toBe(false)
356+
expect(result.refusal).toEqual({
357+
kind: 'inside_subflow',
358+
blockId: 'B',
359+
subflowId: 'outer-loop',
360+
subflowType: 'loop',
361+
})
362+
})
363+
364+
it('refuses a parallel child by its workflow id as inside the parallel, not as missing', () => {
365+
// The DAG holds parallel children only as branch clones, never under the workflow id.
366+
const dag = createDAG([
367+
createNode('P₍0₎', [], { isParallelBranch: true, subflowId: 'par', subflowType: 'parallel' }),
368+
])
369+
dag.parallelConfigs.set('par', { id: 'par', nodes: ['P'], count: 2 } as any)
370+
371+
const result = validateRunFromBlock('P', dag, new Set(), new Map([['par', 'Fan out']]))
372+
373+
expect(result.valid).toBe(false)
374+
expect(result.error).toContain('inside parallel: Fan out')
375+
expect(result.refusal).toEqual({
376+
kind: 'inside_subflow',
377+
blockId: 'P',
378+
subflowId: 'par',
379+
subflowType: 'parallel',
380+
})
381+
})
382+
336383
it('rejects container when sentinel-start upstream dependency was not executed', () => {
337384
const loopId = 'loop-container-1'
338385
const sentinelStartId = `loop-${loopId}-sentinel-start`

‎apps/sim/executor/utils/run-from-block.ts‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ export function validateRunFromBlock(
201201
}
202202
}
203203

204+
const container = findOutermostContainer(blockId, dag)
205+
if (container) return insideSubflow(container.id, container.type)
206+
204207
const node = dag.nodes.get(blockId)
205208
const isLoopContainer = dag.loopConfigs.has(blockId)
206209
const isParallelContainer = dag.parallelConfigs.has(blockId)
@@ -216,12 +219,6 @@ export function validateRunFromBlock(
216219
}
217220

218221
if (isContainer) {
219-
const parentLoopId = findParentLoop(blockId, dag)
220-
if (parentLoopId) return insideSubflow(parentLoopId, 'loop')
221-
222-
const parentParallelId = findParentParallel(blockId, dag)
223-
if (parentParallelId) return insideSubflow(parentParallelId, 'parallel')
224-
225222
const sentinelStartId = resolveContainerToSentinelStart(blockId, dag)
226223
if (!sentinelStartId || !dag.nodes.has(sentinelStartId)) {
227224
return {
@@ -265,6 +262,33 @@ export function validateRunFromBlock(
265262
return { valid: true }
266263
}
267264

265+
/**
266+
* The top-level loop or parallel a block sits in, however deeply nested. A nested container
267+
* is refused as a start too, so naming the immediate parent would point at another refusal.
268+
* Parallel children are matched by their workflow id, which the DAG only holds as branch clones.
269+
*/
270+
function findOutermostContainer(
271+
blockId: string,
272+
dag: DAG
273+
): { id: string; type: 'loop' | 'parallel' } | undefined {
274+
let outermost: { id: string; type: 'loop' | 'parallel' } | undefined
275+
const visited = new Set([blockId])
276+
let current = blockId
277+
while (true) {
278+
const loopId = findParentLoop(current, dag)
279+
const parallelId = loopId ? undefined : findParentParallel(current, dag)
280+
const parent = loopId
281+
? { id: loopId, type: 'loop' as const }
282+
: parallelId
283+
? { id: parallelId, type: 'parallel' as const }
284+
: undefined
285+
if (!parent || visited.has(parent.id)) return outermost
286+
visited.add(parent.id)
287+
outermost = parent
288+
current = parent.id
289+
}
290+
}
291+
268292
function findParentLoop(blockId: string, dag: DAG): string | undefined {
269293
for (const [loopId, config] of dag.loopConfigs) {
270294
if (loopId !== blockId && config.nodes?.includes(blockId)) {

0 commit comments

Comments
 (0)