import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { blake3 } from "@noble/hashes/blake3.js"; import { bytesToHex } from "@noble/hashes/utils.js"; import type { ReleaseFile } from "../src/bucket/repo.ts"; /** * §4.3 of the resolutions: these tests are pinned to one koh3, so a stale binary cannot make a pass * look real. The version is checked before any fixture runs; the message says how to fix it. */ const PINNED_KOH = (await Bun.file(join(import.meta.dir, "koh-version.txt")).text()).trim(); const RUNNING_KOH = (() => { try { return Bun.spawnSync(["koh3", "--version"]).stdout.toString().trim(); } catch { return ""; } })(); if (RUNNING_KOH !== `koh ${PINNED_KOH}`) { throw new Error( `these tests are pinned to koh ${PINNED_KOH}, but koh3 on PATH reports "${RUNNING_KOH}" — ` + "install the matching koh3 (see koh3's AGENTS.md), or update tests/koh-version.txt " + "once you have verified the new one", ); } export type Fixture = { bucketDir: string; repo: string; repoDir: string; /** Every save, newest first, checkpoint included. */ saveIds: string[]; /** Strips a path from every save with koh3, so tests can sync before and after. */ strip(path: string): Promise; }; // The smallest valid PNG (1×1), so the fixture has a real binary file. const PNG = Uint8Array.from( atob( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", ), (c) => c.charCodeAt(0), ); const README = `

DEMO

