From 19fcec9563e2b5a296b05b4712923699c0031c09 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 30 Jun 2026 20:52:52 -0700 Subject: [PATCH] fix(hitl): build the full enabled-block DAG so any persisted resume target exists --- apps/sim/executor/execution/executor.test.ts | 57 ++++++++++++++++++++ apps/sim/executor/execution/executor.ts | 1 + 2 files changed, 58 insertions(+) diff --git a/apps/sim/executor/execution/executor.test.ts b/apps/sim/executor/execution/executor.test.ts index a84553c7564..b157952e4c3 100644 --- a/apps/sim/executor/execution/executor.test.ts +++ b/apps/sim/executor/execution/executor.test.ts @@ -331,6 +331,63 @@ describe('DAGExecutor run-from-block snapshot metadata', () => { }) }) +describe('DAGExecutor resume DAG construction', () => { + it('includes non-starter resume targets when a workflow has a disconnected starter', async () => { + const workflow: SerializedWorkflow = { + version: '1', + blocks: [ + createBlock('start-t2-v2', BlockType.STARTER), + createBlock('webhook-start', 'generic_webhook'), + createBlock('hitl', BlockType.HUMAN_IN_THE_LOOP), + createBlock('generate-report', BlockType.FUNCTION), + ], + connections: [ + { source: 'webhook-start', target: 'hitl', sourceHandle: 'source' }, + { source: 'hitl', target: 'generate-report', sourceHandle: 'source' }, + ], + loops: {}, + parallels: {}, + } + let capturedDag: ReturnType | undefined + const executor = new DAGExecutor({ + workflow, + contextExtensions: { + resumeFromSnapshot: true, + remainingEdges: [{ source: 'hitl', target: 'generate-report', sourceHandle: 'source' }], + dagIncomingEdges: { 'start-t2-v2': [] }, + snapshotState: { + blockStates: {}, + executedBlocks: ['webhook-start', 'hitl'], + blockLogs: [], + decisions: { router: {}, condition: {} }, + completedLoops: [], + activeExecutionPath: [], + }, + }, + }) as unknown as DAGExecutor & { + buildExecutionPipeline: ( + context: ExecutionContext, + dag: ReturnType + ) => { run: () => Promise } + } + executor.buildExecutionPipeline = vi.fn((_context, dag) => { + capturedDag = dag + return { + run: async (): Promise => ({ + success: true, + output: { ok: true }, + metadata: {}, + }), + } + }) + + await executor.execute('wf') + + expect(capturedDag?.nodes.has('generate-report')).toBe(true) + expect(capturedDag?.nodes.get('generate-report')?.incomingEdges.has('hitl')).toBe(true) + }) +}) + describe('DAGExecutor createExecutionContext useDraftState', () => { function buildMetadataUseDraftState(opts: { metadataUseDraftState?: boolean diff --git a/apps/sim/executor/execution/executor.ts b/apps/sim/executor/execution/executor.ts index b88d959a4d3..27d95395051 100644 --- a/apps/sim/executor/execution/executor.ts +++ b/apps/sim/executor/execution/executor.ts @@ -88,6 +88,7 @@ export class DAGExecutor { const dag = this.dagBuilder.build(this.workflow, { triggerBlockId, savedIncomingEdges, + includeAllBlocks: this.contextExtensions.resumeFromSnapshot === true, }) const restoredClonedSubflows = this.restoreSnapshotParallelBatches( dag,