Skip to content

Commit c4ee839

Browse files
committed
fix resolver claim
1 parent 8c7b975 commit c4ee839

File tree

2 files changed

+161
-5
lines changed

2 files changed

+161
-5
lines changed

apps/sim/executor/variables/resolvers/parallel.test.ts

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import type { ResolutionContext } from './reference'
66

77
vi.mock('@sim/logger', () => loggerMock)
88

9+
interface BlockDef {
10+
id: string
11+
name: string
12+
}
13+
914
/**
1015
* Creates a minimal workflow for testing.
1116
*/
@@ -18,7 +23,8 @@ function createTestWorkflow(
1823
distribution?: any
1924
parallelType?: 'count' | 'collection'
2025
}
21-
> = {}
26+
> = {},
27+
blockDefs: BlockDef[] = []
2228
) {
2329
const normalizedParallels: Record<
2430
string,
@@ -37,9 +43,18 @@ function createTestWorkflow(
3743
parallelType: parallel.parallelType,
3844
}
3945
}
46+
const blocks = blockDefs.map((b) => ({
47+
id: b.id,
48+
position: { x: 0, y: 0 },
49+
config: { tool: 'test', params: {} },
50+
inputs: {},
51+
outputs: {},
52+
metadata: { id: 'function', name: b.name },
53+
enabled: true,
54+
}))
4055
return {
4156
version: '1.0',
42-
blocks: [],
57+
blocks,
4358
connections: [],
4459
loops: {},
4560
parallels: normalizedParallels,
@@ -63,13 +78,16 @@ function createParallelScope(items: any[]) {
6378
*/
6479
function createTestContext(
6580
currentNodeId: string,
66-
parallelExecutions?: Map<string, any>
81+
parallelExecutions?: Map<string, any>,
82+
blockOutputs?: Record<string, any>
6783
): ResolutionContext {
6884
return {
6985
executionContext: {
7086
parallelExecutions: parallelExecutions ?? new Map(),
7187
},
72-
executionState: {},
88+
executionState: {
89+
getBlockOutput: (id: string) => blockOutputs?.[id],
90+
},
7391
currentNodeId,
7492
} as ResolutionContext
7593
}
@@ -383,4 +401,119 @@ describe('ParallelResolver', () => {
383401
expect(resolver.resolve('<parallel.currentItem>', createTestContext('block-3₍1₎'))).toBe('p4')
384402
})
385403
})
404+
405+
describe('named parallel references', () => {
406+
it.concurrent('should resolve result from anywhere after parallel completes', () => {
407+
const workflow = createTestWorkflow(
408+
{ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] } },
409+
[{ id: 'parallel-1', name: 'Parallel 1' }]
410+
)
411+
const resolver = new ParallelResolver(workflow)
412+
const results = [[{ response: 'a' }], [{ response: 'b' }]]
413+
const ctx = createTestContext('block-outside', new Map(), {
414+
'parallel-1': { results },
415+
})
416+
417+
expect(resolver.resolve('<parallel1.result>', ctx)).toEqual(results)
418+
expect(resolver.resolve('<parallel1.results>', ctx)).toEqual(results)
419+
})
420+
421+
it.concurrent('should resolve result with nested path', () => {
422+
const workflow = createTestWorkflow(
423+
{ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] } },
424+
[{ id: 'parallel-1', name: 'Parallel 1' }]
425+
)
426+
const resolver = new ParallelResolver(workflow)
427+
const results = [[{ response: 'a' }], [{ response: 'b' }]]
428+
const ctx = createTestContext('block-outside', new Map(), {
429+
'parallel-1': { results },
430+
})
431+
432+
expect(resolver.resolve('<parallel1.result.0>', ctx)).toEqual([{ response: 'a' }])
433+
expect(resolver.resolve('<parallel1.result.1.0.response>', ctx)).toBe('b')
434+
})
435+
436+
it.concurrent('should resolve result with empty currentNodeId', () => {
437+
const workflow = createTestWorkflow(
438+
{ 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] } },
439+
[{ id: 'parallel-1', name: 'Parallel 1' }]
440+
)
441+
const resolver = new ParallelResolver(workflow)
442+
const results = [[{ output: 'x' }], [{ output: 'y' }]]
443+
const ctx = createTestContext('', new Map(), {
444+
'parallel-1': { results },
445+
})
446+
447+
expect(resolver.resolve('<parallel1.results>', ctx)).toEqual(results)
448+
})
449+
450+
it.concurrent('should return undefined when no output stored yet', () => {
451+
const workflow = createTestWorkflow(
452+
{ 'parallel-1': { nodes: ['block-1'], distribution: ['a'] } },
453+
[{ id: 'parallel-1', name: 'Parallel 1' }]
454+
)
455+
const resolver = new ParallelResolver(workflow)
456+
const ctx = createTestContext('block-outside', new Map())
457+
458+
expect(resolver.resolve('<parallel1.results>', ctx)).toBeUndefined()
459+
})
460+
461+
it.concurrent('should resolve iteration properties via named reference', () => {
462+
const workflow = createTestWorkflow(
463+
{
464+
'parallel-1': {
465+
nodes: ['block-1'],
466+
distribution: ['x', 'y', 'z'],
467+
parallelType: 'collection',
468+
},
469+
},
470+
[{ id: 'parallel-1', name: 'Parallel 1' }]
471+
)
472+
const resolver = new ParallelResolver(workflow)
473+
const ctx = createTestContext('block-1₍1₎')
474+
475+
expect(resolver.resolve('<parallel1.index>', ctx)).toBe(1)
476+
expect(resolver.resolve('<parallel1.currentItem>', ctx)).toBe('y')
477+
expect(resolver.resolve('<parallel1.items>', ctx)).toEqual(['x', 'y', 'z'])
478+
})
479+
480+
it.concurrent('should throw InvalidFieldError for unknown property on named ref', () => {
481+
const workflow = createTestWorkflow(
482+
{
483+
'parallel-1': {
484+
nodes: ['block-1'],
485+
distribution: ['a'],
486+
parallelType: 'collection',
487+
},
488+
},
489+
[{ id: 'parallel-1', name: 'Parallel 1' }]
490+
)
491+
const resolver = new ParallelResolver(workflow)
492+
const ctx = createTestContext('block-1₍0₎')
493+
494+
expect(() => resolver.resolve('<parallel1.unknownProp>', ctx)).toThrow(InvalidFieldError)
495+
})
496+
497+
it.concurrent('should not resolve named ref when no matching block exists', () => {
498+
const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'] } }, [
499+
{ id: 'parallel-1', name: 'Parallel 1' },
500+
])
501+
const resolver = new ParallelResolver(workflow)
502+
expect(resolver.canResolve('<parallel99.index>')).toBe(false)
503+
})
504+
505+
it.concurrent('should resolve generic parallel results from inside a branch', () => {
506+
const workflow = createTestWorkflow({
507+
'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b'] },
508+
})
509+
const resolver = new ParallelResolver(workflow)
510+
const results = [[{ response: 'a' }], [{ response: 'b' }]]
511+
const ctx = createTestContext('block-1₍0₎', new Map(), {
512+
'parallel-1': { results },
513+
})
514+
515+
expect(resolver.resolve('<parallel.results>', ctx)).toEqual(results)
516+
expect(resolver.resolve('<parallel.result>', ctx)).toEqual(results)
517+
})
518+
})
386519
})

