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
742 lines
27 KiB
TypeScript
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')
|
|
})
|
|
})
|
|
})
|