"""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"