104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
"""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
|