4976de193715
use koh throughout fixtures and current project guidance (v3.0.204)
- time
- 2026-10-03 14:06 UTC · 1 week ago
- machine
- Junes-MacBook-Pro.local
- agent
- gpt-6.1-sol
- session
- review declared repo version
- parent
- 57c99012f4b4
↓ kepr-4976de19.tar.gzatlas at this save
files changed14 files
package.json+1 −1tests/bucket.test.ts+4 −4tests/fake-s3.ts+3 −3tests/fixture.ts+14 −14tests/kohpaths.test.ts+6 −6tests/kohrelease.test.ts+3 −3tests/kohsteal.test.ts+52 −54tests/kohupdate.test.ts+14 −14tests/pages.test.ts+3 −3tests/releases.test.ts+1 −1tests/sync.test.ts+8 −8tests/tarball.test.ts+2 −2tests/timeout.ts+1 −1tests/worker.test.ts+1 −1package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.203",3+ "version": "3.0.204",44 "private": true,55 "type": "module",66 "scripts": {tests/bucket.test.ts+4 −4
@@ -22,7 +22,7 @@2222 2323 const bucket = () => openBucket({ kind: "folder", path: fx.bucketDir });2424 25-describe("reading a koh3-produced folder remote", () => {25+describe("reading a koh-produced folder remote", () => {2626 test("lists the repo", async () => {2727 expect(await listRepos(bucket())).toEqual([fx.repo]);2828 });@@ -107,7 +107,7 @@107107 expect(sizes).toEqual(await bucket().sizes(`${fx.repo}/`));108108 expect(sizes.get(`${fx.repo}/head`)).toBe(12);109109 expect(await readHead(s3, fx.repo)).toBe(fx.saveIds[0] as string);110- // koh3 writes `<repo>/format` on its first offer (§9.2), so it is present;110+ // koh writes `<repo>/format` on its first offer (§9.2), so it is present;111111 // a key that is not there is what reads as null.112112 const format = await s3.get(`${fx.repo}/format`);113113 expect(format === null ? null : new TextDecoder().decode(format)).toBe("koh-remote 1");@@ -153,7 +153,7 @@153153 });154154 155155 describe("untrusted input", () => {156- // Starts from a genuine koh3 record and corrupts one field, as a tampered bucket would.156+ // Starts from a genuine koh record and corrupts one field, as a tampered bucket would.157157 async function readTampered(change: (record: Record<string, unknown>) => void) {158158 const id = fx.saveIds[0] as string;159159 const real = await bucket().get(`${fx.repo}/saves/${id}`);@@ -290,7 +290,7 @@290290 });291291 292292 describe("event records", () => {293- test("reads a genuine koh3 flag event and refuses a tampered one", async () => {293+ test("reads a genuine koh flag event and refuses a tampered one", async () => {294294 const b = bucket();295295 const [id] = await listEventIds(b, fx.repo);296296 const event = await readEvent(b, fx.repo, id as string);tests/fake-s3.ts+3 −3
@@ -1,8 +1,8 @@11 /**2- * A bucket in memory, served over HTTP for the joint public-steal test: koh3's own S3 backend and3- * Bun's S3Client both talk to it, so Kepr and the two koh3 machines share one bucket.2+ * A bucket in memory, served over HTTP for the joint public-steal test: koh's own S3 backend and3+ * Bun's S3Client both talk to it, so Kepr and the two koh machines share one bucket.44 *5- * It runs as its own process because the test drives koh3 with `Bun.spawnSync`, which would block5+ * It runs as its own process because the test drives koh with `Bun.spawnSync`, which would block66 * the event loop this server needs. It prints its endpoint on the first line of stdout.77 *88 * Only what the two clients use is implemented: GET, HEAD, PUT (with `If-None-Match: *` for thetests/fixture.ts+14 −14
@@ -6,21 +6,21 @@66 import type { ReleaseFile } from "../src/bucket/repo.ts";77 88 /**9- * §4.3 of the resolutions: these tests are pinned to one koh3, so a stale binary cannot make a pass9+ * §4.3 of the resolutions: these tests are pinned to one koh, so a stale binary cannot make a pass1010 * look real. The version is checked before any fixture runs; the message says how to fix it.1111 */1212 const PINNED_KOH = (await Bun.file(join(import.meta.dir, "koh-version.txt")).text()).trim();1313 const RUNNING_KOH = (() => {1414 try {15- return Bun.spawnSync(["koh3", "--version"]).stdout.toString().trim();15+ return Bun.spawnSync(["koh", "--version"]).stdout.toString().trim();1616 } catch {1717 return "";1818 }1919 })();2020 if (RUNNING_KOH !== `koh ${PINNED_KOH}`) {2121 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 " +22+ `these tests are pinned to koh ${PINNED_KOH}, but koh on PATH reports "${RUNNING_KOH}" — ` +23+ "install the matching koh (see koh's AGENTS.md), or update tests/koh-version.txt " +2424 "once you have verified the new one",2525 );2626 }@@ -31,7 +31,7 @@3131 repoDir: string;3232 /** Every save, newest first, checkpoint included. */3333 saveIds: string[];34- /** Strips a path from every save with koh3, so tests can sync before and after. */34+ /** Strips a path from every save with koh, so tests can sync before and after. */3535 strip(path: string): Promise<void>;3636 };3737 @@ -59,7 +59,7 @@5959 `;6060 6161 /**62- * Builds a genuine Koh 3 folder remote with koh3 itself, under a throwaway HOME. The line is:62+ * Builds a genuine Koh 3 folder remote with koh itself, under a throwaway HOME. The line is:6363 * "add docs, a secret and a deep helper" (flagged) → a second later "add readme and entry point"6464 * (agent and session) → a checkpoint holding an edit to the guide → "say hello" (head).6565 */@@ -110,10 +110,10 @@110110 }111111 112112 function run(cwd: string, home: string, args: string[]): string {113- const result = Bun.spawnSync(["koh3", ...args], { cwd, env: env(home) });113+ const result = Bun.spawnSync(["koh", ...args], { cwd, env: env(home) });114114 if (result.exitCode !== 0) {115115 throw new Error(116- `koh3 ${args.join(" ")} failed: ${result.stderr.toString()}${result.stdout.toString()}`,116+ `koh ${args.join(" ")} failed: ${result.stderr.toString()}${result.stdout.toString()}`,117117 );118118 }119119 return result.stdout.toString();@@ -126,7 +126,7 @@126126 /** `koh strip` insists on a terminal and a typed confirmation, so it runs under a Bun PTY. */127127 export async function stripPath(cwd: string, home: string, path: string): Promise<void> {128128 let output = "";129- const proc = Bun.spawn(["koh3", "strip", path], {129+ const proc = Bun.spawn(["koh", "strip", path], {130130 cwd,131131 env: env(home),132132 terminal: { cols: 120, rows: 24, data: (_term, data) => (output += data.toString()) },@@ -134,14 +134,14 @@134134 proc.terminal?.write(`y\n${path}\n`);135135 const code = await proc.exited;136136 proc.terminal?.close();137- if (code !== 0) throw new Error(`koh3 strip ${path} failed: ${output}`);137+ if (code !== 0) throw new Error(`koh strip ${path} failed: ${output}`);138138 }139139 140-/** Repos with one koh3 save each, all offered to one folder remote: enough history for releases. */140+/** Repos with one koh save each, all offered to one folder remote: enough history for releases. */141141 export async function makeRepos(names: string[]): Promise<{142142 bucketDir: string;143143 saves: Record<string, string>;144- /** Runs koh3 in one of the repos, under the fixture's HOME. */144+ /** Runs koh in one of the repos, under the fixture's HOME. */145145 koh(repo: string, ...args: string[]): string;146146 }> {147147 const root = await mkdtemp(join(tmpdir(), "kepr-repos-"));@@ -165,7 +165,7 @@165165 };166166 }167167 168-/** A throwaway HOME, a repo folder and a bucket, with koh3 runnable inside (§4.3's pinned binary). */168+/** A throwaway HOME, a repo folder and a bucket, with koh runnable inside (§4.3's pinned binary). */169169 export async function kohWorkspace(repo = "shipped"): Promise<{170170 root: string;171171 home: string;@@ -195,7 +195,7 @@195195 196196 /**197197 * Publishes a release straight into a folder remote, in §11.2's order: the files, then release.json,198- * then `releases/latest`. `kohWorkspace` plus `koh3 offer --builds` is the real thing198+ * then `releases/latest`. `kohWorkspace` plus `koh offer --builds` is the real thing199199 * (tests/kohrelease.test.ts); this writes them by hand for fixtures that need several versions, an200200 * old release, or hashes that no build produced.201201 */tests/kohpaths.test.ts+6 −6
@@ -12,7 +12,7 @@1212 // §5.5 is a contract between the repos: every path either side drops, the other drops too.1313 const UNSAFE = [1414 "../joint-escape.txt",15- "/tmp/koh3-joint-absolute.txt",15+ "/tmp/koh-joint-absolute.txt",1616 "a/../../joint-nested.txt",1717 "back\\slash.txt",1818 "ctl\u0001name.txt",@@ -47,13 +47,13 @@4747 const work = join(root, "second-work");4848 await mkdir(home);4949 await mkdir(work);50- const koh3 = (cwd: string, args: string[]) =>51- Bun.spawnSync(["koh3", ...args], {50+ const runKoh = (cwd: string, args: string[]) =>51+ Bun.spawnSync(["koh", ...args], {5252 cwd,5353 env: { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" },5454 });55- expect(koh3(work, ["remote", "folder", fx.bucketDir]).exitCode).toBe(0);56- const stolen = koh3(work, ["steal", fx.repo, "--json"]);55+ expect(runKoh(work, ["remote", "folder", fx.bucketDir]).exitCode).toBe(0);56+ const stolen = runKoh(work, ["steal", fx.repo, "--json"]);5757 expect(stolen.exitCode, stolen.stderr.toString()).toBe(0);5858 const dropped: string[] = JSON.parse(stolen.stdout.toString()).unsafe_paths;5959 @@ -83,7 +83,7 @@8383 for (const outside of [8484 join(work, "joint-escape.txt"),8585 join(root, "joint-nested.txt"),86- "/tmp/koh3-joint-absolute.txt",86+ "/tmp/koh-joint-absolute.txt",8787 ]) {8888 expect(existsSync(outside), outside).toBe(false);8989 }tests/kohrelease.test.ts+3 −3
@@ -12,8 +12,8 @@1212 import { kohWorkspace, platform } from "./fixture.ts";1313 1414 /**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 the15+ * The joint contract (§3 item 4): `koh offer --builds` publishes a release into a folder remote16+ * with the real koh binary, Kepr syncs it, and every route that reads a release agrees with the1717 * bytes the build produced.1818 */1919 @@ -39,7 +39,7 @@3939 >;4040 };4141 42-describe("a release published by koh3 offer --builds", () => {42+describe("a release published by koh offer --builds", () => {4343 let app: Hono;4444 let bucketDir: string;4545 let expected = { blake3: "", sha256: "" };tests/kohsteal.test.ts+52 −54
@@ -1,10 +1,10 @@11 /**22 * The joint public-steal contract (addendum A2, item 8d): one machine publishes a repo to a bucket,3- * Kepr serves it read-only over HTTP in Koh's own layout, a second `koh3` steals it — and later,3+ * Kepr serves it read-only over HTTP in Koh's own layout, a second `koh` steals it — and later,44 * once that machine has signed in, offers its work back to the bucket.55 *6- * There is no S3 on a test host, so the bucket is a fake one served here; koh3 is run as the real7- * installed binary (`koh3` on PATH, the same one this suite is pinned to). The account of what the6+ * There is no S3 on a test host, so the bucket is a fake one served here; koh is run as the real7+ * installed binary (`koh` on PATH, the same one this suite is pinned to). The account of what the88 * fake bucket accepts is in its own class: signatures are ignored rather than verified, which is99 * the one way this test is easier than the real thing.1010 */@@ -25,11 +25,11 @@2525 const BUCKET = "kohbucket";2626 const ACCESS = "fake-access";2727 const SECRET = "fake-secret";28-/** The command deadline: two koh3 runs talk to a local server, so this is generous. */28+/** The command deadline: two koh runs talk to a local server, so this is generous. */2929 const RUN = 60_000;3030 3131 /**32- * The fake bucket runs as its own process: the koh3 calls below are synchronous (`koh3` blocks the32+ * The fake bucket runs as its own process: the koh calls below are synchronous (`koh` blocks the3333 * event loop), so an in-process server would never answer them. `tests/fake-s3.ts` prints its3434 * endpoint on stdout.3535 */@@ -52,14 +52,14 @@5252 5353 type Ran = { code: number; out: string; err: string };5454 55-/** The real koh3, with a throwaway HOME so no test ever touches the operator's config. */56-async function koh3(55+/** The real koh, with a throwaway HOME so no test ever touches the operator's config. */56+async function koh(5757 cwd: string,5858 home: string,5959 args: string[],6060 env: Record<string, string> = {},6161 ): Promise<Ran> {62- const proc = Bun.spawn(["koh3", ...args], {62+ const proc = Bun.spawn(["koh", ...args], {6363 cwd,6464 env: { PATH: process.env.PATH, HOME: home, NO_COLOR: "1", ...env },6565 stdout: "pipe",@@ -75,7 +75,7 @@7575 7676 /** The whole line, every field of every save: what a stolen copy must reproduce exactly. */7777 async function line(repo: string, home: string): Promise<unknown> {78- return JSON.parse((await koh3(repo, home, ["view", "--json"])).out).saves;78+ return JSON.parse((await koh(repo, home, ["view", "--json"])).out).saves;7979 }8080 8181 describe.serial("stealing a public repo from Kepr over HTTP", () => {@@ -95,22 +95,22 @@9595 await mkdir(ownerHome, { recursive: true });9696 await mkdir(repoDir, { recursive: true });9797 await mkdir(keptDir, { recursive: true });98- expect((await koh3(repoDir, ownerHome, ["init"], keys)).code).toBe(0);98+ expect((await koh(repoDir, ownerHome, ["init"], keys)).code).toBe(0);9999 expect(100- (await koh3(repoDir, ownerHome, ["remote", "s3", endpoint, BUCKET, "fr-par"], keys)).code,100+ (await koh(repoDir, ownerHome, ["remote", "s3", endpoint, BUCKET, "fr-par"], keys)).code,101101 ).toBe(0);102102 await writeFile(join(repoDir, "README.md"), "# shipped\n\nA repo to steal.\n");103103 await writeFile(join(repoDir, "src.v"), "fn main() {}\n");104- expect((await koh3(repoDir, ownerHome, ["save", "first save"])).code).toBe(0);104+ expect((await koh(repoDir, ownerHome, ["save", "first save"])).code).toBe(0);105105 await writeFile(join(repoDir, "README.md"), "# shipped\n\nA repo to steal, edited.\n");106- expect((await koh3(repoDir, ownerHome, ["save", "second save"])).code).toBe(0);106+ expect((await koh(repoDir, ownerHome, ["save", "second save"])).code).toBe(0);107107 // A second repo, never made public on Kepr: only a signed-in machine may have it.108- expect((await koh3(keptDir, ownerHome, ["init"], keys)).code).toBe(0);108+ expect((await koh(keptDir, ownerHome, ["init"], keys)).code).toBe(0);109109 expect(110- (await koh3(keptDir, ownerHome, ["remote", "s3", endpoint, BUCKET, "fr-par"], keys)).code,110+ (await koh(keptDir, ownerHome, ["remote", "s3", endpoint, BUCKET, "fr-par"], keys)).code,111111 ).toBe(0);112112 await writeFile(join(keptDir, "secret.txt"), "kept in the bucket\n");113- expect((await koh3(keptDir, ownerHome, ["save", "a private repo"])).code).toBe(0);113+ expect((await koh(keptDir, ownerHome, ["save", "a private repo"])).code).toBe(0);114114 115115 // Kepr, reading that bucket: the shipped repo is public, the kept one is not.116116 const dataDir = await mkdtemp(join(tmpdir(), "kepr-steal-kepr-"));@@ -153,25 +153,25 @@153153 const thiefDir = join(root, "thief");154154 await mkdir(thiefHome, { recursive: true });155155 await mkdir(thiefDir, { recursive: true });156- const stolen = await koh3(thiefDir, thiefHome, ["steal", `${host}/${REPO}`]);156+ const stolen = await koh(thiefDir, thiefHome, ["steal", `${host}/${REPO}`]);157157 expect(stolen.code, stolen.err + stolen.out).toBe(0);158158 const copy = join(thiefDir, REPO);159159 expect(await readFile(join(copy, "README.md"), "utf8")).toContain("edited");160- expect((await koh3(copy, thiefHome, ["doctor"])).code).toBe(0);160+ expect((await koh(copy, thiefHome, ["doctor"])).code).toBe(0);161161 // Save for save, the copy is the line the owner published.162162 expect(await line(copy, thiefHome)).toEqual(await line(repoDir, ownerHome));163163 // The source is remembered, and shown above the machine's remote.164- expect((await koh3(copy, thiefHome, ["remote"])).out).toContain(`${host}/${REPO}`);164+ expect((await koh(copy, thiefHome, ["remote"])).out).toContain(`${host}/${REPO}`);165165 // And it cannot be offered: it has no bucket of its own.166- const refused = await koh3(copy, thiefHome, ["offer"]);166+ const refused = await koh(copy, thiefHome, ["offer"]);167167 expect(refused.code).toBe(1);168168 expect(refused.err + refused.out).toContain("which is read-only");169169 170170 // A later public save arrives on a bare `koh steal`.171171 await writeFile(join(repoDir, "README.md"), "# shipped\n\nA third save.\n");172- expect((await koh3(repoDir, ownerHome, ["save", "third save"])).code).toBe(0);172+ expect((await koh(repoDir, ownerHome, ["save", "third save"])).code).toBe(0);173173 await sync();174- expect((await koh3(copy, thiefHome, ["steal"])).code).toBe(0);174+ expect((await koh(copy, thiefHome, ["steal"])).code).toBe(0);175175 expect(await readFile(join(copy, "README.md"), "utf8")).toContain("A third save");176176 177177 // A strip on the source side reaches the mirror, and is honoured: history no longer@@ -181,29 +181,27 @@181181 await stripPath(repoDir, ownerHome, "src.v");182182 // The strip wrote `/src.v` into `.kohignore`; the next save carries the rule everywhere.183183 expect(await readFile(join(repoDir, ".kohignore"), "utf8")).toContain("/src.v\n");184- expect((await koh3(repoDir, ownerHome, ["save", "ignore the stripped src.v"])).code).toBe(185- 0,186- );184+ expect((await koh(repoDir, ownerHome, ["save", "ignore the stripped src.v"])).code).toBe(0);187185 await sync();188- expect((await koh3(copy, thiefHome, ["steal"])).code).toBe(0);189- expect((await koh3(copy, thiefHome, ["doctor"])).code).toBe(0);186+ expect((await koh(copy, thiefHome, ["steal"])).code).toBe(0);187+ expect((await koh(copy, thiefHome, ["doctor"])).code).toBe(0);190188 expect(await Bun.file(join(copy, "src.v")).exists()).toBe(true);191189 // Not listed as a change; the status line may still quote a save message naming it.192- const status = (await koh3(copy, thiefHome, ["status"])).out;190+ const status = (await koh(copy, thiefHome, ["status"])).out;193191 expect(status).not.toContain("+ src.v");194192 expect(status).toContain("no changes");195193 const fresh = join(root, "thief-again");196194 await mkdir(fresh, { recursive: true });197- expect((await koh3(fresh, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0);195+ expect((await koh(fresh, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0);198196 expect(await Bun.file(join(fresh, REPO, "src.v")).exists()).toBe(false);199197 // A third copy stays stolen to the end, for `offer --json` to upgrade.200198 const third = join(root, "thief-three");201199 await mkdir(third, { recursive: true });202- expect((await koh3(third, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0);200+ expect((await koh(third, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0);203201 // And a fourth, for `offer --builds --json` to upgrade.204202 const fourth = join(root, "thief-four");205203 await mkdir(fourth, { recursive: true });206- expect((await koh3(fourth, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0);204+ expect((await koh(fourth, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0);207205 208206 // Signing in: a code from the connect page, read the way `koh` reads it.209207 // This Kepr's domain is the loopback address it is served on, so the same-origin@@ -223,33 +221,33 @@223221 const shown = await browser.post("/connect", { machine }, origin);224222 const code = /class="mono code">([^<]+)</.exec(await shown.text())?.[1]?.trim() ?? "";225223 expect(code).toMatch(/^[A-Z2-9]{4}-[A-Z2-9]{4}$/);226- const signedIn = await koh3(thiefDir, thiefHome, ["remote", host, "--code", code]);224+ const signedIn = await koh(thiefDir, thiefHome, ["remote", host, "--code", code]);227225 expect(signedIn.code, signedIn.err + signedIn.out).toBe(0);228226 229227 // A save made before the upgrade is local work and nothing else: it must not write to230228 // the bucket, and it must not quietly drop the source on its own.231229 await writeFile(join(copy, "local.txt"), "work in progress\n");232- const local = await koh3(copy, thiefHome, ["save", "local only"]);230+ const local = await koh(copy, thiefHome, ["save", "local only"]);233231 expect(local.code, local.err + local.out).toBe(0);234232 expect(local.out + local.err).not.toContain("offered");235- expect((await koh3(copy, thiefHome, ["remote"])).out).toContain(`${host}/${REPO}`);233+ expect((await koh(copy, thiefHome, ["remote"])).out).toContain(`${host}/${REPO}`);236234 237235 // The next offer drops the source: the repo is this machine's own work now; offer is238236 // the path the addendum names for the owner case.239- const upgraded = await koh3(copy, thiefHome, ["offer"]);237+ const upgraded = await koh(copy, thiefHome, ["offer"]);240238 expect(upgraded.code, upgraded.err + upgraded.out).toBe(0);241239 expect(upgraded.out + upgraded.err).toContain("now syncs with your bucket");242240 // The owner can read what that offer published, before anything else is saved: the243241 // local save the copy was carrying went up with it.244- expect((await koh3(repoDir, ownerHome, ["revert", "--yes"])).code).toBe(0);245- expect((await koh3(repoDir, ownerHome, ["steal"])).code).toBe(0);242+ expect((await koh(repoDir, ownerHome, ["revert", "--yes"])).code).toBe(0);243+ expect((await koh(repoDir, ownerHome, ["steal"])).code).toBe(0);246244 expect(await readFile(join(repoDir, "local.txt"), "utf8")).toContain("work in progress");247- expect(JSON.parse((await koh3(repoDir, ownerHome, ["view", "--json"])).out).saves).toEqual(248- JSON.parse((await koh3(copy, thiefHome, ["view", "--json"])).out).saves,245+ expect(JSON.parse((await koh(repoDir, ownerHome, ["view", "--json"])).out).saves).toEqual(246+ JSON.parse((await koh(copy, thiefHome, ["view", "--json"])).out).saves,249247 );250248 await writeFile(join(copy, "README.md"), "# shipped\n\nThe thief's edit.\n");251- expect((await koh3(copy, thiefHome, ["save", "from the thief"])).code).toBe(0);252- expect((await koh3(repoDir, ownerHome, ["steal"])).code).toBe(0);249+ expect((await koh(copy, thiefHome, ["save", "from the thief"])).code).toBe(0);250+ expect((await koh(repoDir, ownerHome, ["steal"])).code).toBe(0);253251 expect(await readFile(join(repoDir, "README.md"), "utf8")).toContain("The thief's edit");254252 255253 // The other copy still rides its source. When both sides changed the same file,@@ -257,18 +255,18 @@257255 // the file is named, steal is where to choose, and the source stays.258256 const other = join(fresh, REPO);259257 await writeFile(join(repoDir, "README.md"), "# shipped\n\nThe owner's edit.\n");260- expect((await koh3(repoDir, ownerHome, ["save", "owner edits the readme"])).code).toBe(0);258+ expect((await koh(repoDir, ownerHome, ["save", "owner edits the readme"])).code).toBe(0);261259 await writeFile(join(other, "README.md"), "# shipped\n\nThe other copy's edit.\n");262- expect((await koh3(other, thiefHome, ["save", "other copy edits"])).code).toBe(0);263- const clashed = await koh3(other, thiefHome, ["offer"]);260+ expect((await koh(other, thiefHome, ["save", "other copy edits"])).code).toBe(0);261+ const clashed = await koh(other, thiefHome, ["offer"]);264262 expect(clashed.code).toBe(1);265263 expect(clashed.err).toContain(266264 'README.md changed on both machines — run "koh steal" to choose, then offer',267265 );268266 expect(await readFile(join(other, "README.md"), "utf8")).toContain("other copy's edit");269- expect((await koh3(other, thiefHome, ["remote"])).out).toContain(`${host}/${REPO}`);267+ expect((await koh(other, thiefHome, ["remote"])).out).toContain(`${host}/${REPO}`);270268 // Choosing there upgrades the copy, and says where the version it gave up lives.271- const chosen = await koh3(other, thiefHome, ["steal", "--theirs"]);269+ const chosen = await koh(other, thiefHome, ["steal", "--theirs"]);272270 expect(chosen.code, chosen.err + chosen.out).toBe(0);273271 expect(chosen.out + chosen.err).toContain("now syncs with your bucket");274272 expect(chosen.out + chosen.err).toContain("your README.md is in");@@ -277,14 +275,14 @@277275 // A repo the public side never serves is stolen from the bucket once signed in.278276 const second = join(root, "thief-two");279277 await mkdir(second, { recursive: true });280- const kept = await koh3(second, thiefHome, ["steal", `${host}/${PRIVATE_REPO}`]);278+ const kept = await koh(second, thiefHome, ["steal", `${host}/${PRIVATE_REPO}`]);281279 expect(kept.code, kept.err + kept.out).toBe(0);282280 expect(await readFile(join(second, PRIVATE_REPO, "secret.txt"), "utf8")).toContain("kept");283281 // And it is a normal repo: the machine's own remote is untouched by any of this.284- expect((await koh3(second, thiefHome, ["remote"])).out).toContain(`s3 ${endpoint}`);282+ expect((await koh(second, thiefHome, ["remote"])).out).toContain(`s3 ${endpoint}`);285283 286284 // Saves that come in flagged during offer's upgrade are named in its JSON, as steal's are.287- expect((await koh3(repoDir, ownerHome, ["steal"])).code).toBe(0);285+ expect((await koh(repoDir, ownerHome, ["steal"])).code).toBe(0);288286 await writeFile(join(repoDir, "risky.txt"), "risky\n");289287 // The same save brings a build file, so the fourth copy can publish a release.290288 await writeFile(join(repoDir, "VERSION"), "1.0.0\n");@@ -298,15 +296,15 @@298296 ].join("\n"),299297 );300298 const risky = JSON.parse(301- (await koh3(repoDir, ownerHome, ["save", "risky", "--json"])).out,299+ (await koh(repoDir, ownerHome, ["save", "risky", "--json"])).out,302300 ).id;303- const flag = await koh3(repoDir, ownerHome, ["flag", risky, "--reason", "broke the build"]);301+ const flag = await koh(repoDir, ownerHome, ["flag", risky, "--reason", "broke the build"]);304302 expect(flag.code, flag.err + flag.out).toBe(0);305- expect((await koh3(repoDir, ownerHome, ["offer"])).code).toBe(0);306- const flaggedOffer = await koh3(join(third, REPO), thiefHome, ["offer", "--json"]);303+ expect((await koh(repoDir, ownerHome, ["offer"])).code).toBe(0);304+ const flaggedOffer = await koh(join(third, REPO), thiefHome, ["offer", "--json"]);307305 expect(flaggedOffer.code, flaggedOffer.err + flaggedOffer.out).toBe(0);308306 expect(JSON.parse(flaggedOffer.out).flagged).toEqual([risky]);309- const flaggedBuilds = await koh3(join(fourth, REPO), thiefHome, [307+ const flaggedBuilds = await koh(join(fourth, REPO), thiefHome, [310308 "offer",311309 "--builds",312310 "--json",tests/kohupdate.test.ts+14 −14
@@ -10,8 +10,8 @@1010 import { kohWorkspace } from "./fixture.ts";1111 1212 /**13- * The joint update contract (§3 item 8): koh3 publishes a newer koh into a folder remote, Kepr14- * syncs and serves it, and a second koh3 — a copy of the installed binary, never the installed one13+ * The joint update contract (§3 item 8): koh publishes a newer koh into a folder remote, Kepr14+ * syncs and serves it, and a second koh — a copy of the installed binary, never the installed one1515 * — replaces itself with what it downloads.1616 *1717 * Two limits are deliberate, and named here so nobody reads more into a green run:@@ -20,7 +20,7 @@2020 * documents it serves for the download point at the `file://` path a folder-bucket Kepr hands2121 * out. The HTTPS hop and Kepr's own redirect are therefore not exercised; June's staging check2222 * (§6.4) covers them.23- * - The artifact is a copy of the koh3 binary, so on macOS the installed file is that artifact plus23+ * - The artifact is a copy of the koh binary, so on macOS the installed file is that artifact plus2424 * the ad-hoc signature koh writes into it, and the bytes cannot be compared. There the proof is2525 * that the file grew, is validly signed and runs; on Linux, where nothing is signed, the bytes2626 * are compared exactly.@@ -33,20 +33,20 @@3333 /** Long enough for a download on a slow machine, short enough not to hang a suite. */3434 const RUN_TIMEOUT_MS = 30_000;3535 36-/** What koh3 reads: the release document (§22.4). */36+/** What koh reads: the release document (§22.4). */3737 type Feed = {3838 version: string;3939 notes?: string;4040 platforms: Record<string, { url: string; blake3: string; sha256: string }>;4141 };4242 43-/** The record koh3 writes, as far as this test reads it (§11.2). */43+/** The record koh writes, as far as this test reads it (§11.2). */4444 type ReleaseRecord = { files: { platform: string; name: string }[] };4545 46-/** The CLI's own `--json` document, `UpdateJson` in koh3's src/cli/cmd_update.v. */46+/** The CLI's own `--json` document, `UpdateJson` in koh's src/cli/cmd_update.v. */4747 type UpdateJson = { available?: boolean; installed?: string };4848 49-describe("koh3 updating itself through Kepr", () => {49+describe("koh updating itself through Kepr", () => {5050 let server: Server<undefined>;5151 let cleanup: string[] = [];5252 let checkSource = "";@@ -70,10 +70,10 @@7070 7171 beforeAll(async () => {7272 const space = await kohWorkspace(REPO);73- const koh = Bun.which("koh3") as string;73+ const koh = Bun.which("koh") as string;7474 const platformName = platform();75- installed = join(space.root, "bin", "koh3");76- published = join(space.repoDir, "dist", "koh3");75+ installed = join(space.root, "bin", "koh");76+ published = join(space.repoDir, "dist", "koh");7777 await mkdir(join(space.root, "bin"), { recursive: true });7878 await mkdir(join(space.repoDir, "dist"), { recursive: true });7979 await cp(koh, installed);@@ -85,8 +85,8 @@8585 join(space.repoDir, "koh.build"),8686 [8787 "# a release is one file: this copies the one the test prepared",88- "command = mkdir -p out && cp dist/koh3 out/koh3",89- "output = out/koh3",88+ "command = mkdir -p out && cp dist/koh out/koh",89+ "output = out/koh",9090 `platforms = ${platformName}`,9191 "",9292 ].join("\n"),@@ -198,7 +198,7 @@198198 });199199 200200 /**201- * Runs a koh3 with the update source pointed at this test's Kepr. Asynchronous on purpose:201+ * Runs a koh with the update source pointed at this test's Kepr. Asynchronous on purpose:202202 * `Bun.spawnSync` would block the event loop this test's own HTTP server runs on, and the two203203 * would wait for each other forever. A slow or wedged updater fails the test rather than204204 * hanging the suite.@@ -219,7 +219,7 @@219219 // apply. Without it a wedged updater hangs the suite instead of failing it.220220 const late = Bun.sleep(RUN_TIMEOUT_MS).then((): [string, string, number] => {221221 proc.kill();222- throw new Error(`koh3 ${args.join(" ")} did not finish in ${RUN_TIMEOUT_MS} ms`);222+ throw new Error(`koh ${args.join(" ")} did not finish in ${RUN_TIMEOUT_MS} ms`);223223 });224224 const [stdout, stderr, exitCode] = await Promise.race([finished, late]);225225 return { stdout, stderr, exitCode };tests/pages.test.ts+3 −3
@@ -34,7 +34,7 @@3434 `${"a".repeat(1024 * 1024 - 1)}é${"b".repeat(1000)}\n`,3535 );3636 await writeFile(join(large.repoDir, "big.bin"), new Uint8Array(1024 * 1024 + 500).fill(0xff));37- Bun.spawnSync(["koh3", "save", "add large files"], {37+ Bun.spawnSync(["koh", "save", "add large files"], {3838 cwd: large.repoDir,3939 env: { PATH: process.env.PATH, HOME: join(large.repoDir, "../../home"), NO_COLOR: "1" },4040 });@@ -47,10 +47,10 @@4747 ["a\nb", "drop the newline"],4848 ] as const) {4949 await writeFile(join(tail.repoDir, "tail.txt"), text);50- Bun.spawnSync(["koh3", "save", message], { cwd: tail.repoDir, env: tailEnv });50+ Bun.spawnSync(["koh", "save", message], { cwd: tail.repoDir, env: tailEnv });5151 }5252 tailSaves = JSON.parse(53- Bun.spawnSync(["koh3", "view", "--json"], {53+ Bun.spawnSync(["koh", "view", "--json"], {5454 cwd: tail.repoDir,5555 env: tailEnv,5656 }).stdout.toString(),tests/releases.test.ts+1 −1
@@ -250,7 +250,7 @@250250 save: repos.saves.hush as string,251251 files: [{ platform: "linux-x86_64", name: "h", bytes: bytes("h") }],252252 });253- // cli's head is a checkpoint made after its releases (koh3 offers it on its own).253+ // cli's head is a checkpoint made after its releases (koh offers it on its own).254254 repos.koh("cli", "checkpoint", "after the releases");255255 checkpoint = JSON.parse(repos.koh("cli", "view", "--json")).head_id;256256 app = await releaseApp(bucketDir, (db) => {tests/sync.test.ts+8 −8
@@ -35,7 +35,7 @@3535 return { ...target, mirror: () => openMirror(target.mirrorDir, fx.repo) };3636 }3737 38-describe("a first sync of a koh3 bucket", () => {38+describe("a first sync of a koh bucket", () => {3939 let fx: Fixture;4040 let t: Awaited<ReturnType<typeof syncTarget>>;4141 beforeAll(async () => {@@ -281,7 +281,7 @@281281 const t = await syncTarget(fx);282282 await syncAll(t);283283 await writeFile(join(fx.repoDir, "new.txt"), "new\n");284- Bun.spawnSync(["koh3", "save", "add new"], {284+ Bun.spawnSync(["koh", "save", "add new"], {285285 cwd: fx.repoDir,286286 env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" },287287 });@@ -381,11 +381,11 @@381381 await syncAll(t);382382 await writeFile(join(fx.repoDir, "src/main.ts"), "// Entry point.\nconsole.log('again');\n");383383 await writeFile(join(fx.repoDir, "added.txt"), "new\n");384- Bun.spawnSync(["koh3", "save", "change main, add a file"], {384+ Bun.spawnSync(["koh", "save", "change main, add a file"], {385385 cwd: fx.repoDir,386386 env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" },387387 });388- Bun.spawnSync(["koh3", "offer"], {388+ Bun.spawnSync(["koh", "offer"], {389389 cwd: fx.repoDir,390390 env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" },391391 });@@ -481,8 +481,8 @@481481 482482 await writeFile(join(fx.repoDir, "later.txt"), "later\n");483483 const env = { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" };484- Bun.spawnSync(["koh3", "save", "a later save"], { cwd: fx.repoDir, env });485- Bun.spawnSync(["koh3", "offer"], { cwd: fx.repoDir, env });484+ Bun.spawnSync(["koh", "save", "a later save"], { cwd: fx.repoDir, env });485+ Bun.spawnSync(["koh", "offer"], { cwd: fx.repoDir, env });486486 await syncAll({487487 bucket: openBucket({ kind: "folder", path: fx.bucketDir }),488488 kepr: inst.kepr,@@ -544,8 +544,8 @@544544 const before = repoStats(t.mirror()).lines;545545 await writeFile(join(fx.repoDir, "big.txt"), "x\n".repeat(4.5 * 1024 * 1024));546546 const env = { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" };547- Bun.spawnSync(["koh3", "save", "add a big text file"], { cwd: fx.repoDir, env });548- Bun.spawnSync(["koh3", "offer"], { cwd: fx.repoDir, env });547+ Bun.spawnSync(["koh", "save", "add a big text file"], { cwd: fx.repoDir, env });548+ Bun.spawnSync(["koh", "offer"], { cwd: fx.repoDir, env });549549 await syncAll(t);550550 expect(history(t.mirror(), 1, 0)[0]?.message).toBe("add a big text file");551551 expect(repoStats(t.mirror()).lines).toBe(before);tests/tarball.test.ts+2 −2
@@ -53,9 +53,9 @@5353 await chmod(join(fx.repoDir, "run.sh"), 0o755);5454 const home = join(fx.repoDir, "../../home");5555 const env = { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" };56- const save = Bun.spawnSync(["koh3", "save", "add build files"], { cwd: fx.repoDir, env });56+ const save = Bun.spawnSync(["koh", "save", "add build files"], { cwd: fx.repoDir, env });5757 expect(save.exitCode).toBe(0);58- const log = Bun.spawnSync(["koh3", "view", "--json"], { cwd: fx.repoDir, env });58+ const log = Bun.spawnSync(["koh", "view", "--json"], { cwd: fx.repoDir, env });5959 head = JSON.parse(log.stdout.toString()).head_id;6060 dataDir = await mkdtemp(join(tmpdir(), "kepr-tarball-"));6161 const kepr = openKeprDb(dataDir);tests/timeout.ts+1 −1
@@ -1,6 +1,6 @@11 import { setDefaultTimeout } from "bun:test";22 3-// The fixture hooks run the real koh3 many times over, and on a busy machine (a Koh suite3+// The fixture hooks run the real koh many times over, and on a busy machine (a Koh suite44 // compiling alongside) one hook or test can pass Bun's 5-second default and skip a whole file.55 // bunfig.toml's own `timeout` key is ignored by this Bun, so the default is set here, preloaded.66 setDefaultTimeout(30_000);tests/worker.test.ts+1 −1
@@ -1,4 +1,4 @@1-// Covers the sync on its own thread (2026-10-02): a real worker syncs a koh3-made bucket, holds1+// Covers the sync on its own thread (2026-10-02): a real worker syncs a koh-made bucket, holds22 // syncs off while the app works, and applies hidden lists.33 44 import { expect, test } from "bun:test";