from __future__ import annotations from datetime import datetime, timedelta, timezone import pytest from maturity_engine.compute import compute_level from maturity_engine.errors import UnevaluableCriterion from maturity_engine.models import Criterion, Evidence, Ladder, Level from conftest import INSTANT, evidence def test_floor_level_with_no_evidence(engine): assessment = engine.assess("estate", "asm", at=INSTANT) assert assessment.level == 0 assert assessment.level_id == "ASM-0" assert assessment.next_level_id == "ASM-1" assert assessment.unmet def test_determinism_same_inputs_same_assessment(engine): kinds = ["central-secret-manager", "secret-rbac", "basic-secret-scanning", "basic-rotation"] for kind in kinds: engine.submit_evidence(evidence(engine, "estate", kind)) first = engine.assess("estate", "asm", at=INSTANT) second = engine.assess("estate", "asm", at=INSTANT) assert first.id == second.id assert first.level == second.level == 1 assert first.as_explanation() == second.as_explanation() def test_explainability_lists_met_and_next(engine): engine.submit_evidence(evidence(engine, "estate", "central-secret-manager")) assessment = engine.assess("estate", "asm", at=INSTANT) assert assessment.level == 0 kinds_unmet = {item.evidence_kind for item in assessment.unmet} assert "secret-rbac" in kinds_unmet explanation = assessment.as_explanation() assert explanation["met"] == [] assert explanation["next_level_id"] == "ASM-1" def test_expiry_demotes_a_subject(engine): until = INSTANT + timedelta(hours=1) kinds = ["central-secret-manager", "secret-rbac", "basic-secret-scanning", "basic-rotation"] for kind in kinds: engine.submit_evidence(evidence(engine, "estate", kind, until=until)) high = engine.assess("estate", "asm", at=INSTANT) assert high.level == 1 later = INSTANT + timedelta(hours=2) low = engine.assess("estate", "asm", at=later) assert low.level == 0 assert low.level_id == "ASM-0" assert low.expired_evidence_ids history = engine.history("estate", "asm") assert [item.level for item in history] == [1, 0] def test_unevaluable_criterion_rejected(): ladder = Ladder( id="bad", version="1", owner="nobody", name="bad", levels=( Level(0, "L0", "floor", ()), Level( 1, "L1", "judged", (Criterion("c1", "judgment", "a human decides"),), ), ), ) with pytest.raises(UnevaluableCriterion): ladder.validate() def test_compute_is_pure(engine): item = Evidence( id="e1", subject="s", kind="central-secret-manager", submitted_by="t", submitted_at=INSTANT, valid_from=INSTANT, valid_until=None, ) ladder = engine.store.get_ladder("asm") a = compute_level(ladder, (item,), subject="s", instant=INSTANT) b = compute_level(ladder, (item,), subject="s", instant=INSTANT) assert a == b other = compute_level( ladder, (item,), subject="s", instant=datetime(2026, 8, 30, tzinfo=timezone.utc) ) assert other.id != a.id