Initial commit
This commit is contained in:
190
tests/test_index.py
Normal file
190
tests/test_index.py
Normal 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")
|
||||
Reference in New Issue
Block a user