111 lines
3.4 KiB
TypeScript
111 lines
3.4 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
BUILD_RECORD_API_VERSION,
|
|
BuildStoreConflictError,
|
|
MemoryBuildStore,
|
|
type BuildRecord,
|
|
} from "../../server/build-store";
|
|
import {
|
|
BUILD_PROTOCOL_VERSION,
|
|
type BuildRequest,
|
|
type Sha256Digest,
|
|
} from "../../shared/build-protocol";
|
|
|
|
const workspace = `sha256:${"a".repeat(64)}` as Sha256Digest;
|
|
|
|
function build(id: string, imageKey = "project\0service\0image"): BuildRecord {
|
|
const request: BuildRequest = {
|
|
version: BUILD_PROTOCOL_VERSION,
|
|
id,
|
|
project: "project",
|
|
service: "service",
|
|
spec: {
|
|
architecture: "amd64",
|
|
image: "registry.test/app:latest",
|
|
context: ".",
|
|
buildArgs: [],
|
|
workspace,
|
|
},
|
|
};
|
|
const status = {
|
|
version: BUILD_PROTOCOL_VERSION,
|
|
id,
|
|
state: "queued" as const,
|
|
createdAt: "2026-09-02T00:00:00.000Z",
|
|
};
|
|
return {
|
|
apiVersion: BUILD_RECORD_API_VERSION,
|
|
kind: "BuildRecord",
|
|
metadata: {
|
|
name: id,
|
|
resourceVersion: "1",
|
|
creationTimestamp: status.createdAt,
|
|
labels: {},
|
|
},
|
|
spec: { request, imageKey, jobName: `job-${id}`, workspaceSubPath: id },
|
|
status: {
|
|
...status,
|
|
logBytes: 0,
|
|
logOffset: 0,
|
|
nextSequence: 1,
|
|
events: [{ type: "status", status }],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("build store", () => {
|
|
test("atomically enforces idempotency and one active image build", async () => {
|
|
const store = new MemoryBuildStore();
|
|
expect((await store.createBuild(build("one"))).created).toBe(true);
|
|
expect((await store.createBuild(build("one"))).created).toBe(false);
|
|
await expect(
|
|
store.createBuild(build("one", "different-key")),
|
|
).rejects.toBeInstanceOf(BuildStoreConflictError);
|
|
await expect(store.createBuild(build("two"))).rejects.toBeInstanceOf(
|
|
BuildStoreConflictError,
|
|
);
|
|
|
|
const first = (await store.getBuild("one"))!;
|
|
const digest = `sha256:${"b".repeat(64)}` as Sha256Digest;
|
|
first.metadata.resourceVersion = "2";
|
|
Object.assign(first.status, {
|
|
state: "succeeded",
|
|
digest,
|
|
finishedAt: first.status.createdAt,
|
|
});
|
|
await store.replaceBuild(first, "1");
|
|
expect((await store.createBuild(build("two"))).created).toBe(true);
|
|
|
|
const changed = (await store.getBuild("one"))!;
|
|
changed.metadata.resourceVersion = "3";
|
|
changed.status.digest = `sha256:${"c".repeat(64)}`;
|
|
await expect(store.replaceBuild(changed, "2")).rejects.toThrow("immutable");
|
|
});
|
|
|
|
test("optimistically updates resumable upload records without leaking mutable data", async () => {
|
|
const store = new MemoryBuildStore();
|
|
const digest = `sha256:${"d".repeat(64)}` as Sha256Digest;
|
|
const upload = {
|
|
apiVersion: BUILD_RECORD_API_VERSION,
|
|
kind: "BuildUpload" as const,
|
|
metadata: {
|
|
name: "upload",
|
|
resourceVersion: "1",
|
|
creationTimestamp: "now",
|
|
},
|
|
spec: { digest, size: 3 },
|
|
status: { offset: 0, data: new Uint8Array() },
|
|
};
|
|
await store.createUpload(upload);
|
|
upload.status.data = Buffer.from("mutated");
|
|
expect((await store.getUpload(digest))?.status.data.byteLength).toBe(0);
|
|
const next = (await store.getUpload(digest))!;
|
|
next.metadata.resourceVersion = "2";
|
|
next.status = { offset: 3, data: Buffer.from("abc") };
|
|
await store.replaceUpload(next, "1");
|
|
await expect(store.replaceUpload(next, "1")).rejects.toBeInstanceOf(
|
|
BuildStoreConflictError,
|
|
);
|
|
});
|
|
});
|