@@ -39,10 +39,24 @@ const {
3939 completeWorkflowExecutionMock,
4040 startWorkflowExecutionMock,
4141 loadWorkflowStateForExecutionMock,
42+ recordExecutionStartedMock,
43+ recordExecutionCompletedMock,
44+ recordExecutionPausedMock,
4245} = vi . hoisted ( ( ) => ( {
4346 completeWorkflowExecutionMock : vi . fn ( ) ,
4447 startWorkflowExecutionMock : vi . fn ( ) ,
4548 loadWorkflowStateForExecutionMock : vi . fn ( ) ,
49+ recordExecutionStartedMock : vi . fn ( ) ,
50+ recordExecutionCompletedMock : vi . fn ( ) ,
51+ recordExecutionPausedMock : vi . fn ( ) ,
52+ } ) )
53+
54+ vi . mock ( '@/lib/monitoring/metrics' , ( ) => ( {
55+ workflowMetrics : {
56+ recordExecutionStarted : recordExecutionStartedMock ,
57+ recordExecutionCompleted : recordExecutionCompletedMock ,
58+ recordExecutionPaused : recordExecutionPausedMock ,
59+ } ,
4660} ) )
4761
4862vi . mock ( '@sim/db' , ( ) => ( {
@@ -648,3 +662,118 @@ describe('LoggingSession.markExecutionAsFailed workflowId scoping', () => {
648662 expect ( combined ) . toContain ( 'force_failed' )
649663 } )
650664} )
665+
666+ describe ( 'LoggingSession workflow metrics' , ( ) => {
667+ beforeEach ( ( ) => {
668+ vi . clearAllMocks ( )
669+ startWorkflowExecutionMock . mockResolvedValue ( { } )
670+ completeWorkflowExecutionMock . mockResolvedValue ( { } )
671+ loadWorkflowStateForExecutionMock . mockResolvedValue ( {
672+ blocks : { } ,
673+ edges : [ ] ,
674+ loops : { } ,
675+ parallels : { } ,
676+ } )
677+ dbMocks . selectLimit . mockResolvedValue ( [ { status : 'running' } ] )
678+ dbMocks . execute . mockResolvedValue ( undefined )
679+ } )
680+
681+ it ( 'emits ExecutionStarted on start and not on resume' , async ( ) => {
682+ const session = new LoggingSession ( 'wf-1' , 'exec-1' , 'api' , 'req-1' )
683+ await session . start ( { workspaceId : 'ws-1' } )
684+ expect ( recordExecutionStartedMock ) . toHaveBeenCalledTimes ( 1 )
685+ expect ( recordExecutionStartedMock ) . toHaveBeenCalledWith ( { trigger : 'api' } )
686+
687+ recordExecutionStartedMock . mockClear ( )
688+ const resumeSession = new LoggingSession ( 'wf-1' , 'exec-1' , 'api' , 'req-1' )
689+ await resumeSession . start ( { workspaceId : 'ws-1' , skipLogCreation : true } )
690+ expect ( recordExecutionStartedMock ) . not . toHaveBeenCalled ( )
691+ } )
692+
693+ it ( 'emits a success completion with trigger and duration' , async ( ) => {
694+ const session = new LoggingSession ( 'wf-1' , 'exec-1' , 'webhook' , 'req-1' )
695+ await session . complete ( { totalDurationMs : 500 } )
696+
697+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledTimes ( 1 )
698+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledWith ( {
699+ trigger : 'webhook' ,
700+ status : 'success' ,
701+ durationMs : 500 ,
702+ } )
703+ } )
704+
705+ it ( 'emits a failed completion via completeWithError' , async ( ) => {
706+ const session = new LoggingSession ( 'wf-1' , 'exec-1' , 'schedule' , 'req-1' )
707+ await session . completeWithError ( { totalDurationMs : 250 , error : { message : 'boom' } } )
708+
709+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledTimes ( 1 )
710+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledWith ( {
711+ trigger : 'schedule' ,
712+ status : 'failed' ,
713+ durationMs : 250 ,
714+ } )
715+ } )
716+
717+ it ( 'emits a cancelled completion via completeWithCancellation' , async ( ) => {
718+ const session = new LoggingSession ( 'wf-1' , 'exec-1' , 'manual' , 'req-1' )
719+ await session . completeWithCancellation ( { totalDurationMs : 100 } )
720+
721+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledWith ( {
722+ trigger : 'manual' ,
723+ status : 'cancelled' ,
724+ durationMs : 100 ,
725+ } )
726+ } )
727+
728+ it ( 'emits ExecutionPaused (not a completion) on pause, then failed if markAsFailed follows' , async ( ) => {
729+ const session = new LoggingSession ( 'wf-1' , 'exec-1' , 'api' , 'req-1' )
730+ await session . completeWithPause ( { totalDurationMs : 100 } )
731+
732+ expect ( recordExecutionPausedMock ) . toHaveBeenCalledTimes ( 1 )
733+ expect ( recordExecutionPausedMock ) . toHaveBeenCalledWith ( { trigger : 'api' } )
734+ expect ( recordExecutionCompletedMock ) . not . toHaveBeenCalled ( )
735+
736+ await session . markAsFailed ( 'pause persistence failed' )
737+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledTimes ( 1 )
738+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledWith ( {
739+ trigger : 'api' ,
740+ status : 'failed' ,
741+ durationMs : undefined ,
742+ } )
743+ } )
744+
745+ it ( 'does not double-emit when markAsFailed runs after a completed session' , async ( ) => {
746+ const session = new LoggingSession ( 'wf-1' , 'exec-1' , 'api' , 'req-1' )
747+ await session . complete ( { totalDurationMs : 500 } )
748+ await session . markAsFailed ( 'timeout' )
749+
750+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledTimes ( 1 )
751+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledWith ( {
752+ trigger : 'api' ,
753+ status : 'success' ,
754+ durationMs : 500 ,
755+ } )
756+ } )
757+
758+ it ( 'emits exactly one completion when the primary write fails and the fallback succeeds' , async ( ) => {
759+ const session = new LoggingSession ( 'wf-1' , 'exec-1' , 'api' , 'req-1' )
760+ completeWorkflowExecutionMock
761+ . mockRejectedValueOnce ( new Error ( 'finalize failed' ) )
762+ . mockResolvedValueOnce ( { } )
763+
764+ await session . safeCompleteWithError ( { error : { message : 'boom' } } )
765+
766+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledTimes ( 1 )
767+ expect ( recordExecutionCompletedMock ) . toHaveBeenCalledWith (
768+ expect . objectContaining ( { trigger : 'api' , status : 'failed' } )
769+ )
770+ } )
771+
772+ it ( 'skips the completion metric when the run was already cancelled elsewhere' , async ( ) => {
773+ dbMocks . selectLimit . mockResolvedValue ( [ { status : 'cancelled' } ] )
774+ const session = new LoggingSession ( 'wf-1' , 'exec-1' , 'api' , 'req-1' )
775+ await session . completeWithError ( { error : { message : 'boom' } } )
776+
777+ expect ( recordExecutionCompletedMock ) . not . toHaveBeenCalled ( )
778+ } )
779+ } )
0 commit comments