import asyncio from pathlib import PurePath from versiond.db import Database from versiond.diff import hunks, unified from versiond.filters import Filters from versiond.ingest import Coalescer, Repository from versiond.store import BlobStore def test_filters_paths(): f = Filters() ok = ["app.py", "src/main.go", ".env", ".env.production", "Makefile", "pkg/package.json", ".gitignore"] for p in ok: assert f.path_reason(PurePath(p)) is None, p rejected = { "node_modules/x/index.js": "ignored-directory", ".git/config": "ignored-directory", "src/__pycache__/a.cpython-312.pyc": "ignored-directory", "a.pyc": "ignored-extension", ".bashrc_local": "hidden-file", "notes.txt~": "temporary-file", "main.py.swp": "ignored-extension", "venv/lib/x.py": "ignored-directory", "target/debug/build.rs": "ignored-directory", "4913": "temporary-file", "src/pkg.egg-info/PKG-INFO": "ignored-directory", } for p, reason in rejected.items(): assert f.path_reason(PurePath(p)) == reason, p assert f.size_reason(204_800) is None assert f.size_reason(204_801) == "file-too-large" assert f.content_reason(b"abc\x00def") == "binary-content" def test_filter_patterns(): f = Filters(patterns=("*.log", "secret/*")) assert f.path_reason(PurePath("x/debug.log")) == "ignored-pattern" assert f.path_reason(PurePath("secret/key.txt")) == "ignored-pattern" assert f.path_reason(PurePath("src/key.txt")) is None def test_directory_patterns(): f = Filters(patterns=("app/data/uploads",)) assert f.skip_dir("uploads", "app/data/uploads") assert not f.skip_dir("uploads", "other/uploads") assert f.path_reason(PurePath("app/data/uploads/a/b.txt")) == "ignored-pattern" assert f.path_reason(PurePath("app/data/config.json")) is None def test_diff_unified(): a = b"one\ntwo\nthree\n" b = b"one\n2\nthree\nfour\n" out = unified(hunks(a, b), "a", "b") assert "-two\n+2\n" in out and "+four\n" in out assert hunks(b"x y\n", b"x y\n", ignore_whitespace=True) == [] def _repo(tmp_path): db = Database(tmp_path / "index.sqlite") return Repository(db, BlobStore(tmp_path / "spool"), Filters()) def test_coalescer_keeps_first_and_last(tmp_path): repo = _repo(tmp_path) path = str(tmp_path / "f.py") async def run(): c = Coalescer(repo, window=0.2, max_hold=1.0, per_hour=100) statuses = [c.submit(path, f"v{i}\n".encode(), "test", "edit")["status"] for i in range(6)] await asyncio.sleep(0.5) return statuses statuses = asyncio.run(run()) assert statuses[0] == "committed" assert set(statuses[1:]) == {"coalesced"} file = repo.db.file_by_path(path) contents = [repo.blobs.get(v["blob_sha256"]) for v in reversed(repo.db.history(file["id"]))] assert contents == [b"v0\n", b"v5\n"] def test_coalescer_rate_limit_keeps_newest(tmp_path): repo = _repo(tmp_path) path = str(tmp_path / "f.py") async def run(): c = Coalescer(repo, window=0.05, max_hold=0.1, per_hour=2) c.submit(path, b"a", "t", "e") await asyncio.sleep(0.1) c.submit(path, b"b", "t", "e") await asyncio.sleep(0.2) status = c.submit(path, b"c", "t", "e")["status"] await asyncio.sleep(0.2) flushed = c.flush() return status, flushed status, flushed = asyncio.run(run()) assert status == "coalesced" assert flushed == 1 # over the limit it waits, and shutdown still stores it file = repo.db.file_by_path(path) assert repo.blobs.get(repo.db.latest_version(file["id"])["blob_sha256"]) == b"c" def test_dedupe_unchanged(tmp_path): repo = _repo(tmp_path) path = str(tmp_path / "f.py") assert repo.commit(path, b"x", "t", "e")["status"] == "committed" assert repo.commit(path, b"x", "t", "e")["status"] == "unchanged"