import { beforeAll, describe, expect, test } from "bun:test"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { openBucket } from "../src/bucket/reader.ts"; import { checkFormat, listRepos, readHead, readObject, readSave } from "../src/bucket/repo.ts"; import { type Fixture, makeKohBucket } from "./fixture.ts"; let fx: Fixture; beforeAll(async () => { fx = await makeKohBucket(); }); const bucket = () => openBucket({ kind: "folder", path: fx.bucketDir }); describe("reading a koh3-produced folder remote", () => { test("lists the repo", async () => { expect(await listRepos(bucket())).toEqual([fx.repo]); }); test("reads format, head, the save record and one verified object end to end", async () => { const b = bucket(); await checkFormat(b, fx.repo); const head = await readHead(b, fx.repo); expect(head).toBe(fx.saveIds[0] as string); const save = await readSave(b, fx.repo, head as string); expect(save.message).toBe("say hello"); expect(save.is_checkpoint).toBe(false); const checkpoint = await readSave(b, fx.repo, save.parent); expect(checkpoint.is_checkpoint).toBe(true); const first = await readSave(b, fx.repo, checkpoint.parent); expect([first.agent, first.session]).toEqual(["test-agent", "fixture"]); const main = save.files.find((file) => file.path === "src/main.ts"); const bytes = await readObject(b, fx.repo, main?.hash as string); expect(new TextDecoder().decode(bytes)).toBe("// Entry point.\nconsole.log('hello');\n"); }); test("a missing key reads as null and a listing is prefix-scoped", async () => { const b = bucket(); expect(await b.get(`${fx.repo}/nothing-here`)).toBeNull(); const saves = await b.list(`${fx.repo}/saves/`); expect(saves.sort()).toEqual(fx.saveIds.map((id) => `${fx.repo}/saves/${id}`).sort()); }); }); test("the S3 reader pages through listings and reads a missing key as null", async () => { const pageSize = 2; const server = Bun.serve({ port: 0, async fetch(req) { const url = new URL(req.url); const folder = bucket(); if (url.searchParams.get("list-type") === "2") { const all = await folder.list(url.searchParams.get("prefix") ?? ""); const start = Number(url.searchParams.get("continuation-token") ?? 0); const page = all.slice(start, start + pageSize); const more = start + pageSize < all.length; const contents = page.map((key) => `${key}`).join(""); const next = more ? `${start + pageSize}` : ""; return new Response( `${more}${contents}${next}`, ); } const bytes = await folder.get(url.pathname.replace("/kohbucket/", "")); if (bytes) return new Response(bytes); return new Response("NoSuchKey", { status: 404 }); }, }); try { const s3 = openBucket({ kind: "s3", endpoint: `http://localhost:${server.port}`, name: "kohbucket", region: "fr-par", accessKeyId: "read", secretAccessKey: "only", }); expect(await s3.list(`${fx.repo}/`)).toEqual(await bucket().list(`${fx.repo}/`)); expect(await readHead(s3, fx.repo)).toBe(fx.saveIds[0] as string); expect(await s3.get(`${fx.repo}/format`)).toBeNull(); } finally { server.stop(); } }); describe("the format rule", () => { test("accepts koh-remote 1 and refuses anything else", async () => { const { bucketDir, repo } = await makeKohBucket("formatted"); const b = openBucket({ kind: "folder", path: bucketDir }); await writeFile(join(bucketDir, repo, "format"), "koh-remote 1"); await checkFormat(b, repo); await writeFile(join(bucketDir, repo, "format"), "koh-remote 2"); await expect(checkFormat(b, repo)).rejects.toThrow('remote format "koh-remote 2"'); }); }); describe("object verification", () => { test("refuses an object whose content does not match its key", async () => { const b = bucket(); const save = await readSave(b, fx.repo, fx.saveIds[0] as string); const [one, two] = save.files; const swapped = { ...b, get: (key: string) => b.get(key.replace(one?.hash ?? "", two?.hash ?? "")), }; await expect(readObject(swapped, fx.repo, one?.hash as string)).rejects.toThrow( "does not match its hash", ); }); });