// Covers addendum A2 part 2: Koh's own remote layout, served read-only from a public repo's mirror. import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import { inflateSync } from "node:zlib"; import { blake3 } from "@noble/hashes/blake3.js"; import { bytesToHex } from "@noble/hashes/utils.js"; import { openBucket } from "../src/bucket/reader.ts"; import { syncAll } from "../src/bucket/sync.ts"; import { instance } from "./browser.ts"; import { makeKohBucket } from "./fixture.ts"; const REPO = "mirror"; const NOT_FOUND = '{"error":"repo_not_found","message":"repo not found"}'; const fixture = await makeKohBucket(REPO); const inst = await instance(fixture.bucketDir); inst.kepr.run("UPDATE repos SET public = 1"); const head = fixture.saveIds[0] ?? ""; const checkpoint = fixture.saveIds[1] ?? ""; const oldest = fixture.saveIds[fixture.saveIds.length - 1] ?? ""; const get = (path: string) => inst.app.request(`/${REPO}/koh/${path}`); const record = async (id: string) => JSON.parse(await (await get(`saves/${id}`)).text()); async function resync() { await syncAll({ bucket: openBucket(inst.config.bucket), kepr: inst.kepr, mirrorDir: join(inst.dataDir, "mirror"), }); } describe.serial("Koh's remote layout from the mirror", () => { test("the format, the head and the caches the addendum names", async () => { const format = await get("format"); expect(format.status).toBe(200); expect(await format.text()).toBe("koh-remote 1"); expect(format.headers.get("cache-control")).toBe("private, max-age=86400"); const headResponse = await get("head"); expect(headResponse.status).toBe(200); expect(await headResponse.text()).toBe(head); expect(headResponse.headers.get("cache-control")).toBe("no-cache"); }); test("a save record is Koh's, field for field, and checkpoints are part of the chain", async () => { const response = await get(`saves/${head}`); expect(response.status).toBe(200); expect(response.headers.get("cache-control")).toBe("private, max-age=60"); const save = await record(head); expect(Object.keys(save)).toEqual([ "id", "parent", "message", "created_at", "machine", "is_checkpoint", "size_bytes", "agent", "session", "restores", "replayed_from", "files", ]); expect(save.id).toBe(head); expect(save.parent).toBe(checkpoint); // The mirror only keeps columns, so this is the proof that what it re-serves is what Koh // wrote, byte shape and all: empty optionals as "", booleans as booleans, no extra member. const written = await Bun.file(join(fixture.bucketDir, REPO, "saves", head)).json(); expect(save).toEqual(written); expect(typeof save.size_bytes).toBe("number"); expect(typeof save.is_checkpoint).toBe("boolean"); expect(save.agent).toBe(""); expect(Object.keys(save.files[0])).toEqual(["path", "hash", "executable"]); // The chain needs checkpoints, so these routes serve what the pages hide. const chain = await get(`saves/${checkpoint}`); expect(chain.status).toBe(200); expect((await record(checkpoint)).is_checkpoint).toBe(true); expect((await inst.app.request(`/${REPO}/saves/${checkpoint}`)).status).toBe(404); }); test("the save and object prefixes list what a steal walks", async () => { const saves = await get("saves"); expect(saves.status).toBe(200); expect(saves.headers.get("cache-control")).toBe("no-cache"); expect((await saves.text()).trim().split("\n")).toContain(head); const save = await record(head); const objects = await get("objects"); expect(objects.status).toBe(200); expect(objects.headers.get("cache-control")).toBe("no-cache"); expect((await objects.text()).trim().split("\n")).toContain(save.files[0].hash); }); test("an object is the stored zlib bytes and hashes back to its key", async () => { const save = await record(head); const file = save.files.find((entry: { path: string }) => entry.path === "src/main.ts"); const object = await get(`objects/${file.hash}`); expect(object.status).toBe(200); expect(object.headers.get("cache-control")).toBe("private, max-age=31536000"); expect(object.headers.get("cache-control")).not.toContain("immutable"); const bytes = new Uint8Array(await object.arrayBuffer()); expect(bytesToHex(blake3(inflateSync(bytes)))).toBe(file.hash); expect(new TextDecoder().decode(inflateSync(bytes))).toContain("hello"); }); test("the events are IDs, one per line, and one event is Koh's record", async () => { const events = await get("events"); expect(events.status).toBe(200); expect(events.headers.get("cache-control")).toBe("no-cache"); const ids = (await events.text()).trim().split("\n"); expect(ids.length).toBeGreaterThan(0); const one = await get(`events/${ids[0]}`); expect(one.status).toBe(200); expect(one.headers.get("cache-control")).toBe("private, max-age=86400"); const event = JSON.parse(await one.text()); expect(Object.keys(event)).toEqual([ "id", "kind", "save_id", "path", "reason", "created_at", "machine", ]); const written = await Bun.file(join(fixture.bucketDir, REPO, "events", ids[0] ?? "")).json(); expect(event).toEqual(written); }); test("a private repo answers exactly as an unknown one", async () => { inst.kepr.run("UPDATE repos SET public = 0"); try { for (const path of [ "format", "head", "events", "saves", "objects", `saves/${head}`, `objects/${"a".repeat(64)}`, `events/${"b".repeat(12)}`, ]) { const private_ = await get(path); const unknown = await inst.app.request(`/no-such-repo/koh/${path}`); const mine = `${private_.status} ${await private_.text()}`; expect(mine).toBe(`${unknown.status} ${await unknown.text()}`); expect(mine).toBe(`404 ${NOT_FOUND}`); expect(private_.headers.get("content-type")).toBe(unknown.headers.get("content-type")); expect(private_.headers.get("cache-control")).toBe(unknown.headers.get("cache-control")); } } finally { inst.kepr.run("UPDATE repos SET public = 1"); } }); test("an ID or hash the mirror does not hold answers the same 404", async () => { for (const path of [ `saves/${"0".repeat(12)}`, "saves/not-an-id", `objects/${"f".repeat(64)}`, ]) { const response = await get(path); expect(`${response.status} ${await response.text()}`).toBe(`404 ${NOT_FOUND}`); } }); test("a stripped path leaves the record and the object the mirror serves", async () => { const before = await record(oldest); const secret = before.files.find((entry: { path: string }) => entry.path === "secret.txt"); expect(secret).toBeDefined(); await fixture.strip("secret.txt"); await resync(); const after = await record(oldest); expect(after.files.some((entry: { path: string }) => entry.path === "secret.txt")).toBe(false); expect((await get(`objects/${secret.hash}`)).status).toBe(404); const events = await (await get("events")).text(); const strip = events .trim() .split("\n") .filter((id) => id.length > 0); expect(strip.length).toBeGreaterThan(0); }); });