// Covers renaming a repo from the console (June, 2026-10-01): the history moves to the new name in // the bucket with a marker left behind, Kepr's own rows and counts move with it, old page URLs // redirect, and Koh follows the marker on its next offer. import { beforeAll, describe, expect, test } from "bun:test"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Browser, type Instance, instance } from "./browser.ts"; import { makeRepos } from "./fixture.ts"; describe("renaming a repo", () => { let inst: Instance; let browser: Browser; let repos: Awaited>; const rename = (name: string) => browser.post("/admin/repos/app/rename", { name }); const keys = (prefix: string) => [...new Bun.Glob(`${prefix}/**`).scanSync(repos.bucketDir)]; const bucketText = (key: string) => Bun.file(join(repos.bucketDir, key)).text(); // The rename runs after its response; wait for the new name to answer. const settled = async (path: string) => { for (let i = 0; i < 100 && (await inst.app.request(path)).status !== 200; i++) { await Bun.sleep(50); } }; beforeAll(async () => { repos = await makeRepos(["app", "other"]); inst = await instance(repos.bucketDir); browser = new Browser(inst.app); await browser.signIn(); await browser.post("/admin/repos/app", { public: "1", category: "tools", hidden_paths: "" }); inst.kepr.run( "INSERT INTO downloads (repo, version, platform, count) VALUES ('app', '1.0.0', 'linux-x64', 7)", ); }); test("refuses a name that is malformed, uppercase, taken or Kepr's own", async () => { for (const name of ["", "a/b", "..", "Tool", "other", "admin", "app"]) { expect(`${name} ${(await rename(name)).status}`).toBe(`${name} 400`); } expect(keys("app").length).toBeGreaterThan(0); }); test("moves the history, Kepr's settings and counts, and leaves a marker for Koh", async () => { const head = await bucketText("app/head"); expect((await rename("tool")).status).toBe(303); await settled("/tool"); expect(await bucketText("tool/head")).toBe(head); expect(keys("app")).toEqual([]); expect(await bucketText(".kepr/renamed/app")).toBe("tool\n"); const row = inst.kepr.query("SELECT public, category FROM repos WHERE name = 'tool'").get(); expect(row).toEqual({ public: 1, category: "tools" }); expect(inst.kepr.query("SELECT COUNT(*) AS n FROM repos WHERE name = 'app'").get()).toEqual({ n: 0, }); const counted = inst.kepr.query("SELECT count FROM downloads WHERE repo = 'tool'").get(); expect(counted).toEqual({ count: 7 }); }); test("old page URLs redirect to the new name; Koh's routes do not", async () => { for (const [from, to] of [ ["/app", "/tool"], ["/app/files/README.md?at=abc123", "/tool/files/README.md?at=abc123"], ]) { const response = await inst.app.request(from as string); expect(`${from} ${response.status} ${response.headers.get("location")}`).toBe( `${from} 301 ${to}`, ); } expect((await inst.app.request("/app/koh/head")).status).toBe(404); }); test("Koh follows the marker on its next offer", async () => { await writeFile(join(repos.bucketDir, "..", "work", "app", "later.txt"), "after\n"); repos.koh("app", "save", "after the rename"); const local = JSON.parse(repos.koh("app", "view", "--json")).head_id as string; expect(await bucketText("tool/head")).toBe(local); expect(keys("app")).toEqual([]); }); });