Ctrl K
ComunidadGuías…

pkg/health/recovery_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package health

import (
	"strings"
	"testing"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestPodDisplayLevel(t *testing.T) {
	now := time.Now()
	unsched := &corev1.Pod{Status: corev1.PodStatus{
		Phase:      corev1.PodPending,
		Conditions: []corev1.PodCondition{{Type: corev1.PodScheduled, Status: corev1.ConditionFalse, Reason: corev1.PodReasonUnschedulable}},
	}}
	if got := PodDisplayLevel(unsched, now); got != LevelDegraded {
		t.Errorf("unschedulable pod display level = %q, want degraded", got)
	}
	// Floor, not ceiling: a stuck-terminating pod that is also crashlooping stays unhealthy.
	stuckCrash := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &metav1.Time{Time: now.Add(-11 * time.Minute)}},
		Status: corev1.PodStatus{
			Phase: corev1.PodRunning,
			ContainerStatuses: []corev1.ContainerStatus{
				{State: corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}}},
			},
		},
	}
	if got := PodDisplayLevel(stuckCrash, now); got != LevelUnhealthy {
		t.Errorf("stuck-terminating + crashloop display level = %q, want unhealthy (floor must not downgrade)", got)
	}
	healthy := &corev1.Pod{Status: corev1.PodStatus{Phase: corev1.PodRunning, ContainerStatuses: []corev1.ContainerStatus{{Ready: true}}}}
	if got := PodDisplayLevel(healthy, now); got != LevelHealthy {
		t.Errorf("healthy pod display level = %q, want healthy", got)
	}
}

func TestIsStuckTerminating(t *testing.T) {
	now := time.Now()
	notTerminating := &corev1.Pod{}
	graceful := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &metav1.Time{Time: now.Add(-2 * time.Minute)}}}
	stuck := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &metav1.Time{Time: now.Add(-11 * time.Minute)}}}
	if IsStuckTerminating(notTerminating, now) {
		t.Error("a pod with no deletionTimestamp is not stuck-terminating")
	}
	if IsStuckTerminating(graceful, now) {
		t.Error("a pod terminating 2m is graceful, not stuck")
	}
	if !IsStuckTerminating(stuck, now) {
		t.Error("a pod terminating 11m is stuck")
	}
}

// legacy is a small helper: these recovery-guard cases were originally written
// against the three-value ClassifyPodHealth ("healthy"/"warning"/"error"); they
// now assert the same via Pod().LegacyString() so the guards stay pinned.
func legacy(pod *corev1.Pod, now time.Time) string { return Pod(pod, now).LegacyString() }

// TestRecoveredAfterCrashIsHealthy: a container that crashed earlier but has run
// continuously past the 5m backoff window has recovered — its stale history must
// not keep it flagged as a crashloop.
func TestRecoveredAfterCrashIsHealthy(t *testing.T) {
	now := time.Now()
	crash := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Error", ExitCode: 1}}

	recovered := &corev1.Pod{Status: corev1.PodStatus{
		Phase: corev1.PodRunning,
		ContainerStatuses: []corev1.ContainerStatus{{
			Ready: true, RestartCount: 2,
			State:                corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-30 * time.Minute))}},
			LastTerminationState: crash,
		}},
	}}
	if got := legacy(recovered, now); got != "healthy" {
		t.Errorf("recovered-after-crash (Running 30m) = %q, want healthy", got)
	}

	looping := recovered.DeepCopy()
	looping.Status.ContainerStatuses[0].State.Running.StartedAt = metav1.NewTime(now.Add(-30 * time.Second))
	if got := legacy(looping, now); got != "warning" {
		t.Errorf("just-restarted crashloop (Running+Ready 30s) = %q, want warning", got)
	}
	if got := PodProblemReason(looping, now); got != "CrashLoopBackOff" {
		t.Errorf("just-restarted crashloop reason = %q, want CrashLoopBackOff", got)
	}

	completedInit := &corev1.Pod{Status: corev1.PodStatus{
		Phase: corev1.PodRunning,
		InitContainerStatuses: []corev1.ContainerStatus{{
			RestartCount:         1,
			State:                corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Completed", ExitCode: 0}},
			LastTerminationState: crash,
		}},
		ContainerStatuses: []corev1.ContainerStatus{{
			Ready: true,
			State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-10 * time.Minute))}},
		}},
	}}
	if got := legacy(completedInit, now); got != "healthy" {
		t.Errorf("retried-then-completed init + healthy main = %q, want healthy", got)
	}
}

