Files
kandev/apps/web/components/settings/kubernetes-config.test.ts
T
2026-09-02 13:50:03 +00:00

288 lines
8.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
DEFAULT_KUBERNETES_IMAGE,
createDefaultKubernetesProfileConfig,
getKubernetesProfileValidationError,
isKubernetesExecutorDirty,
isKubernetesProfileDirty,
parseKubernetesExecutorConfig,
parseKubernetesProfileConfig,
replaceKubernetesProfileConfig,
serializeKubernetesExecutorConfig,
serializeKubernetesProfileConfig,
} from "./kubernetes-config";
describe("Kubernetes executor settings config", () => {
it("uses the multi-architecture Kandev image as the starter workload", () => {
expect(DEFAULT_KUBERNETES_IMAGE).toBe("ghcr.io/kdlbs/kandev:latest");
expect(createDefaultKubernetesProfileConfig().podTemplateYaml).toContain(
`image: ${DEFAULT_KUBERNETES_IMAGE}`,
);
});
it("provides serialization and dirty-baseline helpers", async () => {
const config = (await import("./kubernetes-config")) as Record<string, unknown>;
expect(
[
config.serializeKubernetesExecutorConfig,
config.parseKubernetesProfileConfig,
config.serializeKubernetesProfileConfig,
config.replaceKubernetesProfileConfig,
config.isKubernetesExecutorDirty,
config.isKubernetesProfileDirty,
].every((value) => typeof value === "function"),
).toBe(true);
});
it("parses a persisted kubeconfig executor", () => {
expect(
parseKubernetesExecutorConfig("Production cluster", {
auth_mode: "kubeconfig",
kubeconfig_path: " /etc/kandev/cluster.yaml ",
kube_context: "production",
namespace: "agents",
request_timeout_seconds: "45",
}),
).toEqual({
name: "Production cluster",
authMode: "kubeconfig",
kubeconfigPath: "/etc/kandev/cluster.yaml",
kubeContext: "production",
namespace: "agents",
requestTimeoutSeconds: "45",
});
});
it("clears kubeconfig-only fields for in-cluster auth", () => {
expect(
parseKubernetesExecutorConfig("Cluster", {
auth_mode: "in_cluster",
kubeconfig_path: "/stale/config",
kube_context: "stale",
namespace: "kandev",
}),
).toMatchObject({
authMode: "in_cluster",
kubeconfigPath: "",
kubeContext: "",
namespace: "kandev",
requestTimeoutSeconds: "30",
});
});
it("serializes only fields accepted by kubeconfig auth", () => {
expect(
serializeKubernetesExecutorConfig({
name: " Cluster ",
authMode: "kubeconfig",
kubeconfigPath: " /etc/kandev/config ",
kubeContext: " production ",
namespace: " agents ",
requestTimeoutSeconds: " 45 ",
}),
).toEqual({
auth_mode: "kubeconfig",
kubeconfig_path: "/etc/kandev/config",
kube_context: "production",
namespace: "agents",
request_timeout_seconds: "45",
});
});
it("omits stale kubeconfig fields from in-cluster serialization", () => {
expect(
serializeKubernetesExecutorConfig({
name: "Cluster",
authMode: "in_cluster",
kubeconfigPath: "/stale/config",
kubeContext: "stale",
namespace: "kandev",
requestTimeoutSeconds: "30",
}),
).toEqual({
auth_mode: "in_cluster",
namespace: "kandev",
request_timeout_seconds: "30",
});
});
});
describe("Kubernetes profile config parsing", () => {
it("parses dotted managed-PVC profile config and defaults access mode", () => {
expect(
parseKubernetesProfileConfig({
platform: "linux/arm64",
main_container: "worker",
pod_template_yaml: "template-yaml\n",
"workspace.mode": "managed_pvc",
"workspace.size": "20Gi",
"workspace.storage_class": "fast",
}),
).toEqual({
platform: "linux/arm64",
mainContainer: "worker",
podTemplateYaml: "template-yaml\n",
workspaceMode: "managed_pvc",
workspaceSize: "20Gi",
storageClass: "fast",
accessModes: ["ReadWriteOnce"],
claimName: "",
});
});
it("parses legacy workspace aliases without retaining irrelevant fields", () => {
expect(
parseKubernetesProfileConfig({
platform: "linux/amd64",
main_container: "kandev-agent",
pod_template_yaml: "template-yaml",
workspace_mode: "existing_claim",
workspace_claim_name: "shared-workspace",
workspace_size: "stale",
workspace_access_modes: '["ReadWriteMany"]',
}),
).toMatchObject({
workspaceMode: "existing_claim",
workspaceSize: "",
storageClass: "",
accessModes: [],
claimName: "shared-workspace",
});
});
it("preserves unsupported persisted access modes and reports them as invalid", () => {
const parsed = parseKubernetesProfileConfig({
"workspace.mode": "managed_pvc",
"workspace.access_modes": '["UnsupportedMode"]',
});
expect(parsed.accessModes).toEqual(["UnsupportedMode"]);
expect(getKubernetesProfileValidationError(parsed)).toBe("access_mode_invalid");
});
});
describe("Kubernetes profile config serialization", () => {
it("serializes managed PVC fields and JSON access modes", () => {
expect(
serializeKubernetesProfileConfig({
...createDefaultKubernetesProfileConfig(),
workspaceSize: " 25Gi ",
storageClass: " fast ",
accessModes: ["ReadWriteOnce", "ReadOnlyMany"],
claimName: "stale-claim",
}),
).toMatchObject({
"workspace.mode": "managed_pvc",
"workspace.size": "25Gi",
"workspace.storage_class": "fast",
"workspace.access_modes": '["ReadWriteOnce","ReadOnlyMany"]',
});
});
it.each([
{
mode: "empty_dir" as const,
expected: { "workspace.mode": "empty_dir" },
},
{
mode: "existing_claim" as const,
expected: { "workspace.mode": "existing_claim", "workspace.claim_name": "shared" },
},
])("serializes only conditional $mode workspace fields", ({ mode, expected }) => {
const serialized = serializeKubernetesProfileConfig({
...createDefaultKubernetesProfileConfig(),
workspaceMode: mode,
workspaceSize: "10Gi",
storageClass: "fast",
accessModes: ["ReadWriteMany"],
claimName: " shared ",
});
expect(
Object.fromEntries(Object.entries(serialized).filter(([key]) => key.startsWith("workspace"))),
).toEqual(expected);
});
it("replaces Kubernetes keys while preserving shared profile config", () => {
expect(
replaceKubernetesProfileConfig(
{
remote_credentials: '["git"]',
workspace_mode: "empty_dir",
workspace_size: "stale",
"workspace.claim_name": "stale",
},
{ ...createDefaultKubernetesProfileConfig(), workspaceMode: "empty_dir" },
),
).toMatchObject({
remote_credentials: '["git"]',
"workspace.mode": "empty_dir",
});
});
});
describe("Kubernetes settings baselines and validation", () => {
it("normalizes executor fields before dirty comparison", () => {
const baseline = parseKubernetesExecutorConfig("Cluster", {
auth_mode: "in_cluster",
namespace: "agents",
request_timeout_seconds: "30",
});
expect(isKubernetesExecutorDirty({ ...baseline, namespace: " agents " }, baseline)).toBe(false);
expect(isKubernetesExecutorDirty({ ...baseline, namespace: "other" }, baseline)).toBe(true);
});
it("detects raw template and workspace dirty state", () => {
const baseline = createDefaultKubernetesProfileConfig();
expect(isKubernetesProfileDirty({ ...baseline }, baseline)).toBe(false);
expect(
isKubernetesProfileDirty(
{ ...baseline, podTemplateYaml: `${baseline.podTemplateYaml}\n` },
baseline,
),
).toBe(true);
expect(isKubernetesProfileDirty({ ...baseline, workspaceMode: "empty_dir" }, baseline)).toBe(
true,
);
});
it("validates required and conditional profile fields", () => {
const valid = createDefaultKubernetesProfileConfig();
expect(getKubernetesProfileValidationError(valid)).toBeNull();
expect(getKubernetesProfileValidationError({ ...valid, mainContainer: " " })).toBe(
"main_container_required",
);
expect(getKubernetesProfileValidationError({ ...valid, mainContainer: "NOT_VALID" })).toBe(
"main_container_invalid",
);
expect(getKubernetesProfileValidationError({ ...valid, podTemplateYaml: "" })).toBe(
"pod_template_required",
);
expect(
getKubernetesProfileValidationError({
...valid,
workspaceMode: "managed_pvc",
workspaceSize: " ",
}),
).toBe("workspace_size_required");
expect(
getKubernetesProfileValidationError({
...valid,
workspaceMode: "managed_pvc",
accessModes: [],
}),
).toBe("access_mode_required");
expect(
getKubernetesProfileValidationError({
...valid,
workspaceMode: "existing_claim",
claimName: " ",
}),
).toBe("claim_name_required");
});
});