1import { Database } from "bun:sqlite";2import { beforeAll, describe, expect, test } from "bun:test";3import { mkdtemp, writeFile } from "node:fs/promises";4import { tmpdir } from "node:os";5import { join } from "node:path";6import { openBucket } from "../src/bucket/reader.ts";7import { syncAll } from "../src/bucket/sync.ts";8import {9 fileAt,10 filesAt,11 flagReason,12 headId,13 history,14 objectBytes,15 repoStats,16 saveByPrefix,17 timeline,18} from "../src/mirror/queries.ts";19import { openMirror } from "../src/mirror/store.ts";20import { openKeprDb } from "../src/server/db.ts";21import { type Fixture, makeKohBucket } from "./fixture.ts";2223const text = (bytes: Uint8Array | null) => new TextDecoder().decode(bytes ?? new Uint8Array());2425async function syncTarget(fx: Fixture) {26 const dataDir = await mkdtemp(join(tmpdir(), "kepr-data-"));27 const target = {28 bucket: openBucket({ kind: "folder", path: fx.bucketDir }),29 kepr: openKeprDb(dataDir),30 mirrorDir: join(dataDir, "mirror"),31 tarballDir: join(dataDir, "tarballs"),32 };33 return { ...target, mirror: () => openMirror(target.mirrorDir, fx.repo) };34}3536describe("a first sync of a koh3 bucket", () => {37 let fx: Fixture;38 let t: Awaited<ReturnType<typeof syncTarget>>;39 beforeAll(async () => {40 fx = await makeKohBucket();41 t = await syncTarget(fx);42 await syncAll(t);43 });44 const [head, checkpoint, second, first] = [0, 1, 2, 3].map((i) => () => fx.saveIds[i] as string);4546 test("registers the repo private, with a clean last sync", () => {47 const row = t.kepr.query("SELECT public, last_sync_at, last_sync_error FROM repos").get() as {48 public: number;49 last_sync_at: number;50 last_sync_error: string | null;51 };52 expect(row.public).toBe(0);53 expect(row.last_sync_error).toBeNull();54 expect(row.last_sync_at).toBeGreaterThan(0);55 });5657 test("history is newest first and leaves out the checkpoint", () => {58 expect(headId(t.mirror())).toBe(head());59 expect(history(t.mirror(), 10, 0).map((s) => s.id)).toEqual([head(), second(), first()]);60 expect(history(t.mirror(), 1, 1).map((s) => s.message)).toEqual(["add readme and entry point"]);61 expect(timeline(t.mirror()).map((s) => s.id)).toEqual([first(), second(), head()]);62 expect(saveByPrefix(t.mirror(), checkpoint())?.is_checkpoint).toBe(true);63 });6465 test("finds a save by a unique prefix of six or more characters", () => {66 const save = saveByPrefix(t.mirror(), second().slice(0, 6));67 expect([save?.id, save?.agent, save?.session]).toEqual([second(), "test-agent", "fixture"]);68 expect(saveByPrefix(t.mirror(), second().slice(0, 5))).toBeNull();69 expect(saveByPrefix(t.mirror(), "zzzzzz")).toBeNull();70 });7172 test("reads files and their bytes at any save", () => {73 const paths = filesAt(t.mirror(), head()).map((f) => f.path);74 expect(paths).toContain("src/lib/deep/util.ts");75 const now = fileAt(t.mirror(), head(), "src/main.ts");76 const then = fileAt(t.mirror(), second(), "src/main.ts");77 expect(text(objectBytes(t.mirror(), now?.hash as string))).toContain("hello");78 expect(text(objectBytes(t.mirror(), then?.hash as string))).toContain("'hi'");79 expect(fileAt(t.mirror(), first(), "src/main.ts")).toBeNull();80 expect(fileAt(t.mirror(), head(), "docs/logo.png")?.size).toBe(70);81 });8283 test("counts saves without checkpoints, and lines and language at head", () => {84 const stats = repoStats(t.mirror());85 expect(stats.saves).toBe(3);86 expect(stats.language).toBe("TypeScript");87 88 expect(stats.lines).toBeGreaterThan(16);89 90 expect(stats.lines).toBeLessThan(100);91 expect(stats.updated).toBe(saveByPrefix(t.mirror(), head())?.created_at ?? -1);92 });9394 test("stores the flag event", () => {95 const flag = t.mirror().query("SELECT kind, save_id, reason FROM events").get();96 expect(flag).toEqual({ kind: "flag", save_id: first(), reason: "holds a secret" });97 });9899 test("a save's flag state is its newest flag or clear event, by time and then ID", async () => {100 expect(flagReason(t.mirror(), first())).toBe("holds a secret");101 expect(flagReason(t.mirror(), head())).toBeNull();102 const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-flags-")), "flags");103 const event = mirror.query(104 "INSERT INTO events (id, kind, save_id, reason, created_at, machine) VALUES (?, ?, 'aaaaaaaaaaaa', ?, ?, 'm')",105 );106 event.run("000000000001", "flag", "first", 10);107 expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("first");108 event.run("000000000002", "clear", null, 20);109 expect(flagReason(mirror, "aaaaaaaaaaaa")).toBeNull();110 111 event.run("000000000003", "flag", "again", 20);112 expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("again");113 event.run("000000000000", "clear", null, 20);114 expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("again");115 });116117 test("a strip reaches the mirror and drops the object", async () => {118 const secret = fileAt(t.mirror(), head(), "secret.txt")?.hash as string;119 expect(objectBytes(t.mirror(), secret)).not.toBeNull();120 await fx.strip("secret.txt");121 await syncAll(t);122 for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).toBeNull();123 expect(objectBytes(t.mirror(), secret)).toBeNull();124 expect(fileAt(t.mirror(), head(), "src/main.ts")).not.toBeNull();125 });126});127128test("a new save syncs incrementally and a bad format is recorded as the last sync error", async () => {129 const fx = await makeKohBucket("growing");130 const t = await syncTarget(fx);131 await syncAll(t);132 await writeFile(join(fx.repoDir, "new.txt"), "new\n");133 Bun.spawnSync(["koh3", "save", "add new"], {134 cwd: fx.repoDir,135 env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" },136 });137 await syncAll(t);138 expect(history(t.mirror(), 1, 0)[0]?.message).toBe("add new");139140 await writeFile(join(fx.bucketDir, fx.repo, "format"), "koh-remote 2");141 await syncAll(t);142 const row = t.kepr.query("SELECT last_sync_error FROM repos").get() as {143 last_sync_error: string;144 };145 expect(row.last_sync_error).toContain('remote format "koh-remote 2"');146});147148describe("the mirror's schema version", () => {149 test("a version 2 mirror reopens and any other version is refused as disposable", async () => {150 const dir = await mkdtemp(join(tmpdir(), "kepr-mirror-"));151 const created = openMirror(dir, "one");152 created.run("PRAGMA wal_checkpoint(TRUNCATE)");153 await Bun.write(join(dir, "copy.db"), Bun.file(join(dir, "one.db")));154 expect(openMirror(dir, "copy").query("PRAGMA user_version").get()).toEqual({155 user_version: 2,156 });157158 for (const version of [1, 3]) {159 const other = new Database(join(dir, `v${version}.db`));160 other.run(`PRAGMA user_version = ${version}`);161 other.close();162 expect(() => openMirror(dir, `v${version}`)).toThrow("disposable");163 }164 });165});