Initial commit
Some checks failed
CI / test (push) Failing after 11s
CI / image (push) Has been skipped

This commit is contained in:
2026-08-01 21:03:35 -04:00
commit 8b5cd370ea
41 changed files with 5462 additions and 0 deletions

190
tests/test_index.py Normal file
View File

@@ -0,0 +1,190 @@
"""Identity, generation and sync-token tests (SPEC 12, tests 4-7/11)."""
from __future__ import annotations
from pathlib import Path
import pytest
from mdcaldav.config import Config
from mdcaldav.index import Index
from mdcaldav.model import Status
@pytest.fixture
def index(vault: Path, tmp_path: Path) -> Index:
cfg = Config()
cfg.vault.path = vault
cfg.index.db = tmp_path / "index.db"
idx = Index(cfg)
idx.rescan()
yield idx
idx.close()
def uid_of(index: Index, title: str, rel: str | None = None) -> str:
matches = [
t
for t in index.tasks.values()
if t.title == title and (rel is None or t.source.rel_path == rel)
]
assert len(matches) == 1, f"{title!r} matched {len(matches)} tasks"
return matches[0].uid
def write(vault: Path, rel: str, text: str) -> None:
(vault / rel).write_text(text, encoding="utf-8")
def test_uids_are_stable_across_an_unchanged_rescan(index: Index):
before = {t.uid: t.title for t in index.tasks.values()}
index.rescan()
after = {t.uid: t.title for t in index.tasks.values()}
assert before == after
def test_status_change_preserves_uid(index: Index, vault: Path):
uid = uid_of(index, "Water the seedlings", "daily/2026-01-07.md")
rel = "daily/2026-01-07.md"
text = (vault / rel).read_text()
write(vault, rel, text.replace("- [ ] Water the seedlings", "- [x] Water the seedlings"))
index.rescan()
assert index.tasks[uid].status is Status.COMPLETED
assert uid_of(index, "Water the seedlings", rel) == uid
def test_reorder_preserves_uid(index: Index, vault: Path):
rel = "daily/2026-01-07.md"
uid = uid_of(index, "Call the arborist", rel)
text = (vault / rel).read_text()
lines = text.splitlines(keepends=True)
lines[8], lines[9] = lines[9], lines[8] # swap two task lines
write(vault, rel, "".join(lines))
index.rescan()
assert uid_of(index, "Call the arborist", rel) == uid
def test_retitle_in_place_preserves_uid(index: Index, vault: Path):
rel = "daily/2026-01-07.md"
uid = uid_of(index, "Tidy the potting bench", rel)
text = (vault / rel).read_text()
write(vault, rel, text.replace("Tidy the potting bench", "Tidy the potting bench properly"))
index.rescan()
assert uid_of(index, "Tidy the potting bench properly", rel) == uid
def test_carry_forward_duplicates_stay_distinct(index: Index):
"""SPEC 6.3: the same title in three daily notes is three tasks, not one."""
uids = {
rel: uid_of(index, "Water the seedlings", rel)
for rel in (
"daily/2026-01-05.md",
"daily/2026-01-06.md",
"daily/2026-01-07.md",
)
}
assert len(set(uids.values())) == 3
def test_completing_one_carry_forward_copy_does_not_affect_others(
index: Index, vault: Path
):
monday = uid_of(index, "Water the seedlings", "daily/2026-01-05.md")
wednesday = uid_of(index, "Water the seedlings", "daily/2026-01-07.md")
rel = "daily/2026-01-07.md"
text = (vault / rel).read_text()
write(vault, rel, text.replace("- [ ] Water the seedlings", "- [x] Water the seedlings"))
index.rescan()
assert index.tasks[wednesday].status is Status.COMPLETED
assert index.tasks[monday].status is Status.NEEDS_ACTION
def test_true_cross_file_move_preserves_uid(index: Index, vault: Path):
"""A task that disappears from A and appears in B keeps its UID."""
uid = uid_of(index, "Book the soil test", "daily/2026-01-07.md")
src = "daily/2026-01-07.md"
text = (vault / src).read_text()
write(vault, src, text.replace("- [ ] [#A] Book the soil test\n", ""))
dest = "daily/2026-01-06.md"
write(vault, dest, (vault / dest).read_text() + "\n- [ ] [#A] Book the soil test\n")
index.rescan()
assert uid_of(index, "Book the soil test", dest) == uid
def test_deleted_task_is_tombstoned(index: Index, vault: Path):
rel = "daily/2026-01-07.md"
uid = uid_of(index, "Call the arborist", rel)
text = (vault / rel).read_text()
write(vault, rel, text.replace("- [ ] [#B] Call the arborist\n", ""))
index.rescan()
assert uid not in index.tasks
row = index.db.execute(
"SELECT deleted_gen FROM tasks WHERE uid = ?", (uid,)
).fetchone()
assert row["deleted_gen"] is not None
def test_uids_survive_a_restart(vault: Path, tmp_path: Path):
cfg = Config()
cfg.vault.path = vault
cfg.index.db = tmp_path / "index.db"
first = Index(cfg)
first.rescan()
before = {t.uid for t in first.tasks.values()}
first.close()
second = Index(cfg)
second.rescan()
assert {t.uid for t in second.tasks.values()} == before
second.close()
def test_completed_timestamp_recorded_on_transition(index: Index, vault: Path):
rel = "daily/2026-01-07.md"
uid = uid_of(index, "Water the seedlings", rel)
assert index.completed_at(uid) is None
text = (vault / rel).read_text()
write(vault, rel, text.replace("- [ ] Water the seedlings", "- [x] Water the seedlings"))
index.rescan()
assert index.completed_at(uid) is not None
def test_collections_follow_directories(index: Index):
assert set(index.collections()) == {"daily", "projects"}
def test_sync_returns_only_changed(index: Index, vault: Path):
token, hrefs = index.sync("daily")
assert hrefs # initial sync returns everything
rel = "daily/2026-01-07.md"
uid = uid_of(index, "Water the seedlings", rel)
text = (vault / rel).read_text()
write(vault, rel, text.replace("- [ ] Water the seedlings", "- [x] Water the seedlings"))
index.rescan()
_, changed = index.sync("daily", token)
assert changed == [f"{uid}.ics"]
def test_sync_rejects_stale_token(index: Index):
index.cfg.index.tombstone_retention = 1
for _ in range(4):
index.rescan()
with pytest.raises(ValueError):
index.sync("daily", "http://mdcaldav.local/ns/sync/0")
def test_sync_rejects_malformed_token(index: Index):
with pytest.raises(ValueError):
index.sync("daily", "not-a-token")