import type { Database } from "bun:sqlite"; import { beforeAll, describe, expect, test } from "bun:test"; import { existsSync } from "node:fs"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Hono } from "hono"; import { openBucket } from "../src/bucket/reader.ts"; import type { ReleaseFile } from "../src/bucket/repo.ts"; import { syncAll } from "../src/bucket/sync.ts"; import { compareVersions } from "../src/releases/releases.ts"; import { createApp } from "../src/server/app.ts"; import { openKeprDb } from "../src/server/db.ts"; import { makeRepos, writeRelease } from "./fixture.ts"; // §22.4's contract: Kepr 3 answers every route recorded from kepr.uk (tests/fixtures/kepr2, made by // record.sh) with the recorded bytes, except where the spec makes Kepr 3 differ; each such case says // why and asserts the shape instead. The bucket is rebuilt from the recordings: each release.json // carries the recorded version, notes, platforms, file names and hashes. const FIXTURES = join(import.meta.dir, "fixtures/kepr2"); const RELEASED = ["haven", "kepr", "koh", "nina", "peach-android"]; const ZERO = "0".repeat(64); type Recording = { status: number; headers: Map; body: string }; const slugOf = (path: string) => path.replace(/^\//, "").replaceAll("/", "_"); async function recorded(path: string): Promise { const slug = slugOf(path); const [statusLine, ...lines] = (await Bun.file(join(FIXTURES, `${slug}.headers`)).text()) .trimEnd() .split("\n"); const headers = new Map( lines.map((line) => { const at = line.indexOf(":"); return [line.slice(0, at).toLowerCase(), line.slice(at + 1).trim()]; }), ); const body = Bun.file(join(FIXTURES, `${slug}.body`)); return { status: Number((statusLine as string).split(" ")[1]), headers, body: (await body.exists()) ? await body.text() : "", }; } type Latest = { version: string; notes?: string; files: ReleaseFile[] }; /** * Writes a repo's recorded releases into the bucket: the newest in full (version.txt, its notes and * checksums.txt's hashes), older ones by their history notes. Kepr 2 had no history route for koh. */ async function publishRecorded(bucketDir: string, repo: string, save: string): Promise { const version = (await recorded(`/${repo}/releases/latest/version.txt`)).body; const { notes } = JSON.parse((await recorded(`/api/${repo}/releases/latest`)).body); const sums = (await recorded(`/${repo}/releases/${version}/checksums.txt`)).body; const hash = (algorithm: string, platform: string) => new RegExp(`^${algorithm}:([0-9a-f]{64}) ${repo}-${platform}$`, "m").exec(sums)?.[1] as string; const platforms = [...sums.matchAll(new RegExp(`^sha256:[0-9a-f]{64} ${repo}-(.+)$`, "gm"))].map( (m) => m[1] as string, ); // A file's stored name is the filename Kepr 2's download of it carried; one download per repo // was recorded (record.sh takes the first platform of the /api feed). const downloaded = platforms.find((p) => existsSync(join(FIXTURES, `${slugOf(`/${repo}/releases/${version}/${p}`)}.headers`)), ) as string; const download = await recorded(`/${repo}/releases/${version}/${downloaded}`); const name = /filename="([^"]+)"/.exec(download.headers.get("content-disposition") ?? "")?.[1]; // The recorded download comes first, so the redirect test below can find its name. const files = [downloaded, ...platforms.filter((p) => p !== downloaded)].map((platform, i) => ({ platform, name: i === 0 && name ? name : `${repo}-${platform}`, size: 1, blake3: hash("blake3", platform), sha256: hash("sha256", platform), })); const history = await recorded(`/api/${repo}/releases/history`); const older = history.status === 200 ? (JSON.parse(history.body) as Latest[]) : []; for (const old of older.filter((h) => h.version !== version)) { const standIn = [ { platform: "linux-x86_64", name: "stand-in", size: 1, blake3: ZERO, sha256: ZERO }, ]; await writeRelease(bucketDir, repo, { ...old, save, files: standIn }, false); } await writeRelease(bucketDir, repo, { version, save, notes: notes ?? "", files }); return { version, notes, files }; } async function serve(bucketDir: string, setup: (kepr: Database) => void): Promise { const dataDir = await mkdtemp(join(tmpdir(), "kepr-contract-")); const kepr = openKeprDb(dataDir); await syncAll({ bucket: openBucket({ kind: "folder", path: bucketDir }), kepr, mirrorDir: join(dataDir, "mirror"), }); setup(kepr); const config = { domain: "kepr.uk", dataDir, adminPasswordHash: "$argon2id$x", bucket: { kind: "folder", path: bucketDir } as const, }; return createApp(config, kepr); } function expectHeaders(res: Response, recording: Recording, names: string[]): void { for (const name of names) { expect([name, res.headers.get(name)]).toEqual([name, recording.headers.get(name) ?? null]); } } describe("Kepr 2's generic release routes", () => { let app: Hono; let bucketDir: string; let saves: Record; const latest = new Map(); const get = (path: string) => app.request(path); beforeAll(async () => { const repos = await makeRepos([...RELEASED, "peach-relay"]); bucketDir = repos.bucketDir; saves = repos.saves; for (const repo of RELEASED) { latest.set(repo, await publishRecorded(bucketDir, repo, saves[repo] as string)); } app = await serve(bucketDir, (kepr) => { kepr.run("UPDATE repos SET public = 1"); kepr.run("UPDATE repos SET category = 'utility' WHERE name IN ('haven', 'peach-relay')"); }); }); // Koh 2 uses this same shape through its built-in update URL, tested below. // §30 2026-10-03: `url` alone departs from Kepr 2, ending in the file's stored name. test.each(RELEASED.filter((r) => r !== "koh"))( "/api/%s/releases/latest is byte for byte Kepr 2's but for urls naming the file, with CORS", async (repo) => { const path = `/api/${repo}/releases/latest`; const [res, rec] = [await get(path), await recorded(path)]; expect(res.status).toBe(rec.status); expectHeaders(res, rec, ["content-type", "access-control-allow-origin"]); const body = await res.text(); const blankUrls = (text: string) => text.replaceAll(/"url":"[^"]+"/g, '"url":""'); expect(blankUrls(body)).toBe(blankUrls(rec.body)); for (const [, url] of body.matchAll(/"url":"([^"]+)"/g)) { const { pathname } = new URL(url as string); const location = (await get(pathname)).headers.get("Location") ?? ""; expect(new URL(location).pathname).toEndWith(pathname.slice(pathname.lastIndexOf("/"))); } }, ); // Kepr 2 ordered history by build time; §21.9 and §22.1 order it by version number, so the // recorded entries are compared byte for byte in that order. test.each(RELEASED.filter((r) => r !== "koh"))( "/api/%s/releases/history has Kepr 2's entries, newest version first", async (repo) => { const path = `/api/${repo}/releases/history`; const [res, rec] = [await get(path), await recorded(path)]; expectHeaders(res, rec, ["content-type", "access-control-allow-origin"]); const entries = rec.body.slice(1, -1).split(/,(?=\{"version":)/); const version = (entry: string) => (JSON.parse(entry) as { version: string }).version; entries.sort((a, b) => compareVersions(version(b), version(a))); expect(await res.text()).toBe(`[${entries.join(",")}]`); expect(await (await get(`/${repo}/releases/history.json`)).text()).toBe( `[${entries.join(",")}]`, ); }, ); test.each(RELEASED)( "/%s/releases/latest/version.txt and checksums.txt are Kepr 2's", async (repo) => { for (const path of [ `/${repo}/releases/latest/version.txt`, `/${repo}/releases/${latest.get(repo)?.version}/checksums.txt`, ]) { const [res, rec] = [await get(path), await recorded(path)]; expect([path, res.status]).toEqual([path, rec.status]); expectHeaders(res, rec, ["content-type"]); expect(await res.text()).toBe(rec.body); } }, ); // latest_build is Kepr 2's to the byte. Kepr 2's head was always null (it looked for a `main` // lane that Koh 2 offers never set); §22.4 makes it the newest save on the line. test.each([...RELEASED, "peach-relay"])( "/%s/update-info keeps Kepr 2's latest_build", async (repo) => { const path = `/${repo}/update-info`; const [res, rec] = [await get(path), await recorded(path)]; expectHeaders(res, rec, ["content-type"]); const body = await res.text(); const build = (text: string) => text.slice(text.indexOf(',"latest_build":')); expect(build(body)).toBe(build(rec.body)); expect(rec.body.startsWith('{"head":null,')).toBe(true); const head = JSON.parse(body).head; expect(Object.keys(head)).toEqual(["save_id", "version", "timestamp"]); // The fixture's releases are built from its only save, so head's version is the newest release. const version = latest.get(repo)?.version ?? saves[repo]; expect([head.save_id, head.version]).toEqual([saves[repo], version]); }, ); // The fields, their order and their types are Kepr 2's. The values are this fixture's, except // latest_version, which §22.4 makes the newest release (Kepr 2 read a "v…" from save messages). test.each([...RELEASED, "peach-relay"])("/%s/meta has Kepr 2's fields in order", async (repo) => { const path = `/${repo}/meta`; const [res, rec] = [await get(path), await recorded(path)]; expectHeaders(res, rec, ["content-type"]); type Meta = Record; const [ours, theirs] = [(await res.json()) as Meta, JSON.parse(rec.body) as Meta]; expect(Object.keys(ours)).toEqual(Object.keys(theirs)); // Counts are numbers; every other field is a string or, as Kepr 2 also sent, null. for (const key of Object.keys(theirs)) { const type = (value: unknown) => (value === null ? "string" : typeof value); expect([key, type(ours[key])]).toEqual([key, type(theirs[key])]); } expect(ours.latest_release).toBe(theirs.latest_release); expect(ours.latest_version).toBe(theirs.latest_release); expect(ours.description).toBe(`The ${repo} project.`); expect([ours.save_count, ours.repo]).toEqual([1, repo]); if (repo === "haven" || repo === "peach-relay") expect(ours.categories).toBe(theirs.categories); }); // §22.4 sends old links to the builds panel, where Kepr 2 went to the version's page. test.each(RELEASED)("/%s/releases/latest redirects to the builds panel", async (repo) => { const path = `/${repo}/releases/latest`; const [res, rec] = [await get(path), await recorded(path)]; expect(res.status).toBe(rec.status); expect(rec.headers.get("location")).toBe(`/${repo}/releases/${latest.get(repo)?.version}`); expect(res.headers.get("location")).toBe(`/${repo}/releases`); }); // Kepr 2 streamed the file itself; §22.2 redirects to the stored file, named as Kepr 2 named it. test.each(RELEASED)( "/%s/releases// redirects to the stored file", async (repo) => { const release = latest.get(repo) as Latest; const platform = release.files[0]?.platform as string; for (const tail of [platform, `${repo}-${platform}`]) { const path = `/${repo}/releases/${release.version}/${tail}`; const [res, rec] = [await get(path), await recorded(path)]; expect(rec.status).toBe(200); const name = /filename="([^"]+)"/.exec(rec.headers.get("content-disposition") ?? "")?.[1]; expect(res.status).toBe(302); expect(res.headers.get("location")).toBe( `file://${join(bucketDir, repo, "releases", release.version, platform, name as string)}`, ); } }, ); test("the 404s of a repo without releases, an unknown repo and a missing Koh 2 file are Kepr 2's", async () => { for (const path of [ "/api/peach-relay/releases/latest", "/api/peach-relay/releases/history", "/api/nowhere/releases/latest", "/api/koh/releases/2.1.16/linux-x86_64", ]) { const [res, rec] = [await get(path), await recorded(path)]; expect([path, res.status, await res.text()]).toEqual([path, rec.status, rec.body]); expectHeaders(res, rec, ["content-type"]); // Kepr 2 sent CORS only on success; §22.4 puts it on the /api feed routes, so it wins. const cors = path.startsWith("/api/koh/") ? null : "*"; expect([path, res.headers.get("access-control-allow-origin")]).toEqual([path, cors]); } }); }); describe("Koh 2 upgrades to Koh 3", () => { let app: Hono; let bucketDir: string; const get = (path: string) => app.request(path); const FROZEN = "2.1.16"; beforeAll(async () => { const repos = await makeRepos(["koh"]); bucketDir = repos.bucketDir; const save = repos.saves.koh as string; // Kepr 2 never had a 2.1.16 release, so the recorded 2.1.0 stands in for it under the pinned version. const rec = JSON.parse( (await recorded("/api/koh/releases/latest")).body.replaceAll("2.1.0", FROZEN), ); const sums = (await recorded("/koh/releases/2.1.0/checksums.txt")).body; const files = Object.entries(rec.platforms as Record).map( ([platform, { checksum }]) => ({ platform, name: "koh", size: 1, blake3: checksum.slice("blake3:".length), sha256: new RegExp(`sha256:([0-9a-f]{64}) koh-${platform}`).exec(sums)?.[1] as string, }), ); await writeRelease(bucketDir, "koh", { version: FROZEN, save, notes: rec.notes, files }); const newer = [{ platform: "linux-x86_64", name: "koh", size: 1, blake3: ZERO, sha256: ZERO }]; await writeRelease(bucketDir, "koh", { version: "3.0.0", save, files: newer }); app = await serve(bucketDir, (kepr) => kepr.run("UPDATE repos SET public = 1")); }); test("offers the newest Koh 3 release through the built-in Koh 2 URL", async () => { const res = await get("/api/koh/releases/latest"); expect(res.status).toBe(200); expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*"); expect(await res.text()).toBe(await (await get("/koh/releases/latest.json")).text()); }); test("also upgrades when there is no Koh 2 release", async () => { app = await serve(bucketDir, (kepr) => { kepr.run("UPDATE repos SET public = 1"); kepr.run( `INSERT INTO release_settings (repo, version, hidden) VALUES ('koh', '${FROZEN}', 1)`, ); }); const res = await get("/api/koh/releases/latest"); expect(res.status).toBe(200); const release = (await res.json()) as { version: string }; expect(release.version).toBe("3.0.0"); }); test("its download URLs redirect to the file", async () => { app = await serve(bucketDir, (kepr) => kepr.run("UPDATE repos SET public = 1")); const res = await get(`/api/koh/releases/${FROZEN}/macos-aarch64`); expect(res.status).toBe(302); expect(res.headers.get("location")).toEndWith(`/koh/releases/${FROZEN}/macos-aarch64/koh`); }); });