1import { beforeAll, describe, expect, test } from "bun:test";2import { mkdtemp, writeFile } from "node:fs/promises";3import { tmpdir } from "node:os";4import { join } from "node:path";5import { blake3 } from "@noble/hashes/blake3.js";6import { bytesToHex } from "@noble/hashes/utils.js";7import type { Hono } from "hono";8import { openBucket } from "../src/bucket/reader.ts";9import { syncAll } from "../src/bucket/sync.ts";10import { createApp } from "../src/server/app.ts";11import { openKeprDb } from "../src/server/db.ts";12import { kohWorkspace } from "./fixture.ts";1314151617181920const REPO = "shipped";21const VERSION = "2.4.6";22const NOTES =23 "a release note long enough to clear forty characters and the twelve word rule easily";2425const ARTIFACT = "the artifact";262728function platform(): string {29 const os = process.platform === "darwin" ? "macos" : "linux";30 const arch = process.arch === "arm64" ? "aarch64" : "x86_64";31 return `${os}-${arch}`;32}3334type 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};4849describe("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}`;5556 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);7475 const bytes = new TextEncoder().encode(ARTIFACT);76 expected = {77 blake3: bytesToHex(blake3(bytes)),78 sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex"),79 };8081 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 });100101 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 });115116 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);124125 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);128129 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 }]);134135 const versionText = await app.request(`/${REPO}/releases/latest/version.txt`);136 expect(await versionText.text()).toBe(VERSION);137138 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 });143144 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});