fix: finalize semantic repair and test updates
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# [DEF:test_candidate_manifest_services:Module]
|
||||
# @RELATION: BELONGS_TO -> SrcRoot
|
||||
# @RELATION: BELONGS_TO -> [SrcRoot:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Test lifecycle and manifest versioning for release candidates.
|
||||
# @LAYER: Tests
|
||||
@@ -9,12 +9,17 @@ from datetime import datetime, timezone
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from src.core.database import Base
|
||||
from src.models.clean_release import ReleaseCandidate, DistributionManifest, CandidateArtifact
|
||||
from src.models.clean_release import (
|
||||
ReleaseCandidate,
|
||||
DistributionManifest,
|
||||
CandidateArtifact,
|
||||
)
|
||||
from src.services.clean_release.enums import CandidateStatus
|
||||
from src.services.clean_release.candidate_service import register_candidate
|
||||
from src.services.clean_release.manifest_service import build_manifest_snapshot
|
||||
from src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
@@ -24,8 +29,10 @@ def db_session():
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
|
||||
# [DEF:test_candidate_lifecycle_transitions:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Verify release candidate allows legal status transitions and rejects forbidden back-transitions.
|
||||
def test_candidate_lifecycle_transitions(db_session):
|
||||
"""
|
||||
@PURPOSE: Verify legal state transitions for ReleaseCandidate.
|
||||
@@ -36,7 +43,7 @@ def test_candidate_lifecycle_transitions(db_session):
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="ref-1",
|
||||
created_by="operator",
|
||||
status=CandidateStatus.DRAFT
|
||||
status=CandidateStatus.DRAFT,
|
||||
)
|
||||
db_session.add(candidate)
|
||||
db_session.commit()
|
||||
@@ -47,13 +54,17 @@ def test_candidate_lifecycle_transitions(db_session):
|
||||
|
||||
# Invalid transition: PREPARED -> DRAFT (should raise IllegalTransitionError)
|
||||
from src.services.clean_release.exceptions import IllegalTransitionError
|
||||
|
||||
with pytest.raises(IllegalTransitionError, match="Forbidden transition"):
|
||||
candidate.transition_to(CandidateStatus.DRAFT)
|
||||
|
||||
|
||||
# [/DEF:test_candidate_lifecycle_transitions:Function]
|
||||
|
||||
|
||||
# [DEF:test_manifest_versioning_and_immutability:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Verify manifest versions increment monotonically and older snapshots remain queryable.
|
||||
def test_manifest_versioning_and_immutability(db_session):
|
||||
"""
|
||||
@PURPOSE: Verify manifest versioning and immutability invariants.
|
||||
@@ -70,7 +81,7 @@ def test_manifest_versioning_and_immutability(db_session):
|
||||
source_snapshot_ref="ref1",
|
||||
content_json={},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
created_by="operator"
|
||||
created_by="operator",
|
||||
)
|
||||
db_session.add(m1)
|
||||
|
||||
@@ -84,23 +95,34 @@ def test_manifest_versioning_and_immutability(db_session):
|
||||
source_snapshot_ref="ref1",
|
||||
content_json={},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
created_by="operator"
|
||||
created_by="operator",
|
||||
)
|
||||
db_session.add(m2)
|
||||
db_session.commit()
|
||||
|
||||
latest = db_session.query(DistributionManifest).filter_by(candidate_id=candidate_id).order_by(DistributionManifest.manifest_version.desc()).first()
|
||||
latest = (
|
||||
db_session.query(DistributionManifest)
|
||||
.filter_by(candidate_id=candidate_id)
|
||||
.order_by(DistributionManifest.manifest_version.desc())
|
||||
.first()
|
||||
)
|
||||
assert latest.manifest_version == 2
|
||||
assert latest.id == "manifest-v2"
|
||||
|
||||
all_manifests = db_session.query(DistributionManifest).filter_by(candidate_id=candidate_id).all()
|
||||
all_manifests = (
|
||||
db_session.query(DistributionManifest)
|
||||
.filter_by(candidate_id=candidate_id)
|
||||
.all()
|
||||
)
|
||||
assert len(all_manifests) == 2
|
||||
|
||||
|
||||
# [/DEF:test_manifest_versioning_and_immutability:Function]
|
||||
|
||||
|
||||
# [DEF:_valid_artifacts:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Provide canonical valid artifact payload used by candidate registration tests.
|
||||
def _valid_artifacts():
|
||||
return [
|
||||
{
|
||||
@@ -114,8 +136,10 @@ def _valid_artifacts():
|
||||
|
||||
# [/DEF:_valid_artifacts:Function]
|
||||
|
||||
|
||||
# [DEF:test_register_candidate_rejects_duplicate_candidate_id:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Verify duplicate candidate_id registration is rejected by service invariants.
|
||||
def test_register_candidate_rejects_duplicate_candidate_id():
|
||||
repository = CleanReleaseRepository()
|
||||
register_candidate(
|
||||
@@ -140,8 +164,10 @@ def test_register_candidate_rejects_duplicate_candidate_id():
|
||||
|
||||
# [/DEF:test_register_candidate_rejects_duplicate_candidate_id:Function]
|
||||
|
||||
|
||||
# [DEF:test_register_candidate_rejects_malformed_artifact_input:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Verify candidate registration rejects artifact payloads missing required fields.
|
||||
def test_register_candidate_rejects_malformed_artifact_input():
|
||||
repository = CleanReleaseRepository()
|
||||
bad_artifacts = [{"id": "art-1", "path": "bin/app", "size": 42}] # missing sha256
|
||||
@@ -159,8 +185,10 @@ def test_register_candidate_rejects_malformed_artifact_input():
|
||||
|
||||
# [/DEF:test_register_candidate_rejects_malformed_artifact_input:Function]
|
||||
|
||||
|
||||
# [DEF:test_register_candidate_rejects_empty_artifact_set:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Verify candidate registration rejects empty artifact collections.
|
||||
def test_register_candidate_rejects_empty_artifact_set():
|
||||
repository = CleanReleaseRepository()
|
||||
|
||||
@@ -177,8 +205,10 @@ def test_register_candidate_rejects_empty_artifact_set():
|
||||
|
||||
# [/DEF:test_register_candidate_rejects_empty_artifact_set:Function]
|
||||
|
||||
|
||||
# [DEF:test_manifest_service_rebuild_creates_new_version:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Verify repeated manifest build creates a new incremented immutable version.
|
||||
def test_manifest_service_rebuild_creates_new_version():
|
||||
repository = CleanReleaseRepository()
|
||||
register_candidate(
|
||||
@@ -190,8 +220,12 @@ def test_manifest_service_rebuild_creates_new_version():
|
||||
artifacts=_valid_artifacts(),
|
||||
)
|
||||
|
||||
first = build_manifest_snapshot(repository=repository, candidate_id="manifest-version-1", created_by="operator")
|
||||
second = build_manifest_snapshot(repository=repository, candidate_id="manifest-version-1", created_by="operator")
|
||||
first = build_manifest_snapshot(
|
||||
repository=repository, candidate_id="manifest-version-1", created_by="operator"
|
||||
)
|
||||
second = build_manifest_snapshot(
|
||||
repository=repository, candidate_id="manifest-version-1", created_by="operator"
|
||||
)
|
||||
|
||||
assert first.manifest_version == 1
|
||||
assert second.manifest_version == 2
|
||||
@@ -200,8 +234,10 @@ def test_manifest_service_rebuild_creates_new_version():
|
||||
|
||||
# [/DEF:test_manifest_service_rebuild_creates_new_version:Function]
|
||||
|
||||
|
||||
# [DEF:test_manifest_service_existing_manifest_cannot_be_mutated:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Verify existing manifest snapshot remains immutable when rebuilding newer manifest version.
|
||||
def test_manifest_service_existing_manifest_cannot_be_mutated():
|
||||
repository = CleanReleaseRepository()
|
||||
register_candidate(
|
||||
@@ -213,10 +249,18 @@ def test_manifest_service_existing_manifest_cannot_be_mutated():
|
||||
artifacts=_valid_artifacts(),
|
||||
)
|
||||
|
||||
created = build_manifest_snapshot(repository=repository, candidate_id="manifest-immutable-1", created_by="operator")
|
||||
created = build_manifest_snapshot(
|
||||
repository=repository,
|
||||
candidate_id="manifest-immutable-1",
|
||||
created_by="operator",
|
||||
)
|
||||
original_digest = created.manifest_digest
|
||||
|
||||
rebuilt = build_manifest_snapshot(repository=repository, candidate_id="manifest-immutable-1", created_by="operator")
|
||||
rebuilt = build_manifest_snapshot(
|
||||
repository=repository,
|
||||
candidate_id="manifest-immutable-1",
|
||||
created_by="operator",
|
||||
)
|
||||
old_manifest = repository.get_manifest(created.id)
|
||||
|
||||
assert old_manifest is not None
|
||||
@@ -227,13 +271,20 @@ def test_manifest_service_existing_manifest_cannot_be_mutated():
|
||||
|
||||
# [/DEF:test_manifest_service_existing_manifest_cannot_be_mutated:Function]
|
||||
|
||||
|
||||
# [DEF:test_manifest_service_rejects_missing_candidate:Function]
|
||||
# @RELATION: BINDS_TO -> test_candidate_manifest_services
|
||||
# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]
|
||||
# @PURPOSE: Verify manifest build fails with missing candidate identifier.
|
||||
def test_manifest_service_rejects_missing_candidate():
|
||||
repository = CleanReleaseRepository()
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
build_manifest_snapshot(repository=repository, candidate_id="missing-candidate", created_by="operator")
|
||||
build_manifest_snapshot(
|
||||
repository=repository,
|
||||
candidate_id="missing-candidate",
|
||||
created_by="operator",
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:test_candidate_manifest_services:Module]
|
||||
# [/DEF:test_manifest_service_rejects_missing_candidate:Function]
|
||||
# [/DEF:test_candidate_manifest_services:Module]
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts
|
||||
# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.
|
||||
# @LAYER: Tests
|
||||
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.policy_resolution_service
|
||||
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
|
||||
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.exceptions
|
||||
# @RELATION: DEPENDS_ON -> [policy_resolution_service]
|
||||
# @RELATION: DEPENDS_ON -> [repository]
|
||||
# @RELATION: DEPENDS_ON -> [clean_release_exceptions]
|
||||
# @INVARIANT: Resolution uses only ConfigManager active IDs and rejects runtime override attempts.
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,25 +16,33 @@ import pytest
|
||||
|
||||
from src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot
|
||||
from src.services.clean_release.exceptions import PolicyResolutionError
|
||||
from src.services.clean_release.policy_resolution_service import resolve_trusted_policy_snapshots
|
||||
from src.services.clean_release.policy_resolution_service import (
|
||||
resolve_trusted_policy_snapshots,
|
||||
)
|
||||
from src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
|
||||
# [DEF:_config_manager:Function]
|
||||
# @RELATION: BINDS_TO -> TestPolicyResolutionService
|
||||
# @RELATION: BINDS_TO -> [TestPolicyResolutionService]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Build deterministic ConfigManager-like stub for tests.
|
||||
# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.
|
||||
# @PRE: policy_id and registry_id may be None or non-empty strings.
|
||||
# @POST: Returns object exposing get_config().settings.clean_release active IDs.
|
||||
def _config_manager(policy_id, registry_id):
|
||||
clean_release = SimpleNamespace(active_policy_id=policy_id, active_registry_id=registry_id)
|
||||
clean_release = SimpleNamespace(
|
||||
active_policy_id=policy_id, active_registry_id=registry_id
|
||||
)
|
||||
settings = SimpleNamespace(clean_release=clean_release)
|
||||
config = SimpleNamespace(settings=settings)
|
||||
return SimpleNamespace(get_config=lambda: config)
|
||||
|
||||
|
||||
# [/DEF:_config_manager:Function]
|
||||
|
||||
|
||||
# [DEF:test_resolve_trusted_policy_snapshots_missing_profile:Function]
|
||||
# @RELATION: BINDS_TO -> TestPolicyResolutionService
|
||||
# @RELATION: BINDS_TO -> [TestPolicyResolutionService]
|
||||
# @PURPOSE: Ensure resolution fails when trusted profile is not configured.
|
||||
# @PRE: active_policy_id is None.
|
||||
# @POST: Raises PolicyResolutionError with missing trusted profile reason.
|
||||
@@ -47,11 +55,13 @@ def test_resolve_trusted_policy_snapshots_missing_profile():
|
||||
config_manager=config_manager,
|
||||
repository=repository,
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:test_resolve_trusted_policy_snapshots_missing_profile:Function]
|
||||
|
||||
|
||||
# [DEF:test_resolve_trusted_policy_snapshots_missing_registry:Function]
|
||||
# @RELATION: BINDS_TO -> TestPolicyResolutionService
|
||||
# @RELATION: BINDS_TO -> [TestPolicyResolutionService]
|
||||
# @PURPOSE: Ensure resolution fails when trusted registry is not configured.
|
||||
# @PRE: active_registry_id is None and active_policy_id is set.
|
||||
# @POST: Raises PolicyResolutionError with missing trusted registry reason.
|
||||
@@ -64,11 +74,13 @@ def test_resolve_trusted_policy_snapshots_missing_registry():
|
||||
config_manager=config_manager,
|
||||
repository=repository,
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:test_resolve_trusted_policy_snapshots_missing_registry:Function]
|
||||
|
||||
|
||||
# [DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]
|
||||
# @RELATION: BINDS_TO -> TestPolicyResolutionService
|
||||
# @RELATION: BINDS_TO -> [TestPolicyResolutionService]
|
||||
# @PURPOSE: Ensure runtime override attempt is rejected even if snapshots exist.
|
||||
# @PRE: valid trusted snapshots exist in repository and override is provided.
|
||||
# @POST: Raises PolicyResolutionError with override forbidden reason.
|
||||
@@ -104,6 +116,8 @@ def test_resolve_trusted_policy_snapshots_rejects_override_attempt():
|
||||
repository=repository,
|
||||
policy_id_override="policy-override",
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]
|
||||
|
||||
# [/DEF:TestPolicyResolutionService:Module]
|
||||
|
||||
Reference in New Issue
Block a user