fix: commit semantic repair changes

This commit is contained in:
2026-03-21 11:22:25 +03:00
parent 0900208c1a
commit abee05558f
272 changed files with 4603 additions and 1668 deletions

View File

@@ -1,11 +1,9 @@
# [DEF:backend.tests.services.clean_release.test_approval_service:Module]
# [DEF:TestApprovalService:Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, approval, lifecycle, gate
# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.
# @LAYER: Tests
# @RELATION: TESTS -> src.services.clean_release.approval_service
# @RELATION: TESTS -> src.services.clean_release.enums
# @RELATION: TESTS -> src.services.clean_release.repository
# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; duplicate approve and foreign report must be rejected.
from __future__ import annotations
@@ -21,6 +19,7 @@ from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_seed_candidate_with_report:Function]
# @RELATION: BINDS_TO -> TestApprovalService
# @PURPOSE: Seed candidate and report fixtures for approval gate tests.
# @PRE: candidate_id and report_id are non-empty.
# @POST: Repository contains candidate and report linked by candidate_id.
@@ -61,6 +60,7 @@ def _seed_candidate_with_report(
# [DEF:test_approve_rejects_blocked_report:Function]
# @RELATION: BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure approve is rejected when latest report final status is not PASSED.
# @PRE: Candidate has BLOCKED report.
# @POST: approve_candidate raises ApprovalGateError.
@@ -83,6 +83,7 @@ def test_approve_rejects_blocked_report():
# [DEF:test_approve_rejects_foreign_report:Function]
# @RELATION: BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure approve is rejected when report belongs to another candidate.
# @PRE: Candidate exists, report candidate_id differs.
# @POST: approve_candidate raises ApprovalGateError.
@@ -113,6 +114,7 @@ def test_approve_rejects_foreign_report():
# [DEF:test_approve_rejects_duplicate_approve:Function]
# @RELATION: BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure repeated approve decision for same candidate is blocked.
# @PRE: Candidate has already been approved once.
# @POST: Second approve_candidate call raises ApprovalGateError.
@@ -143,6 +145,7 @@ def test_approve_rejects_duplicate_approve():
# [DEF:test_reject_persists_decision_without_promoting_candidate_state:Function]
# @RELATION: BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure reject decision is immutable and does not promote candidate to APPROVED.
# @PRE: Candidate has PASSED report and CHECK_PASSED lifecycle state.
# @POST: reject_candidate persists REJECTED decision; candidate status remains unchanged.
@@ -167,6 +170,7 @@ def test_reject_persists_decision_without_promoting_candidate_state():
# [DEF:test_reject_then_publish_is_blocked:Function]
# @RELATION: BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure latest REJECTED decision blocks publication gate.
# @PRE: Candidate is rejected for passed report.
# @POST: publish_candidate raises PublicationGateError.
@@ -196,4 +200,4 @@ def test_reject_then_publish_is_blocked():
)
# [/DEF:test_reject_then_publish_is_blocked:Function]
# [/DEF:backend.tests.services.clean_release.test_approval_service:Module]
# [/DEF:TestApprovalService:Module]

View File