func TestRecoveredAfterOOMIsHealthy(t *testing.T) {
	now := time.Now()
	oom := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{
		Reason: "OOMKilled", ExitCode: 137, FinishedAt: metav1.NewTime(now.Add(-30 * time.Minute)),
	}}
	recovered := &corev1.Pod{
		Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app", ReadinessProbe: &corev1.Probe{}}}},
		Status: corev1.PodStatus{
			Phase: corev1.PodRunning,
			ContainerStatuses: []corev1.ContainerStatus{{
				Name: "app", Ready: true, RestartCount: 1,
				State:                corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-20 * time.Minute))}},
				LastTerminationState: oom,
			}},
		},
	}
	if got := legacy(recovered, now); got != "healthy" {
		t.Errorf("recovered-after-OOM = %q, want healthy", got)
	}
	active := recovered.DeepCopy()
	active.Status.ContainerStatuses[0].Ready = false
	active.Status.ContainerStatuses[0].State.Running.StartedAt = metav1.NewTime(now.Add(-30 * time.Second))
	if got := legacy(active, now); got != "error" {
		t.Errorf("recent OOM restart = %q, want error", got)
	}
	if got := PodProblemReason(active, now); got != "OOMKilled" {
		t.Errorf("recent OOM reason = %q, want OOMKilled", got)
	}
}

func TestReadyContainerRemainsWarningUntilSettleWindow(t *testing.T) {
	now := time.Now()
	crash := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{
		Reason:     "Error",
		ExitCode:   1,
		FinishedAt: metav1.NewTime(now.Add(-90 * time.Second)),
	}}
	probedFlapper := &corev1.Pod{
		Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app", ReadinessProbe: &corev1.Probe{}}}},
		Status: corev1.PodStatus{
			Phase: corev1.PodRunning,
			ContainerStatuses: []corev1.ContainerStatus{{
				Name: "app", Ready: true, RestartCount: 2,
				State:                corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-90 * time.Second))}},
				LastTerminationState: crash,
			}},
		},
	}
	if got := legacy(probedFlapper, now); got != "warning" {
		t.Errorf("probed Ready pod restarted 90s ago = %q, want warning", got)
	}
	probelessLooping := probedFlapper.DeepCopy()
	probelessLooping.Spec.Containers[0].ReadinessProbe = nil
	if got := legacy(probelessLooping, now); got != "warning" {
		t.Errorf("probe-less Ready pod Running 90s = %q, want warning during settle window", got)
	}
	settled := probedFlapper.DeepCopy()
	settled.Status.ContainerStatuses[0].State.Running.StartedAt = metav1.NewTime(now.Add(-6 * time.Minute))
	if got := legacy(settled, now); got != "healthy" {
		t.Errorf("probed Ready pod Running 6m = %q, want healthy", got)
	}
	settledWithoutStartedAt := probedFlapper.DeepCopy()
	settledWithoutStartedAt.Status.ContainerStatuses[0].State.Running.StartedAt = metav1.Time{}
	settledWithoutStartedAt.Status.ContainerStatuses[0].LastTerminationState.Terminated.FinishedAt = metav1.NewTime(now.Add(-6 * time.Minute))
	if got := legacy(settledWithoutStartedAt, now); got != "healthy" {
		t.Errorf("probed Ready pod with old termination fallback = %q, want healthy", got)
	}
}

