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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
"""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"
# ----------------------------------------------------------------------
# Duplicate candidate names
#
# Projects are collapsed to bare basenames, so two projects sharing a basename
# across roots (~/code/notes and ~/projects/notes) appear twice in the candidate
# list. Both literal-match, recommend read len(strong) > 1 as an ambiguous tie,
# and a correct strong match was downgraded to weak. Latent when discovered
# 2026-07-24 (27 projects, 27 distinct basenames) but real.
# ----------------------------------------------------------------------
def test_duplicate_candidate_name_keeps_strong_confidence():
assert rr.recommend("fix the notes thing", ["notes", "other"]) == ("notes", "strong")
# The same name twice must not read as a tie.
assert rr.recommend("fix the notes thing", ["notes", "notes", "other"]) == ("notes", "strong")
def test_genuine_ambiguity_still_downgrades():
# Two DIFFERENT projects both matching is a real tie and stays weak — the
# dedupe must collapse identical names only, never real ambiguity.
dest, conf = rr.recommend("notes and other both", ["notes", "other"])
assert conf == "weak"
def test_duplicates_do_not_change_the_chosen_destination():
dest, _ = rr.recommend("fix the notes thing", ["notes", "notes"])
assert dest == "notes"
|