chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { EDGE } from '@/executor/constants'
|
||||
import type { DAG, DAGNode } from '@/executor/dag/builder'
|
||||
import type { BlockExecutor } from '@/executor/execution/block-executor'
|
||||
import type { BlockStateController } from '@/executor/execution/types'
|
||||
import type { LoopOrchestrator } from '@/executor/orchestrators/loop'
|
||||
import { NodeExecutionOrchestrator } from '@/executor/orchestrators/node'
|
||||
import type { ParallelOrchestrator } from '@/executor/orchestrators/parallel'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
|
||||
function createContext(): ExecutionContext {
|
||||
return {
|
||||
workflowId: 'workflow-1',
|
||||
workspaceId: 'workspace-1',
|
||||
executionId: 'execution-1',
|
||||
userId: 'user-1',
|
||||
blockStates: new Map(),
|
||||
executedBlocks: new Set(),
|
||||
blockLogs: [],
|
||||
metadata: { duration: 0 },
|
||||
environmentVariables: {},
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
completedLoops: new Set(),
|
||||
activeExecutionPath: new Set(),
|
||||
workflow: {
|
||||
version: '1',
|
||||
blocks: [],
|
||||
connections: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createState(): BlockStateController {
|
||||
return {
|
||||
getBlockOutput: vi.fn(),
|
||||
hasExecuted: vi.fn(() => false),
|
||||
setBlockOutput: vi.fn(),
|
||||
setBlockState: vi.fn(),
|
||||
deleteBlockState: vi.fn(),
|
||||
unmarkExecuted: vi.fn(),
|
||||
}
|
||||
}
|
||||
|
||||
function createSentinelNode(id: string, sentinelType: 'start' | 'end'): DAGNode {
|
||||
return {
|
||||
id,
|
||||
block: {
|
||||
id,
|
||||
position: { x: 0, y: 0 },
|
||||
enabled: true,
|
||||
metadata: { id: 'parallel', name: id },
|
||||
config: { params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
},
|
||||
incomingEdges: new Set(),
|
||||
outgoingEdges: new Map(),
|
||||
metadata: {
|
||||
isSentinel: true,
|
||||
sentinelType,
|
||||
subflowId: 'parallel-1',
|
||||
subflowType: 'parallel',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createOrchestrator(
|
||||
dag: DAG,
|
||||
state: BlockStateController,
|
||||
parallelOrchestrator: Partial<ParallelOrchestrator>,
|
||||
loopOrchestratorOverrides: Partial<LoopOrchestrator> = {}
|
||||
): NodeExecutionOrchestrator {
|
||||
const blockExecutor = { execute: vi.fn() } as unknown as BlockExecutor
|
||||
const loopOrchestrator = {
|
||||
getLoopScope: vi.fn(),
|
||||
initializeLoopScope: vi.fn(),
|
||||
evaluateInitialCondition: vi.fn(),
|
||||
evaluateLoopContinuation: vi.fn(),
|
||||
clearLoopExecutionState: vi.fn(),
|
||||
restoreLoopEdges: vi.fn(),
|
||||
storeLoopNodeOutput: vi.fn(),
|
||||
...loopOrchestratorOverrides,
|
||||
} as unknown as LoopOrchestrator
|
||||
|
||||
return new NodeExecutionOrchestrator(
|
||||
dag,
|
||||
state,
|
||||
blockExecutor,
|
||||
loopOrchestrator,
|
||||
parallelOrchestrator as ParallelOrchestrator
|
||||
)
|
||||
}
|
||||
|
||||
describe('NodeExecutionOrchestrator parallel sentinel batching', () => {
|
||||
it('returns loop_exit from a loop start sentinel when the initial condition is false', async () => {
|
||||
const startNode = {
|
||||
...createSentinelNode('loop-loop-1-sentinel-start', 'start'),
|
||||
metadata: {
|
||||
isSentinel: true,
|
||||
sentinelType: 'start' as const,
|
||||
subflowId: 'loop-1',
|
||||
subflowType: 'loop' as const,
|
||||
},
|
||||
}
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[startNode.id, startNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const loopOrchestrator = {
|
||||
getLoopScope: vi.fn(() => ({})),
|
||||
evaluateInitialCondition: vi.fn().mockResolvedValue(false),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, {}, loopOrchestrator)
|
||||
|
||||
const result = await orchestrator.executeNode(createContext(), startNode.id)
|
||||
|
||||
expect(result.output).toMatchObject({
|
||||
shouldExit: true,
|
||||
selectedRoute: EDGE.LOOP_EXIT,
|
||||
})
|
||||
})
|
||||
|
||||
it('prepares the current batch when executing a parallel start sentinel', async () => {
|
||||
const startNode = createSentinelNode('parallel-parallel-1-sentinel-start', 'start')
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[startNode.id, startNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const parallelOrchestrator = {
|
||||
getParallelScope: vi.fn(() => ({ parallelId: 'parallel-1', totalBranches: 2 })),
|
||||
initializeParallelScope: vi.fn(),
|
||||
prepareCurrentBatch: vi.fn(),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, parallelOrchestrator)
|
||||
|
||||
const result = await orchestrator.executeNode(createContext(), startNode.id)
|
||||
|
||||
expect(result.output).toEqual({ sentinelStart: true })
|
||||
expect(parallelOrchestrator.prepareCurrentBatch).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
'parallel-1'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns parallel_exit from an empty parallel start sentinel without preparing a batch', async () => {
|
||||
const startNode = createSentinelNode('parallel-parallel-1-sentinel-start', 'start')
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[startNode.id, startNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const parallelOrchestrator = {
|
||||
getParallelScope: vi.fn(() => ({
|
||||
parallelId: 'parallel-1',
|
||||
totalBranches: 0,
|
||||
isEmpty: true,
|
||||
})),
|
||||
initializeParallelScope: vi.fn(),
|
||||
prepareCurrentBatch: vi.fn(),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, parallelOrchestrator)
|
||||
|
||||
const result = await orchestrator.executeNode(createContext(), startNode.id)
|
||||
|
||||
expect(result.output).toMatchObject({
|
||||
shouldExit: true,
|
||||
selectedRoute: EDGE.PARALLEL_EXIT,
|
||||
})
|
||||
expect(parallelOrchestrator.prepareCurrentBatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('prepares a batch continuation when parallel end selects parallel_continue', async () => {
|
||||
const endNode = createSentinelNode('parallel-parallel-1-sentinel-end', 'end')
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[endNode.id, endNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const parallelOrchestrator = {
|
||||
getParallelScope: vi.fn(),
|
||||
prepareForBatchContinuation: vi.fn(),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, parallelOrchestrator)
|
||||
|
||||
await orchestrator.handleNodeCompletion(createContext(), endNode.id, {
|
||||
selectedRoute: EDGE.PARALLEL_CONTINUE,
|
||||
})
|
||||
|
||||
expect(state.setBlockOutput).toHaveBeenCalledWith(endNode.id, {
|
||||
selectedRoute: EDGE.PARALLEL_CONTINUE,
|
||||
})
|
||||
expect(parallelOrchestrator.prepareForBatchContinuation).toHaveBeenCalledWith('parallel-1')
|
||||
})
|
||||
|
||||
it('marks terminal parallel exit output as final when only the continue back edge remains', async () => {
|
||||
const endNode = createSentinelNode('parallel-parallel-1-sentinel-end', 'end')
|
||||
endNode.outgoingEdges.set('continue', {
|
||||
target: 'parallel-parallel-1-sentinel-start',
|
||||
sourceHandle: EDGE.PARALLEL_CONTINUE,
|
||||
})
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[endNode.id, endNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const parallelOrchestrator = {
|
||||
getParallelScope: vi.fn(() => ({ parallelId: 'parallel-1', totalBranches: 1 })),
|
||||
aggregateParallelResults: vi.fn().mockResolvedValue({
|
||||
allBranchesComplete: true,
|
||||
results: [['result']],
|
||||
totalBranches: 1,
|
||||
}),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, createState(), parallelOrchestrator)
|
||||
|
||||
const result = await orchestrator.executeNode(createContext(), endNode.id)
|
||||
|
||||
expect(result.isFinalOutput).toBe(true)
|
||||
expect(result.output).toMatchObject({
|
||||
results: [['result']],
|
||||
selectedRoute: EDGE.PARALLEL_EXIT,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not mark a continuing parallel batch as final output', async () => {
|
||||
const endNode = createSentinelNode('parallel-parallel-1-sentinel-end', 'end')
|
||||
endNode.outgoingEdges.set('continue', {
|
||||
target: 'parallel-parallel-1-sentinel-start',
|
||||
sourceHandle: EDGE.PARALLEL_CONTINUE,
|
||||
})
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[endNode.id, endNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const parallelOrchestrator = {
|
||||
getParallelScope: vi.fn(() => ({ parallelId: 'parallel-1', totalBranches: 3 })),
|
||||
aggregateParallelResults: vi.fn().mockResolvedValue({
|
||||
allBranchesComplete: false,
|
||||
totalBranches: 3,
|
||||
}),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, createState(), parallelOrchestrator)
|
||||
|
||||
const result = await orchestrator.executeNode(createContext(), endNode.id)
|
||||
|
||||
expect(result.isFinalOutput).toBe(false)
|
||||
expect(result.output).toMatchObject({
|
||||
selectedRoute: EDGE.PARALLEL_CONTINUE,
|
||||
})
|
||||
})
|
||||
|
||||
it('records completed nested subflow sentinels as parent parallel branch output', async () => {
|
||||
const endNode = {
|
||||
...createSentinelNode('loop-nested-loop-sentinel-end', 'end'),
|
||||
metadata: {
|
||||
isSentinel: true,
|
||||
sentinelType: 'end' as const,
|
||||
subflowId: 'nested-loop',
|
||||
subflowType: 'loop' as const,
|
||||
},
|
||||
}
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[endNode.id, endNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const parallelOrchestrator = {
|
||||
getParallelScope: vi.fn(),
|
||||
handleParallelBranchCompletion: vi.fn(),
|
||||
prepareForBatchContinuation: vi.fn(),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, parallelOrchestrator)
|
||||
const ctx = createContext()
|
||||
ctx.subflowParentMap = new Map([
|
||||
['nested-loop', { parentId: 'parent-parallel', parentType: 'parallel', branchIndex: 3 }],
|
||||
])
|
||||
|
||||
await orchestrator.handleNodeCompletion(ctx, endNode.id, {
|
||||
results: ['loop-result'],
|
||||
shouldExit: true,
|
||||
selectedRoute: EDGE.LOOP_EXIT,
|
||||
})
|
||||
|
||||
expect(parallelOrchestrator.handleParallelBranchCompletion).toHaveBeenCalledWith(
|
||||
ctx,
|
||||
'parent-parallel',
|
||||
endNode.id,
|
||||
{ results: ['loop-result'] },
|
||||
3
|
||||
)
|
||||
})
|
||||
|
||||
it('does not record continuing nested parallel batches as parent parallel branch output', async () => {
|
||||
const endNode = {
|
||||
...createSentinelNode('parallel-nested-parallel-sentinel-end', 'end'),
|
||||
metadata: {
|
||||
isSentinel: true,
|
||||
sentinelType: 'end' as const,
|
||||
subflowId: 'nested-parallel',
|
||||
subflowType: 'parallel' as const,
|
||||
},
|
||||
}
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[endNode.id, endNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const parallelOrchestrator = {
|
||||
getParallelScope: vi.fn(),
|
||||
handleParallelBranchCompletion: vi.fn(),
|
||||
prepareForBatchContinuation: vi.fn(),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, parallelOrchestrator)
|
||||
const ctx = createContext()
|
||||
ctx.subflowParentMap = new Map([
|
||||
['nested-parallel', { parentId: 'parent-parallel', parentType: 'parallel', branchIndex: 3 }],
|
||||
])
|
||||
|
||||
await orchestrator.handleNodeCompletion(ctx, endNode.id, {
|
||||
sentinelEnd: true,
|
||||
selectedRoute: EDGE.PARALLEL_CONTINUE,
|
||||
})
|
||||
|
||||
expect(parallelOrchestrator.handleParallelBranchCompletion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('writes stable outer-branch output aliases for completed parallel branch nodes', async () => {
|
||||
const branchNode: DAGNode = {
|
||||
id: 'worker₍0₎',
|
||||
block: {
|
||||
id: 'worker',
|
||||
position: { x: 0, y: 0 },
|
||||
enabled: true,
|
||||
metadata: { id: 'function', name: 'Worker' },
|
||||
config: { params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
},
|
||||
incomingEdges: new Set(),
|
||||
outgoingEdges: new Map(),
|
||||
metadata: {
|
||||
isParallelBranch: true,
|
||||
subflowId: 'parallel-1',
|
||||
subflowType: 'parallel',
|
||||
originalBlockId: 'worker',
|
||||
branchIndex: 2,
|
||||
},
|
||||
}
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[branchNode.id, branchNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const parallelOrchestrator = {
|
||||
getParallelScope: vi.fn(() => ({ parallelId: 'parallel-1', totalBranches: 3 })),
|
||||
initializeParallelScope: vi.fn(),
|
||||
handleParallelBranchCompletion: vi.fn(),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, parallelOrchestrator)
|
||||
const output = { result: 'branch-2' }
|
||||
const ctx = createContext()
|
||||
|
||||
await orchestrator.handleNodeCompletion(ctx, branchNode.id, output)
|
||||
|
||||
expect(parallelOrchestrator.handleParallelBranchCompletion).toHaveBeenCalledWith(
|
||||
ctx,
|
||||
'parallel-1',
|
||||
branchNode.id,
|
||||
output
|
||||
)
|
||||
expect(state.setBlockOutput).toHaveBeenCalledWith('worker__obranch-2', output)
|
||||
expect(state.setBlockOutput).toHaveBeenCalledWith(branchNode.id, output)
|
||||
})
|
||||
|
||||
it('records completed nested subflow sentinels as parent loop iteration output', async () => {
|
||||
const endNode = {
|
||||
...createSentinelNode('parallel-nested-parallel-sentinel-end', 'end'),
|
||||
metadata: {
|
||||
isSentinel: true,
|
||||
sentinelType: 'end' as const,
|
||||
subflowId: 'nested-parallel',
|
||||
subflowType: 'parallel' as const,
|
||||
},
|
||||
}
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[endNode.id, endNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const loopOrchestrator = {
|
||||
storeLoopNodeOutput: vi.fn(),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, {}, loopOrchestrator)
|
||||
const ctx = createContext()
|
||||
ctx.subflowParentMap = new Map([
|
||||
['nested-parallel', { parentId: 'parent-loop', parentType: 'loop' }],
|
||||
])
|
||||
|
||||
await orchestrator.handleNodeCompletion(ctx, endNode.id, {
|
||||
results: ['parallel-result'],
|
||||
sentinelEnd: true,
|
||||
selectedRoute: EDGE.PARALLEL_EXIT,
|
||||
})
|
||||
|
||||
expect(loopOrchestrator.storeLoopNodeOutput).toHaveBeenCalledWith(
|
||||
ctx,
|
||||
'parent-loop',
|
||||
'nested-parallel',
|
||||
{ results: ['parallel-result'] }
|
||||
)
|
||||
})
|
||||
|
||||
it('does not record continuing nested loop iterations as parent loop output', async () => {
|
||||
const endNode = {
|
||||
...createSentinelNode('loop-nested-loop-sentinel-end', 'end'),
|
||||
metadata: {
|
||||
isSentinel: true,
|
||||
sentinelType: 'end' as const,
|
||||
subflowId: 'nested-loop',
|
||||
subflowType: 'loop' as const,
|
||||
},
|
||||
}
|
||||
const dag: DAG = {
|
||||
nodes: new Map([[endNode.id, endNode]]),
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
const state = createState()
|
||||
const loopOrchestrator = {
|
||||
storeLoopNodeOutput: vi.fn(),
|
||||
}
|
||||
const orchestrator = createOrchestrator(dag, state, {}, loopOrchestrator)
|
||||
const ctx = createContext()
|
||||
ctx.subflowParentMap = new Map([
|
||||
['nested-loop', { parentId: 'parent-loop', parentType: 'loop' }],
|
||||
])
|
||||
|
||||
await orchestrator.handleNodeCompletion(ctx, endNode.id, {
|
||||
shouldContinue: true,
|
||||
selectedRoute: EDGE.LOOP_CONTINUE,
|
||||
})
|
||||
|
||||
expect(loopOrchestrator.storeLoopNodeOutput).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user