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, resolveProjectName } 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("resolves project names using config, Compose, then directory precedence", () => { expect( resolveProjectName( { project: "directory", projectConfigured: false }, { name: "compose" }, ), ).toBe("compose"); expect( resolveProjectName( { project: "config", projectConfigured: true }, { name: "compose" }, ), ).toBe("config"); expect( resolveProjectName( { project: "directory", projectConfigured: false }, {}, ), ).toBe("directory"); }); 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"); }); });