import { Database } from "bun:sqlite"; import { beforeAll, describe, expect, test } from "bun:test"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { openBucket } from "../src/bucket/reader.ts"; import { syncAll } from "../src/bucket/sync.ts"; import { fileAt, filesAt, flagReason, headId, history, objectBytes, repoStats, saveByPrefix, timeline, } from "../src/mirror/queries.ts"; import { openMirror } from "../src/mirror/store.ts"; import { openKeprDb } from "../src/server/db.ts"; import { type Fixture, makeKohBucket } from "./fixture.ts"; const text = (bytes: Uint8Array | null) => new TextDecoder().decode(bytes ?? new Uint8Array()); async function syncTarget(fx: Fixture) { const dataDir = await mkdtemp(join(tmpdir(), "kepr-data-")); const target = { bucket: openBucket({ kind: "folder", path: fx.bucketDir }), kepr: openKeprDb(dataDir), mirrorDir: join(dataDir, "mirror"), tarballDir: join(dataDir, "tarballs"), }; return { ...target, mirror: () => openMirror(target.mirrorDir, fx.repo) }; } describe("a first sync of a koh3 bucket", () => { let fx: Fixture; let t: Awaited>; beforeAll(async () => { fx = await makeKohBucket(); t = await syncTarget(fx); await syncAll(t); }); const [head, checkpoint, second, first] = [0, 1, 2, 3].map((i) => () => fx.saveIds[i] as string); test("registers the repo private, with a clean last sync", () => { const row = t.kepr.query("SELECT public, last_sync_at, last_sync_error FROM repos").get() as { public: number; last_sync_at: number; last_sync_error: string | null; }; expect(row.public).toBe(0); expect(row.last_sync_error).toBeNull(); expect(row.last_sync_at).toBeGreaterThan(0); }); test("history is newest first and leaves out the checkpoint", () => { expect(headId(t.mirror())).toBe(head()); expect(history(t.mirror(), 10, 0).map((s) => s.id)).toEqual([head(), second(), first()]); expect(history(t.mirror(), 1, 1).map((s) => s.message)).toEqual(["add readme and entry point"]); expect(timeline(t.mirror()).map((s) => s.id)).toEqual([first(), second(), head()]); expect(saveByPrefix(t.mirror(), checkpoint())?.is_checkpoint).toBe(true); }); test("finds a save by a unique prefix of six or more characters", () => { const save = saveByPrefix(t.mirror(), second().slice(0, 6)); expect([save?.id, save?.agent, save?.session]).toEqual([second(), "test-agent", "fixture"]); expect(saveByPrefix(t.mirror(), second().slice(0, 5))).toBeNull(); expect(saveByPrefix(t.mirror(), "zzzzzz")).toBeNull(); }); test("reads files and their bytes at any save", () => { const paths = filesAt(t.mirror(), head()).map((f) => f.path); expect(paths).toContain("src/lib/deep/util.ts"); const now = fileAt(t.mirror(), head(), "src/main.ts"); const then = fileAt(t.mirror(), second(), "src/main.ts"); expect(text(objectBytes(t.mirror(), now?.hash as string))).toContain("hello"); expect(text(objectBytes(t.mirror(), then?.hash as string))).toContain("'hi'"); expect(fileAt(t.mirror(), first(), "src/main.ts")).toBeNull(); expect(fileAt(t.mirror(), head(), "docs/logo.png")?.size).toBe(70); }); test("counts saves without checkpoints, and lines and language at head", () => { const stats = repoStats(t.mirror()); expect(stats.saves).toBe(3); expect(stats.language).toBe("TypeScript"); // README 9 + guide 3 + secret 1 + util 1 + main 2 + .kohignore expect(stats.lines).toBeGreaterThan(16); // vendor/big.c's 100 lines of C count toward neither. expect(stats.lines).toBeLessThan(100); expect(stats.updated).toBe(saveByPrefix(t.mirror(), head())?.created_at ?? -1); }); test("stores the flag event", () => { const flag = t.mirror().query("SELECT kind, save_id, reason FROM events").get(); expect(flag).toEqual({ kind: "flag", save_id: first(), reason: "holds a secret" }); }); test("a save's flag state is its newest flag or clear event, by time and then ID", async () => { expect(flagReason(t.mirror(), first())).toBe("holds a secret"); expect(flagReason(t.mirror(), head())).toBeNull(); const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-flags-")), "flags"); const event = mirror.query( "INSERT INTO events (id, kind, save_id, reason, created_at, machine) VALUES (?, ?, 'aaaaaaaaaaaa', ?, ?, 'm')", ); event.run("000000000001", "flag", "first", 10); expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("first"); event.run("000000000002", "clear", null, 20); expect(flagReason(mirror, "aaaaaaaaaaaa")).toBeNull(); // Same second: the larger ID is the newer. event.run("000000000003", "flag", "again", 20); expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("again"); event.run("000000000000", "clear", null, 20); expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("again"); }); test("a strip reaches the mirror and drops the object", async () => { const secret = fileAt(t.mirror(), head(), "secret.txt")?.hash as string; expect(objectBytes(t.mirror(), secret)).not.toBeNull(); await fx.strip("secret.txt"); await syncAll(t); for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).toBeNull(); expect(objectBytes(t.mirror(), secret)).toBeNull(); expect(fileAt(t.mirror(), head(), "src/main.ts")).not.toBeNull(); }); }); test("a new save syncs incrementally and a bad format is recorded as the last sync error", async () => { const fx = await makeKohBucket("growing"); const t = await syncTarget(fx); await syncAll(t); await writeFile(join(fx.repoDir, "new.txt"), "new\n"); Bun.spawnSync(["koh3", "save", "add new"], { cwd: fx.repoDir, env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" }, }); await syncAll(t); expect(history(t.mirror(), 1, 0)[0]?.message).toBe("add new"); await writeFile(join(fx.bucketDir, fx.repo, "format"), "koh-remote 2"); await syncAll(t); const row = t.kepr.query("SELECT last_sync_error FROM repos").get() as { last_sync_error: string; }; expect(row.last_sync_error).toContain('remote format "koh-remote 2"'); }); describe("the mirror's schema version", () => { test("a version 2 mirror reopens and any other version is refused as disposable", async () => { const dir = await mkdtemp(join(tmpdir(), "kepr-mirror-")); const created = openMirror(dir, "one"); created.run("PRAGMA wal_checkpoint(TRUNCATE)"); await Bun.write(join(dir, "copy.db"), Bun.file(join(dir, "one.db"))); expect(openMirror(dir, "copy").query("PRAGMA user_version").get()).toEqual({ user_version: 2, }); for (const version of [1, 3]) { const other = new Database(join(dir, `v${version}.db`)); other.run(`PRAGMA user_version = ${version}`); other.close(); expect(() => openMirror(dir, `v${version}`)).toThrow("disposable"); } }); });