diff options
Diffstat (limited to 'claude-templates/.ai/scripts/tests')
3 files changed, 195 insertions, 6 deletions
diff --git a/claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py b/claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py index 058b0cd..87008a8 100644 --- a/claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py +++ b/claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py @@ -34,14 +34,33 @@ def test_default_output_path_targets_phone_anki_dir(drill): assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg" -def test_default_deck_name_is_raw_basename(drill): - """Deck name is the input basename with case preserved; #+TITLE is ignored.""" - assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat" +def test_default_deck_name_uses_org_title(drill): + """The #+TITLE drives the Anki deck name, not the filename slug.""" + org = "#+TITLE: Refutations\n* Section\n** Q? :drill:\na\n" + assert drill.default_deck_name(Path("/x/refutation-drill.org"), org) == "Refutations" -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" +def test_default_deck_name_title_is_trimmed(drill): + """Surrounding whitespace on the #+TITLE value is stripped.""" + org = "#+TITLE: DeepSat Flashcards \n" + assert drill.default_deck_name(Path("/x/deepsat.org"), org) == "DeepSat Flashcards" + + +def test_default_deck_name_title_match_is_case_insensitive(drill): + """A lowercase #+title: keyword is still recognized.""" + org = "#+title: Health Flashcards\n" + assert drill.default_deck_name(Path("/x/health-drill.org"), org) == "Health Flashcards" + + +def test_default_deck_name_falls_back_to_basename_without_title(drill): + """No #+TITLE line falls back to the input basename, case preserved.""" + org = "* Section\n** Q? :drill:\na\n" + assert drill.default_deck_name(Path("/x/deepsat.org"), org) == "deepsat" + + +def test_default_deck_name_blank_title_falls_back_to_basename(drill): + """An empty #+TITLE value is ignored in favour of the basename.""" + assert drill.default_deck_name(Path("/x/health-drill.org"), "#+TITLE: \n") == "health-drill" # --- section_to_tag (pure) --- diff --git a/claude-templates/.ai/scripts/tests/test_inbox_send.py b/claude-templates/.ai/scripts/tests/test_inbox_send.py index a0094dc..cb60e63 100644 --- a/claude-templates/.ai/scripts/tests/test_inbox_send.py +++ b/claude-templates/.ai/scripts/tests/test_inbox_send.py @@ -97,6 +97,52 @@ class TestInboxSendDiscovery: result = run_script(["--list"], roots=[tmp_path / "does-not-exist"]) assert result.returncode == 0 + def test_inbox_send_list_displays_dot_stripped_name(self, project_root, run_script, tmp_path): + """Dotted project basenames display dot-stripped (.emacs.d → emacsd).""" + project_root(".emacs.d") + result = run_script(["--list"], roots=[tmp_path / "projects"]) + assert "emacsd" in result.stdout + + +class TestInboxSendDotAlias: + """A dotted project basename resolves both verbatim and dot-stripped.""" + + def test_resolves_by_dot_stripped_alias(self, project_root, run_script, tmp_path): + """'emacsd' delivers to the .emacs.d project.""" + project_root(".emacs.d") + cwd = project_root("source") + run_script( + ["emacsd", "--text", "hi"], + cwd=cwd, roots=[tmp_path / "projects"], + ) + files = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir()) + assert len(files) == 1 + + def test_resolves_by_exact_dotted_name_still(self, project_root, run_script, tmp_path): + """Backward-compat: the verbatim '.emacs.d' target still resolves.""" + project_root(".emacs.d") + cwd = project_root("source") + run_script( + [".emacs.d", "--text", "hi"], + cwd=cwd, roots=[tmp_path / "projects"], + ) + files = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir()) + assert len(files) == 1 + + def test_exact_match_wins_over_alias(self, project_root, run_script, tmp_path): + """An exact basename match is preferred over a dot-stripped collision.""" + project_root("emacsd") # exact + project_root(".emacs.d") # would also normalize to 'emacsd' + cwd = project_root("source") + run_script( + ["emacsd", "--text", "hi"], + cwd=cwd, roots=[tmp_path / "projects"], + ) + exact = list((tmp_path / "projects" / "emacsd" / "inbox").iterdir()) + dotted = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir()) + assert len(exact) == 1 + assert dotted == [] + # ---------------------------------------------------------------------- # Slug derivation from text and from filenames diff --git a/claude-templates/.ai/scripts/tests/test_route_recommend.py b/claude-templates/.ai/scripts/tests/test_route_recommend.py new file mode 100644 index 0000000..acc4755 --- /dev/null +++ b/claude-templates/.ai/scripts/tests/test_route_recommend.py @@ -0,0 +1,124 @@ +"""Tests for route_recommend.py — the wrap-up routing recommendation engine. + +The core is a pure function recommend(item, projects) -> (destination, confidence): +- strong: a project's name (or its dot-stripped form) appears literally in the item +- weak: a distinctive name token overlaps, but the full name doesn't +- none: no overlap; the item stays put (destination is None) + +A multi-way tie at the top tier downgrades to weak with a deterministic pick. +An empty project list yields none. + +The CLI wires this to inbox-send.py's discover_projects (sandboxed here via the +INBOX_SEND_ROOTS env var, the same hook inbox-send's own tests use). +""" + +import subprocess +import sys +from pathlib import Path + +SCRIPTS = Path(__file__).parent.parent +SCRIPT = SCRIPTS / "route_recommend.py" +sys.path.insert(0, str(SCRIPTS)) + +import route_recommend as rr # noqa: E402 + + +# --- pure function: the five spec'd cases ----------------------------------- + +def test_strong_match_named_literally(): + dest, conf = rr.recommend("fix the rulesets refactor command", ["rulesets", "home", "work"]) + assert (dest, conf) == ("rulesets", "strong") + + +def test_strong_match_via_dot_stripped_name(): + # ".emacs.d" addressed as "emacsd" in the item is still a literal hit. + dest, conf = rr.recommend("update the emacsd ai-term module", [".emacs.d", "rulesets"]) + assert (dest, conf) == (".emacs.d", "strong") + + +def test_strong_match_dotted_name_verbatim(): + dest, conf = rr.recommend("patch .emacs.d startup", [".emacs.d", "rulesets"]) + assert (dest, conf) == (".emacs.d", "strong") + + +def test_weak_match_topic_token_only(): + # "wttrin" is a token of "emacs-wttrin" but the full name isn't present. + dest, conf = rr.recommend("the wttrin weather bug", ["emacs-wttrin", "rulesets"]) + assert (dest, conf) == ("emacs-wttrin", "weak") + + +def test_no_match_stays_put(): + dest, conf = rr.recommend("calibrate the telescope mount", ["rulesets", "deepsat"]) + assert dest is None + assert conf == "none" + + +def test_two_project_strong_tie_downgrades_to_weak(): + # Both named literally → ambiguous → weak, deterministic tie-break (alphabetical). + dest, conf = rr.recommend("sync rulesets and home configs", ["rulesets", "home", "work"]) + assert conf == "weak" + assert dest == "home" # tie-break: most-overlap then alphabetical + + +def test_empty_project_list_is_none(): + assert rr.recommend("anything at all", []) == (None, "none") + + +# --- boundary / robustness -------------------------------------------------- + +def test_literal_name_requires_word_boundary(): + # "home" must not match inside "homeowner". + dest, conf = rr.recommend("the homeowner association meeting", ["home", "rulesets"]) + assert dest is None and conf == "none" + + +def test_path_mention_counts_as_literal(): + dest, conf = rr.recommend("edit ~/code/rulesets/Makefile", ["rulesets", "home"]) + assert (dest, conf) == ("rulesets", "strong") + + +def test_strong_beats_weak_when_both_present(): + # "rulesets" named literally (strong) outranks an emacs-wttrin token hit (weak). + dest, conf = rr.recommend("the wttrin fix belongs in rulesets", ["rulesets", "emacs-wttrin"]) + assert (dest, conf) == ("rulesets", "strong") + + +# --- CLI + discovery reuse (sandboxed roots) -------------------------------- + +def _run(args, roots, item): + import os + env = {"PATH": os.environ.get("PATH", ""), "HOME": os.environ.get("HOME", "/tmp"), + "INBOX_SEND_ROOTS": ":".join(str(r) for r in roots)} + return subprocess.run([sys.executable, str(SCRIPT), "--item", item, *args], + capture_output=True, text=True, env=env) + + +def _mk_project(tmp_path, name): + proj = tmp_path / "projects" / name + (proj / ".ai").mkdir(parents=True, exist_ok=True) + (proj / "inbox").mkdir(exist_ok=True) + return proj + + +def test_cli_discovers_and_recommends(tmp_path): + _mk_project(tmp_path, "foo") + _mk_project(tmp_path, "bar") + r = _run([], roots=[tmp_path / "projects"], item="fix the foo widget") + assert r.returncode == 0 + assert r.stdout.strip() == "foo\tstrong" + + +def test_cli_no_match_prints_none(tmp_path): + _mk_project(tmp_path, "foo") + r = _run([], roots=[tmp_path / "projects"], item="unrelated grocery list") + assert r.returncode == 0 + assert r.stdout.strip() == "none" + + +def test_cli_exclude_drops_current_project(tmp_path): + _mk_project(tmp_path, "foo") + _mk_project(tmp_path, "bar") + # Item names foo, but foo is excluded as the current project → no other match. + r = _run(["--exclude", "foo"], roots=[tmp_path / "projects"], item="fix the foo widget") + assert r.returncode == 0 + assert r.stdout.strip() == "none" |
