from __future__ import annotations import json import subprocess from pathlib import Path import pytest from rein_aharness import mail_triage from rein_aharness.llm_connect_client import LLMConnectClient, LLMConnectError def _git_repo(tmp_path: Path) -> Path: repo = tmp_path / "binky-control" repo.mkdir() subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True) subprocess.run( ["git", "config", "user.email", "test@example.com"], cwd=repo, check=True, capture_output=True, ) subprocess.run( ["git", "config", "user.name", "test"], cwd=repo, check=True, capture_output=True, ) (repo / "mailmeta" / "reports").mkdir(parents=True) (repo / "mailmeta" / "mail-log.md").write_text( "# Mail Log\n\n## E-mail triage log\n\n" "| Date | Sender/Topic | Outcome |\n" "|------|--------------|---------|\n" "| 2026-07-01 | old@example.com: Old | ignore |\n", encoding="utf-8", ) csv_path = repo / "mailmeta" / "reports" / "email-channel-evidence-report-test.csv" csv_path.write_text( "mailbox_received_at,source_from,source_subject,detected_message_class," "normalized_event_type,assessment_category,assessment_subclass,confidence\n" "2026-07-21T10:00:00+00:00,stripe@stripe.com,Webhook failed,notification," "notification.fail,fail,fail.webhook,high\n" "2026-07-21T11:00:00+00:00,evil@phish.example,Urgent wire,human_reply," "interaction.reply_received,undef,undef,low\n", encoding="utf-8", ) subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True) subprocess.run( ["git", "commit", "-m", "init"], cwd=repo, check=True, capture_output=True, ) return repo def test_parse_triage_response_strips_fences() -> None: text = """```json {"log_entries": [{"date": "2026-07-21", "sender": "a", "subject": "b", "action": "log", "outcome": "noted"}]} ```""" entries = mail_triage.parse_triage_response(text) assert len(entries) == 1 assert entries[0].action == "log" assert entries[0].sender == "a" def test_parse_suspicious_forces_safe_outcome() -> None: entries = mail_triage.parse_triage_response( json.dumps( { "log_entries": [ { "date": "2026-07-21", "sender": "evil@x", "subject": "wire", "action": "suspicious", "outcome": "click link", } ] } ) ) assert "never acted" in entries[0].outcome.lower() def test_apply_log_entries_inserts_rows(tmp_path: Path) -> None: log = tmp_path / "mail-log.md" log.write_text( "## E-mail triage log\n\n" "| Date | Sender/Topic | Outcome |\n" "|------|--------------|---------|\n" "| 2026-01-01 | old | x |\n", encoding="utf-8", ) n = mail_triage.apply_log_entries( log, [ mail_triage.LogEntry( "2026-07-21", "stripe", "Webhook", "log", "actionable" ), mail_triage.LogEntry( "2026-07-21", "noise", "sale", "ignore", "skip" ), ], ) assert n == 1 text = log.read_text(encoding="utf-8") assert "stripe" in text assert "noise" not in text # new row above old assert text.index("2026-07-21") < text.index("2026-01-01") def test_run_mail_triage_with_mock_complete(tmp_path: Path, monkeypatch) -> None: repo = _git_repo(tmp_path) monkeypatch.setattr(mail_triage.hub, "post_progress_event", lambda **kw: True) def fake_complete(prompt: str) -> str: assert "stripe@stripe.com" in prompt assert "evil@phish.example" in prompt return json.dumps( { "log_entries": [ { "date": "2026-07-21", "sender": "stripe@stripe.com", "subject": "Webhook failed", "action": "queue", "outcome": "Webhook failures — OH cleanup", }, { "date": "2026-07-21", "sender": "evil@phish.example", "subject": "Urgent wire", "action": "suspicious", "outcome": "external unknown", }, ], "notes": "2 items", } ) result = mail_triage.run_mail_triage( repo, complete_fn=fake_complete, report_to_hub=True, commit=True, ) assert result.ok assert result.entries_applied == 2 assert result.committed log = (repo / "mailmeta" / "mail-log.md").read_text(encoding="utf-8") assert "Webhook" in log assert "Suspicious" in log def test_run_mail_triage_no_report(tmp_path: Path, monkeypatch) -> None: repo = tmp_path / "empty" repo.mkdir() (repo / "mailmeta" / "reports").mkdir(parents=True) monkeypatch.setattr(mail_triage.hub, "post_progress_event", lambda **kw: True) result = mail_triage.run_mail_triage(repo, complete_fn=lambda p: "{}") assert not result.ok assert "no CSV" in result.reason def test_llm_connect_client_complete(monkeypatch) -> None: import rein_aharness.llm_connect_client as mod class FakeResp: status_code = 200 def raise_for_status(self) -> None: return None def json(self): return {"content": "hello", "model": "test-model", "usage": {"total": 1}} def fake_post(url, json=None, timeout=None): # noqa: A002 assert url.endswith("/execute") assert "prompt" in json return FakeResp() monkeypatch.setattr(mod.httpx, "post", fake_post) client = LLMConnectClient("http://llm.test", timeout_seconds=5) out = client.complete("hi", model="m") assert out == "hello" assert client.last_response_metadata.get("model") == "test-model" class _ErrorResp: def __init__(self, status_code: int, body) -> None: self.status_code = status_code self._body = body def json(self): if isinstance(self._body, Exception): raise self._body return self._body def test_llm_connect_error_preserves_safe_provider_cause(monkeypatch) -> None: import rein_aharness.llm_connect_client as mod response = _ErrorResp( 502, { "error": "provider_api_error", "provider_status": 401, "provider": "openrouter", "model": "model-1", "message": "No auth credentials found", "api_key": "sk-must-not-appear", "raw_response": {"authorization": "Bearer secret"}, }, ) monkeypatch.setattr(mod.httpx, "post", lambda *args, **kwargs: response) with pytest.raises(LLMConnectError) as excinfo: LLMConnectClient("http://llm.test").complete("hi") text = str(excinfo.value) assert "HTTP 502" in text assert "error=provider_api_error" in text assert "provider_status=401" in text assert "provider=openrouter" in text assert "model=model-1" in text assert "message=No auth credentials found" in text assert "sk-must-not-appear" not in text assert "authorization" not in text def test_llm_connect_error_message_is_bounded(monkeypatch) -> None: import rein_aharness.llm_connect_client as mod response = _ErrorResp(502, {"error": "provider_api_error", "message": "x" * 5000}) monkeypatch.setattr(mod.httpx, "post", lambda *args, **kwargs: response) with pytest.raises(LLMConnectError) as excinfo: LLMConnectClient("http://llm.test").complete("hi") assert len(str(excinfo.value)) < 600 @pytest.mark.parametrize("body", [ValueError("not json"), ["unexpected"]]) def test_llm_connect_error_without_usable_body_reports_status(monkeypatch, body) -> None: import rein_aharness.llm_connect_client as mod response = _ErrorResp(504, body) monkeypatch.setattr(mod.httpx, "post", lambda *args, **kwargs: response) with pytest.raises(LLMConnectError, match="llm-connect returned HTTP 504"): LLMConnectClient("http://llm.test").complete("hi") def test_get_client_requires_env(monkeypatch) -> None: monkeypatch.delenv("LLM_CONNECT_URL", raising=False) with pytest.raises(LLMConnectError, match="LLM_CONNECT_URL"): from rein_aharness.llm_connect_client import get_llm_connect_client get_llm_connect_client()