@@ -316,4 +316,132 @@ describe('GraphRuntime', () => {
316316 // The resume should complete without throwing.
317317 expect ( resumeResult ) . toBeDefined ( ) ;
318318 } ) ;
319+
320+ it ( 'accepts an exact checkpoint id in resume()' , async ( ) => {
321+ const store = new InMemoryCheckpointStore ( ) ;
322+ const executeMock = vi . fn ( ) . mockResolvedValue ( {
323+ success : true ,
324+ output : 'resume-output' ,
325+ } satisfies NodeExecutionResult ) ;
326+
327+ const runtime = new GraphRuntime ( {
328+ checkpointStore : store ,
329+ nodeExecutor : makeExecutorWithMock ( executeMock ) ,
330+ } ) ;
331+
332+ const graph = makeLinearGraph (
333+ 'g-resume-checkpoint-id' ,
334+ [ makeNode ( 'a' ) , makeNode ( 'b' ) ] ,
335+ { checkpointPolicy : 'every_node' } ,
336+ ) ;
337+
338+ await runtime . invoke ( graph , { seed : 7 } ) ;
339+ const checkpoints = await store . list ( 'g-resume-checkpoint-id' ) ;
340+ const checkpointForA = checkpoints . find ( ( cp ) => cp . nodeId === 'a' ) ;
341+ expect ( checkpointForA ) . toBeDefined ( ) ;
342+
343+ executeMock . mockClear ( ) ;
344+ const resumeResult = await runtime . resume ( graph , checkpointForA ! . id ) ;
345+
346+ expect ( resumeResult ) . toBeDefined ( ) ;
347+ expect ( executeMock ) . toHaveBeenCalled ( ) ;
348+ } ) ;
349+
350+ it ( 'halts on node failure and emits error/interruption events' , async ( ) => {
351+ const store = new InMemoryCheckpointStore ( ) ;
352+ const executeMock = vi . fn ( ) . mockImplementation ( async ( node : GraphNode ) : Promise < NodeExecutionResult > => {
353+ if ( node . id === 'a' ) {
354+ return { success : false , error : 'boom' } ;
355+ }
356+ return { success : true , output : `${ node . id } -done` } ;
357+ } ) ;
358+
359+ const runtime = new GraphRuntime ( {
360+ checkpointStore : store ,
361+ nodeExecutor : makeExecutorWithMock ( executeMock ) ,
362+ } ) ;
363+
364+ const graph = makeLinearGraph ( 'g-failure' , [ makeNode ( 'a' ) , makeNode ( 'b' ) ] ) ;
365+ const events = [ ] ;
366+ for await ( const event of runtime . stream ( graph , { } ) ) {
367+ events . push ( event ) ;
368+ }
369+
370+ expect ( executeMock ) . toHaveBeenCalledTimes ( 1 ) ;
371+ expect ( events . some ( ( event ) => event . type === 'error' ) ) . toBe ( true ) ;
372+ expect ( events . some ( ( event ) => event . type === 'interrupt' ) ) . toBe ( true ) ;
373+ expect ( events . some ( ( event ) => event . type === 'run_end' ) ) . toBe ( true ) ;
374+ expect ( events . some ( ( event ) => event . type === 'node_start' && event . nodeId === 'b' ) ) . toBe ( false ) ;
375+ } ) ;
376+
377+ it ( 'persists skipped conditional branches so resume does not execute the bypassed arm' , async ( ) => {
378+ const store = new InMemoryCheckpointStore ( ) ;
379+ const executeMock = vi . fn ( ) . mockImplementation ( async ( node : GraphNode ) : Promise < NodeExecutionResult > => {
380+ if ( node . id === 'a' ) {
381+ return { success : true , output : 'a-done' , scratchUpdate : { goToB : true } } ;
382+ }
383+ return { success : true , output : `${ node . id } -done` } ;
384+ } ) ;
385+
386+ const runtime = new GraphRuntime ( {
387+ checkpointStore : store ,
388+ nodeExecutor : makeExecutorWithMock ( executeMock ) ,
389+ } ) ;
390+
391+ const nodeA = makeNode ( 'a' ) ;
392+ const nodeB = makeNode ( 'b' ) ;
393+ const nodeC = makeNode ( 'c' ) ;
394+
395+ const graph : CompiledExecutionGraph = {
396+ id : 'g-conditional-resume' ,
397+ name : 'conditional-resume-test' ,
398+ nodes : [ nodeA , nodeB , nodeC ] ,
399+ edges : [
400+ { id : 'e0' , source : START , target : 'a' , type : 'static' } ,
401+ {
402+ id : 'e1' ,
403+ source : 'a' ,
404+ target : 'b' ,
405+ type : 'conditional' ,
406+ condition : {
407+ type : 'function' ,
408+ fn : ( state : GraphState ) =>
409+ ( state . scratch as Record < string , unknown > ) . goToB ? 'b' : 'c' ,
410+ } ,
411+ } ,
412+ {
413+ id : 'e2' ,
414+ source : 'a' ,
415+ target : 'c' ,
416+ type : 'conditional' ,
417+ condition : {
418+ type : 'function' ,
419+ fn : ( state : GraphState ) =>
420+ ( state . scratch as Record < string , unknown > ) . goToB ? 'b' : 'c' ,
421+ } ,
422+ } ,
423+ { id : 'e3' , source : 'b' , target : END , type : 'static' } ,
424+ { id : 'e4' , source : 'c' , target : END , type : 'static' } ,
425+ ] ,
426+ stateSchema : { input : { } , scratch : { } , artifacts : { } } ,
427+ reducers : { } ,
428+ checkpointPolicy : 'every_node' ,
429+ memoryConsistency : 'snapshot' ,
430+ } ;
431+
432+ await runtime . invoke ( graph , { } ) ;
433+
434+ const checkpoints = await store . list ( 'g-conditional-resume' ) ;
435+ const checkpointForA = checkpoints . find ( ( cp ) => cp . nodeId === 'a' ) ;
436+ expect ( checkpointForA ) . toBeDefined ( ) ;
437+
438+ const forkedRunId = await store . fork ( checkpointForA ! . id ) ;
439+ executeMock . mockClear ( ) ;
440+
441+ await runtime . resume ( graph , forkedRunId ) ;
442+
443+ const executedNodeIds = executeMock . mock . calls . map ( ( [ node ] ) => ( node as GraphNode ) . id ) ;
444+ expect ( executedNodeIds ) . toContain ( 'b' ) ;
445+ expect ( executedNodeIds ) . not . toContain ( 'c' ) ;
446+ } ) ;
319447} ) ;
0 commit comments