Files
kuber/tests/lib/workspace.test.ts
T
2026-09-27 10:40:50 +00:00

292 lines
11 KiB
TypeScript

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<string> {
const path = await mkdtemp(join(tmpdir(), prefix));
temporaryDirectories.push(path);
return path;
}
async function repository(): Promise<string> {
const root = await temporaryDirectory("kuber-workspace-");
await run("git", ["init", "-q", root]);
await run("git", ["-C", root, "config", "user.email", "[email protected]"]);
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");
});
});