96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
from dataclasses import replace
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
|
|
from aios_autonomous_release_candidate.approval import ApprovalVerificationResult
|
|
from aios_autonomous_release_candidate.errors import ReleaseBlocked
|
|
from aios_autonomous_release_candidate.models import ReleasePlan
|
|
from aios_autonomous_release_candidate.release import ReleaseController, ReleaseState
|
|
from aios_autonomous_release_candidate.release_gate import ReleaseEvidence, ReleaseGate
|
|
from aios_autonomous_release_candidate.tag import TagReference, TagVerifier
|
|
|
|
|
|
OID = "a" * 40
|
|
TAG_OID = "b" * 40
|
|
DIGEST = "c" * 64
|
|
|
|
|
|
def plan() -> ReleasePlan:
|
|
return ReleasePlan(
|
|
task_id="release-task",
|
|
repository="aios/aios",
|
|
branch="integration/autonomous-release",
|
|
version="V2R1C4",
|
|
target_oid=OID,
|
|
tree_oid="d" * 40,
|
|
artifact_sha256=DIGEST,
|
|
manifest_sha256="e" * 64,
|
|
package_closure_sha256="3" * 64,
|
|
evidence_closure_oid="f" * 40,
|
|
approval_plan_id="owner-release-v2r1c4",
|
|
rollback_approval_plan_id="owner-rollback-v2r1c4",
|
|
rollback_artifact_sha256="1" * 64,
|
|
expected_tag_oid=TAG_OID,
|
|
expected_target_oid=OID,
|
|
production_config_sha256="2" * 64,
|
|
)
|
|
|
|
|
|
def approval(plan_id: str) -> ApprovalVerificationResult:
|
|
return ApprovalVerificationResult(
|
|
approval_plan_id=plan_id,
|
|
task_id="release-task",
|
|
signing_key_id="owner-kms-1",
|
|
verified_at=datetime(2026, 7, 26, tzinfo=timezone.utc),
|
|
)
|
|
|
|
|
|
def test_deploy_and_rollback_require_distinct_verified_approvals() -> None:
|
|
controller = ReleaseController()
|
|
controller.attach_plan(plan())
|
|
controller.mark_built()
|
|
controller.verify(artifact_ok=True, evidence_ok=True)
|
|
controller.request_owner_approval()
|
|
with pytest.raises(ReleaseBlocked, match="OWNER_APPROVAL_PLAN_MISMATCH"):
|
|
controller.approve(approval("wrong-plan"))
|
|
controller.approve(approval("owner-release-v2r1c4"))
|
|
controller.start_deployment()
|
|
controller.mark_deployed()
|
|
controller.request_rollback("candidate acceptance regression")
|
|
with pytest.raises(ReleaseBlocked, match="ROLLBACK_APPROVAL_PLAN_MISMATCH"):
|
|
controller.approve_rollback(approval("owner-release-v2r1c4"))
|
|
controller.approve_rollback(approval("owner-rollback-v2r1c4"))
|
|
controller.start_rollback()
|
|
assert controller.mark_rolled_back().state == ReleaseState.ROLLED_BACK
|
|
|
|
|
|
def test_annotated_tag_uses_object_and_peeled_oids_separately() -> None:
|
|
tag = TagReference("refs/tags/V2R1C4", "tag", TAG_OID, OID)
|
|
TagVerifier().verify(tag, expected_tag_oid=TAG_OID, expected_target_oid=OID)
|
|
with pytest.raises(ReleaseBlocked, match="TAG_PEELED_TARGET_MISMATCH"):
|
|
TagVerifier().verify(tag, expected_tag_oid=TAG_OID, expected_target_oid="3" * 40)
|
|
|
|
|
|
def test_release_gate_requires_four_source_tag_and_artifact_closure() -> None:
|
|
evidence = ReleaseEvidence(
|
|
expected_version="V2R1C4",
|
|
registry_version="V2R1C4",
|
|
baseline_version="V2R1C4",
|
|
local_tag_oid=TAG_OID,
|
|
remote_tag_oid=TAG_OID,
|
|
gitea_tag_oid=TAG_OID,
|
|
expected_tag_oid=TAG_OID,
|
|
local_peeled_target_oid=OID,
|
|
remote_peeled_target_oid=OID,
|
|
gitea_peeled_target_oid=OID,
|
|
expected_target_oid=OID,
|
|
artifact_sha256=DIGEST,
|
|
manifest_sha256="e" * 64,
|
|
evidence_sha256="4" * 64,
|
|
rollback_artifact_sha256="1" * 64,
|
|
)
|
|
ReleaseGate().verify(evidence)
|
|
with pytest.raises(ReleaseBlocked, match="TAG_GITEA_MISMATCH"):
|
|
ReleaseGate().verify(replace(evidence, gitea_tag_oid="5" * 40))
|