apps/sim/executor/variables/resolvers/parallel.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class ParallelResolver implements Resolver {
2828
}
2929
}
3030

31+
private static OUTPUT_PROPERTIES = new Set(['result', 'results'])
3132
private static KNOWN_PROPERTIES = new Set(['index', 'currentItem', 'items'])
3233

3334
canResolve(reference: string): boolean {
@@ -73,6 +74,10 @@ export class ParallelResolver implements Resolver {
7374
)
7475
}
7576

77+
if (rest.length > 0 && ParallelResolver.OUTPUT_PROPERTIES.has(rest[0])) {
78+
return this.resolveOutput(targetParallelId, rest.slice(1), context)
79+
}
80+
7681
// Look up config using the original (non-cloned) ID
7782
const originalParallelId = stripOuterBranchSuffix(targetParallelId)
7883
const parallelConfig = this.workflow.parallels?.[originalParallelId]
@@ -116,7 +121,9 @@ export class ParallelResolver implements Resolver {
116121

117122
if (!ParallelResolver.KNOWN_PROPERTIES.has(property)) {
118123
const isCollection = parallelConfig.parallelType === 'collection'
119-
const availableFields = isCollection ? ['index', 'currentItem', 'items'] : ['index']
124+
const availableFields = isCollection
125+
? ['index', 'currentItem', 'items', 'result']
126+
: ['index', 'result']
120127
throw new InvalidFieldError(firstPart, property, availableFields)
121128
}
122129

@@ -216,6 +223,22 @@ export class ParallelResolver implements Resolver {
216223
return undefined
217224
}
218225

226+
private resolveOutput(
227+
parallelId: string,
228+
pathParts: string[],
229+
context: ResolutionContext
230+
): unknown {
231+
const output = context.executionState.getBlockOutput(parallelId)
232+
if (!output || typeof output !== 'object') {
233+
return undefined
234+
}
235+
const value = (output as Record<string, unknown>).results
236+
if (pathParts.length > 0) {
237+
return navigatePath(value, pathParts)
238+
}
239+
return value
240+
}
241+
219242
private getDistributionItems(parallelConfig: SerializedParallel): unknown[] {
220243
const rawItems = parallelConfig.distribution ?? []
221244

0 commit comments

Comments
 (0)