func TestCrashLoopLevelTracksCurrentState(t *testing.T) {
	now := time.Now()
	crash := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{
		Reason:     "Error",
		ExitCode:   1,
		FinishedAt: metav1.NewTime(now.Add(-30 * time.Second)),
	}}
	base := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.NewTime(now.Add(-10 * time.Minute))},
		Status: corev1.PodStatus{
			Phase: corev1.PodRunning,
			ContainerStatuses: []corev1.ContainerStatus{{
				Name:                 "app",
				Ready:                true,
				RestartCount:         2,
				State:                corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-30 * time.Second))}},
				LastTerminationState: crash,
			}},
		},
	}

	tests := []struct {
		name string
		edit func(*corev1.Pod)
		want string
	}{
		{
			name: "recently up",
			edit: func(*corev1.Pod) {},
			want: "warning",
		},
		{
			name: "high cumulative restarts stay warning while up",
			edit: func(pod *corev1.Pod) {
				pod.Status.ContainerStatuses[0].RestartCount = 40
			},
			want: "warning",
		},
		{
			name: "waiting now",
			edit: func(pod *corev1.Pod) {
				cs := &pod.Status.ContainerStatuses[0]
				cs.Ready = false
				cs.State = corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}}
			},
			want: "error",
		},
		{
			name: "running but not ready",
			edit: func(pod *corev1.Pod) {
				pod.Status.ContainerStatuses[0].Ready = false
			},
			want: "error",
		},
		{
			name: "stable beyond settle window",
			edit: func(pod *corev1.Pod) {
				pod.Status.ContainerStatuses[0].State.Running.StartedAt = metav1.NewTime(now.Add(-6 * time.Minute))
			},
			want: "healthy",
		},
		{
			name: "old crash history separated by node outage is healthy",
			edit: func(pod *corev1.Pod) {
				pod.Status.ContainerStatuses[0].LastTerminationState.Terminated.FinishedAt =
					metav1.NewTime(now.Add(-3 * time.Hour))
			},
			want: "healthy",
		},
		{
			name: "maximum backoff gap keeps a real loop visible",
			edit: func(pod *corev1.Pod) {
				pod.Status.ContainerStatuses[0].State.Running.StartedAt =
					metav1.NewTime(now.Add(-time.Minute))
				pod.Status.ContainerStatuses[0].LastTerminationState.Terminated.FinishedAt =
					metav1.NewTime(now.Add(-6 * time.Minute))
			},
			want: "warning",
		},
		{
			name: "missing running timestamp fails closed as warning",
			edit: func(pod *corev1.Pod) {
				pod.Status.ContainerStatuses[0].State.Running.StartedAt = metav1.Time{}
			},
			want: "warning",
		},
		{
			name: "readiness gate does not make ready container down",
			edit: func(pod *corev1.Pod) {
				pod.Status.Conditions = []corev1.PodCondition{{
					Type:   corev1.PodReady,
					Status: corev1.ConditionFalse,
					Reason: "ReadinessGatesNotReady",
				}}
			},
			want: "warning",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pod := base.DeepCopy()
			tt.edit(pod)
			if got := legacy(pod, now); got != tt.want {
				t.Fatalf("health = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestCrashLoopLevelUsesWorstContainerState(t *testing.T) {
	now := time.Now()
	crash := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Error", ExitCode: 1}}
	pod := &corev1.Pod{Status: corev1.PodStatus{
		Phase: corev1.PodRunning,
		ContainerStatuses: []corev1.ContainerStatus{
			{
				Name:                 "serving",
				Ready:                true,
				RestartCount:         1,
				State:                corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-time.Minute))}},
				LastTerminationState: crash,
			},
			{
				Name:                 "down",
				RestartCount:         1,
				State:                corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}},
				LastTerminationState: crash,
			},
		},
	}}

	if got := legacy(pod, now); got != "error" {
		t.Fatalf("one recovered + one down container = %q, want error", got)
	}
}

func TestRecoveredCrashHistoryDoesNotMaskOtherFailures(t *testing.T) {
	now := time.Now()
	recovered := corev1.ContainerStatus{
		Name:         "app",
		Ready:        true,
		RestartCount: 2,
		State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{
			StartedAt: metav1.NewTime(now.Add(-time.Minute)),
		}},
		LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{
			Reason: "Error", ExitCode: 1,
		}},
	}
	tests := []struct {
		name       string
		addFailure func(*corev1.Pod)
		wantReason string
	}{
		{
			name: "sibling image pull",
			addFailure: func(pod *corev1.Pod) {
				pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
					Name:  "image",
					State: corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "ImagePullBackOff"}},
				})
			},
			wantReason: "ImagePullBackOff",
		},
		{
			name: "sibling oom",
			addFailure: func(pod *corev1.Pod) {
				pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
					Name: "memory",
					State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{
						Reason: "OOMKilled", ExitCode: 137,
					}},
				})
			},
			wantReason: "OOMKilled",
		},
		{
			name: "fatal init",
			addFailure: func(pod *corev1.Pod) {
				pod.Status.InitContainerStatuses = []corev1.ContainerStatus{{
					Name:  "setup",
					State: corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CreateContainerConfigError"}},
				}}
			},
			wantReason: "CreateContainerConfigError",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pod := &corev1.Pod{Status: corev1.PodStatus{
				Phase:             corev1.PodRunning,
				ContainerStatuses: []corev1.ContainerStatus{recovered},
			}}
			tt.addFailure(pod)
			if got := legacy(pod, now); got != "error" {
				t.Fatalf("health = %q, want error", got)
			}
			if got := PodProblemReason(pod, now); got != tt.wantReason {
				t.Fatalf("reason = %q, want %q", got, tt.wantReason)
			}
		})
	}
}

