feat(ralph): rate limit budget tracking + whatsapp text-keywords
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>
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 ───────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user