maturity-engine/tests/test_compute.py
tegwick 4cde4e489a Stand up the Engine/PIP surface for MAT-WP-0001
Declare layer.yaml, add a Python engine over a local SQLite store,
and cover deterministic assessment, the §13 gap register, stance-map
inventory, claim guardrails, and the gate-house review path with tests.

Assistant: grok
Assistant-Session: 01a04ceb-150e-7e80-a542-ec8b1372e164
2026-08-29 12:54:37 +02:00

96 lines
3.2 KiB
Python

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