from __future__ import annotations from datetime import UTC, datetime from types import SimpleNamespace import pytest from govoplan_campaign.backend.persistence.campaigns import ( CampaignPersistenceError, ) from govoplan_campaign.backend.persistence.versions import ( _normalize_review_issue_decisions, ) def test_attachment_ask_requires_reason_and_freezes_evidence() -> None: job = _job( issues=[ { "code": "missing_optional_attachment", "behavior": "ask", "source": "attachments", "details": { "effective_policy": { "effective_behavior": "ask", } }, } ] ) with pytest.raises( CampaignPersistenceError, match="require an explicit reason", ): _normalize_review_issue_decisions( [job], [], user_id="reviewer-1", build_token="build-1", ) decisions = _normalize_review_issue_decisions( [job], [ { "job_id": job.id, "decision": "accept", "reason": "Recipient confirmed that no attachment is expected.", } ], user_id="reviewer-1", build_token="build-1", decided_at=datetime(2026, 7, 30, 12, 0, tzinfo=UTC), ) assert decisions == [ { "job_id": "job-1", "review_key": "entry-1", "decision": "accept", "reason": "Recipient confirmed that no attachment is expected.", "actor_user_id": "reviewer-1", "decided_at": "2026-07-30T12:00:00+00:00", "build_token": "build-1", "message_sha256": "a" * 64, "issue_fingerprint": decisions[0]["issue_fingerprint"], "issue_codes": ["missing_optional_attachment"], } ] assert len(decisions[0]["issue_fingerprint"]) == 64 def test_attachment_block_cannot_be_overridden_by_review_decision() -> None: job = _job( issues=[ { "code": "missing_required_attachment", "behavior": "block", "source": "attachments", } ] ) decisions = _normalize_review_issue_decisions( [job], [], user_id="reviewer-1", build_token="build-1", ) assert decisions == [] def _job(*, issues: list[dict[str, object]]) -> SimpleNamespace: return SimpleNamespace( id="job-1", entry_id="entry-1", entry_index=1, validation_status="needs_review", issues_snapshot=issues, eml_sha256="a" * 64, )