@@ -2,10 +2,12 @@ package ui
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "net/http"
78 "net/http/httptest"
89 "testing"
10+ "time"
911
1012 "github.com/Agent-Field/agentfield/control-plane/internal/core/domain"
1113 "github.com/Agent-Field/agentfield/control-plane/internal/services"
@@ -23,7 +25,7 @@ func (s *dashboardOverrideStorage) QueryExecutionRecords(ctx context.Context, fi
2325 if s .queryExecutionRecordsFn != nil {
2426 return s .queryExecutionRecordsFn (ctx , filter )
2527 }
26- return s .overrideStorage . StorageProvider .QueryExecutionRecords (ctx , filter )
28+ return s .StorageProvider .QueryExecutionRecords (ctx , filter )
2729}
2830
2931func TestNodesHandlerAdditionalCoverage (t * testing.T ) {
@@ -98,6 +100,32 @@ func TestDashboardSummaryHandlerAdditionalCoverage(t *testing.T) {
98100 require .Equal (t , http .StatusOK , resp .Code )
99101 })
100102
103+ t .Run ("idle system reports vacuous 100 percent success" , func (t * testing.T ) {
104+ store := & dashboardOverrideStorage {overrideStorage : & overrideStorage {StorageProvider : setupTestStorage (t )}}
105+ handler := NewDashboardHandler (store , & mockLifecycleAgentService {})
106+ router := gin .New ()
107+ router .GET ("/api/ui/v1/dashboard" , handler .GetDashboardSummaryHandler )
108+
109+ store .queryExecutionRecordsFn = func (ctx context.Context , filter types.ExecutionFilter ) ([]* types.Execution , error ) {
110+ return nil , nil
111+ }
112+ store .queryAgentPackagesFn = func (ctx context.Context , filters types.PackageFilters ) ([]* types.AgentPackage , error ) {
113+ return nil , nil
114+ }
115+ store .listAgentsFn = func (ctx context.Context , filters types.AgentFilters ) ([]* types.AgentNode , error ) {
116+ return nil , nil
117+ }
118+
119+ resp := httptest .NewRecorder ()
120+ router .ServeHTTP (resp , httptest .NewRequest (http .MethodGet , "/api/ui/v1/dashboard" , nil ))
121+ require .Equal (t , http .StatusOK , resp .Code )
122+
123+ var body DashboardSummaryResponse
124+ require .NoError (t , json .Unmarshal (resp .Body .Bytes (), & body ))
125+ require .Equal (t , 100.0 , body .SuccessRate )
126+ require .Equal (t , 0 , body .Executions .Today )
127+ })
128+
101129 t .Run ("error when collector fails" , func (t * testing.T ) {
102130 store := & dashboardOverrideStorage {overrideStorage : & overrideStorage {StorageProvider : setupTestStorage (t )}}
103131 handler := NewDashboardHandler (store , & mockLifecycleAgentService {})
@@ -119,3 +147,35 @@ func TestDashboardSummaryHandlerAdditionalCoverage(t *testing.T) {
119147 require .Equal (t , http .StatusInternalServerError , resp .Code )
120148 })
121149}
150+
151+ func TestExecutionsSummarySuccessRateRolling24h (t * testing.T ) {
152+ gin .SetMode (gin .TestMode )
153+
154+ store := & dashboardOverrideStorage {overrideStorage : & overrideStorage {StorageProvider : setupTestStorage (t )}}
155+ handler := NewDashboardHandler (store , & mockLifecycleAgentService {})
156+
157+ // 01:00 UTC: the calendar-today window is only an hour old, so most of the
158+ // rolling 24h window falls in yesterday's calendar window.
159+ now := time .Date (2026 , 7 , 17 , 1 , 0 , 0 , 0 , time .UTC )
160+ today := time .Date (2026 , 7 , 17 , 0 , 0 , 0 , 0 , time .UTC )
161+
162+ runningToday := & types.Execution {Status : string (types .ExecutionStatusRunning ), StartedAt : now .Add (- 15 * time .Minute )}
163+ failedWithin24h := & types.Execution {Status : string (types .ExecutionStatusFailed ), StartedAt : now .Add (- 23 * time .Hour )}
164+ succeededWithin24h := & types.Execution {Status : string (types .ExecutionStatusSucceeded ), StartedAt : now .Add (- 23 * time .Hour - 30 * time .Minute )}
165+ succeededOutside24h := & types.Execution {Status : string (types .ExecutionStatusSucceeded ), StartedAt : now .Add (- 24 * time .Hour - 30 * time .Minute )}
166+
167+ store .queryExecutionRecordsFn = func (ctx context.Context , filter types.ExecutionFilter ) ([]* types.Execution , error ) {
168+ if filter .StartTime != nil && filter .StartTime .Equal (today ) {
169+ return []* types.Execution {runningToday }, nil
170+ }
171+ return []* types.Execution {failedWithin24h , succeededWithin24h , succeededOutside24h }, nil
172+ }
173+
174+ summary , rate , err := handler .getExecutionsSummaryAndSuccessRate (context .Background (), now )
175+ require .NoError (t , err )
176+ require .Equal (t , 1 , summary .Today )
177+ require .Equal (t , 3 , summary .Yesterday )
178+ // Rate basis: failedWithin24h + succeededWithin24h. The in-flight run does
179+ // not count against the rate and the pre-window success is excluded.
180+ require .Equal (t , 50.0 , rate )
181+ }
0 commit comments