# Demo A small repo for **Kepr's** tests. See [the guide](docs/guide.md), [the site](https://example.com) and [a trap](javascript:alert(1)). ![logo](docs/logo.png) `; /** * Builds a genuine Koh 3 folder remote with koh3 itself, under a throwaway HOME. The line is: * "add docs, a secret and a deep helper" (flagged) → a second later "add readme and entry point" * (agent and session) → a checkpoint holding an edit to the guide → "say hello" (head). */ export async function makeKohBucket(repo = "demo"): Promise { const root = await mkdtemp(join(tmpdir(), "kepr-fixture-")); const home = join(root, "home"); const bucketDir = join(root, "bucket"); const repoDir = join(root, "work", repo); await mkdir(home); await mkdir(join(repoDir, "src/lib/deep"), { recursive: true }); await mkdir(join(repoDir, "docs")); await mkdir(join(repoDir, "vendor")); const koh = (...args: string[]) => run(repoDir, home, args); koh("init"); koh("remote", "folder", bucketDir); await writeFile(join(repoDir, "docs/guide.md"), "# Guide\n\nRead me.\n"); await writeFile(join(repoDir, "docs/logo.png"), PNG); await writeFile(join(repoDir, "secret.txt"), "hunter2\n"); await writeFile( join(repoDir, "src/lib/deep/util.ts"), "// The *one* helper. Deep down.\nexport const one = 1;\n", ); await writeFile(join(repoDir, "vendor/big.c"), "int x;\n".repeat(100)); koh("save", "add docs, a secret and a deep helper"); const first = JSON.parse(koh("view", "--json")).head_id as string; koh("flag", first, "--reason", "holds a secret"); // Saves a second apart, so the timeline has time to spread over. Bun.sleepSync(1100); await writeFile(join(repoDir, "README.md"), README); await writeFile(join(repoDir, "src/main.ts"), "// Entry point.\nconsole.log('hi');\n"); koh("save", "add readme and entry point", "--agent", "test-agent", "--session", "fixture"); // The guide's edit is first recorded by the checkpoint; the Atlas must credit it to "say hello". await writeFile(join(repoDir, "docs/guide.md"), "# Guide\n\nRead me. It is short.\n"); koh("checkpoint", "before the second change"); await writeFile(join(repoDir, "src/main.ts"), "// Entry point.\nconsole.log('hello');\n"); koh("save", "say hello"); koh("offer"); const log = JSON.parse(koh("view", "--json")) as { saves: { id: string }[] }; return { bucketDir, repo, repoDir, saveIds: log.saves.map((save) => save.id), strip: (path) => stripPath(repoDir, home, path), }; } function run(cwd: string, home: string, args: string[]): string { const result = Bun.spawnSync(["koh3", ...args], { cwd, env: env(home) }); if (result.exitCode !== 0) { throw new Error( `koh3 ${args.join(" ")} failed: ${result.stderr.toString()}${result.stdout.toString()}`, ); } return result.stdout.toString(); } function env(home: string) { return { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" }; } /** `koh strip` insists on a terminal and a typed confirmation, so it runs under a Bun PTY. */ export async function stripPath(cwd: string, home: string, path: string): Promise { let output = ""; const proc = Bun.spawn(["koh3", "strip", path], { cwd, env: env(home), terminal: { cols: 120, rows: 24, data: (_term, data) => (output += data.toString()) }, }); proc.terminal?.write(`y\n${path}\n`); const code = await proc.exited; proc.terminal?.close(); if (code !== 0) throw new Error(`koh3 strip ${path} failed: ${output}`); } /** Repos with one koh3 save each, all offered to one folder remote: enough history for releases. */ export async function makeRepos(names: string[]): Promise<{ bucketDir: string; saves: Record; /** Runs koh3 in one of the repos, under the fixture's HOME. */ koh(repo: string, ...args: string[]): string; }> { const root = await mkdtemp(join(tmpdir(), "kepr-repos-")); const home = join(root, "home"); const bucketDir = join(root, "bucket"); await mkdir(home); const saves: Record = {}; for (const [i, name] of names.entries()) { const repoDir = join(root, "work", name); await mkdir(repoDir, { recursive: true }); run(repoDir, home, ["init"]); if (i === 0) run(repoDir, home, ["remote", "folder", bucketDir]); await writeFile(join(repoDir, "README.md"), `# ${name}\n\nThe ${name} project.\n`); run(repoDir, home, ["save", `release ${name}`]); saves[name] = JSON.parse(run(repoDir, home, ["view", "--json"])).head_id; } return { bucketDir, saves, koh: (repo, ...args) => run(join(root, "work", repo), home, args), }; } /** A throwaway HOME, a repo folder and a bucket, with koh3 runnable inside (§4.3's pinned binary). */ export async function kohWorkspace(repo = "shipped"): Promise<{ root: string; home: string; repoDir: string; bucketDir: string; run(...args: string[]): string; }> { const root = await mkdtemp(join(tmpdir(), "kepr-workspace-")); const home = join(root, "home"); const repoDir = join(root, "work", repo); const bucketDir = join(root, "bucket"); await mkdir(home); await mkdir(repoDir, { recursive: true }); return { root, home, repoDir, bucketDir, run: (...args) => run(repoDir, home, args) }; } /** A release file with its bytes, or only as a record declares it (the bytes are then a stand-in). */ export type FixtureFile = { platform: string; name: string; bytes: Uint8Array } | ReleaseFile; export type FixtureRelease = { version: string; save: string; notes?: string; publishedAt?: number; files: FixtureFile[]; }; /** * Publishes a release straight into a folder remote, in §11.2's order: the files, then release.json, * then `releases/latest`. `kohWorkspace` plus `koh3 offer --builds` is the real thing * (tests/kohrelease.test.ts); this writes them by hand for fixtures that need several versions, an * old release, or hashes that no build produced. */ export async function writeRelease( bucketDir: string, repo: string, release: FixtureRelease, latest = true, ): Promise { const dir = join(bucketDir, repo, "releases", release.version); const files: ReleaseFile[] = []; for (const file of release.files) { const bytes = "bytes" in file ? file.bytes : new TextEncoder().encode(`stand-in ${file.name}`); await mkdir(join(dir, file.platform), { recursive: true }); await writeFile(join(dir, file.platform, file.name), bytes); files.push( "bytes" in file ? { platform: file.platform, name: file.name, size: bytes.length, blake3: bytesToHex(blake3(bytes)), sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex"), } : file, ); } const record = { format: 1, repo, version: release.version, save: release.save, published_at: release.publishedAt ?? 1790000000, machine: "fixture", notes: release.notes ?? "", files, }; await writeFile(join(dir, "release.json"), JSON.stringify(record, null, 2)); if (latest) await writeFile(join(bucketDir, repo, "releases", "latest"), release.version); } /** This machine's platform, which is what `koh build` builds for with no flags. */ export function platform(): string { const os = process.platform === "darwin" ? "macos" : "linux"; const arch = process.arch === "arm64" ? "aarch64" : "x86_64"; return `${os}-${arch}`; }