"""Phase 6 maintenance loop tests.""" from __future__ import annotations import asyncio from datetime import datetime import pytest from ont_platform.core.maintenance import MaintenanceLoopService, MaintenancePermissionError from ont_platform.storage.candidate_repository import CandidateRepository from ont_platform.storage.models import ( Base, CandidateEntity, MaintenanceProposalStatus, ReviewStatus, SourceDocument, ) from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker def _session() -> Session: engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) return sessionmaker(bind=engine)() def _seed_project(db: Session) -> None: db.add( SourceDocument( id="doc_phase6", project_id="proj_phase6", source_url="https://example.test/phase6", document_type="html", title="Phase 6 Source", text="Alice works at Acme.", content_hash="hash_phase6", fingerprint="fp_phase6", retrieved_at=datetime.utcnow(), extracted_by="trafilatura", ) ) repository = CandidateRepository(db) repository.save_lightweight_result( project_id="proj_phase6", document_id="doc_phase6", result={ "entities": [ { "id": "E_alice_a", "label": "Alice", "type": "person", "confidence": 0.9, "evidence_ids": ["EV_phase6"], }, { "id": "E_alice_b", "label": "Alice", "type": "person", "confidence": 0.5, "evidence_ids": [], }, ], "relations": [], "evidence_spans": [ { "id": "EV_phase6", "text": "Alice works at Acme.", "start_offset": 0, "end_offset": 20, } ], "validation_issues": [ { "severity": "error", "code": "relation_endpoint_missing", "message": "Relation target is missing", "candidate_id": "R_missing", "candidate_kind": "relation", } ], }, validation_passed=True, ) def test_maintenance_loop_creates_pending_proposals_without_mutating_candidates() -> None: async def run(): db = _session() _seed_project(db) service = MaintenanceLoopService(db) run_model = await service.run( project_id="proj_phase6", requested_by="ops", actor_role="admin", ) proposals = service.list_proposals(project_id="proj_phase6") candidate = db.get(CandidateEntity, "E_alice_b") return run_model, proposals, candidate run_model, proposals, candidate = asyncio.run(run()) assert run_model.status.value == "completed" assert run_model.summary["direct_mutations"] == 0 assert run_model.summary["approval_gate"] == "required" assert run_model.budget_summary["usage"]["operation_type"] == "analysis" assert run_model.audit_summary["action"] == "ANALYZE" assert proposals assert {proposal.status for proposal in proposals} == {MaintenanceProposalStatus.PENDING_REVIEW} assert candidate.review_status == ReviewStatus.PENDING def test_maintenance_proposal_requires_admin_approval_permission() -> None: async def run(): db = _session() _seed_project(db) service = MaintenanceLoopService(db) await service.run(project_id="proj_phase6", requested_by="ops", actor_role="admin") proposal = service.list_proposals(project_id="proj_phase6")[0] with pytest.raises(MaintenancePermissionError): await service.review_proposal( proposal_id=proposal.id, reviewed_by="viewer", actor_role="viewer", approve=True, ) approved = await service.review_proposal( proposal_id=proposal.id, reviewed_by="admin", actor_role="admin", approve=True, ) return approved approved = asyncio.run(run()) assert approved.status == MaintenanceProposalStatus.APPROVED assert approved.approved_by == "admin"