feat: govern attachment exceptions and ownership transfers
This commit is contained in:
103
tests/test_review_decisions.py
Normal file
103
tests/test_review_decisions.py
Normal file
@@ -0,0 +1,103 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user