d25d482dc2
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
568 lines
15 KiB
TypeScript
568 lines
15 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { ExecutionContext } from '@/executor/types'
|
|
import {
|
|
buildContainerIterationContext,
|
|
buildUnifiedParentIterations,
|
|
getIterationContext,
|
|
type IterationNodeMetadata,
|
|
} from './iteration-context'
|
|
|
|
function makeCtx(overrides: Partial<ExecutionContext> = {}): ExecutionContext {
|
|
return {
|
|
workflowId: 'wf-1',
|
|
executionId: 'exec-1',
|
|
workspaceId: 'ws-1',
|
|
userId: 'user-1',
|
|
blockStates: {},
|
|
blockLogs: [],
|
|
executedBlocks: [],
|
|
environmentVariables: {},
|
|
decisions: { router: new Map(), condition: new Map() },
|
|
completedLoops: new Set(),
|
|
activeExecutionPath: [],
|
|
executionOrder: 0,
|
|
...overrides,
|
|
} as unknown as ExecutionContext
|
|
}
|
|
|
|
describe('getIterationContext', () => {
|
|
it('returns undefined for undefined metadata', () => {
|
|
const ctx = makeCtx()
|
|
expect(getIterationContext(ctx, undefined)).toBeUndefined()
|
|
})
|
|
|
|
it('resolves parallel branch metadata', () => {
|
|
const ctx = makeCtx({
|
|
parallelExecutions: new Map([
|
|
[
|
|
'p1',
|
|
{
|
|
parallelId: 'p1',
|
|
totalBranches: 3,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const metadata: IterationNodeMetadata = {
|
|
branchIndex: 1,
|
|
branchTotal: 3,
|
|
subflowId: 'p1',
|
|
subflowType: 'parallel',
|
|
}
|
|
const result = getIterationContext(ctx, metadata)
|
|
expect(result).toEqual({
|
|
iterationCurrent: 1,
|
|
iterationTotal: 3,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'p1',
|
|
})
|
|
})
|
|
|
|
it('resolves loop node metadata', () => {
|
|
const ctx = makeCtx({
|
|
loopExecutions: new Map([
|
|
[
|
|
'l1',
|
|
{
|
|
iteration: 2,
|
|
maxIterations: 5,
|
|
currentIterationOutputs: new Map(),
|
|
allIterationOutputs: [],
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const metadata: IterationNodeMetadata = {
|
|
isLoopNode: true,
|
|
subflowId: 'l1',
|
|
subflowType: 'loop',
|
|
}
|
|
const result = getIterationContext(ctx, metadata)
|
|
expect(result).toEqual({
|
|
iterationCurrent: 2,
|
|
iterationTotal: 5,
|
|
iterationType: 'loop',
|
|
iterationContainerId: 'l1',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('buildUnifiedParentIterations', () => {
|
|
it('returns empty array when no parent maps exist', () => {
|
|
const ctx = makeCtx()
|
|
expect(buildUnifiedParentIterations(ctx, 'some-id')).toEqual([])
|
|
})
|
|
|
|
it('resolves loop-in-loop parent chain', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([['inner-loop', { parentId: 'outer-loop', parentType: 'loop' }]]),
|
|
loopExecutions: new Map([
|
|
[
|
|
'outer-loop',
|
|
{
|
|
iteration: 1,
|
|
maxIterations: 3,
|
|
currentIterationOutputs: new Map(),
|
|
allIterationOutputs: [],
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildUnifiedParentIterations(ctx, 'inner-loop')
|
|
expect(result).toEqual([
|
|
{
|
|
iterationCurrent: 1,
|
|
iterationTotal: 3,
|
|
iterationType: 'loop',
|
|
iterationContainerId: 'outer-loop',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('resolves parallel-in-parallel parent chain', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
['inner-p__obranch-2', { parentId: 'outer-p', parentType: 'parallel', branchIndex: 2 }],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'outer-p',
|
|
{
|
|
parallelId: 'outer-p',
|
|
totalBranches: 4,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildUnifiedParentIterations(ctx, 'inner-p__obranch-2')
|
|
expect(result).toEqual([
|
|
{
|
|
iterationCurrent: 2,
|
|
iterationTotal: 4,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'outer-p',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('resolves loop-in-parallel (cross-type nesting)', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
['loop-1__obranch-1', { parentId: 'parallel-1', parentType: 'parallel', branchIndex: 1 }],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'parallel-1',
|
|
{
|
|
parallelId: 'parallel-1',
|
|
totalBranches: 5,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildUnifiedParentIterations(ctx, 'loop-1__obranch-1')
|
|
expect(result).toEqual([
|
|
{
|
|
iterationCurrent: 1,
|
|
iterationTotal: 5,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'parallel-1',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('resolves parallel-in-loop (cross-type nesting)', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([['parallel-1', { parentId: 'loop-1', parentType: 'loop' }]]),
|
|
loopExecutions: new Map([
|
|
[
|
|
'loop-1',
|
|
{
|
|
iteration: 3,
|
|
maxIterations: 5,
|
|
currentIterationOutputs: new Map(),
|
|
allIterationOutputs: [],
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildUnifiedParentIterations(ctx, 'parallel-1')
|
|
expect(result).toEqual([
|
|
{
|
|
iterationCurrent: 3,
|
|
iterationTotal: 5,
|
|
iterationType: 'loop',
|
|
iterationContainerId: 'loop-1',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('resolves deep cross-type nesting: parallel → loop → parallel', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
['inner-p', { parentId: 'mid-loop', parentType: 'loop' }],
|
|
['mid-loop', { parentId: 'outer-p', parentType: 'parallel', branchIndex: 0 }],
|
|
['mid-loop__obranch-2', { parentId: 'outer-p', parentType: 'parallel', branchIndex: 2 }],
|
|
]),
|
|
loopExecutions: new Map([
|
|
[
|
|
'mid-loop',
|
|
{
|
|
iteration: 1,
|
|
maxIterations: 4,
|
|
currentIterationOutputs: new Map(),
|
|
allIterationOutputs: [],
|
|
},
|
|
],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'outer-p',
|
|
{
|
|
parallelId: 'outer-p',
|
|
totalBranches: 3,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildUnifiedParentIterations(ctx, 'inner-p')
|
|
expect(result).toEqual([
|
|
{
|
|
iterationCurrent: 0,
|
|
iterationTotal: 3,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'outer-p',
|
|
},
|
|
{
|
|
iterationCurrent: 1,
|
|
iterationTotal: 4,
|
|
iterationType: 'loop',
|
|
iterationContainerId: 'mid-loop',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('resolves 3-level parallel nesting with branchIndex entries', () => {
|
|
// P1 → P2 → P3, with P2__obranch-1 and P3__clone0__obranch-1
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
['P2', { parentId: 'P1', parentType: 'parallel', branchIndex: 0 }],
|
|
['P3', { parentId: 'P2', parentType: 'parallel', branchIndex: 0 }],
|
|
['P2__obranch-1', { parentId: 'P1', parentType: 'parallel', branchIndex: 1 }],
|
|
[
|
|
'P3__clone0__obranch-1',
|
|
{ parentId: 'P2__obranch-1', parentType: 'parallel', branchIndex: 0 },
|
|
],
|
|
['P3__obranch-1', { parentId: 'P2', parentType: 'parallel', branchIndex: 1 }],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'P1',
|
|
{
|
|
parallelId: 'P1',
|
|
totalBranches: 2,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
[
|
|
'P2',
|
|
{
|
|
parallelId: 'P2',
|
|
totalBranches: 2,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
[
|
|
'P2__obranch-1',
|
|
{
|
|
parallelId: 'P2__obranch-1',
|
|
totalBranches: 2,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
|
|
// P3 (original): inside P2 branch 0, inside P1 branch 0
|
|
expect(buildUnifiedParentIterations(ctx, 'P3')).toEqual([
|
|
{
|
|
iterationCurrent: 0,
|
|
iterationTotal: 2,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'P1',
|
|
},
|
|
{
|
|
iterationCurrent: 0,
|
|
iterationTotal: 2,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'P2',
|
|
},
|
|
])
|
|
|
|
// P3__obranch-1 (runtime clone): inside P2 branch 1, inside P1 branch 0
|
|
expect(buildUnifiedParentIterations(ctx, 'P3__obranch-1')).toEqual([
|
|
{
|
|
iterationCurrent: 0,
|
|
iterationTotal: 2,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'P1',
|
|
},
|
|
{
|
|
iterationCurrent: 1,
|
|
iterationTotal: 2,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'P2',
|
|
},
|
|
])
|
|
|
|
// P3__clone0__obranch-1 (pre-expansion clone): inside P2__obranch-1 branch 0, inside P1 branch 1
|
|
expect(buildUnifiedParentIterations(ctx, 'P3__clone0__obranch-1')).toEqual([
|
|
{
|
|
iterationCurrent: 1,
|
|
iterationTotal: 2,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'P1',
|
|
},
|
|
{
|
|
iterationCurrent: 0,
|
|
iterationTotal: 2,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'P2__obranch-1',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('includes parent iterations in getIterationContext for loop-in-parallel', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
['loop-1__obranch-2', { parentId: 'parallel-1', parentType: 'parallel', branchIndex: 2 }],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'parallel-1',
|
|
{
|
|
parallelId: 'parallel-1',
|
|
totalBranches: 5,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
loopExecutions: new Map([
|
|
[
|
|
'loop-1__obranch-2',
|
|
{
|
|
iteration: 3,
|
|
maxIterations: 5,
|
|
currentIterationOutputs: new Map(),
|
|
allIterationOutputs: [],
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
|
|
const metadata: IterationNodeMetadata = {
|
|
isLoopNode: true,
|
|
subflowId: 'loop-1__obranch-2',
|
|
subflowType: 'loop',
|
|
}
|
|
const result = getIterationContext(ctx, metadata)
|
|
expect(result).toEqual({
|
|
iterationCurrent: 3,
|
|
iterationTotal: 5,
|
|
iterationType: 'loop',
|
|
iterationContainerId: 'loop-1__obranch-2',
|
|
parentIterations: [
|
|
{
|
|
iterationCurrent: 2,
|
|
iterationTotal: 5,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'parallel-1',
|
|
},
|
|
],
|
|
})
|
|
})
|
|
|
|
it('includes parent iterations in getIterationContext for parallel-in-loop', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([['parallel-1', { parentId: 'loop-1', parentType: 'loop' }]]),
|
|
loopExecutions: new Map([
|
|
[
|
|
'loop-1',
|
|
{
|
|
iteration: 2,
|
|
maxIterations: 5,
|
|
currentIterationOutputs: new Map(),
|
|
allIterationOutputs: [],
|
|
},
|
|
],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'parallel-1',
|
|
{
|
|
parallelId: 'parallel-1',
|
|
totalBranches: 3,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
|
|
const metadata: IterationNodeMetadata = {
|
|
branchIndex: 1,
|
|
branchTotal: 3,
|
|
subflowId: 'parallel-1',
|
|
subflowType: 'parallel',
|
|
}
|
|
const result = getIterationContext(ctx, metadata)
|
|
expect(result).toEqual({
|
|
iterationCurrent: 1,
|
|
iterationTotal: 3,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'parallel-1',
|
|
parentIterations: [
|
|
{
|
|
iterationCurrent: 2,
|
|
iterationTotal: 5,
|
|
iterationType: 'loop',
|
|
iterationContainerId: 'loop-1',
|
|
},
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('buildContainerIterationContext', () => {
|
|
it('returns undefined when no parent map exists', () => {
|
|
const ctx = makeCtx()
|
|
expect(buildContainerIterationContext(ctx, 'loop-1')).toBeUndefined()
|
|
})
|
|
|
|
it('returns undefined when container is not in parent map', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map(),
|
|
})
|
|
expect(buildContainerIterationContext(ctx, 'loop-1')).toBeUndefined()
|
|
})
|
|
|
|
it('resolves loop nested inside parallel', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
['loop-1__obranch-2', { parentId: 'parallel-1', parentType: 'parallel', branchIndex: 2 }],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'parallel-1',
|
|
{
|
|
parallelId: 'parallel-1',
|
|
totalBranches: 5,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildContainerIterationContext(ctx, 'loop-1__obranch-2')
|
|
expect(result).toEqual({
|
|
iterationCurrent: 2,
|
|
iterationTotal: 5,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'parallel-1',
|
|
})
|
|
})
|
|
|
|
it('resolves parallel nested inside loop', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([['parallel-1', { parentId: 'loop-1', parentType: 'loop' }]]),
|
|
loopExecutions: new Map([
|
|
[
|
|
'loop-1',
|
|
{
|
|
iteration: 3,
|
|
maxIterations: 10,
|
|
currentIterationOutputs: new Map(),
|
|
allIterationOutputs: [],
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildContainerIterationContext(ctx, 'parallel-1')
|
|
expect(result).toEqual({
|
|
iterationCurrent: 3,
|
|
iterationTotal: 10,
|
|
iterationType: 'loop',
|
|
iterationContainerId: 'loop-1',
|
|
})
|
|
})
|
|
|
|
it('returns undefined when parent scope is missing', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
['loop-1', { parentId: 'parallel-1', parentType: 'parallel', branchIndex: 0 }],
|
|
]),
|
|
parallelExecutions: new Map(),
|
|
})
|
|
expect(buildContainerIterationContext(ctx, 'loop-1')).toBeUndefined()
|
|
})
|
|
|
|
it('resolves pre-expansion clone with explicit branchIndex', () => {
|
|
// P1 → P2 → P3: P3__clone0__obranch-1 is pre-cloned inside P2__obranch-1
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
[
|
|
'P3__clone0__obranch-1',
|
|
{ parentId: 'P2__obranch-1', parentType: 'parallel', branchIndex: 0 },
|
|
],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'P2__obranch-1',
|
|
{
|
|
parallelId: 'P2__obranch-1',
|
|
totalBranches: 5,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildContainerIterationContext(ctx, 'P3__clone0__obranch-1')
|
|
expect(result).toEqual({
|
|
iterationCurrent: 0,
|
|
iterationTotal: 5,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'P2__obranch-1',
|
|
})
|
|
})
|
|
|
|
it('uses branch index 0 for non-cloned container in parallel parent', () => {
|
|
const ctx = makeCtx({
|
|
subflowParentMap: new Map([
|
|
['inner-loop', { parentId: 'outer-parallel', parentType: 'parallel', branchIndex: 0 }],
|
|
]),
|
|
parallelExecutions: new Map([
|
|
[
|
|
'outer-parallel',
|
|
{
|
|
parallelId: 'outer-parallel',
|
|
totalBranches: 3,
|
|
branchOutputs: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
})
|
|
const result = buildContainerIterationContext(ctx, 'inner-loop')
|
|
expect(result).toEqual({
|
|
iterationCurrent: 0,
|
|
iterationTotal: 3,
|
|
iterationType: 'parallel',
|
|
iterationContainerId: 'outer-parallel',
|
|
})
|
|
})
|
|
})
|