import { describe, expect, test } from "bun:test"; import type { V1Deployment, V1Pod, V1ReplicaSet, } from "@kubernetes/client-node"; import { currentReplicaSet, rolloutPodFailure, waitForRollout, } from "../../lib/rollout"; const deployment = { metadata: { name: "web", uid: "deployment-1", generation: 2 }, spec: { replicas: 1, template: { metadata: { labels: { app: "web" } }, spec: { containers: [{ name: "app", image: "new:v2" }] }, }, }, status: { observedGeneration: 2, updatedReplicas: 1, availableReplicas: 0, unavailableReplicas: 1, }, } as unknown as V1Deployment; const set = (uid: string, image: string) => ({ metadata: { uid, ownerReferences: [{ kind: "Deployment", uid: "deployment-1" }], }, spec: { template: { metadata: { labels: { app: "web", "pod-template-hash": uid } }, spec: { containers: [{ name: "app", image }] }, }, }, }) as unknown as V1ReplicaSet; const pod = (uid: string, status: object, init = false) => ({ metadata: { name: `${uid}-pod`, ownerReferences: [{ kind: "ReplicaSet", uid }], }, status: { [init ? "initContainerStatuses" : "containerStatuses"]: [ { name: "app", restartCount: 3, ...status }, ], }, }) as unknown as V1Pod; describe("rollout failure diagnosis", () => { test("ignores crashing pods from the previous ReplicaSet", () => { const current = currentReplicaSet(deployment, [ set("old", "old:v1"), set("new", "new:v2"), ]); expect(current?.metadata?.uid).toBe("new"); expect( rolloutPodFailure(deployment, current!, [ pod("old", { state: { waiting: { reason: "CrashLoopBackOff" } } }), ]), ).toBeUndefined(); expect( currentReplicaSet(deployment, [set("old", "old:v1")]), ).toBeUndefined(); }); test("reports current init and application container failures with safe identifiers", () => { const current = set("new", "new:v2"); expect( rolloutPodFailure(deployment, current, [ pod( "new", { state: { waiting: { reason: "ImagePullBackOff" } } }, true, ), ]), ).toContain("pod new-pod, container app: ImagePullBackOff"); expect( rolloutPodFailure(deployment, current, [ pod("new", { lastState: { terminated: { exitCode: 137, reason: "OOMKilled" } }, }), ]), ).toContain("OOMKilled (exit 137, restarts 3)"); expect( rolloutPodFailure(deployment, current, [ pod("new", { lastState: { terminated: { exitCode: 1, reason: "Error" } }, }), ]), ).toContain("Error (exit 1, restarts 3)"); }); test("detects deadline condition without waiting for pod listing", async () => { const failed = { ...deployment, status: { ...deployment.status, conditions: [ { type: "Progressing", status: "False", reason: "ProgressDeadlineExceeded", }, ], }, } as V1Deployment; await expect( waitForRollout( "web", Date.now() + 100, async () => failed, async () => { throw Error("should not list"); }, async () => [], ), ).rejects.toThrow( "Deployment web rollout failed: ProgressDeadlineExceeded", ); }); test("bounds hung reads by deadline and responds to cancellation", async () => { const never = async (): Promise => new Promise(() => {}); await expect( waitForRollout( "web", Date.now() + 20, never, async () => [], async () => [], ), ).rejects.toThrow("Timed out waiting for deployment web rollout"); const controller = new AbortController(); const waiting = waitForRollout( "web", Date.now() + 10_000, never, async () => [], async () => [], controller.signal, ); controller.abort(); await expect(waiting).rejects.toThrow("cancelled"); }); });