166 lines
5.1 KiB
TypeScript
166 lines
5.1 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import type { V1Pod } from "@kubernetes/client-node";
|
|
import type { ComposeSpecification } from "../../schema/docker.d";
|
|
import {
|
|
getComposeArch,
|
|
getComposeArchPlacement,
|
|
resolveComposeArch,
|
|
withComposeArch,
|
|
} from "../../lib/arch";
|
|
import { createCachedGetter, ctx } from "../../lib/context";
|
|
import { toEnvVars, toTable } from "../../lib/format";
|
|
import { deepMerge, getPodContainerName, throwWhen } from "../../lib/shared";
|
|
|
|
describe("architecture selection", () => {
|
|
test("defaults to arm64 and recognizes amd64", () => {
|
|
expect(resolveComposeArch({} as ComposeSpecification)).toBe("arm64");
|
|
expect(
|
|
resolveComposeArch({ "x-arch": "arm64" } as ComposeSpecification),
|
|
).toBe("arm64");
|
|
expect(
|
|
resolveComposeArch({ "x-arch": "amd64" } as ComposeSpecification),
|
|
).toBe("amd64");
|
|
});
|
|
|
|
test("provides amd64 placement and no arm64 constraints", () => {
|
|
expect(getComposeArchPlacement({} as ComposeSpecification)).toEqual({});
|
|
expect(
|
|
getComposeArchPlacement({ "x-arch": "amd64" } as ComposeSpecification),
|
|
).toEqual({
|
|
nodeSelector: { "kubernetes.io/arch": "amd64" },
|
|
tolerations: [
|
|
{
|
|
key: "arch",
|
|
operator: "Equal",
|
|
value: "amd64",
|
|
effect: "NoExecute",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("scopes architecture through synchronous and asynchronous work", async () => {
|
|
expect(getComposeArch()).toBe("arm64");
|
|
expect(
|
|
withComposeArch({ "x-arch": "amd64" } as ComposeSpecification, () =>
|
|
getComposeArch(),
|
|
),
|
|
).toBe("amd64");
|
|
await withComposeArch(
|
|
{ "x-arch": "amd64" } as ComposeSpecification,
|
|
async () => {
|
|
await Promise.resolve();
|
|
expect(getComposeArch()).toBe("amd64");
|
|
},
|
|
);
|
|
expect(getComposeArch()).toBe("arm64");
|
|
});
|
|
});
|
|
|
|
describe("format helpers", () => {
|
|
test("converts array environments and preserves embedded equals", () => {
|
|
expect(toEnvVars(["EMPTY", "A=one", "TOKEN=a=b=c"])).toEqual([
|
|
{ name: "EMPTY", value: undefined },
|
|
{ name: "A", value: "one" },
|
|
{ name: "TOKEN", value: "a=b=c" },
|
|
]);
|
|
});
|
|
|
|
test("converts object environment scalar values", () => {
|
|
expect(
|
|
toEnvVars({ STRING: "yes", NUMBER: 2, BOOL: false, NIL: null }),
|
|
).toEqual([
|
|
{ name: "STRING", value: "yes" },
|
|
{ name: "NUMBER", value: "2" },
|
|
{ name: "BOOL", value: "false" },
|
|
{ name: "NIL", value: undefined },
|
|
]);
|
|
expect(toEnvVars(undefined)).toBeUndefined();
|
|
});
|
|
|
|
test("renders uppercase table headers and rows", () => {
|
|
const table = toTable([{ name: "app", replicas: 2 }]);
|
|
expect(table).toContain("NAME");
|
|
expect(table).toContain("REPLICAS");
|
|
expect(table).toContain("app");
|
|
expect(table).toContain("2");
|
|
});
|
|
});
|
|
|
|
describe("shared helpers", () => {
|
|
test("deep merges objects and concatenates arrays without mutation", () => {
|
|
const target = { nested: { left: 1 }, list: [1], untouched: true };
|
|
const source = { nested: { right: 2 }, list: [2], added: "yes" };
|
|
expect(deepMerge(target, source)).toEqual({
|
|
nested: { left: 1, right: 2 },
|
|
list: [1, 2],
|
|
untouched: true,
|
|
added: "yes",
|
|
});
|
|
expect(target).toEqual({ nested: { left: 1 }, list: [1], untouched: true });
|
|
});
|
|
|
|
test.each([
|
|
[1, 2, 2],
|
|
[{ value: 1 }, null, null],
|
|
[null, { value: 2 }, { value: 2 }],
|
|
])("replaces non-mergeable values", (target, source, expected) => {
|
|
expect(deepMerge(target, source)).toEqual(expected);
|
|
});
|
|
|
|
test("throwWhen passes unmatched values and throws matched values", () => {
|
|
const rejectEmpty = throwWhen(
|
|
(value: string) => value.length === 0,
|
|
"empty",
|
|
);
|
|
expect(rejectEmpty("value")).toBe("value");
|
|
expect(() => rejectEmpty("")).toThrow("empty");
|
|
});
|
|
|
|
test("gets the first pod container or gives a useful error", () => {
|
|
expect(
|
|
getPodContainerName({
|
|
metadata: { name: "app-1" },
|
|
spec: { containers: [{ name: "app" }] },
|
|
} as V1Pod),
|
|
).toBe("app");
|
|
expect(() =>
|
|
getPodContainerName({ metadata: { name: "empty" } } as V1Pod),
|
|
).toThrow("No container found in pod empty");
|
|
});
|
|
});
|
|
|
|
describe("context helpers", () => {
|
|
test("rejects access outside an application context", () => {
|
|
expect(() => ctx()).toThrow("Not in context");
|
|
});
|
|
|
|
test("cached getters execute once and share an in-flight result", async () => {
|
|
let calls = 0;
|
|
const getter = createCachedGetter(
|
|
`test-${crypto.randomUUID()}`,
|
|
async () => {
|
|
calls += 1;
|
|
return "value";
|
|
},
|
|
);
|
|
expect(await Promise.all([getter(), getter()])).toEqual(["value", "value"]);
|
|
expect(await getter()).toBe("value");
|
|
expect(calls).toBe(1);
|
|
});
|
|
|
|
test("failed getter calls can retry", async () => {
|
|
let calls = 0;
|
|
const getter = createCachedGetter(
|
|
`test-${crypto.randomUUID()}`,
|
|
async () => {
|
|
calls += 1;
|
|
if (calls === 1) throw new Error("temporary");
|
|
return "recovered";
|
|
},
|
|
);
|
|
await expect(getter()).rejects.toThrow("temporary");
|
|
expect(await getter()).toBe("recovered");
|
|
});
|
|
});
|