aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests/test_route_recommend.py
blob: acc475505c79f4ca5c8f79ff2607d0ef7d9d687d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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"