Files
simstudioai--sim/apps/sim/executor/variables/resolvers/parallel.test.ts
T
wehub-resource-sync d25d482dc2
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) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

742 lines
27 KiB
TypeScript

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<string, { id?: string; nodes: string[] }> = {}
) {
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<string, any>,
blockOutputs?: Record<string, any>,
parallelBlockMapping?: Map<string, any>,
subflowParentMap?: Map<string, any>
): 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('<parallel>')).toBe(true)
})
it.concurrent('should return true for known parallel properties', () => {
const resolver = new ParallelResolver(createTestWorkflow())
expect(resolver.canResolve('<parallel.index>')).toBe(true)
expect(resolver.canResolve('<parallel.currentItem>')).toBe(true)
expect(resolver.canResolve('<parallel.items>')).toBe(true)
})
it.concurrent('should return true for parallel references with nested paths', () => {
const resolver = new ParallelResolver(createTestWorkflow())
expect(resolver.canResolve('<parallel.currentItem.name>')).toBe(true)
expect(resolver.canResolve('<parallel.items.0>')).toBe(true)
})
it.concurrent(
'should return true for unknown parallel properties (validates in resolve)',
() => {
const resolver = new ParallelResolver(createTestWorkflow())
expect(resolver.canResolve('<parallel.results>')).toBe(true)
expect(resolver.canResolve('<parallel.output>')).toBe(true)
expect(resolver.canResolve('<parallel.unknownProperty>')).toBe(true)
}
)
it.concurrent('should return false for non-parallel references', () => {
const resolver = new ParallelResolver(createTestWorkflow())
expect(resolver.canResolve('<block.output>')).toBe(false)
expect(resolver.canResolve('<variable.myvar>')).toBe(false)
expect(resolver.canResolve('<loop.index>')).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('<parallel.index')).toBe(false)
expect(resolver.canResolve('parallel.index>')).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('<parallel.index>', 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('<parallel.index>', createTestContext('block-1₍0₎'))).toBe(0)
expect(resolver.resolve('<parallel.index>', createTestContext('block-1₍1₎'))).toBe(1)
expect(resolver.resolve('<parallel.index>', 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('<parallel.index>', ctx)).toBe(2)
expect(resolver.resolve('<parallel.currentItem>', 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('<parallel.index>', 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('<parallel.currentItem>', createTestContext('block-1₍0₎'))).toBe(
'apple'
)
expect(resolver.resolve('<parallel.currentItem>', createTestContext('block-1₍1₎'))).toBe(
'banana'
)
expect(resolver.resolve('<parallel.currentItem>', 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('<parallel.currentItem>', ctx0)
const item1 = resolver.resolve('<parallel.currentItem>', 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('<parallel.currentItem.name>', createTestContext('block-1₍0₎'))).toBe(
'Alice'
)
expect(resolver.resolve('<parallel.currentItem.age>', 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('<parallel.currentItem>', 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('<parallel.items>', 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('<parallel.items.1>', ctx)).toEqual({ id: 2 })
expect(resolver.resolve('<parallel.items.1.id>', 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('<parallel.items>', 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('<parallel>', 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('<parallel>', 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('<parallel.unknownProperty>', ctx)).toThrow(InvalidFieldError)
expect(() => resolver.resolve('<parallel.unknownProperty>', 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('<parallel.index>', 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('<parallel.index>', 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('<parallel.items>', ctx)).toEqual([])
expect(resolver.resolve('<parallel.currentItem>', 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('<parallel.items>', ctx)).toEqual(['x', 'y', 'z'])
expect(resolver.resolve('<parallel.currentItem>', 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('<parallel.items>', ctx)).toEqual(['a', 'b'])
})
it.concurrent('should return empty array for reference strings', () => {
const workflow = createTestWorkflow({
'parallel-1': { nodes: ['block-1'], distribution: '<block.output>' },
})
const resolver = new ParallelResolver(workflow)
const ctx = createTestContext('block-1₍0₎')
expect(resolver.resolve('<parallel.items>', 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('<parallel.items>', 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('<parallel.currentItem>', createTestContext('block-1₍0₎'))).toBe('p1')
expect(resolver.resolve('<parallel.currentItem>', 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('<parallel1.result>', ctx)).toEqual(results)
expect(resolver.resolve('<parallel1.results>', 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<string, any>([
['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('<nestedparallel.results>', 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<string, any>([
[
'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('<nestedparallel.results>', 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('<parallel1.result.0>', ctx)).toEqual([{ response: 'a' }])
expect(resolver.resolve('<parallel1.result.1.0.response>', ctx)).toBe('b')
expect(resolver.resolve('<parallel1.results[1][0].response>', 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('<parallel1.result.1.0.response>', ctx)).toBe('b')
expect(resolver.resolve('<parallel1.results[1][0].response>', ctx)).toBe('b')
expect(() => resolver.resolve('<parallel1.results>', 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('<parallel1.results>', 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('<parallel1.results>', 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('<parallel1.index>', ctx)).toBe(1)
expect(resolver.resolve('<parallel1.currentItem>', ctx)).toBe('y')
expect(resolver.resolve('<parallel1.items>', 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('<parallel1.unknownProp>', ctx)).toThrow(InvalidFieldError)
expect(() => resolver.resolve('<parallel1.unknownProp>', 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('<parallel1.index>', ctx)).toThrow(InvalidFieldError)
expect(() => resolver.resolve('<parallel1.index>', ctx)).toThrow('Available fields: results')
expect(() => resolver.resolve('<parallel1.cooked>', ctx)).toThrow(InvalidFieldError)
expect(() => resolver.resolve('<parallel1.cooked>', 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('<parallel99.index>')).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('<parallel.results>', ctx)).toEqual(results)
expect(resolver.resolve('<parallel.result>', 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('<parallel.index>', ctx)).toBe(2)
expect(resolver.resolve('<parallel.currentItem>', 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('<parallel.index>', ctx)).toBe(1)
expect(resolver.resolve('<parallel.currentItem>', 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('<parallel.index>', ctx)).toBe(0)
expect(resolver.resolve('<parallel.currentItem>', ctx)).toBe('outer0')
})
})
})