Files
AI/ontology_platform/tests/unit/test_phase6_maintenance_loop.py

139 lines
4.5 KiB
Python
Raw Normal View History

2026-05-19 20:31:52 +09:00
"""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"