feat: harden self-managed reconciliation

This commit is contained in:
2026-09-05 10:17:55 +00:00 Unverified
parent 8e9d207915
commit 321f4e807a
40 changed files with 4977 additions and 404 deletions
+129 -9
View File
@@ -13,7 +13,11 @@ import {
const workspace = `sha256:${"a".repeat(64)}` as Sha256Digest;
function build(id: string, imageKey = "project\0service\0image"): BuildRecord {
function build(
id: string,
imageKey = "project\0service\0image",
createdAt = "2026-09-02T00:00:00.000Z",
): BuildRecord {
const request: BuildRequest = {
version: BUILD_PROTOCOL_VERSION,
id,
@@ -31,7 +35,7 @@ function build(id: string, imageKey = "project\0service\0image"): BuildRecord {
version: BUILD_PROTOCOL_VERSION,
id,
state: "queued" as const,
createdAt: "2026-09-02T00:00:00.000Z",
createdAt,
};
return {
apiVersion: BUILD_RECORD_API_VERSION,
@@ -61,25 +65,28 @@ describe("build store", () => {
await expect(
store.createBuild(build("one", "different-key")),
).rejects.toBeInstanceOf(BuildStoreConflictError);
await expect(store.createBuild(build("two"))).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 = "2";
first.metadata.resourceVersion = "3";
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);
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, "2")).rejects.toThrow("immutable");
await expect(store.replaceBuild(changed, "3")).rejects.toThrow("immutable");
});
test("optimistically updates resumable upload records without leaking mutable data", async () => {
@@ -107,4 +114,117 @@ describe("build store", () => {
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"]);
});
});