139 lines
4.5 KiB
Python
139 lines
4.5 KiB
Python
"""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"
|