dedcbd4f3830

test the joint release contract with the real koh3: offer --builds publishes, Kepr serves the release on every route byte for byte, and the suite is pinned to the koh3 it was verified with (v3.0.53)

time
2026-09-27 10:42 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
deepseek/deepseek-flash
session
kepr 3 implementation
parent
2b7a429de79a
4 files package.json+1 −1tests/fixture.ts+40 −2tests/koh-version.txt+1 −0tests/kohrelease.test.ts+154 −0
package.json+1 −1
@@ -1,6 +1,6 @@11 {22 	"name": "kepr",3-	"version": "3.0.52",3+	"version": "3.0.53",44 	"private": true,55 	"type": "module",66 	"scripts": {

view file

tests/fixture.ts+40 −2
@@ -5,6 +5,26 @@55 import { bytesToHex } from "@noble/hashes/utils.js";66 import type { ReleaseFile } from "../src/bucket/repo.ts";77 8+/**9+ * §4.3 of the resolutions: these tests are pinned to one koh3, so a stale binary cannot make a pass10+ * look real. The version is checked before any fixture runs; the message says how to fix it.11+ */12+const PINNED_KOH = (await Bun.file(join(import.meta.dir, "koh-version.txt")).text()).trim();13+const RUNNING_KOH = (() => {14+	try {15+		return Bun.spawnSync(["koh3", "--version"]).stdout.toString().trim();16+	} catch {17+		return "";18+	}19+})();20+if (RUNNING_KOH !== `koh ${PINNED_KOH}`) {21+	throw new Error(22+		`these tests are pinned to koh ${PINNED_KOH}, but koh3 on PATH reports "${RUNNING_KOH}" — ` +23+			"install the matching koh3 (see koh3's AGENTS.md), or update tests/koh-version.txt " +24+			"once you have verified the new one",25+	);26+}27+828 export type Fixture = {929 	bucketDir: string;1030 	repo: string;@@ -145,6 +165,22 @@145165 	};146166 }147167 168+/** A throwaway HOME, a repo folder and a bucket, with koh3 runnable inside (§4.3's pinned binary). */169+export async function kohWorkspace(): Promise<{170+	home: string;171+	repoDir: string;172+	bucketDir: string;173+	run(...args: string[]): string;174+}> {175+	const root = await mkdtemp(join(tmpdir(), "kepr-workspace-"));176+	const home = join(root, "home");177+	const repoDir = join(root, "work", "shipped");178+	const bucketDir = join(root, "bucket");179+	await mkdir(home);180+	await mkdir(repoDir, { recursive: true });181+	return { home, repoDir, bucketDir, run: (...args) => run(repoDir, home, args) };182+}183+148184 /** A release file with its bytes, or only as a record declares it (the bytes are then a stand-in). */149185 export type FixtureFile = { platform: string; name: string; bytes: Uint8Array } | ReleaseFile;150186 @@ -157,8 +193,10 @@157193 };158194 159195 /**160- * Publishes a release into a folder remote as `koh offer --builds` does (§11.2): the files, then161- * release.json, then `releases/latest`. koh3 3.0.72 has no `--builds`, so this writes them itself.196+ * Publishes a release straight into a folder remote, in §11.2's order: the files, then release.json,197+ * then `releases/latest`. `kohWorkspace` plus `koh3 offer --builds` is the real thing198+ * (tests/kohrelease.test.ts); this writes them by hand for fixtures that need several versions, an199+ * old release, or hashes that no build produced.162200  */163201 export async function writeRelease(164202 	bucketDir: string,

view file

tests/koh-version.txt+1 −0
@@ -0,0 +1 @@1+3.0.84

view file

tests/kohrelease.test.ts+154 −0
@@ -0,0 +1,154 @@1+import { beforeAll, describe, expect, test } from "bun:test";2+import { mkdtemp, writeFile } from "node:fs/promises";3+import { tmpdir } from "node:os";4+import { join } from "node:path";5+import { blake3 } from "@noble/hashes/blake3.js";6+import { bytesToHex } from "@noble/hashes/utils.js";7+import type { Hono } from "hono";8+import { openBucket } from "../src/bucket/reader.ts";9+import { syncAll } from "../src/bucket/sync.ts";10+import { createApp } from "../src/server/app.ts";11+import { openKeprDb } from "../src/server/db.ts";12+import { kohWorkspace } from "./fixture.ts";13+14+/**15+ * The joint contract (§3 item 4): `koh3 offer --builds` publishes a release into a folder remote16+ * with the real koh3 binary, Kepr syncs it, and every route that reads a release agrees with the17+ * bytes the build produced.18+ */19+20+const REPO = "shipped";21+const VERSION = "2.4.6";22+const NOTES =23+	"a release note long enough to clear forty characters and the twelve word rule easily";24+// No newline: a real newline inside koh.build's command would end the line there.25+const ARTIFACT = "the artifact";26+27+/** This machine's platform, which is what `koh build` builds for with no flags. */28+function platform(): string {29+	const os = process.platform === "darwin" ? "macos" : "linux";30+	const arch = process.arch === "arm64" ? "aarch64" : "x86_64";31+	return `${os}-${arch}`;32+}33+34+type Latest = {35+	version: string;36+	notes?: string;37+	platforms: Record<38+		string,39+		{40+			url: string;41+			blake3: string;42+			sha256: string;43+			provenance: Record<string, string>;44+			signature: string;45+		}46+	>;47+};48+49+describe("a release published by koh3 offer --builds", () => {50+	let app: Hono;51+	let bucketDir: string;52+	let expected = { blake3: "", sha256: "" };53+	const platformName = platform();54+	const fileName = `demo-${VERSION}-${platformName}`;55+56+	beforeAll(async () => {57+		const space = await kohWorkspace();58+		bucketDir = space.bucketDir;59+		await writeFile(join(space.repoDir, "VERSION"), `${VERSION}\n`);60+		await writeFile(61+			join(space.repoDir, "koh.build"),62+			[63+				"# what koh publishes in this test",64+				`command = mkdir -p out && printf '${ARTIFACT}' > out/${fileName}`,65+				"output = out/demo-{version}-{platform}",66+				`platforms = ${platformName}`,67+				"",68+			].join("\n"),69+		);70+		space.run("init");71+		space.run("remote", "folder", bucketDir);72+		space.run("save", "first");73+		space.run("offer", "--builds", "--notes", NOTES);74+75+		const bytes = new TextEncoder().encode(ARTIFACT);76+		expected = {77+			blake3: bytesToHex(blake3(bytes)),78+			sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex"),79+		};80+81+		const dataDir = await mkdtemp(join(tmpdir(), "kepr-kohrelease-"));82+		const kepr = openKeprDb(dataDir);83+		await syncAll({84+			bucket: openBucket({ kind: "folder", path: bucketDir }),85+			kepr,86+			mirrorDir: join(dataDir, "mirror"),87+			tarballDir: join(dataDir, "tarballs"),88+		});89+		kepr.run(`UPDATE repos SET public = 1 WHERE name = '${REPO}'`);90+		app = createApp(91+			{92+				domain: "kepr.test",93+				dataDir,94+				adminPasswordHash: "$argon2id$x",95+				bucket: { kind: "folder", path: bucketDir },96+			},97+			kepr,98+		);99+	});100+101+	test("the newest release is the one koh built, with the hashes of its bytes", async () => {102+		const res = await app.request(`/api/${REPO}/releases/latest`);103+		expect(res.status).toBe(200);104+		const latest = (await res.json()) as Latest;105+		expect(latest.version).toBe(VERSION);106+		expect(latest.notes).toBe(NOTES);107+		const file = latest.platforms[platformName] as Latest["platforms"][string];108+		expect(file).toBeDefined();109+		expect(file.blake3).toBe(expected.blake3);110+		expect(file.sha256).toBe(expected.sha256);111+		expect(file.signature).toBe("");112+		expect(file.provenance.version).toBe(VERSION);113+		expect(file.provenance.platform).toBe(platformName);114+	});115+116+	test("latest.json, history, version.txt and checksums.txt agree with the record", async () => {117+		const record = JSON.parse(118+			await Bun.file(join(bucketDir, REPO, "releases", VERSION, "release.json")).text(),119+		) as { version: string; notes: string; files: { name: string; size: number }[] };120+		expect(record.version).toBe(VERSION);121+		expect(record.notes).toBe(NOTES);122+		expect(record.files.map((file) => file.name)).toEqual([fileName]);123+		expect(record.files[0]?.size).toBe(ARTIFACT.length);124+125+		const json = (await (await app.request(`/${REPO}/releases/latest.json`)).json()) as Latest;126+		expect(json.version).toBe(VERSION);127+		expect(json.platforms[platformName]?.blake3).toBe(expected.blake3);128+129+		const history = (await (await app.request(`/api/${REPO}/releases/history`)).json()) as {130+			version: string;131+			notes: string;132+		}[];133+		expect(history).toEqual([{ version: VERSION, notes: NOTES }]);134+135+		const versionText = await app.request(`/${REPO}/releases/latest/version.txt`);136+		expect(await versionText.text()).toBe(VERSION);137+138+		const checksums = await app.request(`/${REPO}/releases/${VERSION}/checksums.txt`);139+		const text = await checksums.text();140+		expect(text).toContain(`sha256:${expected.sha256}  ${REPO}-${platformName}`);141+		expect(text).toContain(`blake3:${expected.blake3}  ${REPO}-${platformName}`);142+	});143+144+	test("the download redirect serves the bytes koh built", async () => {145+		const res = await app.request(`/${REPO}/releases/${VERSION}/${platformName}`, {146+			redirect: "manual",147+		});148+		expect(res.status).toBe(302);149+		const target = res.headers.get("location") ?? "";150+		expect(target).toContain(fileName);151+		const path = target.replace(/^file:\/\//, "");152+		expect(await Bun.file(path).text()).toBe(ARTIFACT);153+	});154+});

view file