import { afterEach, describe, expect, test } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { ApiRequestInit } from "../../lib/api"; import { buildServices, resolveBuildImages, uploadWorkspaceSnapshot, type ApiRequester, } from "../../lib/build"; import { workspaceManifestDigest, type WorkspaceSnapshot, } from "../../lib/workspace"; import type { BuildRequest } from "../../shared/build-protocol"; const directories: string[] = []; function emptySnapshot(): WorkspaceSnapshot { const manifest = { version: 1 as const, files: [] }; return { manifest, digest: workspaceManifestDigest(manifest), blobs: [] }; } afterEach(async () => { await Promise.all( directories .splice(0) .map((path) => rm(path, { recursive: true, force: true })), ); }); describe("authenticated build API pipeline", () => { test("negotiates and uploads the manifest through resumable blob routes", async () => { const snapshot = emptySnapshot(); const calls: Array<{ path: string; init?: ApiRequestInit }> = []; let negotiations = 0; const request: ApiRequester = async ( path: string, init?: ApiRequestInit, ) => { calls.push({ path, init }); if (path === "/snapshots/negotiate") { negotiations += 1; return ( negotiations === 1 ? { workspace: snapshot.digest, missing: [snapshot.digest], ready: false, } : { workspace: snapshot.digest, missing: [], ready: true } ) as T; } if (init?.method === "POST" && !path.endsWith("/complete")) return { offset: 0, complete: false } as T; if (init?.method === "PATCH") return { offset: (init.body as Uint8Array).byteLength } as T; return { complete: true } as T; }; await uploadWorkspaceSnapshot(snapshot, request); expect( calls.map(({ path, init }) => [init?.method ?? "GET", path]), ).toEqual([ ["POST", "/snapshots/negotiate"], ["POST", `/blobs/${encodeURIComponent(snapshot.digest)}/uploads`], ["PATCH", `/blobs/${encodeURIComponent(snapshot.digest)}/uploads`], [ "POST", `/blobs/${encodeURIComponent(snapshot.digest)}/uploads/complete`, ], ["POST", "/snapshots/negotiate"], ]); expect(new Headers(calls[2]!.init?.headers).get("upload-offset")).toBe("0"); }); test("submits, reconciles, reports logs, and returns the server image reference", async () => { const root = await mkdtemp(join(tmpdir(), "kuber-build-api-")); directories.push(root); const git = Bun.spawn(["git", "init", "-q", root]); expect(await git.exited).toBe(0); const snapshot = emptySnapshot(); const calls: Array<{ path: string; init?: ApiRequestInit }> = []; let submitted: BuildRequest | undefined; const output: string[] = []; const request: ApiRequester = async ( path: string, init?: ApiRequestInit, ) => { calls.push({ path, init }); if (path === "/snapshots/negotiate") return { workspace: snapshot.digest, missing: [], ready: true } as T; if (path === "/builds") { submitted = init?.json as BuildRequest; return { version: 1, id: submitted.id, state: "queued", createdAt: "2026-01-01T00:00:00Z", } as T; } if (path.includes("/events")) return [ { type: "log", id: submitted!.id, sequence: 1, message: "build log\n", }, ] as T; if (path.endsWith("/reconcile")) return { version: 1, id: submitted!.id, state: "succeeded", createdAt: "2026-01-01T00:00:00Z", digest: `sha256:${"a".repeat(64)}`, } as T; if (path.endsWith("/result")) return { image: "registry.server/kuber/shop-web", digest: `sha256:${"a".repeat(64)}`, reference: `registry.server/kuber/shop-web@sha256:${"a".repeat(64)}`, } as T; throw new Error(`Unexpected request ${path}`); }; const result = await buildServices( "shop", { services: { web: { build: { context: ".", args: { MODE: "prod" } } } }, }, root, { progress: (message) => { output.push(message); }, }, { request, snapshot, sleep: async () => {}, pollIntervalMs: 0 }, ); expect(submitted?.spec).toMatchObject({ architecture: "arm64", context: ".", buildArgs: ["MODE=prod"], workspace: snapshot.digest, }); expect(result).toEqual({ built: ["web"], changed: ["web"], images: { web: `registry.server/kuber/shop-web@sha256:${"a".repeat(64)}`, }, }); expect(output).toContain("build log"); expect(calls.some(({ path }) => path.endsWith("/result"))).toBe(true); }); test("no-build image resolution uses only the resolve route", async () => { const calls: string[] = []; const images = await resolveBuildImages( "shop", { services: { web: { build: "." }, cache: { image: "redis" } } }, { request: async (path: string, init?: ApiRequestInit) => { calls.push(`${init?.method}:${path}:${JSON.stringify(init?.json)}`); return { reference: "registry/web@sha256:immutable" } as T; }, }, ); expect(images).toEqual({ web: "registry/web@sha256:immutable" }); expect(calls).toEqual([ 'POST:/images/resolve:{"project":"shop","service":"web"}', ]); }); });