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