feat: v2
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { execFile } from "node:child_process";
|
||||
import {
|
||||
mkdtemp,
|
||||
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("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("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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user