sync.test.ts

22.7 KB · raw

1import { Database } from "bun:sqlite";2import { beforeAll, describe, expect, test } from "bun:test";3import { cp, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";4import { tmpdir } from "node:os";5import { join } from "node:path";6import { deflateSync } from "node:zlib";7import { type Bucket, openBucket } from "../src/bucket/reader.ts";8import { readSave } from "../src/bucket/repo.ts";9import { syncAll, syncRepo } from "../src/bucket/sync.ts";10import {11	fileAt,12	filesAt,13	flagReason,14	headId,15	history,16	objectBytes,17	repoStats,18	saveByPrefix,19	timeline,20} from "../src/mirror/queries.ts";21import { applyHidden, closeMirror, mirrorState, openMirror } from "../src/mirror/store.ts";22import { openKeprDb } from "../src/server/db.ts";23import { Browser, instance } from "./browser.ts";24import { type Fixture, makeKohBucket } from "./fixture.ts";2526const text = (bytes: Uint8Array | null) => new TextDecoder().decode(bytes ?? new Uint8Array());2728async function syncTarget(fx: Fixture) {29	const dataDir = await mkdtemp(join(tmpdir(), "kepr-data-"));30	const target = {31		bucket: openBucket({ kind: "folder", path: fx.bucketDir }),32		kepr: openKeprDb(dataDir),33		mirrorDir: join(dataDir, "mirror"),34	};35	return { ...target, mirror: () => openMirror(target.mirrorDir, fx.repo) };36}3738describe("a first sync of a koh bucket", () => {39	let fx: Fixture;40	let t: Awaited<ReturnType<typeof syncTarget>>;41	beforeAll(async () => {42		fx = await makeKohBucket();43		t = await syncTarget(fx);44		await syncAll(t);45	});46	const [head, checkpoint, second, first] = [0, 1, 2, 3].map((i) => () => fx.saveIds[i] as string);4748	test("registers the repo private, with a clean last sync", () => {49		const row = t.kepr.query("SELECT public, last_sync_at, last_sync_error FROM repos").get() as {50			public: number;51			last_sync_at: number;52			last_sync_error: string | null;53		};54		expect(row.public).toBe(0);55		expect(row.last_sync_error).toBeNull();56		expect(row.last_sync_at).toBeGreaterThan(0);57	});5859	test("history is newest first and leaves out the checkpoint", () => {60		expect(headId(t.mirror())).toBe(head());61		expect(history(t.mirror(), 10, 0).map((s) => s.id)).toEqual([head(), second(), first()]);62		expect(history(t.mirror(), 1, 1).map((s) => s.message)).toEqual(["add readme and entry point"]);63		expect(timeline(t.mirror()).map((s) => s.id)).toEqual([first(), second(), head()]);64		expect(saveByPrefix(t.mirror(), checkpoint())?.is_checkpoint).toBe(true);65	});6667	test("finds a save by a unique prefix of six or more characters", () => {68		const save = saveByPrefix(t.mirror(), second().slice(0, 6));69		expect([save?.id, save?.agent, save?.session]).toEqual([second(), "test-agent", "fixture"]);70		expect(saveByPrefix(t.mirror(), second().slice(0, 5))).toBeNull();71		expect(saveByPrefix(t.mirror(), "zzzzzz")).toBeNull();72	});7374	test("reads files and their bytes at any save", () => {75		const paths = filesAt(t.mirror(), head()).map((f) => f.path);76		expect(paths).toContain("src/lib/deep/util.ts");77		const now = fileAt(t.mirror(), head(), "src/main.ts");78		const then = fileAt(t.mirror(), second(), "src/main.ts");79		expect(text(objectBytes(t.mirror(), now?.hash as string))).toContain("hello");80		expect(text(objectBytes(t.mirror(), then?.hash as string))).toContain("'hi'");81		expect(fileAt(t.mirror(), first(), "src/main.ts")).toBeNull();82		expect(fileAt(t.mirror(), head(), "docs/logo.png")?.size).toBe(70);83	});8485	test("counts saves without checkpoints, and lines and language at head", () => {86		const stats = repoStats(t.mirror());87		expect(stats.saves).toBe(3);88		expect(stats.language).toBe("TypeScript");89		// Source lines only: util.ts 2 + main.ts 2. The README, guide, secret and .kohignore are not90		// code, and vendor/big.c's 100 lines of C are someone else's.91		expect(stats.lines).toBe(4);92		expect(stats.updated).toBe(saveByPrefix(t.mirror(), head())?.created_at ?? -1);93	});9495	test("stores the flag event", () => {96		const flag = t.mirror().query("SELECT kind, save_id, reason FROM events").get();97		expect(flag).toEqual({ kind: "flag", save_id: first(), reason: "holds a secret" });98	});99100	test("a save's flag state is its newest flag or clear event, by time and then ID", async () => {101		expect(flagReason(t.mirror(), first())).toBe("holds a secret");102		expect(flagReason(t.mirror(), head())).toBeNull();103		const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-flags-")), "flags");104		const event = mirror.query(105			"INSERT INTO events (id, kind, save_id, reason, created_at, machine) VALUES (?, ?, 'aaaaaaaaaaaa', ?, ?, 'm')",106		);107		event.run("000000000001", "flag", "first", 10);108		expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("first");109		event.run("000000000002", "clear", null, 20);110		expect(flagReason(mirror, "aaaaaaaaaaaa")).toBeNull();111		// Same second: the larger ID is the newer.112		event.run("000000000003", "flag", "again", 20);113		expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("again");114		event.run("000000000000", "clear", null, 20);115		expect(flagReason(mirror, "aaaaaaaaaaaa")).toBe("again");116	});117118	test("a strip reaches the mirror and drops the object", async () => {119		const secret = fileAt(t.mirror(), head(), "secret.txt")?.hash as string;120		expect(objectBytes(t.mirror(), secret)).not.toBeNull();121		await fx.strip("secret.txt");122		await syncAll(t);123		for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).toBeNull();124		expect(objectBytes(t.mirror(), secret)).toBeNull();125		expect(fileAt(t.mirror(), head(), "src/main.ts")).not.toBeNull();126	});127});128129test("a hidden path that is then stripped stays gone when it is unhidden", async () => {130	const fx = await makeKohBucket("hidden-strip");131	const t = await syncTarget(fx);132	await syncAll(t);133	const secret = fileAt(t.mirror(), fx.saveIds[0] as string, "secret.txt")?.hash as string;134	t.kepr.query("UPDATE repos SET hidden_paths = 'secret.txt' WHERE name = ?").run(fx.repo);135	await syncAll(t);136	for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).toBeNull();137	expect(objectBytes(t.mirror(), secret)).not.toBeNull();138139	await fx.strip("secret.txt");140	await syncAll(t);141	applyHidden(t.mirror(), []);142	for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).toBeNull();143	expect(objectBytes(t.mirror(), secret)).toBeNull();144	expect(fileAt(t.mirror(), fx.saveIds[0] as string, "src/main.ts")).not.toBeNull();145});146147test("a whole six-character ID finds its own save, not a longer one it begins", async () => {148	const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-mirror-")), "ids");149	const add = mirror.query(150		"INSERT INTO saves (id, parent, message, created_at, machine) VALUES (?, NULL, ?, 1, 'm')",151	);152	add.run("af96c8", "migrated from Koh 2");153	add.run("af96c8123456", "made by Koh 3");154	expect(saveByPrefix(mirror, "af96c8")?.message).toBe("migrated from Koh 2");155	expect(saveByPrefix(mirror, "af96c81")?.message).toBe("made by Koh 3");156});157158test("a sync lands history in batches, so one cut short resumes where it stopped", async () => {159	const fx = await makeKohBucket("batched");160	const bucket = openBucket({ kind: "folder", path: fx.bucketDir });161	const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-mirror-")), fx.repo);162	// The bucket goes away as soon as the first batch has landed.163	const failing: Bucket = {164		...bucket,165		get: (key) =>166			key.includes("/objects/") && mirrorState(mirror, "head") !== null167				? Promise.reject(new Error("connection reset"))168				: bucket.get(key),169	};170	await expect(syncRepo(failing, mirror, fx.repo, [], 1)).rejects.toThrow("connection reset");171	const first = fx.saveIds[3] as string;172	expect(mirrorState(mirror, "head")).toBe(first);173	expect(history(mirror, 10, 0).map((s) => s.id)).toEqual([first]);174175	await syncRepo(bucket, mirror, fx.repo, [], 1);176	expect(headId(mirror)).toBe(fx.saveIds[0] as string);177	expect(history(mirror, 10, 0)).toHaveLength(3);178	expect(flagReason(mirror, first)).toBe("holds a secret");179});180181test("a save larger than a batch is stored ahead of itself, and a sync cut short within it resumes", async () => {182	const fx = await makeKohBucket("ahead");183	const bucket = openBucket({ kind: "folder", path: fx.bucketDir });184	const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-mirror-")), fx.repo);185	// Reads go out in groups, so the cut falls on the first object the second save adds.186	const first = await readSave(bucket, fx.repo, fx.saveIds.at(-1) as string);187	const firstObjects = new Set(first.files.map((file) => file.hash)).size;188	let objectGets = 0;189	const failing: Bucket = {190		...bucket,191		get: (key) =>192			key.includes("/objects/") && ++objectGets > firstObjects193				? Promise.reject(new Error("connection reset"))194				: bucket.get(key),195	};196	await expect(syncRepo(failing, mirror, fx.repo, [], 1)).rejects.toThrow("connection reset");197	const stored = () =>198		mirror.query<{ n: number }, []>("SELECT COUNT(*) AS n FROM objects").get()?.n;199	expect(mirrorState(mirror, "head")).toBeNull();200	expect(stored()).toBe(firstObjects);201202	await syncRepo(bucket, mirror, fx.repo, [], 1);203	expect(headId(mirror)).toBe(fx.saveIds[0] as string);204	expect(history(mirror, 10, 0)).toHaveLength(3);205	const guide = fileAt(mirror, fx.saveIds[0] as string, "docs/guide.md");206	expect(text(objectBytes(mirror, guide?.hash as string))).toBe(207		"# Guide\n\nRead me. It is short.\n",208	);209});210211test("object reads overlap, but never past the bytes allowed in flight", async () => {212	const fx = await makeKohBucket("in-flight");213	const bucket = openBucket({ kind: "folder", path: fx.bucketDir });214	const peak = async (readBytes?: number) => {215		let reading = 0;216		let most = 0;217		const counting: Bucket = {218			...bucket,219			get: async (key) => {220				if (!key.includes("/objects/")) return bucket.get(key);221				most = Math.max(most, ++reading);222				await Bun.sleep(5);223				reading--;224				return bucket.get(key);225			},226		};227		const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-mirror-")), fx.repo);228		await syncRepo(counting, mirror, fx.repo, [], undefined, readBytes);229		expect(headId(mirror)).toBe(fx.saveIds[0] as string);230		return most;231	};232	expect(await peak()).toBeGreaterThan(1);233	// Every object is larger than one byte, so each is read alone.234	expect(await peak(1)).toBe(1);235});236237test("a read the gate admits after the batch filled waits for the next batch", async () => {238	const fx = await makeKohBucket("deferred");239	const bucket = openBucket({ kind: "folder", path: fx.bucketDir });240	const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-mirror-")), fx.repo);241	const stored = () =>242		mirror.query<{ n: number }, []>("SELECT COUNT(*) AS n FROM objects").get()?.n ?? 0;243	let read = 0;244	const unstored: number[] = [];245	const counting: Bucket = {246		...bucket,247		get: async (key) => {248			// Each object fills a one-byte batch, so it is stored before the next read starts.249			if (key.includes("/objects/")) unstored.push(read++ - stored());250			return bucket.get(key);251		},252	};253	await syncRepo(counting, mirror, fx.repo, [], 1, 1);254	expect(read).toBeGreaterThan(2);255	expect(new Set(unstored)).toEqual(new Set([0]));256});257258test("a failed read reports only once every other read has settled", async () => {259	const fx = await makeKohBucket("settled");260	const bucket = openBucket({ kind: "folder", path: fx.bucketDir });261	const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-mirror-")), fx.repo);262	let reading = 0;263	let reads = 0;264	const failing: Bucket = {265		...bucket,266		get: async (key) => {267			if (!key.includes("/objects/")) return bucket.get(key);268			if (reads++ === 0) throw new Error("connection reset");269			reading++;270			await Bun.sleep(30);271			reading--;272			return bucket.get(key);273		},274	};275	await expect(syncRepo(failing, mirror, fx.repo)).rejects.toThrow("connection reset");276	expect(reading).toBe(0);277});278279test("a new save syncs incrementally and a bad format is recorded as the last sync error", async () => {280	const fx = await makeKohBucket("growing");281	const t = await syncTarget(fx);282	await syncAll(t);283	await writeFile(join(fx.repoDir, "new.txt"), "new\n");284	Bun.spawnSync(["koh", "save", "add new"], {285		cwd: fx.repoDir,286		env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" },287	});288	await syncAll(t);289	expect(history(t.mirror(), 1, 0)[0]?.message).toBe("add new");290291	await writeFile(join(fx.bucketDir, fx.repo, "format"), "koh-remote 2");292	await syncAll(t);293	const row = t.kepr.query("SELECT last_sync_error FROM repos").get() as {294		last_sync_error: string;295	};296	expect(row.last_sync_error).toContain('remote format "koh-remote 2"');297});298299test("a whole-history strip reaches every save, those arriving after it too, whatever their time (§10.2)", async () => {300	const fx = await makeKohBucket("late");301	const view = await mkdtemp(join(tmpdir(), "kepr-view-"));302	const viewRepo = join(view, fx.repo);303	await cp(join(fx.bucketDir, fx.repo), viewRepo, { recursive: true });304	// A strip older than every save here: a strip bans its path from all of history, so no save305	// keeps it, however much later it was made or arrives.306	await mkdir(join(viewRepo, "events"), { recursive: true });307	await writeFile(308		join(viewRepo, "events", "0000000000bb"),309		JSON.stringify({310			id: "0000000000bb",311			kind: "strip",312			save_id: "",313			path: "secret.txt",314			reason: "a secret got saved",315			created_at: 1,316			machine: "elsewhere",317		}),318	);319	const oldest = fx.saveIds.at(-1) as string;320	const newest = fx.saveIds[0] as string;321	// First sync: the strip and the oldest save only.322	await writeFile(join(viewRepo, "head"), oldest);323	const t = await syncTarget({ ...fx, bucketDir: view });324	await syncAll(t);325	expect(fileAt(t.mirror(), oldest, "secret.txt")).toBeNull();326	// Then the rest arrive, each made long after the strip.327	await writeFile(join(viewRepo, "head"), newest);328	await syncAll(t);329	expect(headId(t.mirror())).toBe(newest);330	for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).toBeNull();331	expect(fileAt(t.mirror(), newest, "src/main.ts")).not.toBeNull();332});333334describe("the mirror's schema version", () => {335	test("a version 4 mirror reopens and any other version is refused as disposable", async () => {336		const dir = await mkdtemp(join(tmpdir(), "kepr-mirror-"));337		const created = openMirror(dir, "one");338		created.run("PRAGMA wal_checkpoint(TRUNCATE)");339		await Bun.write(join(dir, "copy.db"), Bun.file(join(dir, "one.db")));340		expect(openMirror(dir, "copy").query("PRAGMA user_version").get()).toEqual({341			user_version: 4,342		});343344		for (const version of [1, 5]) {345			const other = new Database(join(dir, `v${version}.db`));346			other.run(`PRAGMA user_version = ${version}`);347			other.close();348			expect(() => openMirror(dir, `v${version}`)).toThrow("disposable");349		}350	});351352	test("a version 2 mirror moves to 4 in place, emptied only when it deleted hidden rows", async () => {353		const dir = await mkdtemp(join(tmpdir(), "kepr-mirror-"));354		for (const [name, hidden] of [355			["plain", ""],356			["hiding", "secret.txt"],357		] as const) {358			const db = openMirror(dir, name);359			for (const table of ["hidden_files", "touches", "facts", "known_objects"]) {360				db.run(`DROP TABLE ${table}`);361			}362			db.run("PRAGMA user_version = 2");363			db.query("INSERT INTO state (key, value) VALUES ('head', 'abcdef123456'), ('hidden', ?)").run(364				hidden,365			);366			closeMirror(dir, name);367		}368		const head = (name: string) => mirrorState(openMirror(dir, name), "head");369		expect(head("plain")).toBe("abcdef123456");370		expect(head("hiding")).toBeNull();371		for (const name of ["plain", "hiding"]) {372			const db = openMirror(dir, name);373			expect(db.query("PRAGMA user_version").get()).toEqual({ user_version: 4 });374			expect(db.query("SELECT COUNT(*) AS n FROM hidden_files").get()).toEqual({ n: 0 });375		}376	});377378	test("touches recorded save by save match touches worked out all at once", async () => {379		const fx = await makeKohBucket("stepwise");380		const t = await syncTarget(fx);381		await syncAll(t);382		await writeFile(join(fx.repoDir, "src/main.ts"), "// Entry point.\nconsole.log('again');\n");383		await writeFile(join(fx.repoDir, "added.txt"), "new\n");384		Bun.spawnSync(["koh", "save", "change main, add a file"], {385			cwd: fx.repoDir,386			env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" },387		});388		Bun.spawnSync(["koh", "offer"], {389			cwd: fx.repoDir,390			env: { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" },391		});392		await syncAll(t);393		const touches = () => t.mirror().query("SELECT path, n FROM touches ORDER BY n, path").all();394		const stepwise = touches();395		expect(stepwise).toContainEqual(expect.objectContaining({ path: "added.txt" }));396		// Reopened as version 3, the mirror works every save's touches out again in one go.397		for (const table of ["touches", "facts", "known_objects"]) {398			t.mirror().run(`DROP TABLE ${table}`);399		}400		t.mirror().run("PRAGMA user_version = 3");401		closeMirror(t.mirrorDir, fx.repo);402		expect(touches()).toEqual(stepwise);403	});404405	test("a recount cut short is done when the mirror next opens", async () => {406		const fx = await makeKohBucket("recount");407		const t = await syncTarget(fx);408		await syncAll(t);409		const known = () => t.mirror().query("SELECT COUNT(*) AS n FROM known_objects").get();410		const counted = known();411		// What a landing leaves if Kepr stops before its recount commits.412		t.mirror().run("DELETE FROM known_objects");413		t.mirror().run("DELETE FROM state WHERE key = 'stats_rule'");414		closeMirror(t.mirrorDir, fx.repo);415		expect(known()).toEqual(counted);416	});417418	test("a version 3 mirror gains the touches and facts a sync would have recorded", async () => {419		const fx = await makeKohBucket("derived");420		const t = await syncTarget(fx);421		await syncAll(t);422		const derived = () => [423			t.mirror().query("SELECT path, n FROM touches ORDER BY n, path").all(),424			t.mirror().query("SELECT * FROM facts ORDER BY path").all(),425		];426		const recorded = derived();427		expect((recorded[0] as unknown[]).length).toBeGreaterThan(0);428		for (const table of ["touches", "facts", "known_objects"]) {429			t.mirror().run(`DROP TABLE ${table}`);430		}431		t.mirror().run("PRAGMA user_version = 3");432		closeMirror(t.mirrorDir, fx.repo);433		expect(derived()).toEqual(recorded);434	});435});436437test("a strip that arrives older than the saves already mirrored still strips them (§10.2, R6)", async () => {438	const fx = await makeKohBucket("older-strip");439	const t = await syncTarget(fx);440	await syncAll(t);441	// Every save in the mirror holds secret.txt …442	for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).not.toBeNull();443444	// … and now a strip arrives whose own time is older than all of them, which is what a445	// strip made on another machine and offered late looks like. Every save is reached, whatever446	// its time.447	const events = join(fx.bucketDir, fx.repo, "events");448	await mkdir(events, { recursive: true });449	await writeFile(450		join(events, "0000000000aa"),451		JSON.stringify({452			id: "0000000000aa",453			kind: "strip",454			save_id: "",455			path: "secret.txt",456			reason: "a secret got saved",457			created_at: 1,458			machine: "elsewhere",459		}),460	);461	await syncAll(t);462463	for (const id of fx.saveIds) expect(fileAt(t.mirror(), id, "secret.txt")).toBeNull();464	expect(fileAt(t.mirror(), fx.saveIds[0] as string, "src/main.ts")).not.toBeNull();465});466467test("a save with an unsafe path lands without it, keeps syncing, and the admin status names it (A3)", async () => {468	const fx = await makeKohBucket("quarantine");469	const head = fx.saveIds[0] as string;470	const key = join(fx.bucketDir, fx.repo, "saves", head);471	const record = JSON.parse(await readFile(key, "utf8"));472	record.files.push({ ...record.files[0], path: "Icon\r" });473	await writeFile(key, JSON.stringify(record));474	const inst = await instance(fx.bucketDir);475	const mirror = openMirror(join(inst.dataDir, "mirror"), fx.repo);476	expect(inst.kepr.query("SELECT last_sync_error FROM repos").get()).toEqual({477		last_sync_error: null,478	});479	expect(filesAt(mirror, head).map((file) => file.path)).toContain("README.md");480	expect(filesAt(mirror, head).map((file) => file.path)).not.toContain("Icon\r");481482	await writeFile(join(fx.repoDir, "later.txt"), "later\n");483	const env = { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" };484	Bun.spawnSync(["koh", "save", "a later save"], { cwd: fx.repoDir, env });485	Bun.spawnSync(["koh", "offer"], { cwd: fx.repoDir, env });486	await syncAll({487		bucket: openBucket({ kind: "folder", path: fx.bucketDir }),488		kepr: inst.kepr,489		mirrorDir: join(inst.dataDir, "mirror"),490	});491	expect(history(mirror, 1, 0)[0]?.message).toBe("a later save");492493	const browser = new Browser(inst.app);494	await browser.signIn();495	const page = await (await browser.get("/admin/repos")).text();496	expect(page).toContain("1 path quarantined: Icon\\x0d");497});498499test("a strip event with an illegal path is ignored, as Koh ignores it (§5.5)", async () => {500	const fx = await makeKohBucket("illegal-strip");501	for (const [id, path] of [502		["aaaaaaaaaaaa", "src//"],503		["bbbbbbbbbbbb", ""],504		["cccccccccccc", "src\ud800"],505	]) {506		const event = { id, kind: "strip", save_id: "", path, reason: "", created_at: 1, machine: "x" };507		await writeFile(join(fx.bucketDir, fx.repo, "events", id as string), JSON.stringify(event));508	}509	const t = await syncTarget(fx);510	await syncAll(t);511	expect(t.kepr.query("SELECT last_sync_error FROM repos").get()).toEqual({512		last_sync_error: null,513	});514	expect(filesAt(t.mirror(), fx.saveIds[0] as string).map((file) => file.path)).toContain(515		"src/main.ts",516	);517});518519test("an object that inflates past 512 MiB is quarantined without stopping the sync (A3)", async () => {520	const fx = await makeKohBucket("oversized");521	const head = fx.saveIds[0] as string;522	const key = join(fx.bucketDir, fx.repo, "saves", head);523	const record = JSON.parse(await readFile(key, "utf8"));524	const hash = "ab".repeat(32);525	record.files.push({ path: "huge.bin", hash, executable: false });526	await writeFile(key, JSON.stringify(record));527	await writeFile(528		join(fx.bucketDir, fx.repo, "objects", hash),529		deflateSync(new Uint8Array(512 * 1024 * 1024 + 1)),530	);531	const t = await syncTarget(fx);532	await syncAll(t);533	expect(t.kepr.query("SELECT last_sync_error FROM repos").get()).toEqual({534		last_sync_error: null,535	});536	expect(filesAt(t.mirror(), head).map((file) => file.path)).not.toContain("huge.bin");537	expect(JSON.parse(mirrorState(t.mirror(), "quarantined") ?? "[]")).toEqual(["huge.bin"]);538});539540test("the line count and the Atlas's facts skip a file over 8 MiB (A3)", async () => {541	const fx = await makeKohBucket("large-text");542	const t = await syncTarget(fx);543	await syncAll(t);544	const before = repoStats(t.mirror()).lines;545	await writeFile(join(fx.repoDir, "big.txt"), "x\n".repeat(4.5 * 1024 * 1024));546	const env = { PATH: process.env.PATH, HOME: join(fx.repoDir, "../../home"), NO_COLOR: "1" };547	Bun.spawnSync(["koh", "save", "add a big text file"], { cwd: fx.repoDir, env });548	Bun.spawnSync(["koh", "offer"], { cwd: fx.repoDir, env });549	await syncAll(t);550	expect(history(t.mirror(), 1, 0)[0]?.message).toBe("add a big text file");551	expect(repoStats(t.mirror()).lines).toBe(before);552	expect(553		t.mirror().query("SELECT lines, description FROM facts WHERE path = 'big.txt'").get(),554	).toEqual({ lines: null, description: null });555});