"""Path filtering, including Syncthing-shaped vaults (SPEC 2.6).""" from __future__ import annotations from pathlib import Path import pytest from mdcaldav.config import Config from mdcaldav.globs import matches from mdcaldav.index import Index CONFLICT = "daily/2026-01-05.sync-conflict-20260731-120000-ABCDEFG.md" @pytest.mark.parametrize( "path,pattern,expected", [ # `**/` must mean "zero or more segments", so top-level files match. ("README.md", "**/*.md", True), ("daily/a.md", "**/*.md", True), ("a/b/c/d.md", "**/*.md", True), # `*` must not cross a separator. ("a/b.tmp", "*.tmp", False), ("b.tmp", "*.tmp", True), # Syncthing artefacts. (".stversions/daily/2026-01-05.md", ".stversions/**", True), (CONFLICT, "**/*.sync-conflict-*", True), ("daily/.syncthing.a.md.tmp", "**/.syncthing.*", True), ("daily/~syncthing~a.md.tmp", "**/~syncthing~*", True), # Ordinary notes must survive all of the above. ("daily/2026-01-05.md", "**/*.sync-conflict-*", False), ("daily/2026-01-05.md", ".stversions/**", False), ], ) def test_glob_semantics(path: str, pattern: str, expected: bool): assert matches(path, pattern) is expected @pytest.fixture def syncthing_vault(vault: Path) -> Path: """A vault shaped like a live Syncthing folder.""" (vault / ".stfolder").mkdir() (vault / ".stversions/daily").mkdir(parents=True) # An old copy of a real note, exactly as Syncthing retains it. (vault / ".stversions/daily/2026-01-07.md").write_text( (vault / "daily/2026-01-07.md").read_text(), encoding="utf-8" ) (vault / CONFLICT).write_text("- [ ] Book the soil test\n", encoding="utf-8") (vault / "daily/.syncthing.2026-01-07.md.tmp").write_text("- [ ] partial\n") (vault / "ROADMAP.md").write_text("- [ ] Top level task\n", encoding="utf-8") return vault @pytest.fixture def index(syncthing_vault: Path, tmp_path: Path): cfg = Config() cfg.vault.path = syncthing_vault cfg.index.db = tmp_path / "index.db" idx = Index(cfg) idx.rescan() yield idx idx.close() def test_top_level_notes_are_indexed(index: Index): """Regression: `**/*.md` used to skip files at the vault root.""" assert "Top level task" in {t.title for t in index.tasks.values()} def test_syncthing_artefacts_are_not_indexed(index: Index): sources = {t.source.rel_path for t in index.tasks.values()} assert not any(s.startswith(".stversions/") for s in sources) assert not any(s.startswith(".stfolder/") for s in sources) assert CONFLICT not in sources assert not any(".syncthing." in s for s in sources) def test_versioned_copies_do_not_duplicate_tasks(index: Index): """The failure mode that matters: one task per note, not one per version.""" titles = [t.title for t in index.tasks.values()] assert titles.count("Book the soil test") == 1 def test_targeted_rescan_also_filters(index: Index): """The watcher path must filter too, or Syncthing writes poison the index.""" index.rescan([".stversions/daily/2026-01-07.md", CONFLICT]) titles = [t.title for t in index.tasks.values()] assert titles.count("Book the soil test") == 1 sources = {t.source.rel_path for t in index.tasks.values()} assert not any(s.startswith(".stversions/") for s in sources) def test_doctor_reports_conflict_files(syncthing_vault: Path, tmp_path, capsys, monkeypatch): from mdcaldav.cli import main monkeypatch.setenv("HOME", str(tmp_path)) main(["--vault", str(syncthing_vault), "doctor"]) out = capsys.readouterr().out assert "sync-conflict files present" in out assert "merge and delete them" in out