Files
paperclip/packages/plugins/sandbox-providers/kubernetes/test/unit/job-orchestrator.test.ts
T

102 lines
4.6 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { createJob, deleteJob, getJobStatus, findPodForJob, JobTimeoutError, streamPodLogs, waitForJobCompletion } from "../../src/job-orchestrator.js";
describe("createJob", () => {
it("calls batch.createNamespacedJob with the manifest", async () => {
const create = vi.fn().mockResolvedValue({ metadata: { uid: "abc-uid" } });
const clients = { batch: { createNamespacedJob: create } };
const jobManifest = { apiVersion: "batch/v1", kind: "Job", metadata: { name: "r-1", namespace: "ns" }, spec: { template: {} } };
const result = await createJob(clients as never, "ns", jobManifest);
expect(create).toHaveBeenCalledWith({ namespace: "ns", body: jobManifest });
expect(result.uid).toBe("abc-uid");
});
});
describe("getJobStatus", () => {
it("returns phase=Succeeded when succeeded count is 1", async () => {
const get = vi.fn().mockResolvedValue({ status: { succeeded: 1, conditions: [{ type: "Complete", status: "True" }] } });
const clients = { batch: { readNamespacedJobStatus: get } };
const status = await getJobStatus(clients as never, "ns", "r-1");
expect(status.phase).toBe("Succeeded");
expect(status.complete).toBe(true);
});
it("returns phase=Failed when failed count is >0", async () => {
const get = vi.fn().mockResolvedValue({ status: { failed: 1, conditions: [{ type: "Failed", status: "True", reason: "DeadlineExceeded" }] } });
const clients = { batch: { readNamespacedJobStatus: get } };
const status = await getJobStatus(clients as never, "ns", "r-1");
expect(status.phase).toBe("Failed");
expect(status.reason).toBe("DeadlineExceeded");
});
it("returns phase=Running when active count is >0", async () => {
const get = vi.fn().mockResolvedValue({ status: { active: 1 } });
const clients = { batch: { readNamespacedJobStatus: get } };
const status = await getJobStatus(clients as never, "ns", "r-1");
expect(status.phase).toBe("Running");
});
it("returns phase=Pending when no active/succeeded/failed counters set", async () => {
const get = vi.fn().mockResolvedValue({ status: {} });
const clients = { batch: { readNamespacedJobStatus: get } };
const status = await getJobStatus(clients as never, "ns", "r-1");
expect(status.phase).toBe("Pending");
});
});
describe("findPodForJob", () => {
it("lists pods by job-name label and returns the first running pod", async () => {
const list = vi.fn().mockResolvedValue({ items: [{ metadata: { name: "r-1-xyz" }, status: { phase: "Running" } }] });
const clients = { core: { listNamespacedPod: list } };
const podName = await findPodForJob(clients as never, "ns", "r-1");
expect(list).toHaveBeenCalledWith(expect.objectContaining({ namespace: "ns", labelSelector: "job-name=r-1" }));
expect(podName).toBe("r-1-xyz");
});
it("returns null when no pod is found", async () => {
const list = vi.fn().mockResolvedValue({ items: [] });
const clients = { core: { listNamespacedPod: list } };
const podName = await findPodForJob(clients as never, "ns", "r-1");
expect(podName).toBeNull();
});
});
describe("deleteJob", () => {
it("calls batch.deleteNamespacedJob with foreground propagation", async () => {
const del = vi.fn().mockResolvedValue({});
const clients = { batch: { deleteNamespacedJob: del } };
await deleteJob(clients as never, "ns", "r-1");
expect(del).toHaveBeenCalledWith(
expect.objectContaining({
namespace: "ns",
name: "r-1",
propagationPolicy: "Foreground",
}),
);
});
});
describe("streamPodLogs", () => {
it("emits pod log response bodies as stdout because Kubernetes pod logs are combined", async () => {
const readNamespacedPodLog = vi.fn().mockResolvedValue({ body: "hello\n" });
const clients = { core: { readNamespacedPodLog } };
const chunks: { stream: "stdout" | "stderr"; text: string }[] = [];
await streamPodLogs(clients as never, "ns", "pod-1", async (stream, text) => {
chunks.push({ stream, text });
});
expect(readNamespacedPodLog).toHaveBeenCalledWith({ namespace: "ns", name: "pod-1" });
expect(chunks).toEqual([{ stream: "stdout", text: "hello\n" }]);
});
});
describe("waitForJobCompletion", () => {
it("throws JobTimeoutError when the deadline is exceeded", async () => {
const get = vi.fn().mockResolvedValue({ status: { active: 1 } });
const clients = { batch: { readNamespacedJobStatus: get } };
await expect(
waitForJobCompletion(clients as never, "ns", "r-1", { timeoutMs: 50, pollMs: 10 }),
).rejects.toBeInstanceOf(JobTimeoutError);
});
});