func TestRestartableInitCrashLoopUsesServingState(t *testing.T) {
	now := time.Now()
	always := corev1.ContainerRestartPolicyAlways
	crash := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Error", ExitCode: 1}}
	sidecar := &corev1.Pod{
		Spec: corev1.PodSpec{InitContainers: []corev1.Container{{
			Name:          "sidecar",
			RestartPolicy: &always,
		}}},
		Status: corev1.PodStatus{
			Phase: corev1.PodRunning,
			InitContainerStatuses: []corev1.ContainerStatus{{
				Name:                 "sidecar",
				Ready:                true,
				RestartCount:         1,
				State:                corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-time.Minute))}},
				LastTerminationState: crash,
			}},
		},
	}
	if got := legacy(sidecar, now); got != "warning" {
		t.Errorf("probe-less serving sidecar = %q, want warning", got)
	}

	probed := sidecar.DeepCopy()
	probed.Spec.InitContainers[0].ReadinessProbe = &corev1.Probe{}
	if got := legacy(probed, now); got != "warning" {
		t.Errorf("probe-gated Ready sidecar = %q, want warning during settle window", got)
	}

	ordinaryInit := sidecar.DeepCopy()
	ordinaryInit.Spec.InitContainers[0].RestartPolicy = nil
	if got := legacy(ordinaryInit, now); got != "error" {
		t.Errorf("ordinary running init with crash history = %q, want error", got)
	}
}

// TestStableCrashLoopPreservesSpecificReasons: OOMKilled is not folded to
// CrashLoopBackOff.
func TestStableCrashLoopPreservesSpecificReasons(t *testing.T) {
	now := time.Now()
	oom := &corev1.Pod{Status: corev1.PodStatus{
		Phase: corev1.PodRunning,
		ContainerStatuses: []corev1.ContainerStatus{{
			RestartCount:         4,
			State:                corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "OOMKilled"}},
			LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "OOMKilled"}},
		}},
	}}
	if got := PodProblemReason(oom, now); got != "OOMKilled" {
		t.Errorf("OOMKilled reason = %q, want OOMKilled (must not fold to crashloop)", got)
	}
	if got := legacy(oom, now); got != "error" {
		t.Errorf("OOMKilled health = %q, want error", got)
	}
}

func TestActiveOOMKilledContainers(t *testing.T) {
	now := time.Now()
	pod := &corev1.Pod{
		Spec: corev1.PodSpec{Containers: []corev1.Container{
			{Name: "terminated"},
			{Name: "recent"},
			{Name: "recovered", ReadinessProbe: &corev1.Probe{}},
		}},
		Status: corev1.PodStatus{
			ContainerStatuses: []corev1.ContainerStatus{
				{
					Name:  "terminated",
					State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "OOMKilled"}},
				},
				{
					Name:                 "recent",
					State:                corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}},
					LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "OOMKilled"}},
				},
				{
					Name:                 "recovered",
					Ready:                true,
					State:                corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-30 * time.Minute))}},
					LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "OOMKilled"}},
				},
			},
			InitContainerStatuses: []corev1.ContainerStatus{{
				Name:  "init",
				State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "OOMKilled"}},
			}},
		},
	}

	got := ActiveOOMKilledContainers(pod, now)
	if len(got) != 2 || got[0].Name != "terminated" || got[1].Name != "recent" {
		t.Fatalf("active regular OOM containers = %+v, want terminated and recent only", got)
	}
}

func TestActiveCrashLoopContainerStatuses(t *testing.T) {
	now := time.Now()
	crash := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Error", ExitCode: 1}}
	pod := &corev1.Pod{
		Spec: corev1.PodSpec{Containers: []corev1.Container{
			{Name: "active"},
			{Name: "ready", ReadinessProbe: &corev1.Probe{}},
			{Name: "stable"},
			{Name: "oom"},
		}},
		Status: corev1.PodStatus{
			ContainerStatuses: []corev1.ContainerStatus{
				{Name: "active", RestartCount: 2, State: corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}}, LastTerminationState: crash},
				{Name: "ready", Ready: true, RestartCount: 2, State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-30 * time.Second))}}, LastTerminationState: crash},
				{Name: "stable", RestartCount: 2, State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-10 * time.Minute))}}, LastTerminationState: crash},
				{Name: "oom", RestartCount: 2, State: corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}}, LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "OOMKilled", ExitCode: 137}}},
			},
			InitContainerStatuses: []corev1.ContainerStatus{
				{Name: "init-active", RestartCount: 1, State: corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}}, LastTerminationState: crash},
				{Name: "init-done", RestartCount: 1, State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Completed", ExitCode: 0}}, LastTerminationState: crash},
			},
		},
	}

	got := ActiveCrashLoopContainerStatuses(pod, now)
	if len(got) != 3 || got[0].Name != "active" || got[1].Name != "ready" || got[2].Name != "init-active" {
		t.Fatalf("active crashloop containers = %+v, want active, ready flapper, and init-active", got)
	}
	if got := ActiveCrashLoopContainerStatuses(nil, now); got != nil {
		t.Fatalf("nil pod returned %+v, want nil", got)
	}
}

