Files
kuber/tests/server/build-store.test.ts
T
2026-09-27 10:40:50 +00:00

324 lines
11 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",
createdAt = "2026-09-02T00:00:00.000Z",
): 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,
};
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);
const replacement = await store.createBuild(build("two"));
expect(replacement.created).toBe(true);
expect(replacement.superseded?.[0]?.metadata.name).toBe("one");
const first = (await store.getBuild("one"))!;
const digest = `sha256:${"b".repeat(64)}` as Sha256Digest;
first.metadata.resourceVersion = "3";
Object.assign(first.status, {
state: "succeeded",
digest,
finishedAt: first.status.createdAt,
});
await store.replaceBuild(first, "2");
expect(
(await store.createBuild(build("three", "2026-09-02T00:00:02.000Z")))
.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, "3")).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,
);
});
test("supersedes only the current image and leaves terminal history", async () => {
const store = new MemoryBuildStore();
await store.createBuild(
build("old", "project\0service\0image", "2026-09-02T00:00:00.000Z"),
);
const result = await store.createBuild(
build("new", "project\0service\0image", "2026-09-02T00:00:01.000Z"),
);
expect(result.superseded?.[0]?.status.error).toBe(
"Superseded by newer build",
);
expect((await store.getBuild("old"))?.status.state).toBe("failed");
expect((await store.createBuild(build("new"))).created).toBe(false);
expect(
(await store.createBuild(build("other", "project\0service\0other")))
.created,
).toBe(true);
});
test("concurrent same-image submissions have one active record", async () => {
const store = new MemoryBuildStore();
const results = await Promise.all([
store.createBuild(build("first")),
store.createBuild(build("second")),
store.createBuild(build("third")),
]);
expect(results.filter((result) => result.created)).toHaveLength(3);
const active = (await store.listBuilds()).filter(
(record) =>
record.status.state !== "succeeded" && record.status.state !== "failed",
);
expect(active).toHaveLength(1);
expect(active[0]?.metadata.name).toBe("third");
});
test("recovers an active record whose in-memory lock was lost", async () => {
const store = new MemoryBuildStore();
await store.createBuild(build("orphan"));
(store as unknown as { active: Map<string, string> }).active.clear();
const replacement = await store.createBuild(build("replacement"));
expect(replacement.superseded?.[0]?.metadata.name).toBe("orphan");
expect((await store.getBuild("orphan"))?.status.state).toBe("failed");
expect(
await store.ownsBuild("project\0service\0image", "replacement"),
).toBe(true);
});
test("a delayed older retry cannot reclaim a newer same-image lock", async () => {
const store = new MemoryBuildStore();
await store.createBuild(
build("a", "project\0service\0image", "2026-09-02T00:00:00.000Z"),
);
(store as unknown as { active: Map<string, string> }).active.clear();
await store.createBuild(
build("b", "project\0service\0image", "2026-09-02T00:00:01.000Z"),
);
const delayed = await store.createBuild(
build("a", "project\0service\0image", "2026-09-02T00:00:00.000Z"),
);
expect(delayed.created).toBe(false);
expect(delayed.record.status.error).toBe("Superseded by newer build");
expect(await store.ownsBuild("project\0service\0image", "b")).toBe(true);
});
test("returns cleanup metadata when a delayed candidate loses to a newer record", async () => {
const store = new MemoryBuildStore();
await store.createBuild(
build("newer", "project\0service\0image", "2026-09-02T00:00:01.000Z"),
);
const result = await store.createBuild(
build("delayed", "project\0service\0image", "2026-09-02T00:00:00.000Z"),
);
expect(result.created).toBe(false);
expect(result.superseded?.map((record) => record.metadata.name)).toEqual([
"delayed",
]);
expect(
(await store.createBuild(build("delayed"))).superseded,
).toBeUndefined();
});
test("orders and reconciles legacy records with invalid metadata timestamps", async () => {
const store = new MemoryBuildStore();
const legacy = build(
"legacy",
"project\0service\0image",
"2026-09-02T00:00:00.000Z",
);
legacy.metadata.creationTimestamp = undefined as unknown as string;
await store.createBuild(legacy);
const replacement = await store.createBuild(
build(
"replacement",
"project\0service\0image",
"2026-09-02T00:00:01.000Z",
),
);
expect(replacement.created).toBe(true);
expect((await store.getBuild("legacy"))?.status.state).toBe("failed");
const malformed = build("a", "project\0service\0other");
malformed.metadata.creationTimestamp = 0 as unknown as string;
await store.createBuild(malformed);
expect(
(await store.listBuilds()).map((record) => record.metadata.name),
).toEqual(["a", "legacy", "replacement"]);
});
test("prunes only old terminal builds and keeps protected or active records", async () => {
const store = new MemoryBuildStore();
for (let i = 0; i < 55; i++) {
const record = build(
`build-${String(i).padStart(2, "0")}`,
`image-${i}`,
new Date(Date.UTC(2026, 0, 1, 0, 0, i)).toISOString(),
);
await store.createBuild(record);
if (i < 53) {
const current = (await store.getBuild(record.metadata.name))!;
const next = structuredClone(current);
next.metadata.resourceVersion = String(
Number(current.metadata.resourceVersion) + 1,
);
next.status.state = "failed";
next.status.finishedAt = current.status.createdAt;
await store.replaceBuild(next, current.metadata.resourceVersion);
}
}
const leased = (await store.getBuild("build-04"))!;
const leasedNext = structuredClone(leased);
leasedNext.metadata.resourceVersion = String(
Number(leased.metadata.resourceVersion) + 1,
);
leasedNext.status.reconcileLease = {
holder: "worker",
token: "token",
expiresAt: new Date(Date.now() + 60_000).toISOString(),
};
await store.replaceBuild(leasedNext, leased.metadata.resourceVersion);
await store.pruneBuilds(50);
const remaining = await store.listBuilds();
expect(remaining.map((record) => record.metadata.name)).toContain(
"build-04",
);
expect(remaining.map((record) => record.metadata.name)).toContain(
"build-53",
);
expect(remaining.map((record) => record.metadata.name)).toContain(
"build-54",
);
expect(
remaining.some((record) => record.metadata.name === "build-00"),
).toBe(false);
expect(remaining.some((record) => record.status.state === "queued")).toBe(
true,
);
expect(remaining).toHaveLength(50);
await store.pruneBuilds(50);
expect(await store.getBuild("build-00")).toBeUndefined();
});
test("continues past protected oldest records to prune eligible terminals", async () => {
const store = new MemoryBuildStore();
for (let i = 0; i < 52; i++) {
const record = build(`retention-${i}`, `image-${i}`,
new Date(Date.UTC(2026, 0, 1, 0, 0, i)).toISOString());
await store.createBuild(record);
const current = (await store.getBuild(record.metadata.name))!;
const next = structuredClone(current);
next.metadata.resourceVersion = "2";
next.status.state = i === 51 ? "queued" : "failed";
if (i < 2) next.status.reconcileLease = {
holder: "worker", token: `lease-${i}`,
expiresAt: new Date(Date.now() + 60_000).toISOString(),
};
await store.replaceBuild(next, "1");
}
await store.pruneBuilds(50);
const remaining = await store.listBuilds();
expect(remaining).toHaveLength(50);
expect(remaining.map((record) => record.metadata.name)).toContain("retention-0");
expect(remaining.map((record) => record.metadata.name)).toContain("retention-1");
expect(remaining.map((record) => record.metadata.name)).toContain("retention-51");
expect(await store.getBuild("retention-2")).toBeUndefined();
});
test("preserves more active records than the retention limit", async () => {
const store = new MemoryBuildStore();
for (let i = 0; i < 52; i++)
await store.createBuild(
build(
`active-${i}`,
`image-${i}`,
new Date(Date.UTC(2026, 0, 1, 0, 0, i)).toISOString(),
),
);
await store.pruneBuilds(50);
expect(await store.listBuilds()).toHaveLength(52);
});
});