1import type { Database } from "bun:sqlite";2import { beforeAll, describe, expect, test } from "bun:test";3import { mkdtemp, unlink, writeFile } from "node:fs/promises";4import { tmpdir } from "node:os";5import { join } from "node:path";6import type { Hono } from "hono";7import { openBucket } from "../src/bucket/reader.ts";8import { syncAll } from "../src/bucket/sync.ts";9import { openMirror } from "../src/mirror/store.ts";10import { createApp } from "../src/server/app.ts";11import { openKeprDb } from "../src/server/db.ts";12import { makeRepos } from "./fixture.ts";1314let app: Hono;15let kepr: Database;16let dataDir: string;17let alphaSaves: { id: string; message: string; is_checkpoint: boolean }[];18let vaultSave: string;19const body = async (path: string) => (await app.request(path)).text();20212223beforeAll(async () => {24 const repos = await makeRepos(["alpha", "vault"]);25 const root = join(repos.bucketDir, "..");26 await writeFile(join(root, "work/alpha/draft.txt"), "a draft only a checkpoint holds\n");27 repos.koh("alpha", "checkpoint", "checkpoint message never shown");28 await unlink(join(root, "work/alpha/draft.txt"));29 await writeFile(join(root, "work/alpha/README.md"), "# alpha\n\nThe alpha project, grown.\n");30 await writeFile(join(root, "work/alpha/main.ts"), "export const a = 1;\n");31 repos.koh("alpha", "save", "grow alpha");32 await writeFile(join(root, "work/vault/main.zig"), "const x = 1;\n");33 repos.koh("vault", "save", "vault message");34 alphaSaves = JSON.parse(repos.koh("alpha", "log", "--json")).saves;35 vaultSave = JSON.parse(repos.koh("vault", "log", "--json")).head_id;3637 dataDir = await mkdtemp(join(tmpdir(), "kepr-front-"));38 kepr = openKeprDb(dataDir);39 const bucket = openBucket({ kind: "folder", path: repos.bucketDir });40 await syncAll({41 bucket,42 kepr,43 mirrorDir: join(dataDir, "mirror"),44 tarballDir: join(dataDir, "tarballs"),45 });46 kepr.run("UPDATE repos SET public = 1, category = 'tools' WHERE name = 'alpha'");47 kepr.run("UPDATE repos SET category = 'secretcat' WHERE name = 'vault'");48 for (const [key, value] of [49 ["handle", "ada"],50 ["tagline", "A library of small tools."],51 ["bio", "Writes small tools."],52 ["location", "Lisbon"],53 ["link", "https://ada.example"],54 ["building_since", "may 2026"],55 ]) {56 kepr.query("INSERT INTO settings (key, value) VALUES (?, ?)").run(key, value);57 }58 kepr.run(59 `INSERT INTO issues (repo, number, title, body, type, filed_at, hidden) VALUES60 ('alpha', 1, 'a', 'b', 'bug', 1, 0), ('alpha', 2, 'c', 'd', 'bug', 1, 1),61 ('vault', 1, 'e', 'f', 'bug', 1, 0)`,62 );63 kepr.run(64 `INSERT INTO downloads (repo, version, platform, count) VALUES65 ('alpha', '1.0.0', 'linux-x86_64', 7), ('alpha', '0.9.0', 'linux-x86_64', 100),66 ('vault', '1.0.0', 'linux-x86_64', 1000)`,67 );68 kepr.run("INSERT INTO release_settings (repo, version, hidden) VALUES ('alpha', '0.9.0', 1)");69 app = createApp(70 {71 domain: "kepr.example",72 dataDir,73 adminPasswordHash: "$argon2id$x",74 bucket: { kind: "folder", path: repos.bucketDir },75 },76 kepr,77 );78});7980const checkpoint = () => alphaSaves.find((save) => save.is_checkpoint)?.id as string;81const realSaves = () => alphaSaves.filter((save) => !save.is_checkpoint);8283describe("front page", () => {84 test("counts distinct objects of public repos' real saves only", async () => {85 86 87 const mirror = openMirror(join(dataDir, "mirror"), "alpha");88 expect(mirror.query("SELECT COUNT(*) AS n FROM objects").get()).toEqual({ n: 5 });89 expect(await body("/")).toContain("The library knows<br>4 things.");90 });9192 test("the ticker, cloud and cards show public saves, never checkpoints or private repos", async () => {93 const html = await body("/");94 expect(html).toContain('<div class="ticker" aria-hidden="true">');95 expect(html).toContain("<span>grow alpha</span>");96 expect(html).toContain("<span>release alpha</span>");97 expect(html).toContain('class="tc-tag tc-5">tools<');98 expect(html).toContain('class="tc-tag tc-5">TypeScript<');99 expect(html).toContain('<span class="repo-card-saves mono">2 saves</span>');100 expect(html).toContain("The alpha project, grown.");101 expect(html).toContain("A library of small tools.");102 expect(html).toContain('href="/docs">run your own library →</a>');103 for (const secret of [104 "checkpoint message",105 "vault",106 "secretcat",107 "Zig",108 checkpoint(),109 vaultSave,110 ]) {111 expect(html).not.toContain(secret);112 }113 });114115 test("the nav has browse, docs and support, and no login link", async () => {116 const html = await body("/");117 expect(html).toContain(118 '<a href="/browse">browse</a><a href="/docs">docs</a><a href="/support">support</a>',119 );120 expect(html).not.toContain("sign-in");121 });122123 test("browse lists public repos as cards", async () => {124 const html = await body("/browse");125 expect(html).toContain('<a class="repo-card" href="/alpha">');126 expect(html).not.toContain("vault");127 128 expect(html.match(/<line x1=/g)?.length).toBe(realSaves().length);129 });130});131132test("motion stops under prefers-reduced-motion", async () => {133 const css = await Bun.file(join(import.meta.dir, "../public/css/kepr.css")).text();134 const blocks = [...css.matchAll(/@media \(prefers-reduced-motion: reduce\) \{([\s\S]*?)\n\}/g)]135 .map((m) => m[1] as string)136 .join("\n");137 for (const selector of [".ticker-row", ".ticker-track", ".tri-outer"]) {138 expect(blocks).toContain(selector);139 }140 expect(blocks).toContain("animation: none;");141});