Task #1 — Rate limit budget tracking MVP: - tools/ralph_usage.py: pure functions (extract_usage_entry, parse_usage_jsonl, aggregate_by_day/_project, filter_by_days, summarize) + CLI append/summarize subcommands. Atomic write via temp+rename. - tools/ralph/ralph.sh: după fiecare claude -p, append usage entry derivat din JSON envelope la <project>/scripts/ralph/usage.jsonl. Best-effort, niciodată blochează rularea (|| true). - dashboard/handlers/ralph.py: GET /api/ralph/usage[?days=N] aggregează cross- project și returnează {today_cost, today_runs, by_project, by_day, ...}. Task #2 — WhatsApp text-keyword commands: - src/router.py: helper _translate_whatsapp_text mapează "aprob"/"stop <slug>"/ "stare [<slug>]" → /a, /k, /l. Apelat DOAR pe adapter whatsapp în _try_ralph_dispatch (Discord/TG nu sunt afectate). NU acoperim propose intentionat — descrierea liberă e prea fragilă pentru parsing text-only. Tests: 49 noi (test_ralph_usage 28 + test_whatsapp_keywords 21) + 4 noi în test_dashboard_ralph_endpoint pentru /api/ralph/usage. Toate trec; regression suite (test_router, test_router_planning, test_dashboard_ralph_endpoint, test_whatsapp) — 90/90 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
5.6 KiB
Python
140 lines
5.6 KiB
Python
"""Tests for WhatsApp text-keyword commands → slash translation.
|
|
|
|
Acoperă `_translate_whatsapp_text` și integrarea cu `_try_ralph_dispatch`:
|
|
- aprob / aprob <slug>
|
|
- stop <slug>
|
|
- stare / stare <slug>
|
|
- case-insensitive pe keyword
|
|
- Discord/Telegram NU sunt afectate
|
|
- propose intentionally NOT supported
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from src.router import _translate_whatsapp_text, _try_ralph_dispatch
|
|
|
|
|
|
# ── _translate_whatsapp_text (pure helper) ────────────────────────
|
|
|
|
|
|
class TestTranslate:
|
|
def test_aprob_alone_lists_pending(self):
|
|
# `aprob` fără slug → /a (listează pending)
|
|
assert _translate_whatsapp_text("aprob") == "/a"
|
|
|
|
def test_aprob_with_slug(self):
|
|
assert _translate_whatsapp_text("aprob roa2web") == "/a roa2web"
|
|
|
|
def test_aprob_case_insensitive(self):
|
|
assert _translate_whatsapp_text("APROB roa2web") == "/a roa2web"
|
|
assert _translate_whatsapp_text("Aprob roa2web") == "/a roa2web"
|
|
|
|
def test_stop_with_slug(self):
|
|
assert _translate_whatsapp_text("stop roa2web") == "/k roa2web"
|
|
|
|
def test_stop_case_insensitive(self):
|
|
assert _translate_whatsapp_text("STOP roa2web") == "/k roa2web"
|
|
|
|
def test_stop_alone_not_translated(self):
|
|
# `stop` fără slug poate fi colocvial → nu translatăm
|
|
assert _translate_whatsapp_text("stop") is None
|
|
|
|
def test_stare_alone(self):
|
|
assert _translate_whatsapp_text("stare") == "/l"
|
|
|
|
def test_stare_with_slug(self):
|
|
assert _translate_whatsapp_text("stare roa2web") == "/l roa2web"
|
|
|
|
def test_stare_case_insensitive(self):
|
|
assert _translate_whatsapp_text("STARE") == "/l"
|
|
|
|
def test_other_text_not_translated(self):
|
|
assert _translate_whatsapp_text("hello") is None
|
|
assert _translate_whatsapp_text("ce mai faci") is None
|
|
assert _translate_whatsapp_text("propose roa2web descriere") is None
|
|
# Slash commands pass through unchanged (None — don't override)
|
|
assert _translate_whatsapp_text("/a") is None
|
|
|
|
def test_empty_input(self):
|
|
assert _translate_whatsapp_text("") is None
|
|
assert _translate_whatsapp_text(" ") is None
|
|
|
|
def test_propose_not_covered(self):
|
|
# Verifică explicit că nu acoperim propose (descrierea fragilă)
|
|
assert _translate_whatsapp_text("propose foo bar baz") is None
|
|
assert _translate_whatsapp_text("propune foo bar baz") is None
|
|
|
|
|
|
# ── Integration: _try_ralph_dispatch with adapter_name ────────────
|
|
|
|
|
|
class TestDispatchIntegration:
|
|
def test_whatsapp_aprob_routes_to_approve(self):
|
|
# `aprob` pe whatsapp → trebuie să intre în Ralph dispatch
|
|
with patch("src.router._ralph_approve") as mock:
|
|
mock.return_value = "ok"
|
|
result = _try_ralph_dispatch("aprob foo", adapter_name="whatsapp")
|
|
assert result == "ok"
|
|
mock.assert_called_once_with(["foo"])
|
|
|
|
def test_whatsapp_stop_routes_to_stop(self):
|
|
with patch("src.router._ralph_stop") as mock:
|
|
mock.return_value = "stopped"
|
|
result = _try_ralph_dispatch("stop foo", adapter_name="whatsapp")
|
|
assert result == "stopped"
|
|
mock.assert_called_once_with("foo")
|
|
|
|
def test_whatsapp_stare_routes_to_status(self):
|
|
with patch("src.router._ralph_status") as mock:
|
|
mock.return_value = "status"
|
|
result = _try_ralph_dispatch("stare", adapter_name="whatsapp")
|
|
# Status returnează cu redirect hint pe whatsapp
|
|
assert "status" in result
|
|
mock.assert_called_once_with(None)
|
|
|
|
def test_whatsapp_stare_with_slug(self):
|
|
with patch("src.router._ralph_status") as mock:
|
|
mock.return_value = "status"
|
|
_try_ralph_dispatch("stare roa2web", adapter_name="whatsapp")
|
|
mock.assert_called_once_with("roa2web")
|
|
|
|
def test_discord_keyword_not_translated(self):
|
|
# Pe Discord, "stop foo" NU ar trebui să match — nu e adapter whatsapp
|
|
with patch("src.router._ralph_stop") as mock:
|
|
result = _try_ralph_dispatch("stop foo", adapter_name="discord")
|
|
assert result is None
|
|
mock.assert_not_called()
|
|
|
|
def test_telegram_keyword_not_translated(self):
|
|
with patch("src.router._ralph_approve") as mock:
|
|
result = _try_ralph_dispatch("aprob foo", adapter_name="telegram")
|
|
assert result is None
|
|
mock.assert_not_called()
|
|
|
|
def test_no_adapter_keyword_not_translated(self):
|
|
# adapter_name=None → nu e whatsapp → no translation
|
|
with patch("src.router._ralph_approve") as mock:
|
|
result = _try_ralph_dispatch("aprob foo", adapter_name=None)
|
|
assert result is None
|
|
mock.assert_not_called()
|
|
|
|
def test_whatsapp_slash_command_still_works(self):
|
|
# Slash-uri normale pe WhatsApp NU trebuie sparte de translation
|
|
with patch("src.router._ralph_approve") as mock:
|
|
mock.return_value = "ok"
|
|
result = _try_ralph_dispatch("/a foo", adapter_name="whatsapp")
|
|
assert result == "ok"
|
|
mock.assert_called_once_with(["foo"])
|
|
|
|
def test_whatsapp_chat_message_passthrough(self):
|
|
# Mesajul normal pe whatsapp (fără keyword) → None (cade pe Claude)
|
|
result = _try_ralph_dispatch("hello echo, ce mai faci", adapter_name="whatsapp")
|
|
assert result is None
|