@@ -1,4 +1,5 @@
# [DEF:test_candidate_manifest_services:Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @PURPOSE: Test lifecycle and manifest versioning for release candidates.
# @LAYER: Tests
@@ -23,6 +24,8 @@ def db_session():
yield session
session.close()
# [DEF:test_candidate_lifecycle_transitions:Function]
# @RELATION: BINDS_TO -> test_candidate_manifest_services
def test_candidate_lifecycle_transitions(db_session):
"""
@PURPOSE: Verify legal state transitions for ReleaseCandidate.
@@ -47,6 +50,10 @@ def test_candidate_lifecycle_transitions(db_session):
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
def test_manifest_versioning_and_immutability(db_session):
"""
@PURPOSE: Verify manifest versioning and immutability invariants.
@@ -90,6 +97,10 @@ def test_manifest_versioning_and_immutability(db_session):
assert len(all_manifests) == 2
# [/DEF:test_manifest_versioning_and_immutability:Function]
# [DEF:_valid_artifacts:Function]
# @RELATION: BINDS_TO -> test_candidate_manifest_services
def _valid_artifacts():
return [
{
@@ -101,6 +112,10 @@ def _valid_artifacts():
]
# [/DEF:_valid_artifacts:Function]
# [DEF:test_register_candidate_rejects_duplicate_candidate_id:Function]
# @RELATION: BINDS_TO -> test_candidate_manifest_services
def test_register_candidate_rejects_duplicate_candidate_id():
repository = CleanReleaseRepository()
register_candidate(
@@ -123,6 +138,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
def test_register_candidate_rejects_malformed_artifact_input():
repository = CleanReleaseRepository()
bad_artifacts = [{"id": "art-1", "path": "bin/app", "size": 42}] # missing sha256
@@ -138,6 +157,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
def test_register_candidate_rejects_empty_artifact_set():
repository = CleanReleaseRepository()
@@ -152,6 +175,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
def test_manifest_service_rebuild_creates_new_version():
repository = CleanReleaseRepository()
register_candidate(
@@ -171,6 +198,10 @@ def test_manifest_service_rebuild_creates_new_version():
assert first.id != second.id
# [/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
def test_manifest_service_existing_manifest_cannot_be_mutated():
repository = CleanReleaseRepository()
register_candidate(
@@ -194,6 +225,10 @@ def test_manifest_service_existing_manifest_cannot_be_mutated():
assert rebuilt.id != created.id
# [/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
def test_manifest_service_rejects_missing_candidate():
repository = CleanReleaseRepository()
@@ -201,3 +236,4 @@ def test_manifest_service_rejects_missing_candidate():
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]

View File

@@ -1,10 +1,9 @@
# [DEF:backend.tests.services.clean_release.test_compliance_execution_service:Module]
# [DEF:TestComplianceExecutionService:Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization
# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.
# @LAYER: Tests
# @RELATION: TESTS -> backend.src.services.clean_release.compliance_orchestrator
# @RELATION: TESTS -> backend.src.services.clean_release.report_builder
# @INVARIANT: Missing manifest prevents run startup; failed execution cannot finalize as PASSED.
from __future__ import annotations
@@ -27,6 +26,7 @@ from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_seed_with_candidate_policy_registry:Function]
# @RELATION: BINDS_TO -> TestComplianceExecutionService
# @PURPOSE: Build deterministic repository state for run startup tests.
# @PRE: candidate_id and snapshot ids are non-empty.
# @POST: Returns repository with candidate, policy and registry; manifest is optional.
@@ -100,6 +100,7 @@ def _seed_with_candidate_policy_registry(
# [DEF:test_run_without_manifest_rejected:Function]
# @RELATION: BINDS_TO -> TestComplianceExecutionService
# @PURPOSE: Ensure compliance run cannot start when manifest is unresolved.
# @PRE: Candidate/policy exist but manifest is missing.
# @POST: start_check_run raises ValueError and no run is persisted.
@@ -120,6 +121,7 @@ def test_run_without_manifest_rejected():
# [DEF:test_task_crash_mid_run_marks_failed:Function]
# @RELATION: BINDS_TO -> TestComplianceExecutionService
# @PURPOSE: Ensure execution crash conditions force FAILED run status.
# @PRE: Run exists, then required dependency becomes unavailable before execute_stages.
# @POST: execute_stages persists run with FAILED status.
@@ -143,6 +145,7 @@ def test_task_crash_mid_run_marks_failed():
# [DEF:test_blocked_run_finalization_blocks_report_builder:Function]
# @RELATION: BINDS_TO -> TestComplianceExecutionService
# @PURPOSE: Ensure blocked runs require blocking violations before report creation.
# @PRE: Manifest contains prohibited artifacts leading to BLOCKED decision.
# @POST: finalize keeps BLOCKED and report_builder rejects zero blocking violations.
@@ -170,4 +173,4 @@ def test_blocked_run_finalization_blocks_report_builder():
builder.build_report_payload(run, [])
# [/DEF:test_blocked_run_finalization_blocks_report_builder:Function]
# [/DEF:backend.tests.services.clean_release.test_compliance_execution_service:Module]
# [/DEF:TestComplianceExecutionService:Module]

View File

@@ -1,10 +1,9 @@
# [DEF:backend.tests.services.clean_release.test_compliance_task_integration:Module]
# [DEF:TestComplianceTaskIntegration:Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, compliance, task-manager, integration
# @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.
# @LAYER: Tests
# @RELATION: TESTS -> backend.src.core.task_manager.manager.TaskManager
# @RELATION: TESTS -> backend.src.services.clean_release.compliance_orchestrator.CleanComplianceOrchestrator
# @INVARIANT: Compliance execution triggered as task produces terminal task status and persists run evidence.
from __future__ import annotations
@@ -24,16 +23,21 @@ from src.models.clean_release import (
ReleaseCandidate,
SourceRegistrySnapshot,
)
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
from src.services.clean_release.compliance_orchestrator import (
CleanComplianceOrchestrator,
)
from src.services.clean_release.enums import CandidateStatus, RunStatus
from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_seed_repository:Function]
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
# @PURPOSE: Prepare deterministic candidate/policy/registry/manifest fixtures for task integration tests.
# @PRE: with_manifest controls manifest availability.
# @POST: Returns initialized repository and identifiers for compliance run startup.
def _seed_repository(*, with_manifest: bool) -> tuple[CleanReleaseRepository, str, str, str]:
def _seed_repository(
*, with_manifest: bool
) -> tuple[CleanReleaseRepository, str, str, str]:
repository = CleanReleaseRepository()
candidate_id = "cand-task-int-1"
policy_id = "policy-task-int-1"
@@ -94,10 +98,13 @@ def _seed_repository(*, with_manifest: bool) -> tuple[CleanReleaseRepository, st
)
return repository, candidate_id, policy_id, manifest_id
# [/DEF:_seed_repository:Function]
# [DEF:CleanReleaseCompliancePlugin:Class]
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
# @PURPOSE: TaskManager plugin shim that executes clean release compliance orchestration.
class CleanReleaseCompliancePlugin:
@property
@@ -125,12 +132,21 @@ class CleanReleaseCompliancePlugin:
if context is not None:
context.logger.info("Compliance run completed via TaskManager plugin")
return {"run_id": run.id, "run_status": run.status, "final_status": run.final_status}
return {
"run_id": run.id,
"run_status": run.status,
"final_status": run.final_status,
}
# [/DEF:CleanReleaseCompliancePlugin:Class]
# [DEF:_PluginLoaderStub:Class]
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
# @COMPLEXITY: 2
# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.
# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.
class _PluginLoaderStub:
def __init__(self, plugin: CleanReleaseCompliancePlugin):
self._plugin = plugin
@@ -142,18 +158,26 @@ class _PluginLoaderStub:
if plugin_id != self._plugin.id:
raise ValueError("Plugin not found")
return self._plugin
# [/DEF:_PluginLoaderStub:Class]
# [DEF:_make_task_manager:Function]
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
# @PURPOSE: Build TaskManager with mocked persistence services for isolated integration tests.
# @POST: Returns TaskManager ready for async task execution.
def _make_task_manager() -> TaskManager:
plugin_loader = _PluginLoaderStub(CleanReleaseCompliancePlugin())
with patch("src.core.task_manager.manager.TaskPersistenceService") as mock_persistence, patch(
"src.core.task_manager.manager.TaskLogPersistenceService"
) as mock_log_persistence:
with (
patch(
"src.core.task_manager.manager.TaskPersistenceService"
) as mock_persistence,
patch(
"src.core.task_manager.manager.TaskLogPersistenceService"
) as mock_log_persistence,
):
mock_persistence.return_value.load_tasks.return_value = []
mock_persistence.return_value.persist_task = MagicMock()
mock_log_persistence.return_value.add_logs = MagicMock()
@@ -162,14 +186,19 @@ def _make_task_manager() -> TaskManager:
mock_log_persistence.return_value.get_sources = MagicMock(return_value=[])
return TaskManager(plugin_loader)
# [/DEF:_make_task_manager:Function]
# [DEF:_wait_for_terminal_task:Function]
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
# @PURPOSE: Poll task registry until target task reaches terminal status.
# @PRE: task_id exists in manager registry.
# @POST: Returns task with SUCCESS or FAILED status, otherwise raises TimeoutError.
async def _wait_for_terminal_task(manager: TaskManager, task_id: str, timeout_seconds: float = 3.0):
async def _wait_for_terminal_task(
manager: TaskManager, task_id: str, timeout_seconds: float = 3.0
):
started = asyncio.get_running_loop().time()
while True:
task = manager.get_task(task_id)
@@ -178,16 +207,21 @@ async def _wait_for_terminal_task(manager: TaskManager, task_id: str, timeout_se
if asyncio.get_running_loop().time() - started > timeout_seconds:
raise TimeoutError(f"Task {task_id} did not reach terminal status")
await asyncio.sleep(0.05)
# [/DEF:_wait_for_terminal_task:Function]
# [DEF:test_compliance_run_executes_as_task_manager_task:Function]
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
# @PURPOSE: Verify successful compliance execution is observable as TaskManager SUCCESS task.
# @PRE: Candidate, policy and manifest are available in repository.
# @POST: Task ends with SUCCESS; run is persisted with SUCCEEDED status and task binding.
@pytest.mark.asyncio
async def test_compliance_run_executes_as_task_manager_task():
repository, candidate_id, policy_id, manifest_id = _seed_repository(with_manifest=True)
repository, candidate_id, policy_id, manifest_id = _seed_repository(
with_manifest=True
)
manager = _make_task_manager()
try:
@@ -214,16 +248,21 @@ async def test_compliance_run_executes_as_task_manager_task():
finally:
manager._flusher_stop_event.set()
manager._flusher_thread.join(timeout=2)
# [/DEF:test_compliance_run_executes_as_task_manager_task:Function]
# [DEF:test_compliance_run_missing_manifest_marks_task_failed:Function]
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration
# @PURPOSE: Verify missing manifest startup failure is surfaced as TaskManager FAILED task.
# @PRE: Candidate/policy exist but manifest is absent.
# @POST: Task ends with FAILED and run history remains empty.
@pytest.mark.asyncio
async def test_compliance_run_missing_manifest_marks_task_failed():
repository, candidate_id, policy_id, manifest_id = _seed_repository(with_manifest=False)
repository, candidate_id, policy_id, manifest_id = _seed_repository(
with_manifest=False
)
manager = _make_task_manager()
try:
@@ -241,10 +280,14 @@ async def test_compliance_run_missing_manifest_marks_task_failed():
assert finished.status == TaskStatus.FAILED
assert len(repository.check_runs) == 0
assert any("Manifest or Policy not found" in log.message for log in finished.logs)
assert any(
"Manifest or Policy not found" in log.message for log in finished.logs
)
finally:
manager._flusher_stop_event.set()
manager._flusher_thread.join(timeout=2)
# [/DEF:test_compliance_run_missing_manifest_marks_task_failed:Function]
# [/DEF:backend.tests.services.clean_release.test_compliance_task_integration:Module]
# [/DEF:TestComplianceTaskIntegration:Module]

View File

@@ -1,4 +1,4 @@
# [DEF:backend.tests.services.clean_release.test_demo_mode_isolation:Module]
# [DEF:TestDemoModeIsolation:Module]
# @COMPLEXITY: 3
# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository
# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.
@@ -18,6 +18,7 @@ from src.services.clean_release.demo_data_service import (
# [DEF:test_resolve_namespace_separates_demo_and_real:Function]
# @RELATION: BINDS_TO -> TestDemoModeIsolation
# @PURPOSE: Ensure namespace resolver returns deterministic and distinct namespaces.
# @PRE: Mode names are provided as user/runtime strings.
# @POST: Demo and real namespaces are different and stable.
@@ -32,6 +33,7 @@ def test_resolve_namespace_separates_demo_and_real() -> None:
# [DEF:test_build_namespaced_id_prevents_cross_mode_collisions:Function]
# @RELATION: BINDS_TO -> TestDemoModeIsolation
# @PURPOSE: Ensure ID generation prevents demo/real collisions for identical logical IDs.
# @PRE: Same logical candidate id is used in two different namespaces.
# @POST: Produced physical IDs differ by namespace prefix.
@@ -47,6 +49,7 @@ def test_build_namespaced_id_prevents_cross_mode_collisions() -> None:
# [DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]
# @RELATION: BINDS_TO -> TestDemoModeIsolation
# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.
# @PRE: Two repositories are created for distinct modes.
# @POST: Candidate mutations in one mode are not visible in the other mode.
@@ -84,4 +87,4 @@ def test_create_isolated_repository_keeps_mode_data_separate() -> None:
assert real_repo.get_candidate(demo_candidate_id) is None
# [/DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]
# [/DEF:backend.tests.services.clean_release.test_demo_mode_isolation:Module]
# [/DEF:TestDemoModeIsolation:Module]

View File

@@ -1,4 +1,4 @@
# [DEF:backend.tests.services.clean_release.test_policy_resolution_service:Module]
# [DEF:TestPolicyResolutionService:Module]
# @COMPLEXITY: 5
# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts
# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.
@@ -21,6 +21,7 @@ from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_config_manager:Function]
# @RELATION: BINDS_TO -> TestPolicyResolutionService
# @PURPOSE: Build deterministic ConfigManager-like stub for tests.
# @PRE: policy_id and registry_id may be None or non-empty strings.
# @POST: Returns object exposing get_config().settings.clean_release active IDs.
@@ -33,6 +34,7 @@ def _config_manager(policy_id, registry_id):
# [DEF:test_resolve_trusted_policy_snapshots_missing_profile:Function]
# @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.
@@ -49,6 +51,7 @@ def test_resolve_trusted_policy_snapshots_missing_profile():
# [DEF:test_resolve_trusted_policy_snapshots_missing_registry:Function]
# @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.
@@ -65,6 +68,7 @@ def test_resolve_trusted_policy_snapshots_missing_registry():
# [DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]
# @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.
@@ -102,4 +106,4 @@ def test_resolve_trusted_policy_snapshots_rejects_override_attempt():
)
# [/DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]
# [/DEF:backend.tests.services.clean_release.test_policy_resolution_service:Module]
# [/DEF:TestPolicyResolutionService:Module]

View File

@@ -1,11 +1,9 @@
# [DEF:backend.tests.services.clean_release.test_publication_service:Module]
# [DEF:TestPublicationService:Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, publication, revoke, gate
# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.
# @LAYER: Tests
# @RELATION: TESTS -> src.services.clean_release.publication_service
# @RELATION: TESTS -> src.services.clean_release.approval_service
# @RELATION: TESTS -> src.services.clean_release.repository
# @INVARIANT: Publish requires approval; revoke requires existing publication; republish after revoke is allowed as a new record.
from __future__ import annotations
@@ -21,6 +19,7 @@ from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_seed_candidate_with_passed_report:Function]
# @RELATION: BINDS_TO -> TestPublicationService
# @PURPOSE: Seed candidate/report fixtures for publication gate scenarios.
# @PRE: candidate_id and report_id are non-empty.
# @POST: Repository contains candidate and PASSED report.
@@ -57,6 +56,7 @@ def _seed_candidate_with_passed_report(
# [DEF:test_publish_without_approval_rejected:Function]
# @RELATION: BINDS_TO -> TestPublicationService
# @PURPOSE: Ensure publish action is blocked until candidate is approved.
# @PRE: Candidate has PASSED report but status is not APPROVED.
# @POST: publish_candidate raises PublicationGateError.
@@ -80,6 +80,7 @@ def test_publish_without_approval_rejected():
# [DEF:test_revoke_unknown_publication_rejected:Function]
# @RELATION: BINDS_TO -> TestPublicationService
# @PURPOSE: Ensure revocation is rejected for unknown publication id.
# @PRE: Repository has no matching publication record.
# @POST: revoke_publication raises PublicationGateError.
@@ -99,6 +100,7 @@ def test_revoke_unknown_publication_rejected():
# [DEF:test_republish_after_revoke_creates_new_active_record:Function]
# @RELATION: BINDS_TO -> TestPublicationService
# @PURPOSE: Ensure republish after revoke is allowed and creates a new ACTIVE record.
# @PRE: Candidate is APPROVED and first publication has been revoked.
# @POST: New publish call returns distinct publication id with ACTIVE status.
@@ -145,4 +147,4 @@ def test_republish_after_revoke_creates_new_active_record():
assert second.status == PublicationStatus.ACTIVE.value
# [/DEF:test_republish_after_revoke_creates_new_active_record:Function]
# [/DEF:backend.tests.services.clean_release.test_publication_service:Module]
# [/DEF:TestPublicationService:Module]

View File

@@ -1,11 +1,9 @@
# [DEF:backend.tests.services.clean_release.test_report_audit_immutability:Module]
# [DEF:TestReportAuditImmutability:Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, report, audit, immutability, append-only
# @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2.
# @LAYER: Tests
# @RELATION: TESTS -> src.services.clean_release.report_builder.ComplianceReportBuilder
# @RELATION: TESTS -> src.services.clean_release.audit_service
# @RELATION: TESTS -> src.services.clean_release.repository.CleanReleaseRepository
# @INVARIANT: Built reports are immutable snapshots; audit hooks produce append-only event traces.
from __future__ import annotations
@@ -15,18 +13,30 @@ from unittest.mock import patch
import pytest
from src.models.clean_release import ComplianceReport, ComplianceRun, ComplianceViolation
from src.services.clean_release.audit_service import audit_check_run, audit_preparation, audit_report, audit_violation
from src.models.clean_release import (
ComplianceReport,
ComplianceRun,
ComplianceViolation,
)
from src.services.clean_release.audit_service import (
audit_check_run,
audit_preparation,
audit_report,
audit_violation,
)
from src.services.clean_release.enums import ComplianceDecision, RunStatus
from src.services.clean_release.report_builder import ComplianceReportBuilder
from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_terminal_run:Function]
# @RELATION: BINDS_TO -> TestReportAuditImmutability
# @PURPOSE: Build deterministic terminal run fixture for report snapshot tests.
# @PRE: final_status is a valid ComplianceDecision value.
# @POST: Returns a terminal ComplianceRun suitable for report generation.
def _terminal_run(final_status: ComplianceDecision = ComplianceDecision.PASSED) -> ComplianceRun:
def _terminal_run(
final_status: ComplianceDecision = ComplianceDecision.PASSED,
) -> ComplianceRun:
return ComplianceRun(
id="run-immut-1",
candidate_id="cand-immut-1",
@@ -41,10 +51,13 @@ def _terminal_run(final_status: ComplianceDecision = ComplianceDecision.PASSED)
status=RunStatus.SUCCEEDED,
final_status=final_status,
)
# [/DEF:_terminal_run:Function]
# [DEF:test_report_builder_sets_immutable_snapshot_flag:Function]
# @RELATION: BINDS_TO -> TestReportAuditImmutability
# @PURPOSE: Ensure generated report payload is marked immutable and persisted as snapshot.
# @PRE: Terminal run exists.
# @POST: Built report has immutable=True and repository stores same immutable object.
@@ -59,10 +72,13 @@ def test_report_builder_sets_immutable_snapshot_flag():
assert report.immutable is True
assert persisted.immutable is True
assert repository.get_report(report.id) is persisted
# [/DEF:test_report_builder_sets_immutable_snapshot_flag:Function]
# [DEF:test_repository_rejects_report_overwrite_for_same_report_id:Function]
# @RELATION: BINDS_TO -> TestReportAuditImmutability
# @PURPOSE: Define immutability contract that report snapshots cannot be overwritten by same identifier.
# @PRE: Existing report with id is already persisted.
# @POST: Second save for same report id is rejected with explicit immutability error.
@@ -73,7 +89,11 @@ def test_repository_rejects_report_overwrite_for_same_report_id():
run_id="run-immut-1",
candidate_id="cand-immut-1",
final_status=ComplianceDecision.PASSED,
summary_json={"operator_summary": "original", "violations_count": 0, "blocking_violations_count": 0},
summary_json={
"operator_summary": "original",
"violations_count": 0,
"blocking_violations_count": 0,
},
generated_at=datetime.now(timezone.utc),
immutable=True,
)
@@ -82,7 +102,11 @@ def test_repository_rejects_report_overwrite_for_same_report_id():
run_id="run-immut-2",
candidate_id="cand-immut-2",
final_status=ComplianceDecision.ERROR,
summary_json={"operator_summary": "mutated", "violations_count": 1, "blocking_violations_count": 1},
summary_json={
"operator_summary": "mutated",
"violations_count": 1,
"blocking_violations_count": 1,
},
generated_at=datetime.now(timezone.utc),
immutable=True,
)
@@ -91,10 +115,13 @@ def test_repository_rejects_report_overwrite_for_same_report_id():
with pytest.raises(ValueError, match="immutable"):
repository.save_report(mutated)
# [/DEF:test_repository_rejects_report_overwrite_for_same_report_id:Function]
# [DEF:test_audit_hooks_emit_append_only_event_stream:Function]
# @RELATION: BINDS_TO -> TestReportAuditImmutability
# @PURPOSE: Verify audit hooks emit one event per action call and preserve call order.
# @PRE: Logger backend is patched.
# @POST: Three calls produce three ordered info entries with molecular prefixes.
@@ -109,6 +136,8 @@ def test_audit_hooks_emit_append_only_event_stream(mock_logger):
assert logged_messages[0].startswith("[REASON]")
assert logged_messages[1].startswith("[REFLECT]")
assert logged_messages[2].startswith("[EXPLORE]")
# [/DEF:test_audit_hooks_emit_append_only_event_stream:Function]
# [/DEF:backend.tests.services.clean_release.test_report_audit_immutability:Module]
# [/DEF:TestReportAuditImmutability:Module]