diff --git a/go/core/internal/controller/reconciler.go b/go/core/internal/controller/reconciler.go index 0a958b9875..82b5e19a98 100644 --- a/go/core/internal/controller/reconciler.go +++ b/go/core/internal/controller/reconciler.go @@ -540,26 +540,24 @@ func (r *Reconciler) reconcileModelConfigStatus(ctx context.Context, key string) } func statusWithTransitionTimes(desired, current kagentv1alpha3.AgentStatus) kagentv1alpha3.AgentStatus { - desired.Conditions = append([]metav1.Condition(nil), desired.Conditions...) - for i := range desired.Conditions { - condition := &desired.Conditions[i] - previous := apimeta.FindStatusCondition(current.Conditions, condition.Type) - if previous != nil && previous.Status == condition.Status { - condition.LastTransitionTime = previous.LastTransitionTime - } else { - condition.LastTransitionTime = metav1.Now() - } - } + desired.Conditions = conditionsWithTransitionTimes(desired.Conditions, current.Conditions) return desired } func modelConfigStatusWithTransitionTimes(desired, current kagentv1alpha3.ModelConfigStatus) kagentv1alpha3.ModelConfigStatus { - desired.Conditions = append([]metav1.Condition(nil), desired.Conditions...) - for conditionIndex := range desired.Conditions { - condition := &desired.Conditions[conditionIndex] - if previous := apimeta.FindStatusCondition(current.Conditions, condition.Type); previous != nil && - previous.Status == condition.Status && previous.Reason == condition.Reason && - previous.Message == condition.Message && previous.ObservedGeneration == condition.ObservedGeneration { + desired.Conditions = conditionsWithTransitionTimes(desired.Conditions, current.Conditions) + return desired +} + +// conditionsWithTransitionTimes keeps the transition time a condition's +// status holds and stamps the current time when the status changes, as +// apimeta.SetStatusCondition does; a new reason or message alone is no +// transition. +func conditionsWithTransitionTimes(desired, current []metav1.Condition) []metav1.Condition { + desired = append([]metav1.Condition(nil), desired...) + for conditionIndex := range desired { + condition := &desired[conditionIndex] + if previous := apimeta.FindStatusCondition(current, condition.Type); previous != nil && previous.Status == condition.Status { condition.LastTransitionTime = previous.LastTransitionTime continue } diff --git a/go/core/internal/controller/reconciler_test.go b/go/core/internal/controller/reconciler_test.go index 669e3f44c3..5b135b01af 100644 --- a/go/core/internal/controller/reconciler_test.go +++ b/go/core/internal/controller/reconciler_test.go @@ -613,3 +613,21 @@ func reconciliationTestCard(name string) *a2apb.AgentCard { SupportedInterfaces: []*a2apb.AgentInterface{{Url: "http://127.0.0.1:80", ProtocolBinding: "GRPC", ProtocolVersion: "1.0"}}, } } + +func TestModelConfigConditionTransitionTimesMoveOnlyOnStatusChange(t *testing.T) { + then := metav1.NewTime(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)) + current := kagentv1alpha3.ModelConfigStatus{Conditions: []metav1.Condition{ + {Type: "Accepted", Status: metav1.ConditionFalse, Reason: "SecretNotFound", Message: "secret team-a/a not found", ObservedGeneration: 1, LastTransitionTime: then}, + {Type: "Ready", Status: metav1.ConditionTrue, Reason: "Resolved", ObservedGeneration: 1, LastTransitionTime: then}, + }} + desired := kagentv1alpha3.ModelConfigStatus{Conditions: []metav1.Condition{ + {Type: "Accepted", Status: metav1.ConditionFalse, Reason: "SecretKeyNotFound", Message: "key missing in secret team-a/b", ObservedGeneration: 2}, + {Type: "Ready", Status: metav1.ConditionFalse, Reason: "Unresolved", ObservedGeneration: 2}, + }} + + got := modelConfigStatusWithTransitionTimes(desired, current) + + require.Equal(t, then, got.Conditions[0].LastTransitionTime, "a new reason, message or generation alone is no transition") + require.True(t, got.Conditions[1].LastTransitionTime.After(then.Time), "a status change stamps a new transition time") + require.True(t, desired.Conditions[0].LastTransitionTime.IsZero(), "the desired conditions are not mutated") +}