aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests/test_drill_to_anki.py
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-30 13:27:29 -0500
committerCraig Jennings <c@cjennings.net>2026-05-30 13:27:29 -0500
commita6313954fc297ee4a6c1c42ba903730a364cd5df (patch)
treef55cc085d966684253c6e7daaeee27593ca08801 /.ai/scripts/tests/test_drill_to_anki.py
parent0234e52b727b34ade93961eb05b5638685f4406f (diff)
downloadrulesets-a6313954fc297ee4a6c1c42ba903730a364cd5df.tar.gz
rulesets-a6313954fc297ee4a6c1c42ba903730a364cd5df.zip
test(scripts): cover drill-to-anki internals, broadcast, and daily-prep
I backfilled the gaps left after the flashcard work landed. drill-to-anki.py had tests only for its two default helpers. I added coverage for the core parser and its pieces: parse (section-to-tag mapping, drawer-only body, blank trimming, multiline join, no-card input), strip_org_metadata (drawer and planning-line stripping, unclosed drawer), section_to_tag, escape_html, and the deterministic stable_id. I also filled the remaining drill-deck-stats / drill-deck-diff-ids branches (missing-title and PROPERTIES-mismatch warnings, the appeared-IDs note path). I added test_cross_project_broadcast.py for the two scripts that had none here: is_broadcastable / discover (SEARCH_ROOTS pointed at a tmp tree) / sender_project / inbox_send_path, plus an ERT suite for daily-prep-agenda.el (dp-iso-date, dp-bucket with the clock pinned, dp-format-entry, and dp-collect end to end on a temp org file). daily-prep-agenda.el needed one change to be loadable under ERT: its batch entrypoint fired on any load. I gated it behind dp--cli-invocation-p, the same readable-files check lint-org.el already uses, so requiring the file for tests no longer runs the extractor. A real invocation with a file argument still fires. A no-argument run now no-ops instead of printing an empty header.
Diffstat (limited to '.ai/scripts/tests/test_drill_to_anki.py')
-rw-r--r--.ai/scripts/tests/test_drill_to_anki.py122
1 files changed, 122 insertions, 0 deletions
diff --git a/.ai/scripts/tests/test_drill_to_anki.py b/.ai/scripts/tests/test_drill_to_anki.py
index 6490e58..6c5ef9b 100644
--- a/.ai/scripts/tests/test_drill_to_anki.py
+++ b/.ai/scripts/tests/test_drill_to_anki.py
@@ -42,3 +42,125 @@ def test_default_deck_name_is_raw_basename(drill):
def test_default_deck_name_keeps_hyphens(drill):
"""A hyphenated basename is kept verbatim rather than title-cased."""
assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill"
+
+
+# --- section_to_tag (pure) ---
+
+def test_section_to_tag_slugifies_words(drill):
+ assert drill.section_to_tag("Orbital Regimes") == "orbital-regimes"
+
+
+def test_section_to_tag_strips_leading_and_trailing_nonalnum(drill):
+ assert drill.section_to_tag(" People & Roles! ") == "people-roles"
+
+
+def test_section_to_tag_empty_string(drill):
+ assert drill.section_to_tag("") == ""
+
+
+# --- escape_html (pure) ---
+
+def test_escape_html_escapes_amp_lt_gt(drill):
+ assert drill.escape_html("a & b < c > d") == "a &amp; b &lt; c &gt; d"
+
+
+def test_escape_html_plain_text_unchanged(drill):
+ assert drill.escape_html("plain text") == "plain text"
+
+
+def test_escape_html_escapes_amp_first_so_existing_entity_is_literal(drill):
+ # & is replaced before < / >, so a literal "&lt;" becomes "&amp;lt;",
+ # not silently treated as an already-escaped entity.
+ assert drill.escape_html("&lt;") == "&amp;lt;"
+
+
+def test_escape_html_empty_string(drill):
+ assert drill.escape_html("") == ""
+
+
+# --- stable_id (pure) ---
+
+def test_stable_id_is_deterministic(drill):
+ assert drill.stable_id("DeepSat", "deck") == drill.stable_id("DeepSat", "deck")
+
+
+def test_stable_id_salt_changes_the_result(drill):
+ assert drill.stable_id("DeepSat", "deck") != drill.stable_id("DeepSat", "model")
+
+
+def test_stable_id_stays_within_the_reserved_range(drill):
+ value = drill.stable_id("anything", "deck")
+ assert drill.ID_BASE <= value < drill.ID_BASE + drill.ID_RANGE
+
+
+# --- strip_org_metadata (pure) ---
+
+def test_strip_org_metadata_drops_properties_drawer(drill):
+ body = [":PROPERTIES:", ":ID: x", ":END:", "real content"]
+ assert drill.strip_org_metadata(body) == ["real content"]
+
+
+def test_strip_org_metadata_drops_planning_lines(drill):
+ body = ["SCHEDULED: <2026-05-30>", "DEADLINE: <2026-06-01>",
+ "CLOSED: [2026-05-29]", "body"]
+ assert drill.strip_org_metadata(body) == ["body"]
+
+
+def test_strip_org_metadata_leaves_plain_body_unchanged(drill):
+ body = ["line one", "line two"]
+ assert drill.strip_org_metadata(body) == ["line one", "line two"]
+
+
+def test_strip_org_metadata_empty_list(drill):
+ assert drill.strip_org_metadata([]) == []
+
+
+def test_strip_org_metadata_unclosed_drawer_swallows_the_rest(drill):
+ # An unterminated :PROPERTIES: drawer consumes everything after it.
+ body = [":PROPERTIES:", ":ID: x", "still in drawer"]
+ assert drill.strip_org_metadata(body) == []
+
+
+# --- parse (pure, core parser) ---
+
+SECTIONED = """* Orbital Regimes
+** What is LEO? :drill:
+Low Earth Orbit.
+** What is GEO? :drill:
+Geostationary Earth Orbit.
+"""
+
+
+def test_parse_returns_front_back_tag_per_card(drill):
+ cards = drill.parse(SECTIONED)
+ assert len(cards) == 2
+ assert cards[0] == ("What is LEO?", "Low Earth Orbit.", "orbital-regimes")
+ assert cards[1][0] == "What is GEO?"
+
+
+def test_parse_card_without_a_section_gets_the_drill_tag(drill):
+ assert drill.parse("** Lone card? :drill:\nbody\n") == [("Lone card?", "body", "drill")]
+
+
+def test_parse_strips_properties_drawer_from_back(drill):
+ text = "** Q? :drill:\n:PROPERTIES:\n:ID: abc\n:END:\nThe answer.\n"
+ assert drill.parse(text) == [("Q?", "The answer.", "drill")]
+
+
+def test_parse_trims_leading_and_trailing_blank_body_lines(drill):
+ cards = drill.parse("** Q? :drill:\n\n\nanswer\n\n\n")
+ assert cards[0][1] == "answer"
+
+
+def test_parse_card_with_only_a_drawer_has_empty_back(drill):
+ text = "** Q? :drill:\n:PROPERTIES:\n:ID: x\n:END:\n"
+ assert drill.parse(text) == [("Q?", "", "drill")]
+
+
+def test_parse_joins_multiline_body_with_br(drill):
+ cards = drill.parse("** Q? :drill:\nline one\nline two\n")
+ assert cards[0][1] == "line one<br>line two"
+
+
+def test_parse_no_drill_cards_returns_empty(drill):
+ assert drill.parse("* Section\nno drill cards here\n") == []