Files
kandev/apps/web/e2e/scripts/kubernetes-helpers.test.ts
2026-09-07 13:45:28 +01:00

123 lines
3.7 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { KubernetesCluster, KubernetesPod } from "../fixtures/kubernetes-tools";
import { waitForKubernetesPod, waitForTaskResourceCleanupAttempt } from "../helpers/kubernetes";
import { DatabaseSync } from "../helpers/node-sqlite";
const tempRoots: string[] = [];
afterEach(() => {
for (const root of tempRoots.splice(0)) {
fs.rmSync(root, { recursive: true, force: true });
}
});
function podSnapshot(ready: boolean, running: boolean): KubernetesPod {
return {
metadata: {
name: "kandev-session-1",
uid: "pod-uid-1",
labels: {
"kandev.ai/task-id": "task-1",
"kandev.ai/session-id": "session-1",
},
},
status: {
phase: running ? "Running" : "Pending",
containerStatuses: [
{
name: "kandev-agent",
ready,
restartCount: 0,
state: running
? { running: { startedAt: "2026-08-24T22:00:00Z" } }
: { waiting: { reason: "ContainerCreating" } },
},
],
},
} as KubernetesPod;
}
describe("Kubernetes E2E causal helpers", () => {
it("waits for the exact main container to be running and ready", async () => {
const snapshots = [
podSnapshot(false, false),
podSnapshot(false, true),
podSnapshot(true, true),
];
let reads = 0;
const cluster = {
namespace: "kandev-e2e-workloads",
json: () => ({ items: [snapshots[Math.min(reads++, snapshots.length - 1)]] }),
} as unknown as KubernetesCluster;
const pod = await waitForKubernetesPod(cluster, "task-1", "session-1", 2_000);
expect(reads).toBe(3);
expect(
pod.status?.containerStatuses?.find((container) => container.name === "kandev-agent")?.ready,
).toBe(true);
});
it("retries a transient Kubernetes API read before accepting the ready state", async () => {
let reads = 0;
const cluster = {
namespace: "kandev-e2e-workloads",
json: () => {
if (reads++ === 0) throw new Error("kubectl unavailable");
return { items: [podSnapshot(true, true)] };
},
} as unknown as KubernetesCluster;
const pod = await waitForKubernetesPod(cluster, "task-1", "session-1", 2_000);
expect(reads).toBe(2);
expect(pod.metadata.uid).toBe("pod-uid-1");
});
it("observes the newest archive-family cleanup retry with its durable aggregate error", async () => {
const backendTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kandev-cleanup-helper-"));
tempRoots.push(backendTmpDir);
const database = path.join(backendTmpDir, "kandev.db");
const sqlite = new DatabaseSync(database);
try {
sqlite.exec(`CREATE TABLE task_resource_cleanup_jobs (
task_id TEXT NOT NULL,
trigger TEXT NOT NULL,
state TEXT NOT NULL,
attempts INTEGER NOT NULL,
last_error TEXT NOT NULL,
created_at TEXT NOT NULL
);
INSERT INTO task_resource_cleanup_jobs VALUES
('task-1', 'archive', 'succeeded', 1, '', '2026-08-24T21:00:00Z'),
('task-1', 'cascade_archive', 'retry_wait', 1,
'1 runtime stop operations failed', '2026-08-24T22:00:00Z');`);
} finally {
sqlite.close();
}
const originalPath = process.env.PATH;
process.env.PATH = "";
let cleanup;
try {
cleanup = await waitForTaskResourceCleanupAttempt(
backendTmpDir,
"task-1",
/runtime stop operations failed/i,
2_000,
);
} finally {
process.env.PATH = originalPath;
}
expect(cleanup).toMatchObject({
attempts: 1,
last_error: "1 runtime stop operations failed",
state: "retry_wait",
});
});
});