fix: finalize semantic repair and test updates

This commit is contained in:
2026-03-21 15:07:06 +03:00
parent 005797334b
commit 9b47b9b667
99 changed files with 2484 additions and 985 deletions

View File

@@ -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]

View File

@@ -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]