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

193
tests/test_parser.py Normal file
View File

@@ -0,0 +1,193 @@
"""Parser tests against synthetic note fixtures (SPEC 12, tests 1/2/9)."""
from __future__ import annotations
from mdcaldav.config import Config
from mdcaldav.model import Status
from mdcaldav.parser import parse
def titles(tasks):
return [t.title for t in tasks]
def by_title(tasks, title):
return next(t for t in tasks if t.title == title)
def test_flat_daily_note(vault, tasks_of):
tasks = tasks_of(vault, "daily/2026-01-07.md")
assert titles(tasks) == [
"Book the soil test",
"Call the arborist",
"Tidy the potting bench",
"Water the seedlings",
]
assert [t.priority for t in tasks] == [1, 5, 9, None]
assert all(t.status is Status.NEEDS_ACTION for t in tasks)
def test_nesting_and_parentage(vault, tasks_of):
tasks = tasks_of(vault, "daily/2026-01-06.md")
parent = by_title(tasks, "Replace the cracked pane")
assert parent.parent_uid is None
assert parent.depth == 0
tasks = tasks_of(vault, "projects/irrigation.md")
trench = by_title(tasks, "Trench the main line")
rent = by_title(tasks, "Rent the trencher")
assert rent.parent_uid == trench.uid
assert rent.uid in trench.children
assert (trench.depth, rent.depth) == (0, 1)
def test_deep_nesting_five_levels(vault, tasks_of):
tasks = tasks_of(vault, "daily/2026-01-05.md")
deep = by_title(tasks, "Email the council office")
assert deep.depth == 4
lookup = {t.uid: t for t in tasks}
chain, node = [], deep
while node.parent_uid:
node = lookup[node.parent_uid]
chain.append(node.title)
assert chain == [
"Check the fence bylaw",
"Decide on trellis height",
"Draft the bed layout",
"Plan the seed order",
]
def test_sub_bullet_becomes_description(vault, tasks_of):
"""SPEC 2.3: non-checkbox bullet with no checkbox descendants."""
tasks = tasks_of(vault, "daily/2026-01-05.md")
assert by_title(tasks, "Order the cover crop").description == (
"Arrived on the 12th, invoice is in the binder"
)
assert by_title(tasks, "Sharpen the pruners").description is None
def test_multiple_description_bullets_join(vault, tasks_of):
tasks = tasks_of(vault, "daily/2026-01-06.md")
assert by_title(tasks, "Replace the cracked pane").description == (
"Used the offcut from the shed\nSilicone needs a day to cure"
)
def test_group_node_is_not_a_task(vault, tasks_of):
"""SPEC 2.3: a plain bullet parenting checkboxes contributes context only."""
tasks = tasks_of(vault, "daily/2026-01-06.md")
label = "set up the cold frame"
assert label not in titles(tasks)
cut = by_title(tasks, "Cut the polycarbonate")
assert cut.group_path == (label,)
assert label in cut.categories
assert cut.parent_uid is None # re-parented to nearest task ancestor: none
assert cut.depth == 0
def test_note_bullets_produce_nothing(vault, tasks_of):
"""SPEC 2.3: plain bullets with no task ancestor are dropped entirely."""
tasks = tasks_of(vault, "daily/2026-01-05.md")
assert "Buy fewer squash varieties" not in titles(tasks)
assert not any(
t.description and "squash" in t.description for t in tasks
)
def test_tasks_collected_under_any_heading(vault, tasks_of):
tasks = tasks_of(vault, "daily/2026-01-06.md")
# heading_path is the full stack, so the H1 note title leads.
assert by_title(tasks, "Reseal the base").heading_path == (
"Tuesday, 6 January 2026",
"Greenhouse repairs",
)
def test_file_with_no_tasks(vault, tasks_of):
assert tasks_of(vault, "reference/glossary.md") == []
def test_code_blocks_are_not_tasks(vault, tasks_of):
"""SPEC 2.6: fenced and indented code must not yield tasks."""
tasks = tasks_of(vault, "projects/irrigation.md")
assert titles(tasks) == [
"Trench the main line",
"Rent the trencher",
"Price the manifold",
]
def test_tilde_fence(cfg: Config):
src = b"- [ ] real\n\n~~~\n- [ ] fake\n~~~\n"
assert titles(parse(src, "f.md", cfg)) == ["real"]
def test_states(cfg: Config):
tasks = parse(b"- [ ] a\n- [x] b\n- [X] c\n- [/] d\n- [-] e\n- [?] f\n", "s.md", cfg)
assert [t.status for t in tasks] == [
Status.NEEDS_ACTION,
Status.COMPLETED,
Status.COMPLETED,
Status.IN_PROCESS,
Status.CANCELLED,
Status.NEEDS_ACTION,
]
assert tasks[-1].raw_marker == "?" # unknown marker preserved verbatim
def test_frontmatter_is_not_content(cfg: Config):
src = b"---\ndate: 2026-01-05\ntags: [daily]\n---\n\n- [ ] only\n"
assert titles(parse(src, "f.md", cfg)) == ["only"]
def test_spans_locate_exact_bytes(cfg: Config):
src = b"- [ ] [#A] Ship it\n"
(task,) = parse(src, "s.md", cfg)
assert src[slice(*task.source.marker_span)] == b" "
assert src[slice(*task.source.title_span)] == b"Ship it"
assert src[slice(*task.source.prio_span)] == b"[#A]"
def test_spans_are_byte_offsets_with_unicode(cfg: Config):
"""Byte spans must stay correct when the line contains multi-byte characters."""
src = "- [x] café → naïve\n".encode()
(task,) = parse(src, "u.md", cfg)
assert src[slice(*task.source.marker_span)] == b"x"
assert src[slice(*task.source.title_span)].decode() == "café → naïve"
def test_crlf_line_endings(cfg: Config):
(task,) = parse(b"- [ ] windows\r\n", "w.md", cfg)
assert task.title == "windows"
assert task.source.line_span == (0, 13) # excludes the \r\n
def test_bullet_variants(cfg: Config):
tasks = parse(b"- [ ] dash\n* [ ] star\n+ [ ] plus\n", "b.md", cfg)
assert titles(tasks) == ["dash", "star", "plus"]
assert [t.source.bullet for t in tasks] == ["-", "*", "+"]
def test_tags_become_categories(cfg: Config):
(task,) = parse(b"- [ ] Fix #1923 for #garden\n", "t.md", cfg)
assert "garden" in task.categories
assert "1923" not in task.categories # bare numbers are not tags
assert task.title == "Fix #1923 for #garden" # tags stay in the title
def test_sibling_index_disambiguates_duplicates(cfg: Config):
tasks = parse(b"- [ ] same\n- [ ] same\n- [ ] same\n", "d.md", cfg)
assert [t.sibling_index for t in tasks] == [0, 1, 2]
def test_due_syntax_opt_in(cfg: Config):
src = b"- [ ] Prune the apple tree @due(2026-03-01)\n"
assert parse(src, "n.md", cfg)[0].due is None # disabled by default
cfg.tasks.due_syntax = ["@due(%Y-%m-%d)"]
task = parse(src, "n.md", cfg)[0]
assert task.due is not None and task.due.isoformat() == "2026-03-01"
assert task.title == "Prune the apple tree"