import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { cp, mkdir, mkdtemp, realpath, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Server } from "bun"; import { openBucket } from "../src/bucket/reader.ts"; import { syncAll } from "../src/bucket/sync.ts"; import { createApp } from "../src/server/app.ts"; import { openKeprDb } from "../src/server/db.ts"; import { kohWorkspace } from "./fixture.ts"; /** * The joint update contract (§3 item 8): koh3 publishes a newer koh into a folder remote, Kepr * syncs and serves it, and a second koh3 — a copy of the installed binary, never the installed one * — replaces itself with what it downloads. * * The release file is a real copy of the koh3 binary, so the ad-hoc codesign on macOS is the real * thing. One environment limit is documented below: Kepr builds its download URLs as `https://…` * by design, and this test's server has no TLS, so the document it serves for the install points * the download at the `file://` path a folder-bucket Kepr hands out (which koh3 reads). */ const NEW_VERSION = "9.9.9"; const REPO = "koh"; const NOTES = "the updater replaces itself, end to end, through a Kepr that serves the release it was told about"; /** What koh3 reads: the release document (§22.4). */ type Feed = { version: string; notes?: string; platforms: Record; }; /** The record koh3 writes, as far as this test reads it (§11.2). */ type ReleaseRecord = { files: { platform: string; name: string }[] }; /** The CLI's own `--json` document, `UpdateJson` in koh3's src/cli/cmd_update.v. */ type UpdateJson = { available?: boolean; installed?: string }; describe("koh3 updating itself through Kepr", () => { let server: Server; let checkSource = ""; let installSource = ""; let installed = ""; let published = ""; let checks: { code: number; json: UpdateJson; install: UpdateJson; refused: string; refusedStdout: string; baseline: Uint8Array; kept: Uint8Array; target: string; }; beforeAll(async () => { const space = await kohWorkspace(REPO); const koh = Bun.which("koh3") as string; const platformName = platform(); installed = join(space.root, "bin", "koh3"); published = join(space.repoDir, "dist", "koh3"); await mkdir(join(space.root, "bin"), { recursive: true }); await mkdir(join(space.repoDir, "dist"), { recursive: true }); await cp(koh, installed); await cp(koh, published); const baseline = new Uint8Array(await Bun.file(installed).arrayBuffer()); await writeFile(join(space.repoDir, "VERSION"), `${NEW_VERSION}\n`); await writeFile( join(space.repoDir, "koh.build"), [ "# a release is one file: this copies the one the test prepared", "command = mkdir -p out && cp dist/koh3 out/koh3", "output = out/koh3", `platforms = ${platformName}`, "", ].join("\n"), ); space.run("init"); space.run("remote", "folder", space.bucketDir); space.run("save", "the new koh"); space.run("offer", "--builds", "--notes", NOTES); const dataDir = await mkdtemp(join(tmpdir(), "kepr-update-")); const kepr = openKeprDb(dataDir); await syncAll({ bucket: openBucket({ kind: "folder", path: space.bucketDir }), kepr, mirrorDir: join(dataDir, "mirror"), tarballDir: join(dataDir, "tarballs"), }); kepr.run("UPDATE repos SET public = 1 WHERE name = 'koh'"); // A download URL in the feed carries the configured domain, so the app is built // with the real one once the port is known. const config = (domain: string) => ({ domain, dataDir, adminPasswordHash: "$argon2id$x", bucket: { kind: "folder" as const, path: space.bucketDir }, }); let app = createApp(config("127.0.0.1:1"), kepr); let local: Feed | null = null; const localPath = `/${REPO}/releases/latest.local.json`; server = Bun.serve({ port: 0, fetch: (request) => { const url = new URL(request.url); if (url.pathname === localPath && local) return Response.json(local); return app.fetch(request); }, }); app = createApp(config(`127.0.0.1:${server.port}`), kepr); checkSource = `http://127.0.0.1:${server.port}/${REPO}/releases/latest.json`; installSource = `http://127.0.0.1:${server.port}${localPath}`; // The document Kepr served, with its download pointed at the very file the // folder bucket holds (the TLS gap above). const served: Feed = (await (await fetch(checkSource)).json()) as Feed; const record: ReleaseRecord = (await Bun.file( join(space.bucketDir, REPO, "releases", NEW_VERSION, "release.json"), ).json()) as ReleaseRecord; const artifact = record.files.find((file) => file.platform === platformName); if (!artifact) throw new Error("the published record has no file for this platform"); const artifactPath = join( space.bucketDir, REPO, "releases", NEW_VERSION, platformName, artifact.name, ); local = { ...served, platforms: { ...served.platforms, [platformName]: { ...served.platforms[platformName], url: `file://${artifactPath}` }, }, }; const check = await run(installed, space.home, checkSource, "update", "--check"); const checkJson = await run(installed, space.home, checkSource, "update", "--check", "--json"); const refused = await run(installed, space.home, checkSource, "update", "--yes", "--json"); const kept = new Uint8Array(await Bun.file(installed).arrayBuffer()); const install = await run(installed, space.home, installSource, "update", "--yes", "--json"); checks = { code: check.exitCode, json: JSON.parse(checkJson.stdout) as UpdateJson, install: JSON.parse(install.stdout) as UpdateJson, refused: refused.stderr, refusedStdout: refused.stdout, baseline, kept, // koh names the binary it replaced by its real path, and /var is a symlink. target: await realpath(installed), }; }); afterAll(() => { server?.stop(true); }); /** * Runs a koh3 with the update source pointed at this test's Kepr. Asynchronous on purpose: * `Bun.spawnSync` would block the event loop this test's own HTTP server runs on, and the two * would wait for each other forever. */ async function run(path: string, home: string, source: string, ...args: string[]) { const proc = Bun.spawn([path, ...args], { env: { PATH: process.env.PATH, HOME: home, NO_COLOR: "1", KOH_UPDATE_SOURCE: source }, stdout: "pipe", stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited, ]); return { stdout, stderr, exitCode }; } test("--check reports an update and exits 1, and --json says the same thing", () => { expect(checks.code).toBe(1); expect(checks.json.available).toBe(true); }); test("a download it cannot reach is refused, and the binary is left alone", () => { // Kepr's own URL is https on a server that has no TLS, so this is what an // unreachable release file looks like: a refusal, never a half-install. expect(checks.refused).toContain("could not download"); expect(checks.refusedStdout).toBe(""); expect(Buffer.compare(checks.kept, checks.baseline)).toBe(0); }); test("the update replaced the running binary with the published file", async () => { expect(checks.install.installed).toBe(checks.target); const downloaded = new Uint8Array(await Bun.file(installed).arrayBuffer()); const released = new Uint8Array(await Bun.file(published).arrayBuffer()); if (process.platform === "darwin") { // Apple silicon refuses to run an unsigned binary, and the ad-hoc signature // koh writes is part of the file, so the installed copy is the artifact plus // that signature. Being validly signed and running is the proof here; the // Blake3 of the bytes that arrived is checked inside koh before signing. const signed = Bun.spawn(["codesign", "--verify", installed]); expect(await signed.exited).toBe(0); } else { expect(Buffer.compare(downloaded, released)).toBe(0); } // And it is a working koh still: the replaced binary runs. const version = (await run(installed, tmpdir(), checkSource, "--version")).stdout.trim(); expect(version.startsWith("koh ")).toBe(true); }); }); /** This machine's platform, which is what `koh build` uses with no flags. */ function platform(): string { const os = process.platform === "darwin" ? "macos" : "linux"; const arch = process.arch === "arm64" ? "aarch64" : "x86_64"; return `${os}-${arch}`; }