"""Tests for the two-phase email flow: review-required notice, then final report.""" import threading import backend.jobs from backend.email_sender import send_review_required class _SyncThread: """Drop-in threading.Thread replacement that runs the target inline.""" def __init__(self, target=None, args=(), **kwargs): self._target = target self._args = args def start(self): self._target(*self._args) def test_review_required_email_skips_without_smtp(monkeypatch): monkeypatch.setattr("backend.email_sender._smtp_ready", lambda: False) assert send_review_required("user@example.com", {"source": "set.pdf", "summary": {}}, "http://localhost:8099/?job=abc") is False def test_review_required_email_sends_with_smtp(monkeypatch): """With SMTP ready, the message goes out with recipient, review URL, and the required-item count (0 when the report has no review summary).""" sent = [] monkeypatch.setattr("backend.email_sender._smtp_ready", lambda: True) monkeypatch.setattr("backend.email_sender._send", lambda msg: sent.append(msg) or True) review_url = "http://localhost:8099/?job=abc" report = {"source": "set.pdf", "summary": {"review": {"required": 3}}} assert send_review_required("user@example.com", report, review_url) is True assert len(sent) == 1 msg = sent[0] assert msg["To"] == "user@example.com" assert "review" in msg["Subject"].lower() body = msg.get_content() assert "review" in body.lower() assert review_url in body assert "3" in body # Missing review summary -> required count defaults to 0. sent.clear() assert send_review_required("user@example.com", {"source": "set.pdf", "summary": {}}, review_url) is True assert "0" in sent[0].get_content() def test_agent_needs_review_sends_review_email_not_report(monkeypatch, tmp_path): """An agent job entering needs_review emails the review-required notice exactly once and never sends the final conflict report.""" review_emails = [] report_emails = [] monkeypatch.setattr("backend.config.OUTPUT_DIR", str(tmp_path)) monkeypatch.setattr("backend.jobs.run_agent_pipeline", lambda pdf_path, **kw: { "summary": {"agent_status": "needs_review"}, "conflicts": [], }) monkeypatch.setattr("backend.jobs.send_review_required", lambda *a, **kw: review_emails.append((a, kw))) monkeypatch.setattr("backend.jobs.send_conflict_report", lambda *a, **kw: report_emails.append((a, kw))) monkeypatch.setattr(threading, "Thread", _SyncThread) pdf = tmp_path / "upload.pdf" pdf.write_bytes(b"%PDF-1.4 dummy") job_id = backend.jobs.create_job(str(pdf), source_filename="set.pdf", email="arch@example.com", pipeline_mode="agent") try: job = backend.jobs.get_job(job_id) assert job["status"] == "needs_review" assert len(review_emails) == 1 args, _ = review_emails[0] assert args[0] == "arch@example.com" assert f"/?job={job_id}" in args[2] assert report_emails == [] finally: backend.jobs._jobs.pop(job_id, None) def test_classic_job_sends_only_conflict_report(monkeypatch, tmp_path): """Classic pipeline is untouched: only the final report email fires.""" review_emails = [] report_emails = [] monkeypatch.setattr("backend.config.OUTPUT_DIR", str(tmp_path)) monkeypatch.setattr("backend.jobs.run_pipeline", lambda pdf_path, **kw: { "summary": {}, "conflicts": [], }) monkeypatch.setattr("backend.jobs.send_review_required", lambda *a, **kw: review_emails.append((a, kw))) monkeypatch.setattr("backend.jobs.send_conflict_report", lambda *a, **kw: report_emails.append((a, kw))) monkeypatch.setattr(threading, "Thread", _SyncThread) pdf = tmp_path / "upload.pdf" pdf.write_bytes(b"%PDF-1.4 dummy") job_id = backend.jobs.create_job(str(pdf), source_filename="set.pdf", email="arch@example.com", pipeline_mode="classic") try: assert backend.jobs.get_job(job_id)["status"] == "done" assert len(report_emails) == 1 assert review_emails == [] finally: backend.jobs._jobs.pop(job_id, None)