// TestPodCrashLoopDiagnosisOrdersCandidates: the current waiting crashloop wins
// over a newer running tick.
func TestPodCrashLoopDiagnosisOrdersCandidates(t *testing.T) {
	now := time.Now()
	older := metav1.NewTime(now.Add(-30 * time.Second))
	newer := metav1.NewTime(now.Add(-20 * time.Second))
	pod := &corev1.Pod{Status: corev1.PodStatus{
		Phase: corev1.PodRunning,
		ContainerStatuses: []corev1.ContainerStatus{
			{
				Name: "running-newer", RestartCount: 2,
				State:                corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-1 * time.Second))}},
				LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Error", ExitCode: 139, FinishedAt: newer}},
			},
			{
				Name: "waiting-older", RestartCount: 2,
				State:                corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}},
				LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Error", ExitCode: 126, FinishedAt: older}},
			},
		},
	}}
	cause, _ := PodCrashLoopDiagnosis(pod, now)
	if !strings.Contains(cause, `container "waiting-older"`) || !strings.Contains(cause, "code 126") {
		t.Fatalf("cause = %q, want current waiting crashloop to win over newer running tick", cause)
	}
}

func TestPodCrashLoopDiagnosisUsesServingCopyDuringSettleWindow(t *testing.T) {
	now := time.Now()
	pod := &corev1.Pod{Status: corev1.PodStatus{
		Phase: corev1.PodRunning,
		ContainerStatuses: []corev1.ContainerStatus{{
			Name:         "app",
			Ready:        true,
			RestartCount: 2,
			State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{
				StartedAt: metav1.NewTime(now.Add(-time.Minute)),
			}},
			LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{
				Reason: "Error", ExitCode: 1, FinishedAt: metav1.NewTime(now.Add(-time.Minute)),
			}},
		}},
	}}

	cause, action := PodCrashLoopDiagnosis(pod, now)
	if !strings.Contains(cause, "is serving again") || strings.Contains(cause, "is crashlooping") {
		t.Fatalf("cause = %q, want recovered serving language", cause)
	}
	if !strings.Contains(action, "Watch for another restart") {
		t.Fatalf("action = %q, want repeat-crash guidance", action)
	}
}

// TestPodCrashLoopDiagnosisShortRunStableAcrossReplicas: the short-run context is
// identical across replicas that crashed after slightly different sub-window
// durations (no per-replica duration leaks into the message).
func TestPodCrashLoopDiagnosisShortRunStableAcrossReplicas(t *testing.T) {
	now := time.Now()
	mkPod := func(runFor time.Duration) *corev1.Pod {
		finished := metav1.NewTime(now.Add(-1 * time.Second))
		started := metav1.NewTime(finished.Time.Add(-runFor))
		return &corev1.Pod{Status: corev1.PodStatus{
			Phase: corev1.PodRunning,
			ContainerStatuses: []corev1.ContainerStatus{{
				Name: "app", RestartCount: 2,
				State:                corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}},
				LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Error", ExitCode: 139, StartedAt: started, FinishedAt: finished}},
			}},
		}}
	}
	fastCause, _ := PodCrashLoopDiagnosis(mkPod(2*time.Second), now)
	slowerCause, _ := PodCrashLoopDiagnosis(mkPod(4*time.Second), now)
	if fastCause == "" || fastCause != slowerCause {
		t.Fatalf("short-run causes should be identical across replicas: %q vs %q", fastCause, slowerCause)
	}
	if strings.Contains(fastCause, "2s") || strings.Contains(slowerCause, "4s") {
		t.Fatalf("short-run cause must not include per-replica duration: %q / %q", fastCause, slowerCause)
	}
}