mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-06 04:35:03 -05:00
Compare commits
2 Commits
v0.5.81
...
fix/execut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f25a4893e | ||
|
|
9f1f77e2fb |
@@ -491,6 +491,13 @@ export function useWorkflowExecution() {
|
|||||||
updateActiveBlocks(data.blockId, false)
|
updateActiveBlocks(data.blockId, false)
|
||||||
setBlockRunStatus(data.blockId, 'error')
|
setBlockRunStatus(data.blockId, 'error')
|
||||||
|
|
||||||
|
executedBlockIds.add(data.blockId)
|
||||||
|
accumulatedBlockStates.set(data.blockId, {
|
||||||
|
output: { error: data.error },
|
||||||
|
executed: true,
|
||||||
|
executionTime: data.durationMs || 0,
|
||||||
|
})
|
||||||
|
|
||||||
accumulatedBlockLogs.push(
|
accumulatedBlockLogs.push(
|
||||||
createBlockLogEntry(data, { success: false, output: {}, error: data.error })
|
createBlockLogEntry(data, { success: false, output: {}, error: data.error })
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -349,7 +349,15 @@ export function PreviewWorkflow({
|
|||||||
if (block.type === 'loop' || block.type === 'parallel') {
|
if (block.type === 'loop' || block.type === 'parallel') {
|
||||||
const isSelected = selectedBlockId === blockId
|
const isSelected = selectedBlockId === blockId
|
||||||
const dimensions = calculateContainerDimensions(blockId, workflowState.blocks)
|
const dimensions = calculateContainerDimensions(blockId, workflowState.blocks)
|
||||||
const subflowExecutionStatus = getSubflowExecutionStatus(blockId)
|
|
||||||
|
// Check for direct error on the subflow block itself (e.g., loop resolution errors)
|
||||||
|
// before falling back to children-derived status
|
||||||
|
const directExecution = blockExecutionMap.get(blockId)
|
||||||
|
const subflowExecutionStatus: ExecutionStatus | undefined =
|
||||||
|
directExecution?.status === 'error'
|
||||||
|
? 'error'
|
||||||
|
: (getSubflowExecutionStatus(blockId) ??
|
||||||
|
(directExecution ? (directExecution.status as ExecutionStatus) : undefined))
|
||||||
|
|
||||||
nodeArray.push({
|
nodeArray.push({
|
||||||
id: blockId,
|
id: blockId,
|
||||||
|
|||||||
@@ -2478,6 +2478,9 @@ describe('EdgeManager', () => {
|
|||||||
expect(readyNodes).toContain(otherBranchId)
|
expect(readyNodes).toContain(otherBranchId)
|
||||||
expect(readyNodes).not.toContain(sentinelStartId)
|
expect(readyNodes).not.toContain(sentinelStartId)
|
||||||
|
|
||||||
|
// sentinel_end should NOT be ready - it's on a fully deactivated path
|
||||||
|
expect(readyNodes).not.toContain(sentinelEndId)
|
||||||
|
|
||||||
// afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated
|
// afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated
|
||||||
expect(readyNodes).not.toContain(afterLoopId)
|
expect(readyNodes).not.toContain(afterLoopId)
|
||||||
|
|
||||||
@@ -2545,6 +2548,84 @@ describe('EdgeManager', () => {
|
|||||||
expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true)
|
expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not queue loop sentinel-end when upstream condition deactivates entire loop branch', () => {
|
||||||
|
// Regression test for: upstream condition → (if) → ... many blocks ... → sentinel_start → body → sentinel_end
|
||||||
|
// → (else) → exit_block
|
||||||
|
// When condition takes "else", the deep cascade deactivation should NOT queue sentinel_end.
|
||||||
|
// Previously, sentinel_end was flagged as a cascadeTarget (terminal control node) and
|
||||||
|
// spuriously queued, causing it to attempt loop scope initialization and fail.
|
||||||
|
|
||||||
|
const conditionId = 'condition'
|
||||||
|
const intermediateId = 'intermediate'
|
||||||
|
const sentinelStartId = 'sentinel-start'
|
||||||
|
const loopBodyId = 'loop-body'
|
||||||
|
const sentinelEndId = 'sentinel-end'
|
||||||
|
const afterLoopId = 'after-loop'
|
||||||
|
const exitBlockId = 'exit-block'
|
||||||
|
|
||||||
|
const conditionNode = createMockNode(conditionId, [
|
||||||
|
{ target: intermediateId, sourceHandle: 'condition-if' },
|
||||||
|
{ target: exitBlockId, sourceHandle: 'condition-else' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const intermediateNode = createMockNode(
|
||||||
|
intermediateId,
|
||||||
|
[{ target: sentinelStartId }],
|
||||||
|
[conditionId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const sentinelStartNode = createMockNode(
|
||||||
|
sentinelStartId,
|
||||||
|
[{ target: loopBodyId }],
|
||||||
|
[intermediateId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const loopBodyNode = createMockNode(
|
||||||
|
loopBodyId,
|
||||||
|
[{ target: sentinelEndId }],
|
||||||
|
[sentinelStartId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const sentinelEndNode = createMockNode(
|
||||||
|
sentinelEndId,
|
||||||
|
[
|
||||||
|
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
|
||||||
|
{ target: afterLoopId, sourceHandle: 'loop_exit' },
|
||||||
|
],
|
||||||
|
[loopBodyId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
|
||||||
|
const exitBlockNode = createMockNode(exitBlockId, [], [conditionId])
|
||||||
|
|
||||||
|
const nodes = new Map<string, DAGNode>([
|
||||||
|
[conditionId, conditionNode],
|
||||||
|
[intermediateId, intermediateNode],
|
||||||
|
[sentinelStartId, sentinelStartNode],
|
||||||
|
[loopBodyId, loopBodyNode],
|
||||||
|
[sentinelEndId, sentinelEndNode],
|
||||||
|
[afterLoopId, afterLoopNode],
|
||||||
|
[exitBlockId, exitBlockNode],
|
||||||
|
])
|
||||||
|
|
||||||
|
const dag = createMockDAG(nodes)
|
||||||
|
const edgeManager = new EdgeManager(dag)
|
||||||
|
|
||||||
|
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, {
|
||||||
|
selectedOption: 'else',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Only exitBlock should be ready
|
||||||
|
expect(readyNodes).toContain(exitBlockId)
|
||||||
|
|
||||||
|
// Nothing on the deactivated path should be queued
|
||||||
|
expect(readyNodes).not.toContain(intermediateId)
|
||||||
|
expect(readyNodes).not.toContain(sentinelStartId)
|
||||||
|
expect(readyNodes).not.toContain(loopBodyId)
|
||||||
|
expect(readyNodes).not.toContain(sentinelEndId)
|
||||||
|
expect(readyNodes).not.toContain(afterLoopId)
|
||||||
|
})
|
||||||
|
|
||||||
it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => {
|
it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => {
|
||||||
// When a loop actually executes and exits normally, after_loop should become ready
|
// When a loop actually executes and exits normally, after_loop should become ready
|
||||||
const sentinelStartId = 'sentinel-start'
|
const sentinelStartId = 'sentinel-start'
|
||||||
|
|||||||
@@ -71,7 +71,13 @@ export class EdgeManager {
|
|||||||
|
|
||||||
for (const targetId of cascadeTargets) {
|
for (const targetId of cascadeTargets) {
|
||||||
if (!readyNodes.includes(targetId) && !activatedTargets.includes(targetId)) {
|
if (!readyNodes.includes(targetId) && !activatedTargets.includes(targetId)) {
|
||||||
if (this.isTargetReady(targetId)) {
|
// Only queue cascade terminal control nodes when ALL outgoing edges from the
|
||||||
|
// current node were deactivated (dead-end scenario). When some edges are
|
||||||
|
// activated, terminal control nodes on deactivated branches should NOT be
|
||||||
|
// queued - they will be reached through the normal activated path's completion.
|
||||||
|
// This prevents loop/parallel sentinels on fully deactivated paths (e.g., an
|
||||||
|
// upstream condition took a different branch) from being spuriously executed.
|
||||||
|
if (activatedTargets.length === 0 && this.isTargetReady(targetId)) {
|
||||||
readyNodes.push(targetId)
|
readyNodes.push(targetId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user