rein-aharness/tests/test_mail_triage.py
tegwick 0f01c3e421 feat: route profiled ops runs through Glas
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02b6f-7db1-7222-918b-e813a6bda38d
2026-08-22 23:55:29 +02:00

259 lines
8.5 KiB
Python

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()