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
↓ kepr-dedcbd4f.tar.gzatlas at this save
files changed4 files
package.json+1 −1tests/fixture.ts+40 −2tests/koh-version.txt+1 −0tests/kohrelease.test.ts+154 −0package.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": {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,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+});