sync.test.ts

4.9 KB · raw

1import { beforeAll, describe, expect, test } from "bun:test";2import { mkdtemp, writeFile } from "node:fs/promises";3import { tmpdir } from "node:os";4import { join } from "node:path";5import { openBucket } from "../src/bucket/reader.ts";6import { syncAll } from "../src/bucket/sync.ts";7import {8	fileAt,9	filesAt,10	headId,11	history,12	objectBytes,13	repoStats,14	saveByPrefix,15	timeline,16} from "../src/mirror/queries.ts";17import { openMirror } from "../src/mirror/store.ts";18import { openKeprDb } from "../src/server/db.ts";19import { type Fixture, makeKohBucket } from "./fixture.ts";2021const text = (bytes: Uint8Array | null) => new TextDecoder().decode(bytes ?? new Uint8Array());2223async function syncTarget(fx: Fixture) {24	const dataDir = await mkdtemp(join(tmpdir(), "kepr-data-"));25	const target = {26		bucket: openBucket({ kind: "folder", path: fx.bucketDir }),27		kepr: openKeprDb(dataDir),28		mirrorDir: join(dataDir, "mirror"),29	};30	return { ...target, mirror: () => openMirror(target.mirrorDir, fx.repo) };31}3233describe("a first sync of a koh3 bucket", () => {34	let fx: Fixture;35	let t: Awaited<ReturnType<typeof syncTarget>>;36	beforeAll(async () => {37		fx = await makeKohBucket();38		t = await syncTarget(fx);39		await syncAll(t);40	});41	const [head, checkpoint, second, first] = [0, 1, 2, 3].map((i) => () => fx.saveIds[i] as string);4243	test("registers the repo private, with a clean last sync", () => {44		const row = t.kepr.query("SELECT public, last_sync_at, last_sync_error FROM repos").get() as {45			public: number;46			last_sync_at: number;47			last_sync_error: string | null;48		};49		expect(row.public).toBe(0);50		expect(row.last_sync_error).toBeNull();51		expect(row.last_sync_at).toBeGreaterThan(0);52	});5354	test("history is newest first and leaves out the checkpoint", () => {55		expect(headId(t.mirror())).toBe(head());56		expect(history(t.mirror(), 10, 0).map((s) => s.id)).toEqual([head(), second(), first()]);57		expect(history(t.mirror(), 1, 1).map((s) => s.message)).toEqual(["add readme and entry point"]);58		expect(timeline(t.mirror()).map((s) => s.id)).toEqual([first(), second(), head()]);59		expect(saveByPrefix(t.mirror(), checkpoint())?.is_checkpoint).toBe(true);60	});6162	test("finds a save by a unique prefix of six or more characters", () => {63		const save = saveByPrefix(t.mirror(), second().slice(0, 6));64		expect([save?.id, save?.agent, save?.session]).toEqual([second(), "test-agent", "fixture"]);65		expect(saveByPrefix(t.mirror(), second().slice(0, 5))).toBeNull();66		expect(saveByPrefix(t.mirror(), "zzzzzz")).toBeNull();67	});6869	test("reads files and their bytes at any save", () => {70		const paths = filesAt(t.mirror(), head()).map((f) => f.path);71		expect(paths).toContain("src/lib/deep/util.ts");72		const now = fileAt(t.mirror(), head(), "src/main.ts");73		const then = fileAt(t.mirror(), second(), "src/main.ts");74		expect(text(objectBytes(t.mirror(), now?.hash as string))).toContain("hello");75		expect(text(objectBytes(t.mirror(), then?.hash as string))).toContain("'hi'");76		expect(fileAt(t.mirror(), first(), "src/main.ts")).toBeNull();77		expect(fileAt(t.mirror(), head(), "docs/logo.png")?.size).toBe(70);78	});7980	test("counts saves without checkpoints, and lines and language at head", () => {81		const stats = repoStats(t.mirror());82		expect(stats.saves).toBe(3);83		expect(stats.language).toBe("TypeScript");84		// README 9 + guide 3 + secret 1 + util 1 + main 2 + .kohignore85		expect(stats.lines).toBeGreaterThan(16);86		expect(stats.updated).toBe(saveByPrefix(t.mirror(), head())?.created_at ?? -1);87	});8889	test("stores the flag event", () => {90		const flag = t.mirror().query("SELECT kind, save_id, reason FROM events").get();91		expect(flag).toEqual({ kind: "flag", save_id: first(), reason: "holds a secret" });92	});9394	test("a strip reaches the mirror and drops the object", async () => {95		const secret = fileAt(t.mirror(), head(), "secret.txt")?.hash as string;96		expect(objectBytes(t.mirror(), secret)).not.toBeNull();97		await fx.strip("secret.txt");98		await syncAll(t);99		for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).toBeNull();100		expect(objectBytes(t.mirror(), secret)).toBeNull();101		expect(fileAt(t.mirror(), head(), "src/main.ts")).not.toBeNull();102	});103});104105test("a new save syncs incrementally and a bad format is recorded as the last sync error", async () => {106	const fx = await makeKohBucket("growing");107	const t = await syncTarget(fx);108	await syncAll(t);109	await writeFile(join(fx.repoDir, "new.txt"), "new\n");110	Bun.spawnSync(["koh3", "save", "add new"], {111		cwd: fx.repoDir,112		env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" },113	});114	await syncAll(t);115	expect(history(t.mirror(), 1, 0)[0]?.message).toBe("add new");116117	await writeFile(join(fx.bucketDir, fx.repo, "format"), "koh-remote 2");118	await syncAll(t);119	const row = t.kepr.query("SELECT last_sync_error FROM repos").get() as {120		last_sync_error: string;121	};122	expect(row.last_sync_error).toContain('remote format "koh-remote 2"');123});