import { afterEach, describe, expect, test } from "bun:test"; import { execFile } from "node:child_process"; import { mkdtemp, mkdir, readFile, readlink, rm, stat, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { enumerateWorkspace, materializeWorkspace, serializeWorkspaceManifest, validateWorkspaceManifest, validateWorkspacePath, workspaceManifestDigest, } from "../../lib/workspace"; import { BUILD_PROTOCOL_VERSION, type WorkspaceManifest, } from "../../shared/build-protocol"; const run = promisify(execFile); const temporaryDirectories: string[] = []; async function temporaryDirectory(prefix: string): Promise { const path = await mkdtemp(join(tmpdir(), prefix)); temporaryDirectories.push(path); return path; } async function repository(): Promise { const root = await temporaryDirectory("kuber-workspace-"); await run("git", ["init", "-q", root]); await run("git", ["-C", root, "config", "user.email", "test@example.com"]); await run("git", ["-C", root, "config", "user.name", "Test"]); return root; } afterEach(async () => { await Promise.all( temporaryDirectories .splice(0) .map((path) => rm(path, { recursive: true, force: true })), ); }); describe("workspace snapshots", () => { test("snapshots Gitless directories deterministically without secrets", async () => { const root = await temporaryDirectory("kuber-workspace-filesystem-"); await writeFile(join(root, "Dockerfile"), "FROM scratch\n"); await writeFile(join(root, "app.txt"), "application"); await writeFile(join(root, ".env.local"), "SECRET=hidden"); await writeFile(join(root, "db_credentials.json"), "hidden"); const first = await enumerateWorkspace(root); const second = await enumerateWorkspace(root); expect(first).toEqual(second); expect(first.manifest.files.map((file) => file.path)).toEqual([ "Dockerfile", "app.txt", ]); }); test("works without Git and conservatively prunes ignored/generated and credential files", async () => { const root = await temporaryDirectory("kuber-workspace-no-git-"); await writeFile(join(root, ".gitignore"), "local-only/\n*.generated\nsecrets/\n!secrets/keep.txt\n"); await writeFile(join(root, "app.ts"), "source"); await mkdir(join(root, "local-only")); await writeFile(join(root, "local-only", "hidden"), "secret"); await writeFile(join(root, "cache.generated"), "generated"); await writeFile(join(root, "credentials.json"), "credential"); await writeFile(join(root, ".env.local"), "SECRET=hidden"); await mkdir(join(root, "secrets")); await writeFile(join(root, "secrets/keep.txt"), "must remain excluded"); await mkdir(join(root, "services/secrets"), { recursive: true }); await writeFile(join(root, "services/secrets/production.yaml"), "secret"); await mkdir(join(root, "app_credentials")); await writeFile(join(root, "app_credentials/key.json"), "credential"); await mkdir(join(root, "config")); await writeFile(join(root, "config/private.yaml"), "config secret"); await mkdir(join(root, "secretary")); await writeFile(join(root, "secretary/notes.txt"), "ordinary directory"); await mkdir(join(root, ".next")); await writeFile(join(root, ".next/generated"), "generated"); const previousPath = process.env.PATH; try { process.env.PATH = ""; const snapshot = await enumerateWorkspace(root); expect(snapshot.manifest.files.map((file) => file.path)).toEqual([ ".gitignore", "app.ts", "secretary/notes.txt", ]); } finally { if (previousPath === undefined) delete process.env.PATH; else process.env.PATH = previousPath; } }); test("uses conservative filesystem mode for an initialized repo when Git is absent from PATH", async () => { const root = await repository(); await writeFile(join(root, ".gitignore"), "secrets/\n!secrets/keep.txt\n"); await writeFile(join(root, "source.ts"), "source"); await writeFile(join(root, ".env.local"), "SECRET=hidden"); await mkdir(join(root, "secrets")); await writeFile(join(root, "secrets/keep.txt"), "hidden"); const previousPath = process.env.PATH; try { process.env.PATH = ""; const first = await enumerateWorkspace(root); const second = await enumerateWorkspace(root); expect(first).toEqual(second); expect(first.manifest.files.map((file) => file.path)).toEqual([ ".gitignore", "source.ts", ]); } finally { if (previousPath === undefined) delete process.env.PATH; else process.env.PATH = previousPath; } }); test("captures working tracked, untracked, and ignored dotenv files deterministically", async () => { const root = await repository(); await writeFile(join(root, ".gitignore"), "ignored*\n.env*\nsub/.env*\n"); await writeFile(join(root, "tracked.txt"), "committed"); await writeFile(join(root, "script.sh"), "#!/bin/sh\n"); await run("chmod", ["755", join(root, "script.sh")]); await run("git", [ "-C", root, "add", ".gitignore", "tracked.txt", "script.sh", ]); await run("git", ["-C", root, "commit", "-qm", "initial"]); await writeFile(join(root, "tracked.txt"), "working tree"); await writeFile(join(root, "untracked.txt"), "untracked"); await writeFile(join(root, "ignored.bin"), "excluded"); await writeFile(join(root, ".env.local"), "SECRET=root"); await run("mkdir", [join(root, "sub")]); await writeFile(join(root, "sub/.env.test"), "SECRET=sub"); await run("ln", ["-s", "tracked.txt", join(root, "link")]); const first = await enumerateWorkspace(root); const second = await enumerateWorkspace(root); expect(first).toEqual(second); expect(first.manifest.files.map((file) => file.path)).toEqual([ ".env.local", ".gitignore", "link", "script.sh", "sub/.env.test", "tracked.txt", "untracked.txt", ]); expect( first.manifest.files.find((file) => file.path === "script.sh")?.mode, ).toBe(0o755); expect( first.manifest.files.find((file) => file.path === "link")?.type, ).toBe("symlink"); expect( first.manifest.files.some((file) => file.path === "ignored.bin"), ).toBe(false); const destination = join( await temporaryDirectory("kuber-materialized-parent-"), "tree", ); const blobs = new Map(first.blobs.map((blob) => [blob.digest, blob.data])); await materializeWorkspace(destination, first.manifest, async (digest) => blobs.get(digest)!, ); expect(await readFile(join(destination, "tracked.txt"), "utf8")).toBe( "working tree", ); expect(await readFile(join(destination, ".env.local"), "utf8")).toBe( "SECRET=root", ); expect(await readlink(join(destination, "link"))).toBe("tracked.txt"); expect((await stat(join(destination, "script.sh"))).mode & 0o777).toBe( 0o755, ); }); test("omits tracked files deleted in the worktree", async () => { const root = await repository(); await writeFile(join(root, "deleted"), "value"); await run("git", ["-C", root, "add", "deleted"]); await run("git", ["-C", root, "commit", "-qm", "initial"]); await rm(join(root, "deleted")); expect((await enumerateWorkspace(root)).manifest.files).toEqual([]); }); test("rejects escaping symlinks and special files", async () => { const symlinkRoot = await repository(); await run("ln", ["-s", "../outside", join(symlinkRoot, "escape")]); await expect(enumerateWorkspace(symlinkRoot)).rejects.toThrow( "Symlink escapes workspace", ); const specialRoot = await repository(); await run("mkfifo", [join(specialRoot, "pipe")]); await expect(enumerateWorkspace(specialRoot)).rejects.toThrow( "Special files", ); }); test("allows ignored special files and prunes ignored directories", async () => { const root = await repository(); await writeFile(join(root, ".gitignore"), "ignored-pipe\nignored-dir/\n"); await run("mkfifo", [join(root, "ignored-pipe")]); await run("mkdir", [join(root, "ignored-dir")]); await run("mkfifo", [join(root, "ignored-dir/pipe")]); await expect(enumerateWorkspace(root)).resolves.toMatchObject({ manifest: { files: [{ path: ".gitignore" }] }, }); }); test("protects dotenv special files even when ignored", async () => { const root = await repository(); await writeFile(join(root, ".gitignore"), "*.pipe\n!keep.pipe\nsub/\n"); await run("mkfifo", [join(root, "blocked.pipe")]); await expect(enumerateWorkspace(root)).resolves.toBeDefined(); await run("mkfifo", [join(root, ".env.pipe")]); await expect(enumerateWorkspace(root)).rejects.toThrow( "Special files are not allowed in workspaces: .env.pipe", ); }); test("rejects traversal, unsorted manifests, ancestor collisions, and corrupt blobs", async () => { expect(() => validateWorkspacePath("../secret")).toThrow( "Unsafe workspace path", ); const digest = `sha256:${"a".repeat(64)}` as const; const unsorted: WorkspaceManifest = { version: BUILD_PROTOCOL_VERSION, files: [ { path: "b", type: "file", digest, size: 0, mode: 0o644 }, { path: "a", type: "file", digest, size: 0, mode: 0o644 }, ], }; expect(() => validateWorkspaceManifest(unsorted)).toThrow( "bytewise sorted", ); const canonical = { version: BUILD_PROTOCOL_VERSION, files: [{ path: "a", type: "file", digest, size: 0, mode: 0o644 }], } satisfies WorkspaceManifest; const reordered = JSON.parse( `{"files":[{"mode":420,"size":0,"digest":"${digest}","type":"file","path":"a"}],"version":1}`, ) as WorkspaceManifest; expect(workspaceManifestDigest(reordered)).toBe( workspaceManifestDigest(canonical), ); expect( JSON.parse(Buffer.from(serializeWorkspaceManifest(reordered)).toString()), ).toEqual(canonical); expect(() => validateWorkspaceManifest({ version: BUILD_PROTOCOL_VERSION, files: [ { path: "a", type: "symlink", digest, size: 0, mode: 0o777 }, { path: "a/b", type: "file", digest, size: 0, mode: 0o644 }, ], }), ).toThrow("used as a directory"); const parent = await temporaryDirectory("kuber-materialized-invalid-"); await expect( materializeWorkspace( join(parent, "tree"), { version: BUILD_PROTOCOL_VERSION, files: [{ path: "file", type: "file", digest, size: 1, mode: 0o644 }], }, async () => Buffer.from("wrong"), ), ).rejects.toThrow("Blob verification failed"); }); });