0336dc246093
test that Koh and Kepr drop the same unsafe paths and keep the same file list for every save; pin koh3 3.2.3 (v3.0.87)
- time
- 2026-09-28 15:35 UTC · 2 weeks ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- quarantine unsafe paths
- parent
- 1252116e72ce
↓ kepr-0336dc24.tar.gzatlas at this save
files changed3 files
package.json+1 −1tests/koh-version.txt+1 −1tests/kohpaths.test.ts+77 −0package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.86",3+ "version": "3.0.87",44 "private": true,55 "type": "module",66 "scripts": {tests/kohpaths.test.ts+77 −0
@@ -0,0 +1,77 @@1+import { Database } from "bun:sqlite";2+import { expect, test } from "bun:test";3+import { existsSync } from "node:fs";4+import { mkdir, readFile, writeFile } from "node:fs/promises";5+import { dirname, join } from "node:path";6+import { openBucket } from "../src/bucket/reader.ts";7+import { syncAll } from "../src/bucket/sync.ts";8+import { openMirror, quarantinedPaths } from "../src/mirror/store.ts";9+import { openKeprDb } from "../src/server/db.ts";10+import { makeKohBucket } from "./fixture.ts";11+12+// §5.5 is a contract between the repos: every path either side drops, the other drops too.13+const UNSAFE = [14+ "../joint-escape.txt",15+ "/tmp/koh3-joint-absolute.txt",16+ "a/../../joint-nested.txt",17+ "back\\slash.txt",18+ "ctl\u0001name.txt",19+ "Icon\r",20+];21+22+test("Koh and Kepr drop the same unsafe paths and keep the same file list for every save (A3)", async () => {23+ const fx = await makeKohBucket("joint-paths");24+ const head = fx.saveIds[0] as string;25+ const key = join(fx.bucketDir, fx.repo, "saves", head);26+ const record = JSON.parse(await readFile(key, "utf8"));27+ const hash = record.files[0].hash;28+ for (const path of UNSAFE) record.files.push({ path, hash, executable: false });29+ await writeFile(key, JSON.stringify(record));30+31+ const root = dirname(dirname(fx.repoDir));32+ const home = join(root, "second-home");33+ const work = join(root, "second-work");34+ await mkdir(home);35+ await mkdir(work);36+ const koh3 = (cwd: string, args: string[]) =>37+ Bun.spawnSync(["koh3", ...args], {38+ cwd,39+ env: { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" },40+ });41+ expect(koh3(work, ["remote", "folder", fx.bucketDir]).exitCode).toBe(0);42+ const stolen = koh3(work, ["steal", fx.repo, "--json"]);43+ expect(stolen.exitCode, stolen.stderr.toString()).toBe(0);44+ const dropped: string[] = JSON.parse(stolen.stdout.toString()).unsafe_paths;45+46+ const dataDir = join(root, "kepr-data");47+ const kepr = openKeprDb(dataDir);48+ await syncAll({49+ bucket: openBucket({ kind: "folder", path: fx.bucketDir }),50+ kepr,51+ mirrorDir: join(dataDir, "mirror"),52+ tarballDir: join(dataDir, "tarballs"),53+ });54+ expect(kepr.query("SELECT last_sync_error FROM repos").get()).toEqual({ last_sync_error: null });55+56+ const fileLists = (db: Database) =>57+ db.query("SELECT save_id, path FROM save_files ORDER BY save_id, path").all();58+ const koh = new Database(join(work, fx.repo, ".koh"));59+ const mirror = openMirror(join(dataDir, "mirror"), fx.repo);60+ expect(fileLists(koh)).toEqual(fileLists(mirror));61+ expect(fileLists(koh).length).toBeGreaterThan(0);62+ // Same verdict and the same escaped name on both sides, name for name.63+ expect(dropped.length).toBe(UNSAFE.length);64+ expect([...dropped].sort()).toEqual(quarantinedPaths(mirror));65+ for (const path of UNSAFE) {66+ expect(fileLists(koh)).not.toContainEqual({ save_id: head, path });67+ }68+ koh.close();69+70+ for (const outside of [71+ join(work, "joint-escape.txt"),72+ join(root, "joint-nested.txt"),73+ "/tmp/koh3-joint-absolute.txt",74+ ]) {75+ expect(existsSync(outside), outside).toBe(false);76+ }77+});