import { describe, expect, it } from 'vitest' import { compactExecutionPayload } from '@/lib/execution/payloads/serializer' import { InvalidFieldError } from '@/executor/utils/block-reference' import { ParallelResolver } from './parallel' import type { ResolutionContext } from './reference' interface BlockDef { id: string name: string } /** * Creates a minimal workflow for testing. */ function createTestWorkflow( parallels: Record< string, { nodes: string[] id?: string distribution?: any parallelType?: 'count' | 'collection' } > = {}, blockDefs: BlockDef[] = [], loops: Record = {} ) { const normalizedParallels: Record< string, { id: string nodes: string[] distribution?: any parallelType?: 'count' | 'collection' } > = {} for (const [key, parallel] of Object.entries(parallels)) { normalizedParallels[key] = { id: parallel.id ?? key, nodes: parallel.nodes, distribution: parallel.distribution, parallelType: parallel.parallelType, } } const blocks = blockDefs.map((b) => ({ id: b.id, position: { x: 0, y: 0 }, config: { tool: 'test', params: {} }, inputs: {}, outputs: {}, metadata: { id: 'function', name: b.name }, enabled: true, })) return { version: '1.0', blocks, connections: [], loops, parallels: normalizedParallels, } } /** * Creates a parallel scope for runtime context. */ function createParallelScope(items: any[]) { return { parallelId: 'parallel-1', totalBranches: items.length, branchOutputs: new Map(), items, } } /** * Creates a minimal ResolutionContext for testing. */ function createTestContext( currentNodeId: string, parallelExecutions?: Map, blockOutputs?: Record, parallelBlockMapping?: Map, subflowParentMap?: Map ): ResolutionContext { return { executionContext: { workflowId: 'workflow-1', workspaceId: 'workspace-1', executionId: 'execution-1', parallelExecutions: parallelExecutions ?? new Map(), parallelBlockMapping, subflowParentMap, }, executionState: { getBlockOutput: (id: string) => blockOutputs?.[id], }, currentNodeId, } as ResolutionContext } describe('ParallelResolver', () => { describe('canResolve', () => { it.concurrent('should return true for bare parallel reference', () => { const resolver = new ParallelResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(true) }) it.concurrent('should return true for known parallel properties', () => { const resolver = new ParallelResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(true) expect(resolver.canResolve('')).toBe(true) expect(resolver.canResolve('')).toBe(true) }) it.concurrent('should return true for parallel references with nested paths', () => { const resolver = new ParallelResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(true) expect(resolver.canResolve('')).toBe(true) }) it.concurrent( 'should return true for unknown parallel properties (validates in resolve)', () => { const resolver = new ParallelResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(true) expect(resolver.canResolve('')).toBe(true) expect(resolver.canResolve('')).toBe(true) } ) it.concurrent('should return false for non-parallel references', () => { const resolver = new ParallelResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(false) expect(resolver.canResolve('')).toBe(false) expect(resolver.canResolve('')).toBe(false) expect(resolver.canResolve('plain text')).toBe(false) expect(resolver.canResolve('{{ENV_VAR}}')).toBe(false) }) it.concurrent('should return false for malformed references', () => { const resolver = new ParallelResolver(createTestWorkflow()) expect(resolver.canResolve('parallel.index')).toBe(false) expect(resolver.canResolve('')).toBe(false) }) }) describe('resolve index property', () => { it.concurrent('should resolve branch index from node ID', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b', 'c'] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toBe(0) }) it.concurrent('should resolve different branch indices', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b', 'c'] }, }) const resolver = new ParallelResolver(workflow) expect(resolver.resolve('', createTestContext('block-1₍0₎'))).toBe(0) expect(resolver.resolve('', createTestContext('block-1₍1₎'))).toBe(1) expect(resolver.resolve('', createTestContext('block-1₍2₎'))).toBe(2) }) it.concurrent('uses runtime branch mapping for batched local branch node IDs', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b', 'c', 'd'] }, }) const resolver = new ParallelResolver(workflow) const parallelScope = createParallelScope(['a', 'b', 'c', 'd']) const parallelExecutions = new Map([['parallel-1', parallelScope]]) const parallelBlockMapping = new Map([ [ 'block-1₍0₎', { originalBlockId: 'block-1', parallelId: 'parallel-1', iterationIndex: 2, }, ], ]) const ctx = createTestContext( 'block-1₍0₎', parallelExecutions, undefined, parallelBlockMapping ) expect(resolver.resolve('', ctx)).toBe(2) expect(resolver.resolve('', ctx)).toBe('c') }) it.concurrent('should return undefined when branch index cannot be extracted', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1') expect(resolver.resolve('', ctx)).toBeUndefined() }) }) describe('resolve currentItem property', () => { it.concurrent('should resolve current item from array distribution', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['apple', 'banana', 'cherry'] }, }) const resolver = new ParallelResolver(workflow) expect(resolver.resolve('', createTestContext('block-1₍0₎'))).toBe( 'apple' ) expect(resolver.resolve('', createTestContext('block-1₍1₎'))).toBe( 'banana' ) expect(resolver.resolve('', createTestContext('block-1₍2₎'))).toBe( 'cherry' ) }) it.concurrent('should resolve current item from object distribution as entries', () => { // When an object is used as distribution, it gets converted to entries [key, value] const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: { key1: 'value1', key2: 'value2' }, }, }) const resolver = new ParallelResolver(workflow) const ctx0 = createTestContext('block-1₍0₎') const ctx1 = createTestContext('block-1₍1₎') const item0 = resolver.resolve('', ctx0) const item1 = resolver.resolve('', ctx1) // Object entries are returned as [key, value] tuples expect(item0).toEqual(['key1', 'value1']) expect(item1).toEqual(['key2', 'value2']) }) it.concurrent('should resolve current item with nested path', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: [ { name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }, ], }, }) const resolver = new ParallelResolver(workflow) expect(resolver.resolve('', createTestContext('block-1₍0₎'))).toBe( 'Alice' ) expect(resolver.resolve('', createTestContext('block-1₍1₎'))).toBe( 25 ) }) it.concurrent('should use runtime parallelScope items when available', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['static1', 'static2'] }, }) const resolver = new ParallelResolver(workflow) const parallelScope = createParallelScope(['runtime1', 'runtime2', 'runtime3']) const parallelExecutions = new Map([['parallel-1', parallelScope]]) const ctx = createTestContext('block-1₍1₎', parallelExecutions) expect(resolver.resolve('', ctx)).toBe('runtime2') }) }) describe('resolve items property', () => { it.concurrent('should resolve all items from array distribution', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: [1, 2, 3] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toEqual([1, 2, 3]) }) it.concurrent('should resolve items with nested path', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: [{ id: 1 }, { id: 2 }, { id: 3 }], }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toEqual({ id: 2 }) expect(resolver.resolve('', ctx)).toBe(2) }) it.concurrent('should use runtime parallelScope items when available', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['static'] }, }) const resolver = new ParallelResolver(workflow) const parallelScope = createParallelScope(['runtime1', 'runtime2']) const parallelExecutions = new Map([['parallel-1', parallelScope]]) const ctx = createTestContext('block-1₍0₎', parallelExecutions) expect(resolver.resolve('', ctx)).toEqual(['runtime1', 'runtime2']) }) }) describe('edge cases', () => { it.concurrent('should return context object for bare parallel reference', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b', 'c'] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍1₎') expect(resolver.resolve('', ctx)).toEqual({ index: 1, currentItem: 'b', items: ['a', 'b', 'c'], }) }) it.concurrent('should return minimal context object when no distribution', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') const result = resolver.resolve('', ctx) expect(result).toHaveProperty('index', 0) expect(result).toHaveProperty('items') }) it.concurrent('should throw InvalidFieldError for unknown parallel property', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['a'] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(() => resolver.resolve('', ctx)).toThrow(InvalidFieldError) expect(() => resolver.resolve('', ctx)).toThrow( 'Available fields: index' ) }) it.concurrent('should return undefined when block is not in any parallel', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['other-block'], distribution: ['a'] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toBeUndefined() }) it.concurrent('should return undefined when parallel config not found', () => { const workflow = createTestWorkflow({}) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toBeUndefined() }) it.concurrent('should handle empty distribution array', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: [] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toEqual([]) expect(resolver.resolve('', ctx)).toBeUndefined() }) it.concurrent('should handle JSON string distribution', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: '["x", "y", "z"]' }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍1₎') expect(resolver.resolve('', ctx)).toEqual(['x', 'y', 'z']) expect(resolver.resolve('', ctx)).toBe('y') }) it.concurrent('should handle JSON string with single quotes', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: "['a', 'b']" }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toEqual(['a', 'b']) }) it.concurrent('should return empty array for reference strings', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: '' }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toEqual([]) }) it.concurrent('should resolve distribution items from distribution property', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['fallback1', 'fallback2'] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(resolver.resolve('', ctx)).toEqual(['fallback1', 'fallback2']) }) }) describe('nested parallel blocks', () => { it.concurrent('should resolve for block with multiple parallel parents', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1', 'block-2'], distribution: ['p1', 'p2'] }, 'parallel-2': { nodes: ['block-3'], distribution: ['p3', 'p4'] }, }) const resolver = new ParallelResolver(workflow) expect(resolver.resolve('', createTestContext('block-1₍0₎'))).toBe('p1') expect(resolver.resolve('', createTestContext('block-3₍1₎'))).toBe('p4') }) }) describe('named parallel references', () => { it.concurrent('should resolve result from anywhere after parallel completes', () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] } }, [{ id: 'parallel-1', name: 'Parallel 1' }] ) const resolver = new ParallelResolver(workflow) const results = [[{ response: 'a' }], [{ response: 'b' }]] const ctx = createTestContext('block-outside', new Map(), { 'parallel-1': { results }, }) expect(resolver.resolve('', ctx)).toEqual(results) expect(resolver.resolve('', ctx)).toEqual(results) }) it('uses parallel block mappings to resolve cloned parallel outputs in later batches', () => { const workflow = createTestWorkflow( { 'nested-parallel': { nodes: ['block-1'], distribution: ['a', 'b'] } }, [{ id: 'nested-parallel', name: 'Nested Parallel' }] ) const resolver = new ParallelResolver(workflow) const parallelExecutions = new Map([ ['nested-parallel', { parallelId: 'nested-parallel', branchOutputs: new Map() }], [ 'nested-parallel__obranch-2', { parallelId: 'nested-parallel__obranch-2', branchOutputs: new Map() }, ], ]) const ctx = createTestContext( 'consumer₍0₎', parallelExecutions, { 'nested-parallel': { results: ['branch-0'] }, 'nested-parallel__obranch-2': { results: ['branch-2'] }, }, new Map([ [ 'consumer₍0₎', { originalBlockId: 'consumer', parallelId: 'parallel-1', iterationIndex: 2 }, ], ]) ) expect(resolver.resolve('', ctx)).toEqual(['branch-2']) }) it('uses outer branch suffix over inner parallel mappings for cloned parallel outputs', () => { const workflow = createTestWorkflow( { 'nested-parallel': { nodes: ['block-1'], distribution: ['a', 'b'] } }, [{ id: 'nested-parallel', name: 'Nested Parallel' }] ) const resolver = new ParallelResolver(workflow) const parallelExecutions = new Map([ [ 'nested-parallel__obranch-1', { parallelId: 'nested-parallel__obranch-1', branchOutputs: new Map() }, ], [ 'nested-parallel__obranch-2', { parallelId: 'nested-parallel__obranch-2', branchOutputs: new Map() }, ], ]) const ctx = createTestContext( 'consumer__cloneabc__obranch-2₍0₎', parallelExecutions, { 'nested-parallel__obranch-1': { results: ['outer-branch-1'] }, 'nested-parallel__obranch-2': { results: ['outer-branch-2'] }, }, new Map([ [ 'consumer__cloneabc__obranch-2₍0₎', { originalBlockId: 'consumer', parallelId: 'inner-parallel', iterationIndex: 1 }, ], ]) ) expect(resolver.resolve('', ctx)).toEqual(['outer-branch-2']) }) it.concurrent('should resolve result with nested path', () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] } }, [{ id: 'parallel-1', name: 'Parallel 1' }] ) const resolver = new ParallelResolver(workflow) const results = [[{ response: 'a' }], [{ response: 'b' }]] const ctx = createTestContext('block-outside', new Map(), { 'parallel-1': { results }, }) expect(resolver.resolve('', ctx)).toEqual([{ response: 'a' }]) expect(resolver.resolve('', ctx)).toBe('b') expect(resolver.resolve('', ctx)).toBe('b') }) it('should resolve nested paths inside compacted result references', async () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] } }, [{ id: 'parallel-1', name: 'Parallel 1' }] ) const resolver = new ParallelResolver(workflow) const compacted = await compactExecutionPayload( { results: [[{ response: 'a' }], [{ response: 'b', payload: 'x'.repeat(2048) }]] }, { thresholdBytes: 256, workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1', } ) const ctx = createTestContext('block-outside', new Map(), { 'parallel-1': compacted, }) expect(resolver.resolve('', ctx)).toBe('b') expect(resolver.resolve('', ctx)).toBe('b') expect(() => resolver.resolve('', ctx)).toThrow('too large to inline') }) it.concurrent('should resolve result with empty currentNodeId', () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] } }, [{ id: 'parallel-1', name: 'Parallel 1' }] ) const resolver = new ParallelResolver(workflow) const results = [[{ output: 'x' }], [{ output: 'y' }]] const ctx = createTestContext('', new Map(), { 'parallel-1': { results }, }) expect(resolver.resolve('', ctx)).toEqual(results) }) it.concurrent('should return undefined when no output stored yet', () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['block-1'], distribution: ['a'] } }, [{ id: 'parallel-1', name: 'Parallel 1' }] ) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-outside', new Map()) expect(resolver.resolve('', ctx)).toBeUndefined() }) it.concurrent('should resolve iteration properties via named reference', () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['block-1'], distribution: ['x', 'y', 'z'], parallelType: 'collection', }, }, [{ id: 'parallel-1', name: 'Parallel 1' }] ) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍1₎') expect(resolver.resolve('', ctx)).toBe(1) expect(resolver.resolve('', ctx)).toBe('y') expect(resolver.resolve('', ctx)).toEqual(['x', 'y', 'z']) }) it.concurrent('should throw InvalidFieldError for unknown property on named ref', () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['block-1'], distribution: ['a'], parallelType: 'collection', }, }, [{ id: 'parallel-1', name: 'Parallel 1' }] ) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') expect(() => resolver.resolve('', ctx)).toThrow(InvalidFieldError) expect(() => resolver.resolve('', ctx)).toThrow( 'Available fields: index, currentItem, items' ) }) it.concurrent('should list only results for contextual fields outside a named parallel', () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['block-1'], distribution: ['a'], parallelType: 'collection', }, }, [{ id: 'parallel-1', name: 'Parallel 1' }] ) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-outside', new Map()) expect(() => resolver.resolve('', ctx)).toThrow(InvalidFieldError) expect(() => resolver.resolve('', ctx)).toThrow('Available fields: results') expect(() => resolver.resolve('', ctx)).toThrow(InvalidFieldError) expect(() => resolver.resolve('', ctx)).toThrow('Available fields: results') }) it.concurrent('should not resolve named ref when no matching block exists', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'] } }, [ { id: 'parallel-1', name: 'Parallel 1' }, ]) const resolver = new ParallelResolver(workflow) expect(resolver.canResolve('')).toBe(false) }) it.concurrent('should resolve generic parallel results from inside a branch', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] }, }) const resolver = new ParallelResolver(workflow) const results = [[{ response: 'a' }], [{ response: 'b' }]] const ctx = createTestContext('block-1₍0₎', new Map(), { 'parallel-1': { results }, }) expect(resolver.resolve('', ctx)).toEqual(results) expect(resolver.resolve('', ctx)).toEqual(results) }) it('resolves generic parallel context from inside a loop nested in a parallel', () => { const workflow = createTestWorkflow( { 'parallel-1': { nodes: ['loop-1'], distribution: ['a', 'b', 'c'], parallelType: 'collection', }, }, [], { 'loop-1': { id: 'loop-1', nodes: ['block-1'] } } ) const resolver = new ParallelResolver(workflow) const ctx = createTestContext( 'block-1__cloneaaa__obranch-2', new Map([['parallel-1', createParallelScope(['a', 'b', 'c'])]]) ) expect(resolver.resolve('', ctx)).toBe(2) expect(resolver.resolve('', ctx)).toBe('c') }) it('resolves inner parallel branch context independently from the outer clone index', () => { const workflow = createTestWorkflow({ 'outer-parallel': { nodes: ['inner-parallel'], distribution: ['outer0', 'outer1', 'outer2'], }, 'inner-parallel': { nodes: ['block-1'], distribution: ['inner0', 'inner1'], }, }) const resolver = new ParallelResolver(workflow) const parallelExecutions = new Map([ ['inner-parallel__obranch-2', createParallelScope(['inner0', 'inner1'])], ]) const ctx = createTestContext('block-1__cloneabc__obranch-2₍1₎', parallelExecutions) expect(resolver.resolve('', ctx)).toBe(1) expect(resolver.resolve('', ctx)).toBe('inner1') }) it('resolves parent parallel context for branch-zero nested subflow descendants', () => { const workflow = createTestWorkflow( { 'outer-parallel': { nodes: ['inner-loop'], distribution: ['outer0', 'outer1'], }, }, [], { 'inner-loop': { id: 'inner-loop', nodes: ['loop-task'] } } ) const resolver = new ParallelResolver(workflow) const parallelExecutions = new Map([ ['outer-parallel', createParallelScope(['outer0', 'outer1'])], ]) const ctx = createTestContext( 'loop-task', parallelExecutions, undefined, undefined, new Map([ ['inner-loop', { parentId: 'outer-parallel', parentType: 'parallel', branchIndex: 0 }], ]) ) expect(resolver.resolve('', ctx)).toBe(0) expect(resolver.resolve('', ctx)).toBe('outer0') }) }) })