Merge branch 'ralph/instrumentation' — rate limit budget + WhatsApp keywords
Rate limit budget tracking (TODO P2): - tools/ralph_usage.py — pure functions extract/parse/aggregate; CLI subcomenzi append/summarize. Atomic write JSONL. - tools/ralph/ralph.sh: după fiecare claude -p, append usage entry la workspace/<slug>/scripts/ralph/usage.jsonl (best-effort) - dashboard/handlers/ralph.py: GET /api/ralph/usage[?days=N] cross-project aggregation cu today_cost, today_runs, by_project, by_day WhatsApp text-keyword commands (TODO P3): - src/router.py: helper _translate_whatsapp_text — `aprob <slug>` → `/a <slug>`, `stop <slug>` → `/k <slug>`, `stare`/`stare <slug>` → `/l`/`/l <slug>`. Aplicat DOAR pe adapter whatsapp în _try_ralph_dispatch (Discord/TG nu sunt afectate). Propose intentionally NOT covered (descrierea fragilă). Tests: 53 noi (28 ralph_usage + 21 whatsapp_keywords + 4 dashboard endpoint extend) + 0 regressions pe modulele atinse.
This commit is contained in:
@@ -186,6 +186,54 @@ class TestPrd:
|
||||
assert handler.captured_code == 400
|
||||
|
||||
|
||||
# ── /api/ralph/usage ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestUsageEndpoint:
|
||||
def test_usage_empty_workspace(self, handler):
|
||||
handler.path = "/api/ralph/usage"
|
||||
handler.handle_ralph_usage()
|
||||
assert handler.captured_code == 200
|
||||
assert handler.captured["today_runs"] == 0
|
||||
assert handler.captured["total_runs"] == 0
|
||||
assert handler.captured["by_project"] == {}
|
||||
|
||||
def test_usage_aggregates_across_projects(self, handler, tmp_path):
|
||||
# Create two projects, each with usage.jsonl
|
||||
for slug, cost, ts in [("proj-a", 0.5, "2026-04-26T10:00:00+00:00"),
|
||||
("proj-b", 0.3, "2026-04-26T11:00:00+00:00")]:
|
||||
ralph_dir = tmp_path / slug / "scripts" / "ralph"
|
||||
ralph_dir.mkdir(parents=True)
|
||||
(ralph_dir / "usage.jsonl").write_text(
|
||||
json.dumps({"slug": slug, "ts": ts, "total_cost_usd": cost,
|
||||
"input_tokens": 100, "output_tokens": 50, "cache_read": 0}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
handler.path = "/api/ralph/usage?days=30"
|
||||
handler.handle_ralph_usage()
|
||||
assert handler.captured_code == 200
|
||||
# Should have both projects
|
||||
assert "proj-a" in handler.captured["by_project"]
|
||||
assert "proj-b" in handler.captured["by_project"]
|
||||
assert handler.captured["total_runs"] == 2
|
||||
assert handler.captured["window_runs"] == 2
|
||||
|
||||
def test_usage_invalid_days_falls_back(self, handler):
|
||||
handler.path = "/api/ralph/usage?days=abc"
|
||||
handler.handle_ralph_usage()
|
||||
assert handler.captured_code == 200
|
||||
assert handler.captured["window_days"] == 7
|
||||
|
||||
def test_usage_handles_corrupt_jsonl(self, handler, tmp_path):
|
||||
# Project with corrupt usage.jsonl shouldn't 500
|
||||
ralph_dir = tmp_path / "broken" / "scripts" / "ralph"
|
||||
ralph_dir.mkdir(parents=True)
|
||||
(ralph_dir / "usage.jsonl").write_text("not json\n", encoding="utf-8")
|
||||
handler.path = "/api/ralph/usage"
|
||||
handler.handle_ralph_usage()
|
||||
assert handler.captured_code == 200
|
||||
|
||||
|
||||
# ── _ralph_validate_slug ───────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
366
tests/test_ralph_usage.py
Normal file
366
tests/test_ralph_usage.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""Tests for tools/ralph_usage.py — rate limit budget tracking.
|
||||
|
||||
Acoperă:
|
||||
- extract_usage_entry: shape corect, missing fields, JSON corupt → None
|
||||
- parse_usage_jsonl: skip linii corupte, file lipsă → []
|
||||
- aggregate_by_day / aggregate_by_project: sume corecte, deduplicare
|
||||
- filter_by_days: window inclusiv vs exclusiv
|
||||
- summarize: today_cost/today_runs corecte
|
||||
- append_entry: atomic write, JSONL roundtrip
|
||||
- CLI append: idempotent la JSON corupt (no-op + exit 0)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOLS = PROJECT_ROOT / "tools"
|
||||
if str(TOOLS) not in sys.path:
|
||||
sys.path.insert(0, str(TOOLS))
|
||||
|
||||
import ralph_usage # noqa: E402
|
||||
|
||||
|
||||
# ── Sample claude -p --output-format json envelopes ────────────────
|
||||
|
||||
|
||||
def _claude_envelope(
|
||||
*,
|
||||
cost: float = 0.55,
|
||||
input_tokens: int = 1234,
|
||||
output_tokens: int = 567,
|
||||
cache_read: int = 890,
|
||||
duration_ms: int = 49000,
|
||||
model: str = "claude-opus-4-7-20260101",
|
||||
) -> dict:
|
||||
return {
|
||||
"type": "result",
|
||||
"subtype": "completed",
|
||||
"session_id": "abc123",
|
||||
"result": "Story implementat",
|
||||
"is_error": False,
|
||||
"total_cost_usd": cost,
|
||||
"duration_ms": duration_ms,
|
||||
"num_turns": 5,
|
||||
"usage": {
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
"cache_creation_input_tokens": 0,
|
||||
"cache_read_input_tokens": cache_read,
|
||||
},
|
||||
"model": model,
|
||||
}
|
||||
|
||||
|
||||
# ── extract_usage_entry ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestExtractEntry:
|
||||
def test_full_envelope_extracts_all_fields(self):
|
||||
env = _claude_envelope()
|
||||
entry = ralph_usage.extract_usage_entry(
|
||||
env, slug="proj-a", story_id="US-001", iter_n=3,
|
||||
ts="2026-04-26T12:00:00+00:00",
|
||||
)
|
||||
assert entry == {
|
||||
"ts": "2026-04-26T12:00:00+00:00",
|
||||
"slug": "proj-a",
|
||||
"story_id": "US-001",
|
||||
"iter": 3,
|
||||
"total_cost_usd": 0.55,
|
||||
"input_tokens": 1234,
|
||||
"output_tokens": 567,
|
||||
"cache_read": 890,
|
||||
"model": "claude-opus-4-7-20260101",
|
||||
"duration_ms": 49000,
|
||||
}
|
||||
|
||||
def test_accepts_raw_string(self):
|
||||
env = _claude_envelope()
|
||||
entry = ralph_usage.extract_usage_entry(
|
||||
json.dumps(env), slug="x", story_id=None, iter_n=None,
|
||||
ts="2026-04-26T00:00:00+00:00",
|
||||
)
|
||||
assert entry is not None
|
||||
assert entry["story_id"] is None
|
||||
assert entry["iter"] is None
|
||||
assert entry["total_cost_usd"] == 0.55
|
||||
|
||||
def test_corrupt_json_returns_none(self):
|
||||
assert ralph_usage.extract_usage_entry("{not json", slug="x") is None
|
||||
assert ralph_usage.extract_usage_entry("", slug="x") is None
|
||||
assert ralph_usage.extract_usage_entry("null", slug="x") is None
|
||||
|
||||
def test_missing_usage_field_zeros(self):
|
||||
env = {"total_cost_usd": 0.1, "duration_ms": 1000}
|
||||
entry = ralph_usage.extract_usage_entry(env, slug="x")
|
||||
assert entry["input_tokens"] == 0
|
||||
assert entry["output_tokens"] == 0
|
||||
assert entry["cache_read"] == 0
|
||||
assert entry["model"] is None
|
||||
|
||||
def test_missing_cost_defaults_zero(self):
|
||||
env = {"usage": {"input_tokens": 100}}
|
||||
entry = ralph_usage.extract_usage_entry(env, slug="x")
|
||||
assert entry["total_cost_usd"] == 0.0
|
||||
assert entry["input_tokens"] == 100
|
||||
|
||||
def test_non_dict_returns_none(self):
|
||||
assert ralph_usage.extract_usage_entry([], slug="x") is None
|
||||
assert ralph_usage.extract_usage_entry(123, slug="x") is None
|
||||
|
||||
def test_alternative_cache_field_name(self):
|
||||
# Defensive: dacă viitor schema folosește `cache_read`
|
||||
env = {"usage": {"cache_read": 42}, "total_cost_usd": 0.1}
|
||||
entry = ralph_usage.extract_usage_entry(env, slug="x")
|
||||
assert entry["cache_read"] == 42
|
||||
|
||||
|
||||
# ── parse_usage_jsonl ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestParseJsonl:
|
||||
def test_file_missing_returns_empty(self, tmp_path):
|
||||
assert ralph_usage.parse_usage_jsonl(tmp_path / "ghost.jsonl") == []
|
||||
|
||||
def test_skips_corrupt_lines(self, tmp_path):
|
||||
p = tmp_path / "u.jsonl"
|
||||
p.write_text(
|
||||
'{"slug": "a", "ts": "2026-04-26T00:00:00+00:00", "total_cost_usd": 0.1}\n'
|
||||
"{not json}\n"
|
||||
'{"slug": "b", "ts": "2026-04-26T01:00:00+00:00", "total_cost_usd": 0.2}\n'
|
||||
"\n"
|
||||
"[]\n", # not a dict
|
||||
encoding="utf-8",
|
||||
)
|
||||
entries = ralph_usage.parse_usage_jsonl(p)
|
||||
slugs = [e["slug"] for e in entries]
|
||||
assert slugs == ["a", "b"]
|
||||
|
||||
def test_empty_file_returns_empty(self, tmp_path):
|
||||
p = tmp_path / "u.jsonl"
|
||||
p.write_text("", encoding="utf-8")
|
||||
assert ralph_usage.parse_usage_jsonl(p) == []
|
||||
|
||||
|
||||
# ── aggregate_by_day / aggregate_by_project ───────────────────────
|
||||
|
||||
|
||||
class TestAggregate:
|
||||
@pytest.fixture
|
||||
def entries(self):
|
||||
return [
|
||||
{"slug": "proj-a", "ts": "2026-04-26T10:00:00+00:00",
|
||||
"total_cost_usd": 0.5, "input_tokens": 100, "output_tokens": 50, "cache_read": 200},
|
||||
{"slug": "proj-a", "ts": "2026-04-26T11:00:00+00:00",
|
||||
"total_cost_usd": 0.3, "input_tokens": 80, "output_tokens": 30, "cache_read": 100},
|
||||
{"slug": "proj-b", "ts": "2026-04-25T22:00:00+00:00",
|
||||
"total_cost_usd": 1.2, "input_tokens": 500, "output_tokens": 200, "cache_read": 0},
|
||||
]
|
||||
|
||||
def test_aggregate_by_day(self, entries):
|
||||
result = ralph_usage.aggregate_by_day(entries)
|
||||
assert result["2026-04-26"]["cost_usd"] == 0.8
|
||||
assert result["2026-04-26"]["runs"] == 2
|
||||
assert result["2026-04-26"]["input_tokens"] == 180
|
||||
assert result["2026-04-26"]["output_tokens"] == 80
|
||||
assert result["2026-04-26"]["cache_read"] == 300
|
||||
assert result["2026-04-25"]["cost_usd"] == 1.2
|
||||
assert result["2026-04-25"]["runs"] == 1
|
||||
# Sortare descrescătoare în iteration order
|
||||
keys = list(result.keys())
|
||||
assert keys == ["2026-04-26", "2026-04-25"]
|
||||
|
||||
def test_aggregate_by_project(self, entries):
|
||||
result = ralph_usage.aggregate_by_project(entries)
|
||||
assert result["proj-a"]["cost_usd"] == 0.8
|
||||
assert result["proj-a"]["runs"] == 2
|
||||
assert result["proj-b"]["cost_usd"] == 1.2
|
||||
assert result["proj-b"]["runs"] == 1
|
||||
|
||||
def test_aggregate_handles_missing_slug(self):
|
||||
entries = [{"ts": "2026-04-26T00:00:00+00:00", "total_cost_usd": 0.1}]
|
||||
result = ralph_usage.aggregate_by_project(entries)
|
||||
assert "unknown" in result
|
||||
|
||||
def test_aggregate_handles_missing_ts(self):
|
||||
entries = [{"slug": "x", "total_cost_usd": 0.1}]
|
||||
# Missing ts → skipped from by_day
|
||||
result = ralph_usage.aggregate_by_day(entries)
|
||||
assert result == {}
|
||||
|
||||
def test_aggregate_empty_entries(self):
|
||||
assert ralph_usage.aggregate_by_day([]) == {}
|
||||
assert ralph_usage.aggregate_by_project([]) == {}
|
||||
|
||||
|
||||
# ── filter_by_days ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFilterByDays:
|
||||
def test_window_inclusive_today(self):
|
||||
entries = [
|
||||
{"ts": "2026-04-26T00:00:00+00:00", "slug": "a"},
|
||||
{"ts": "2026-04-25T00:00:00+00:00", "slug": "a"},
|
||||
{"ts": "2026-04-20T00:00:00+00:00", "slug": "a"},
|
||||
]
|
||||
kept = ralph_usage.filter_by_days(entries, 7, today="2026-04-26")
|
||||
# 7-day window inclusiv de la today: 2026-04-20 .. 2026-04-26
|
||||
slugs = [e["ts"][:10] for e in kept]
|
||||
assert slugs == ["2026-04-26", "2026-04-25", "2026-04-20"]
|
||||
|
||||
def test_window_exclusive_older(self):
|
||||
entries = [
|
||||
{"ts": "2026-04-26T00:00:00+00:00"},
|
||||
{"ts": "2026-04-19T00:00:00+00:00"}, # 7 days before today → exclus
|
||||
]
|
||||
kept = ralph_usage.filter_by_days(entries, 7, today="2026-04-26")
|
||||
assert len(kept) == 1
|
||||
assert kept[0]["ts"] == "2026-04-26T00:00:00+00:00"
|
||||
|
||||
def test_zero_days_empty(self):
|
||||
entries = [{"ts": "2026-04-26T00:00:00+00:00"}]
|
||||
assert ralph_usage.filter_by_days(entries, 0, today="2026-04-26") == []
|
||||
|
||||
def test_corrupt_ts_skipped(self):
|
||||
entries = [{"ts": "garbage"}]
|
||||
assert ralph_usage.filter_by_days(entries, 7, today="2026-04-26") == []
|
||||
|
||||
|
||||
# ── summarize ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestSummarize:
|
||||
def test_summary_shape_and_today_split(self):
|
||||
entries = [
|
||||
{"ts": "2026-04-26T10:00:00+00:00", "slug": "a", "total_cost_usd": 0.5,
|
||||
"input_tokens": 100, "output_tokens": 50, "cache_read": 0},
|
||||
{"ts": "2026-04-26T11:00:00+00:00", "slug": "a", "total_cost_usd": 0.3,
|
||||
"input_tokens": 80, "output_tokens": 30, "cache_read": 0},
|
||||
{"ts": "2026-04-25T00:00:00+00:00", "slug": "b", "total_cost_usd": 1.0,
|
||||
"input_tokens": 0, "output_tokens": 0, "cache_read": 0},
|
||||
]
|
||||
s = ralph_usage.summarize(entries, days=7, today="2026-04-26")
|
||||
assert s["today"] == "2026-04-26"
|
||||
assert s["today_cost"] == 0.8
|
||||
assert s["today_runs"] == 2
|
||||
assert s["window_days"] == 7
|
||||
assert s["window_runs"] == 3
|
||||
assert "by_project" in s
|
||||
assert "by_day" in s
|
||||
assert s["total_runs"] == 3
|
||||
assert s["by_project"]["a"]["runs"] == 2
|
||||
assert s["by_project"]["b"]["runs"] == 1
|
||||
|
||||
def test_summary_empty_entries(self):
|
||||
s = ralph_usage.summarize([], days=7, today="2026-04-26")
|
||||
assert s["today_cost"] == 0
|
||||
assert s["today_runs"] == 0
|
||||
assert s["by_project"] == {}
|
||||
assert s["by_day"] == {}
|
||||
assert s["total_runs"] == 0
|
||||
|
||||
|
||||
# ── append_entry ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestAppendEntry:
|
||||
def test_append_creates_file_with_jsonl_format(self, tmp_path):
|
||||
usage = tmp_path / "usage.jsonl"
|
||||
entry = {"slug": "x", "ts": "2026-04-26T00:00:00+00:00", "total_cost_usd": 0.1}
|
||||
ralph_usage.append_entry(usage, entry)
|
||||
text = usage.read_text(encoding="utf-8")
|
||||
assert text.endswith("\n")
|
||||
loaded = json.loads(text.strip())
|
||||
assert loaded == entry
|
||||
|
||||
def test_append_preserves_existing_entries(self, tmp_path):
|
||||
usage = tmp_path / "usage.jsonl"
|
||||
usage.write_text(
|
||||
'{"slug": "a", "ts": "2026-04-25T00:00:00+00:00", "total_cost_usd": 0.5}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
ralph_usage.append_entry(usage, {"slug": "b", "ts": "2026-04-26T00:00:00+00:00",
|
||||
"total_cost_usd": 0.3})
|
||||
entries = ralph_usage.parse_usage_jsonl(usage)
|
||||
assert len(entries) == 2
|
||||
assert entries[0]["slug"] == "a"
|
||||
assert entries[1]["slug"] == "b"
|
||||
|
||||
def test_append_handles_missing_trailing_newline(self, tmp_path):
|
||||
usage = tmp_path / "usage.jsonl"
|
||||
usage.write_text(
|
||||
'{"slug": "a", "ts": "2026-04-25T00:00:00+00:00"}', # no trailing \n
|
||||
encoding="utf-8",
|
||||
)
|
||||
ralph_usage.append_entry(usage, {"slug": "b", "ts": "2026-04-26T00:00:00+00:00"})
|
||||
entries = ralph_usage.parse_usage_jsonl(usage)
|
||||
assert [e["slug"] for e in entries] == ["a", "b"]
|
||||
|
||||
|
||||
# ── CLI: append subcommand ─────────────────────────────────────────
|
||||
|
||||
|
||||
class TestCliAppend:
|
||||
def test_append_from_log_file(self, tmp_path):
|
||||
log = tmp_path / "iter.log"
|
||||
log.write_text(json.dumps(_claude_envelope(cost=0.42)), encoding="utf-8")
|
||||
usage = tmp_path / "usage.jsonl"
|
||||
|
||||
rc = ralph_usage.main([
|
||||
"append", str(usage), str(log),
|
||||
"--slug", "proj-a",
|
||||
"--story-id", "US-001",
|
||||
"--iter", "3",
|
||||
])
|
||||
assert rc == 0
|
||||
entries = ralph_usage.parse_usage_jsonl(usage)
|
||||
assert len(entries) == 1
|
||||
e = entries[0]
|
||||
assert e["slug"] == "proj-a"
|
||||
assert e["story_id"] == "US-001"
|
||||
assert e["iter"] == 3
|
||||
assert e["total_cost_usd"] == 0.42
|
||||
|
||||
def test_append_corrupt_log_no_op(self, tmp_path):
|
||||
log = tmp_path / "iter.log"
|
||||
log.write_text("not json", encoding="utf-8")
|
||||
usage = tmp_path / "usage.jsonl"
|
||||
|
||||
rc = ralph_usage.main([
|
||||
"append", str(usage), str(log),
|
||||
"--slug", "proj-a",
|
||||
])
|
||||
# Idempotent: corrupt JSON → exit 0, no entry written
|
||||
assert rc == 0
|
||||
assert not usage.exists() or ralph_usage.parse_usage_jsonl(usage) == []
|
||||
|
||||
def test_append_missing_log_no_op(self, tmp_path):
|
||||
usage = tmp_path / "usage.jsonl"
|
||||
rc = ralph_usage.main([
|
||||
"append", str(usage), str(tmp_path / "missing.log"),
|
||||
"--slug", "x",
|
||||
])
|
||||
assert rc == 0
|
||||
|
||||
|
||||
# ── CLI: summarize subcommand ──────────────────────────────────────
|
||||
|
||||
|
||||
class TestCliSummarize:
|
||||
def test_summarize_outputs_json(self, tmp_path, capsys):
|
||||
usage = tmp_path / "usage.jsonl"
|
||||
usage.write_text(
|
||||
json.dumps({"slug": "x", "ts": "2026-04-26T00:00:00+00:00", "total_cost_usd": 0.5}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
rc = ralph_usage.main(["summarize", str(usage), "--days", "7"])
|
||||
assert rc == 0
|
||||
out = json.loads(capsys.readouterr().out)
|
||||
assert "today" in out
|
||||
assert "by_project" in out
|
||||
assert "by_day" in out
|
||||
139
tests/test_whatsapp_keywords.py
Normal file
139
tests/test_whatsapp_keywords.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user