Files
echo-core/tests/test_fast_commands.py
MoltBot Service c8ce94611b feat: add 19 fast commands (no-LLM) + incremental embeddings indexing
Fast commands for git, email, calendar, notes, search, reminders, and
diagnostics — all execute instantly without Claude CLI. Incremental
embeddings indexing in heartbeat (1h cooldown) + inline indexing after
/note, /jurnal, /email save. Fix Ollama URL (localhost → 10.0.20.161),
fix email_process.py KB path (kb/ → memory/kb/).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:10:44 +00:00

510 lines
16 KiB
Python

"""Tests for src/fast_commands.py — fast (non-LLM) commands."""
import json
import pytest
from datetime import date, datetime
from pathlib import Path
from unittest.mock import patch, MagicMock
from src.fast_commands import (
dispatch,
cmd_commit,
cmd_push,
cmd_pull,
cmd_test,
cmd_email,
cmd_calendar,
cmd_note,
cmd_jurnal,
cmd_search,
cmd_kb,
cmd_remind,
cmd_logs,
cmd_doctor,
cmd_heartbeat,
cmd_help,
COMMANDS,
_auto_commit_message,
)
# --- Dispatch ---
class TestDispatch:
def test_known_command(self):
"""Known commands return a string."""
result = dispatch("help", [])
assert result is not None
assert "Comenzi disponibile" in result
def test_unknown_command(self):
"""Unknown commands return None."""
assert dispatch("nonexistent", []) is None
def test_all_commands_registered(self):
expected = {
"commit", "push", "pull", "test", "email", "calendar",
"note", "jurnal", "search", "kb", "remind", "logs",
"doctor", "heartbeat", "help",
}
assert set(COMMANDS.keys()) == expected
def test_handler_exception_caught(self):
"""Exceptions in handlers are caught and returned as error strings."""
with patch.dict(COMMANDS, {"boom": lambda args: 1 / 0}):
result = dispatch("boom", [])
assert result is not None
assert "Error in /boom" in result
# --- Git ---
class TestGitCommit:
@patch("src.fast_commands._git")
def test_nothing_to_commit(self, mock_git):
mock_git.return_value = ""
assert cmd_commit([]) == "Nothing to commit."
@patch("src.fast_commands._git")
def test_commit_with_custom_message(self, mock_git):
mock_git.side_effect = [
" M file.py\n", # status --porcelain
"", # add -A
"", # commit -m
"abc1234", # rev-parse --short HEAD
]
result = cmd_commit(["fix", "bug"])
assert "abc1234" in result
assert "fix bug" in result
assert "1 files" in result
@patch("src.fast_commands._git")
def test_commit_auto_message(self, mock_git):
mock_git.side_effect = [
" M src/router.py\n?? newfile.txt\n", # status
"", # add -A
"", # commit -m
"def5678", # rev-parse
]
result = cmd_commit([])
assert "def5678" in result
assert "2 files" in result
class TestGitPush:
@patch("src.fast_commands._git")
def test_push_with_uncommitted(self, mock_git):
mock_git.return_value = " M dirty.py\n"
result = cmd_push([])
assert "uncommitted" in result.lower()
@patch("src.fast_commands._git")
def test_push_up_to_date(self, mock_git):
mock_git.side_effect = [
"", # status --porcelain
"Everything up-to-date", # push
]
result = cmd_push([])
assert "up to date" in result.lower()
@patch("src.fast_commands._git")
def test_push_success(self, mock_git):
mock_git.side_effect = [
"", # status --porcelain
"abc..def master -> master", # push
"master", # branch --show-current
]
result = cmd_push([])
assert "origin/master" in result
class TestGitPull:
@patch("src.fast_commands._git")
def test_pull_up_to_date(self, mock_git):
mock_git.return_value = "Already up to date."
assert "up to date" in cmd_pull([]).lower()
@patch("src.fast_commands._git")
def test_pull_with_changes(self, mock_git):
mock_git.return_value = "Fast-forward\n file.py | 2 +-"
result = cmd_pull([])
assert "Pulled" in result
# --- Test ---
class TestTestCmd:
@patch("subprocess.run")
def test_basic_run(self, mock_run):
mock_run.return_value = MagicMock(
stdout="5 passed in 1.2s",
stderr="",
returncode=0,
)
result = cmd_test([])
assert "passed" in result
@patch("subprocess.run")
def test_with_pattern(self, mock_run):
mock_run.return_value = MagicMock(
stdout="2 passed",
stderr="",
returncode=0,
)
cmd_test(["router"])
args = mock_run.call_args[0][0]
assert "-k" in args
assert "router" in args
@patch("subprocess.run")
def test_timeout(self, mock_run):
import subprocess as sp
mock_run.side_effect = sp.TimeoutExpired(cmd="pytest", timeout=120)
result = cmd_test([])
assert "timed out" in result.lower()
# --- Email ---
class TestEmail:
@patch("subprocess.run")
def test_email_check_clean(self, mock_run):
mock_run.return_value = MagicMock(
stdout=json.dumps({"ok": True, "unread_count": 0, "emails": []}),
stderr="",
returncode=0,
)
result = cmd_email([])
assert "curat" in result.lower()
@patch("subprocess.run")
def test_email_check_unread(self, mock_run):
mock_run.return_value = MagicMock(
stdout=json.dumps({
"ok": True,
"unread_count": 2,
"emails": [
{"subject": "Hello"},
{"subject": "Meeting"},
],
}),
stderr="",
returncode=0,
)
result = cmd_email([])
assert "2 necitite" in result
assert "Hello" in result
def test_email_unknown_sub(self):
result = cmd_email(["foo"])
assert "Unknown email sub-command" in result
@patch("subprocess.run")
def test_email_send(self, mock_run):
mock_run.return_value = MagicMock(
stdout=json.dumps({"ok": True}),
stderr="",
returncode=0,
)
result = cmd_email(["send", "test@x.com", "Subject", "::", "Body"])
assert "trimis" in result.lower()
def test_email_send_bad_format(self):
result = cmd_email(["send", "no-separator"])
assert "Format" in result
@patch("subprocess.run")
def test_email_save_nothing(self, mock_run):
mock_run.return_value = MagicMock(
stdout="Niciun email nou de la adrese whitelisted.",
stderr="",
returncode=0,
)
result = cmd_email(["save"])
assert "Nimic de salvat" in result
# --- Calendar ---
class TestCalendar:
@patch("subprocess.run")
def test_calendar_today(self, mock_run):
mock_run.return_value = MagicMock(
stdout=json.dumps({
"today": [{"time": "09:00", "summary": "Standup"}],
"tomorrow": [],
}),
stderr="",
returncode=0,
)
result = cmd_calendar([])
assert "Standup" in result
assert "09:00" in result
@patch("subprocess.run")
def test_calendar_week(self, mock_run):
mock_run.return_value = MagicMock(
stdout=json.dumps({
"week_start": "10 Feb",
"week_end": "16 Feb",
"events": [{"date": "10 Feb", "time": "10:00", "summary": "Review"}],
}),
stderr="",
returncode=0,
)
result = cmd_calendar(["week"])
assert "Review" in result
@patch("subprocess.run")
def test_calendar_busy_free(self, mock_run):
mock_run.return_value = MagicMock(
stdout=json.dumps({"busy": False}),
stderr="",
returncode=0,
)
result = cmd_calendar(["busy"])
assert "Liber" in result
@patch("subprocess.run")
def test_calendar_busy_occupied(self, mock_run):
mock_run.return_value = MagicMock(
stdout=json.dumps({"busy": True, "event": "Meeting", "ends": "16:30"}),
stderr="",
returncode=0,
)
result = cmd_calendar(["busy"])
assert "Ocupat" in result
assert "Meeting" in result
def test_calendar_unknown_sub(self):
result = cmd_calendar(["foo"])
assert "Unknown calendar sub-command" in result
# --- Notes ---
class TestNote:
def test_note_empty(self):
assert "Usage" in cmd_note([])
def test_note_creates_file(self, tmp_path):
with patch("src.fast_commands.MEMORY_DIR", tmp_path):
result = cmd_note(["test", "note"])
assert result == "Notat."
today = date.today().isoformat()
filepath = tmp_path / f"{today}.md"
assert filepath.exists()
content = filepath.read_text()
assert "test note" in content
assert f"# {today}" in content
def test_note_appends(self, tmp_path):
today = date.today().isoformat()
filepath = tmp_path / f"{today}.md"
filepath.write_text(f"# {today}\n\n- existing\n")
with patch("src.fast_commands.MEMORY_DIR", tmp_path):
cmd_note(["new", "note"])
content = filepath.read_text()
assert "existing" in content
assert "new note" in content
class TestJurnal:
def test_jurnal_empty(self):
assert "Usage" in cmd_jurnal([])
def test_jurnal_creates_file(self, tmp_path):
with patch("src.fast_commands.MEMORY_DIR", tmp_path):
result = cmd_jurnal(["my", "entry"])
assert result == "Jurnal actualizat."
today = date.today().isoformat()
filepath = tmp_path / f"{today}.md"
content = filepath.read_text()
assert "## Jurnal" in content
assert "my entry" in content
def test_jurnal_adds_section_if_missing(self, tmp_path):
today = date.today().isoformat()
filepath = tmp_path / f"{today}.md"
filepath.write_text(f"# {today}\n\n- some note\n")
with patch("src.fast_commands.MEMORY_DIR", tmp_path):
cmd_jurnal(["entry"])
content = filepath.read_text()
assert "## Jurnal" in content
assert "entry" in content
# --- Search ---
class TestSearch:
def test_search_empty(self):
assert "Usage" in cmd_search([])
@patch("src.memory_search.search")
def test_search_no_results(self, mock_search):
mock_search.return_value = []
result = cmd_search(["query"])
assert "Niciun rezultat" in result
@patch("src.memory_search.search")
def test_search_with_results(self, mock_search):
mock_search.return_value = [
{"file": "notes.md", "chunk": "important info here", "score": 0.92},
]
result = cmd_search(["important"])
assert "Rezultate" in result
assert "notes.md" in result
assert "0.92" in result
# --- KB ---
class TestKB:
def test_kb_no_index(self, tmp_path):
with patch("src.fast_commands.MEMORY_DIR", tmp_path):
result = cmd_kb([])
assert "not found" in result.lower()
def test_kb_list(self, tmp_path):
kb_dir = tmp_path / "kb"
kb_dir.mkdir()
index = {
"notes": [
{"title": "Note 1", "date": "2026-02-15", "category": "insights"},
{"title": "Note 2", "date": "2026-02-14", "category": "projects"},
]
}
(kb_dir / "index.json").write_text(json.dumps(index))
with patch("src.fast_commands.MEMORY_DIR", tmp_path):
result = cmd_kb([])
assert "Note 1" in result
assert "Note 2" in result
def test_kb_filter_category(self, tmp_path):
kb_dir = tmp_path / "kb"
kb_dir.mkdir()
index = {
"notes": [
{"title": "Note A", "date": "2026-02-15", "category": "insights"},
{"title": "Note B", "date": "2026-02-14", "category": "projects"},
]
}
(kb_dir / "index.json").write_text(json.dumps(index))
with patch("src.fast_commands.MEMORY_DIR", tmp_path):
result = cmd_kb(["insights"])
assert "Note A" in result
assert "Note B" not in result
def test_kb_filter_no_match(self, tmp_path):
kb_dir = tmp_path / "kb"
kb_dir.mkdir()
index = {"notes": [{"title": "X", "date": "2026-01-01", "category": "a"}]}
(kb_dir / "index.json").write_text(json.dumps(index))
with patch("src.fast_commands.MEMORY_DIR", tmp_path):
result = cmd_kb(["nonexistent"])
assert "Nicio nota" in result
# --- Remind ---
class TestRemind:
def test_remind_too_few_args(self):
assert "Usage" in cmd_remind([])
assert "Usage" in cmd_remind(["15:00"])
def test_remind_bad_time(self):
result = cmd_remind(["nottime", "test"])
assert "Invalid time" in result
@patch("src.fast_commands.TOOLS_DIR", Path("/tmp/fake_tools"))
def test_remind_with_date(self):
with patch.dict("sys.modules", {
"calendar_check": MagicMock(
create_event=MagicMock(return_value={"id": "123"})
),
}):
result = cmd_remind(["2026-03-01", "15:00", "Test", "reminder"])
assert "Reminder setat" in result
assert "15:00" in result
# --- Logs ---
class TestLogs:
def test_logs_no_file(self, tmp_path):
with patch("src.fast_commands.LOGS_DIR", tmp_path):
result = cmd_logs([])
assert "No log file" in result
def test_logs_default(self, tmp_path):
log_file = tmp_path / "echo-core.log"
lines = [f"line {i}" for i in range(30)]
log_file.write_text("\n".join(lines))
with patch("src.fast_commands.LOGS_DIR", tmp_path):
result = cmd_logs([])
assert "line 29" in result
assert "line 10" in result
assert "line 0" not in result
def test_logs_custom_count(self, tmp_path):
log_file = tmp_path / "echo-core.log"
lines = [f"line {i}" for i in range(30)]
log_file.write_text("\n".join(lines))
with patch("src.fast_commands.LOGS_DIR", tmp_path):
result = cmd_logs(["5"])
assert "line 29" in result
assert "line 20" not in result
# --- Doctor ---
class TestDoctor:
@patch("shutil.which", return_value="/usr/bin/claude")
def test_doctor_runs(self, mock_which):
result = cmd_doctor([])
assert "Doctor:" in result
assert "Claude CLI" in result
# --- Help ---
class TestHelp:
def test_help_lists_commands(self):
result = cmd_help([])
assert "/commit" in result
assert "/email" in result
assert "/calendar" in result
assert "/search" in result
assert "/help" in result
assert "/clear" in result
# --- Auto commit message ---
class TestAutoCommitMessage:
def test_single_modified(self):
msg = _auto_commit_message([" M src/router.py"])
assert "src" in msg
assert "~1" in msg
def test_mixed_changes(self):
files = [
" M src/a.py",
"?? tests/new.py",
" D old.py",
]
msg = _auto_commit_message(files)
assert "+1" in msg
assert "~1" in msg
assert "-1" in msg
def test_many_areas(self):
files = [
" M a/x.py",
" M b/y.py",
" M c/z.py",
" M d/w.py",
]
msg = _auto_commit_message(files)
assert "+1 more" in msg