Skip to content

Commit eff814b

Browse files
committed
Add chat routing unit tests
1 parent 4508a67 commit eff814b

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""Unit tests for chat routing helpers."""
2+
3+
from src.bot.features.chat_routing import (
4+
HISTORY_CONTEXT_SIZE,
5+
MAX_BUFFER_SIZE,
6+
append_to_buffer,
7+
build_group_prompt,
8+
format_history,
9+
get_working_directory,
10+
is_group_triggered,
11+
strip_group_trigger_prefix,
12+
)
13+
from src.config import create_test_config
14+
15+
16+
def test_append_to_buffer_adds_message() -> None:
17+
"""Messages are appended in sender/text form."""
18+
buffer: list[dict[str, str]] = []
19+
20+
append_to_buffer(buffer, "Alice", "hello")
21+
22+
assert buffer == [{"sender": "Alice", "text": "hello"}]
23+
24+
25+
def test_append_to_buffer_trims_oldest_messages() -> None:
26+
"""The buffer keeps only the most recent MAX_BUFFER_SIZE messages."""
27+
buffer: list[dict[str, str]] = []
28+
29+
for idx in range(MAX_BUFFER_SIZE + 5):
30+
append_to_buffer(buffer, f"User {idx}", f"msg {idx}")
31+
32+
assert len(buffer) == MAX_BUFFER_SIZE
33+
assert buffer[0] == {"sender": "User 5", "text": "msg 5"}
34+
assert buffer[-1] == {
35+
"sender": f"User {MAX_BUFFER_SIZE + 4}",
36+
"text": f"msg {MAX_BUFFER_SIZE + 4}",
37+
}
38+
39+
40+
def test_format_history_handles_empty_messages() -> None:
41+
"""Formatting an empty history returns an empty string."""
42+
assert format_history([]) == ""
43+
44+
45+
def test_format_history_formats_multiple_messages() -> None:
46+
"""History lines are rendered as Sender: text."""
47+
messages = [
48+
{"sender": "Alice", "text": "Hello"},
49+
{"sender": "Bob", "text": "World"},
50+
]
51+
52+
assert format_history(messages) == "Alice: Hello\nBob: World"
53+
54+
55+
def test_get_working_directory_prefers_personal_chat(tmp_path) -> None:
56+
"""Personal-chat mapping wins when the chat ID matches."""
57+
personal_dir = tmp_path / "personal"
58+
personal_dir.mkdir()
59+
group_dir = tmp_path / "group"
60+
group_dir.mkdir()
61+
settings = create_test_config(
62+
approved_directory=str(tmp_path),
63+
personal_chat_id=123,
64+
personal_chat_directory=str(personal_dir),
65+
group_chat_id=-100,
66+
group_chat_directory=str(group_dir),
67+
)
68+
69+
assert get_working_directory(123, settings) == personal_dir.resolve()
70+
71+
72+
def test_get_working_directory_uses_group_chat_directory(tmp_path) -> None:
73+
"""Group-chat mapping is used when the group chat matches."""
74+
group_dir = tmp_path / "group"
75+
group_dir.mkdir()
76+
settings = create_test_config(
77+
approved_directory=str(tmp_path),
78+
group_chat_id=-100,
79+
group_chat_directory=str(group_dir),
80+
)
81+
82+
assert get_working_directory(-100, settings) == group_dir.resolve()
83+
84+
85+
def test_get_working_directory_falls_back_to_approved_directory(tmp_path) -> None:
86+
"""Unknown chats use the global approved directory."""
87+
settings = create_test_config(approved_directory=str(tmp_path))
88+
89+
assert get_working_directory(999, settings) == tmp_path.resolve()
90+
91+
92+
def test_is_group_triggered_matches_plain_prefix() -> None:
93+
"""The plain prefix triggers with and without trailing text."""
94+
assert is_group_triggered("claude", "claude") is True
95+
assert is_group_triggered("claude summarize this", "claude") is True
96+
97+
98+
def test_is_group_triggered_matches_slash_prefix_variants() -> None:
99+
"""Slash commands trigger in both plain and @botname forms."""
100+
assert is_group_triggered("/claude", "claude") is True
101+
assert is_group_triggered("/claude summarize this", "claude") is True
102+
assert is_group_triggered("/claude@test_bot", "claude") is True
103+
assert is_group_triggered("/claude@test_bot summarize this", "claude") is True
104+
105+
106+
def test_is_group_triggered_rejects_non_matching_messages() -> None:
107+
"""Messages without the configured prefix do not trigger."""
108+
assert is_group_triggered("please ask claude", "claude") is False
109+
assert is_group_triggered("/other@test_bot summarize this", "claude") is False
110+
111+
112+
def test_strip_group_trigger_prefix_handles_plain_prefix() -> None:
113+
"""Plain-prefix messages are stripped down to their payload."""
114+
assert strip_group_trigger_prefix("claude", "claude") == ""
115+
assert (
116+
strip_group_trigger_prefix("claude summarize this", "claude")
117+
== "summarize this"
118+
)
119+
120+
121+
def test_strip_group_trigger_prefix_handles_slash_prefix() -> None:
122+
"""Slash commands strip both the slash and any @botname suffix."""
123+
assert strip_group_trigger_prefix("/claude", "claude") == ""
124+
assert (
125+
strip_group_trigger_prefix("/claude summarize this", "claude")
126+
== "summarize this"
127+
)
128+
assert strip_group_trigger_prefix("/claude@test_bot", "claude") == ""
129+
assert (
130+
strip_group_trigger_prefix("/claude@test_bot summarize this", "claude")
131+
== "summarize this"
132+
)
133+
134+
135+
def test_build_group_prompt_returns_stripped_text_without_history() -> None:
136+
"""Triggered messages without history do not get a history wrapper."""
137+
assert (
138+
build_group_prompt([], "claude summarize this", "claude") == "summarize this"
139+
)
140+
141+
142+
def test_build_group_prompt_injects_recent_history() -> None:
143+
"""History is prepended ahead of the stripped group prompt."""
144+
history = [
145+
{"sender": "Alice", "text": "First"},
146+
{"sender": "Bob", "text": "Second"},
147+
]
148+
149+
prompt = build_group_prompt(history, "claude summarize this", "claude")
150+
151+
assert prompt == (
152+
"[Recent group conversation:\nAlice: First\nBob: Second\n]\n\n"
153+
"summarize this"
154+
)
155+
156+
157+
def test_build_group_prompt_limits_history_to_context_window() -> None:
158+
"""Only the last HISTORY_CONTEXT_SIZE entries are injected."""
159+
history = [
160+
{"sender": f"User {idx}", "text": f"msg {idx}"}
161+
for idx in range(HISTORY_CONTEXT_SIZE + 3)
162+
]
163+
164+
prompt = build_group_prompt(history, "claude summarize this", "claude")
165+
166+
assert "User 0: msg 0" not in prompt
167+
assert "User 1: msg 1" not in prompt
168+
assert "User 2: msg 2" not in prompt
169+
assert f"User 3: msg 3" in prompt
170+
assert prompt.endswith("]\n\nsummarize this")

0 commit comments

Comments
 (0)