@@ -555,3 +555,210 @@ func TestExtractOAuthConfigError(t *testing.T) {
555555 })
556556 }
557557}
558+
559+ // T033: Test health status output per refresh state (Spec 023)
560+ func TestCalculateHealth_RefreshStateRetrying (t * testing.T ) {
561+ nextAttempt := time .Now ().Add (30 * time .Second )
562+ input := HealthCalculatorInput {
563+ Name : "test-server" ,
564+ Enabled : true ,
565+ State : "connected" ,
566+ Connected : true ,
567+ OAuthRequired : true ,
568+ OAuthStatus : "authenticated" ,
569+ ToolCount : 5 ,
570+ RefreshState : RefreshStateRetrying ,
571+ RefreshRetryCount : 3 ,
572+ RefreshLastError : "connection timeout" ,
573+ RefreshNextAttempt : & nextAttempt ,
574+ }
575+
576+ result := CalculateHealth (input , nil )
577+
578+ assert .Equal (t , LevelDegraded , result .Level , "RefreshStateRetrying should return degraded" )
579+ assert .Equal (t , StateEnabled , result .AdminState )
580+ assert .Equal (t , "Token refresh pending" , result .Summary )
581+ assert .Contains (t , result .Detail , "Refresh retry 3" )
582+ assert .Contains (t , result .Detail , "connection timeout" )
583+ assert .Equal (t , ActionViewLogs , result .Action , "Retrying should suggest view_logs action" )
584+ }
585+
586+ func TestCalculateHealth_RefreshStateFailed (t * testing.T ) {
587+ input := HealthCalculatorInput {
588+ Name : "test-server" ,
589+ Enabled : true ,
590+ State : "connected" ,
591+ Connected : true ,
592+ OAuthRequired : true ,
593+ OAuthStatus : "authenticated" ,
594+ ToolCount : 5 ,
595+ RefreshState : RefreshStateFailed ,
596+ RefreshLastError : "invalid_grant: refresh token expired" ,
597+ }
598+
599+ result := CalculateHealth (input , nil )
600+
601+ assert .Equal (t , LevelUnhealthy , result .Level , "RefreshStateFailed should return unhealthy" )
602+ assert .Equal (t , StateEnabled , result .AdminState )
603+ assert .Equal (t , "Refresh token expired" , result .Summary )
604+ assert .Contains (t , result .Detail , "Re-authentication required" )
605+ assert .Contains (t , result .Detail , "invalid_grant" )
606+ assert .Equal (t , ActionLogin , result .Action , "Failed should suggest login action" )
607+ }
608+
609+ func TestCalculateHealth_RefreshStateFailedNoError (t * testing.T ) {
610+ input := HealthCalculatorInput {
611+ Name : "test-server" ,
612+ Enabled : true ,
613+ State : "connected" ,
614+ Connected : true ,
615+ OAuthRequired : true ,
616+ OAuthStatus : "authenticated" ,
617+ RefreshState : RefreshStateFailed ,
618+ // No RefreshLastError
619+ }
620+
621+ result := CalculateHealth (input , nil )
622+
623+ assert .Equal (t , LevelUnhealthy , result .Level )
624+ assert .Equal (t , "Refresh token expired" , result .Summary )
625+ assert .Equal (t , "Re-authentication required" , result .Detail )
626+ assert .Equal (t , ActionLogin , result .Action )
627+ }
628+
629+ func TestCalculateHealth_RefreshStateIdle (t * testing.T ) {
630+ input := HealthCalculatorInput {
631+ Name : "test-server" ,
632+ Enabled : true ,
633+ State : "connected" ,
634+ Connected : true ,
635+ OAuthRequired : true ,
636+ OAuthStatus : "authenticated" ,
637+ ToolCount : 5 ,
638+ RefreshState : RefreshStateIdle , // Idle state
639+ }
640+
641+ result := CalculateHealth (input , nil )
642+
643+ // RefreshStateIdle should not affect health - server should be healthy
644+ assert .Equal (t , LevelHealthy , result .Level )
645+ assert .Equal (t , "Connected (5 tools)" , result .Summary )
646+ assert .Equal (t , ActionNone , result .Action )
647+ }
648+
649+ func TestCalculateHealth_RefreshStateScheduled (t * testing.T ) {
650+ input := HealthCalculatorInput {
651+ Name : "test-server" ,
652+ Enabled : true ,
653+ State : "connected" ,
654+ Connected : true ,
655+ OAuthRequired : true ,
656+ OAuthStatus : "authenticated" ,
657+ ToolCount : 5 ,
658+ RefreshState : RefreshStateScheduled , // Scheduled for proactive refresh
659+ }
660+
661+ result := CalculateHealth (input , nil )
662+
663+ // RefreshStateScheduled should not affect health - server should be healthy
664+ assert .Equal (t , LevelHealthy , result .Level )
665+ assert .Equal (t , "Connected (5 tools)" , result .Summary )
666+ assert .Equal (t , ActionNone , result .Action )
667+ }
668+
669+ // Test that higher priority issues take precedence over refresh state
670+ func TestCalculateHealth_RefreshStatePriority (t * testing.T ) {
671+ t .Run ("disabled takes priority over refresh retrying" , func (t * testing.T ) {
672+ input := HealthCalculatorInput {
673+ Name : "test-server" ,
674+ Enabled : false ,
675+ RefreshState : RefreshStateRetrying ,
676+ }
677+
678+ result := CalculateHealth (input , nil )
679+
680+ assert .Equal (t , StateDisabled , result .AdminState )
681+ assert .Equal (t , ActionEnable , result .Action )
682+ })
683+
684+ t .Run ("quarantine takes priority over refresh failed" , func (t * testing.T ) {
685+ input := HealthCalculatorInput {
686+ Name : "test-server" ,
687+ Enabled : true ,
688+ Quarantined : true ,
689+ RefreshState : RefreshStateFailed ,
690+ }
691+
692+ result := CalculateHealth (input , nil )
693+
694+ assert .Equal (t , StateQuarantined , result .AdminState )
695+ assert .Equal (t , ActionApprove , result .Action )
696+ })
697+
698+ t .Run ("connection error takes priority over refresh retrying" , func (t * testing.T ) {
699+ input := HealthCalculatorInput {
700+ Name : "test-server" ,
701+ Enabled : true ,
702+ State : "error" ,
703+ LastError : "connection refused" ,
704+ RefreshState : RefreshStateRetrying ,
705+ }
706+
707+ result := CalculateHealth (input , nil )
708+
709+ assert .Equal (t , "Connection refused" , result .Summary )
710+ assert .Equal (t , ActionRestart , result .Action )
711+ })
712+
713+ t .Run ("OAuth expired takes priority over refresh retrying" , func (t * testing.T ) {
714+ input := HealthCalculatorInput {
715+ Name : "test-server" ,
716+ Enabled : true ,
717+ State : "connected" ,
718+ OAuthRequired : true ,
719+ OAuthStatus : "expired" ,
720+ RefreshState : RefreshStateRetrying ,
721+ }
722+
723+ result := CalculateHealth (input , nil )
724+
725+ assert .Equal (t , "Token expired" , result .Summary )
726+ assert .Equal (t , ActionLogin , result .Action )
727+ })
728+ }
729+
730+ // Test formatRefreshRetryDetail helper function
731+ func TestFormatRefreshRetryDetail (t * testing.T ) {
732+ t .Run ("with next attempt time and error" , func (t * testing.T ) {
733+ nextAttempt := time .Date (2024 , 1 , 15 , 10 , 30 , 0 , 0 , time .UTC )
734+ result := formatRefreshRetryDetail (3 , & nextAttempt , "network timeout" )
735+
736+ assert .Contains (t , result , "Refresh retry 3" )
737+ assert .Contains (t , result , "2024-01-15T10:30:00Z" )
738+ assert .Contains (t , result , "network timeout" )
739+ })
740+
741+ t .Run ("without next attempt time" , func (t * testing.T ) {
742+ result := formatRefreshRetryDetail (5 , nil , "connection refused" )
743+
744+ assert .Contains (t , result , "Refresh retry 5 pending" )
745+ assert .Contains (t , result , "connection refused" )
746+ })
747+
748+ t .Run ("without error" , func (t * testing.T ) {
749+ nextAttempt := time .Date (2024 , 1 , 15 , 10 , 30 , 0 , 0 , time .UTC )
750+ result := formatRefreshRetryDetail (1 , & nextAttempt , "" )
751+
752+ assert .Contains (t , result , "Refresh retry 1" )
753+ assert .Contains (t , result , "2024-01-15T10:30:00Z" )
754+ assert .NotContains (t , result , ": :" )
755+ })
756+
757+ t .Run ("truncates long error" , func (t * testing.T ) {
758+ longError := "This is a very long error message that exceeds 100 characters and should be truncated to prevent overly long detail messages in the health status"
759+ result := formatRefreshRetryDetail (2 , nil , longError )
760+
761+ assert .Contains (t , result , "..." )
762+ assert .LessOrEqual (t , len (result ), 200 ) // Reasonable max length
763+ })
764+ }
0 commit comments