tenant-engine/tests/test_guardrail_domain.py
tegwick 672cf4da6e
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Implement TEN-WP-0011 security layer conformance
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client
scan). Writes persist a decision record or the published fail-closed
stance, live-lookup freshness is published, events_for is tenant-scoped,
and mutation evidence drains to audit-core from a local outbox without
blocking the mutation.

Sender registration is requested as AUDIT-IN-0002. Boundary-contract
amendment is requested as NET-IN-0002.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
2026-08-29 13:02:51 +02:00

333 lines
11 KiB
Python

from datetime import UTC, datetime
import pytest
from tenant_engine.domain import GROUPINGS, RESERVED_IDENTIFIERS, Tenant
from tenant_engine.guardrail import (
UNLIMITED,
ConflictingLimitError,
GuardrailChange,
GuardrailRegistryInvalidError,
InvalidLimitError,
LimitDefinition,
LimitKind,
LimitRegistry,
LimitValue,
Provenance,
UnknownLimitKeyError,
resolve_limit,
resolve_limits,
)
from tenant_engine.guardrail.registry import (
DEFAULT_REGISTRY,
SPEND_MONTHLY,
unlimited_reserved_default,
)
NOW = datetime(2026, 8, 16, 12, 0, tzinfo=UTC)
def tenant(identifier: str, *, retired: bool = False) -> Tenant:
t = Tenant.create(tenant_id="t-1", identifier=identifier, created_at=NOW)
return t.retire(at=NOW) if retired else t
def eur(amount) -> LimitValue:
return LimitValue(kind=LimitKind.SPEND, amount=amount, currency="EUR", period="P1M")
# --- LimitValue invariants ------------------------------------------------
def test_spend_limit_requires_currency_and_period():
with pytest.raises(InvalidLimitError):
LimitValue(kind=LimitKind.SPEND, amount=100, period="P1M")
with pytest.raises(InvalidLimitError):
LimitValue(kind=LimitKind.SPEND, amount=100, currency="EUR")
def test_count_limits_reject_currency_and_enforce_period_shape():
with pytest.raises(InvalidLimitError):
LimitValue(kind=LimitKind.ENTITY_COUNT, amount=5, currency="EUR")
with pytest.raises(InvalidLimitError):
# an entity count is a point-in-time cap, not a rate
LimitValue(kind=LimitKind.ENTITY_COUNT, amount=5, period="P1M")
with pytest.raises(InvalidLimitError):
LimitValue(kind=LimitKind.ACTION_COUNT, amount=5)
def test_negative_and_bool_amounts_are_rejected():
with pytest.raises(InvalidLimitError):
eur(-1)
with pytest.raises(InvalidLimitError):
eur(True)
def test_unlimited_is_explicit_and_never_an_int():
value = LimitValue(kind=LimitKind.ENTITY_COUNT, amount=UNLIMITED)
assert value.is_unlimited
assert not eur(0).is_unlimited
def test_reduced_to_floor_only_ever_reduces():
floor = eur(0)
assert eur(500).reduced_to_floor(floor) == floor
assert eur(0).reduced_to_floor(eur(500)) == eur(0)
assert (
LimitValue(kind=LimitKind.ENTITY_COUNT, amount=UNLIMITED)
.reduced_to_floor(LimitValue(kind=LimitKind.ENTITY_COUNT, amount=3))
.amount
== 3
)
def test_clamping_across_kinds_is_a_conflict():
with pytest.raises(ConflictingLimitError):
eur(10).reduced_to_floor(LimitValue(kind=LimitKind.ENTITY_COUNT, amount=0))
def test_currency_is_not_part_of_compatibility():
# precedence resolves per key and the top layer wins outright, so two
# currencies never combine -- tenant-engine holds no exchange rate
usd = LimitValue(kind=LimitKind.SPEND, amount=100, currency="USD", period="P1M")
assert eur(100).compatible_with(usd)
# --- Registry -------------------------------------------------------------
def test_registry_requires_a_default_for_every_grouping():
partial = {g: eur(0) for g in list(GROUPINGS)[:-1]}
with pytest.raises(GuardrailRegistryInvalidError):
LimitDefinition(
key="spend.monthly",
kind=LimitKind.SPEND,
floor=eur(0),
grouping_defaults=partial,
reserved_default=eur(0),
owner="tenant-engine",
)
def test_registry_floor_may_not_be_unlimited():
with pytest.raises(GuardrailRegistryInvalidError):
LimitDefinition(
key="entity.workspace",
kind=LimitKind.ENTITY_COUNT,
floor=LimitValue(kind=LimitKind.ENTITY_COUNT, amount=UNLIMITED),
grouping_defaults={
g: LimitValue(kind=LimitKind.ENTITY_COUNT, amount=1) for g in GROUPINGS
},
reserved_default=LimitValue(kind=LimitKind.ENTITY_COUNT, amount=1),
owner="some-service",
)
def test_key_prefix_must_match_kind():
with pytest.raises(InvalidLimitError):
LimitDefinition(
key="entity.workspace",
kind=LimitKind.SPEND,
floor=eur(0),
grouping_defaults={g: eur(0) for g in GROUPINGS},
reserved_default=eur(0),
owner="x",
)
def test_duplicate_keys_are_rejected():
with pytest.raises(GuardrailRegistryInvalidError):
LimitRegistry((SPEND_MONTHLY, SPEND_MONTHLY))
def test_unregistered_key_errors_rather_than_resolving():
# not zero (would break a misspelled key) and not unlimited (fails open)
with pytest.raises(UnknownLimitKeyError):
resolve_limit("spend.weekly", tenant=tenant("tenant:small:acme"))
with pytest.raises(UnknownLimitKeyError):
DEFAULT_REGISTRY.get("entity.unregistered")
def test_reserved_spend_default_is_never_unlimited():
with pytest.raises(InvalidLimitError):
unlimited_reserved_default(LimitKind.SPEND)
# --- Resolution: groupings ------------------------------------------------
@pytest.mark.parametrize("grouping", sorted(GROUPINGS))
def test_every_grouping_resolves_to_exactly_one_value(grouping):
effective = resolve_limit("spend.monthly", tenant=tenant(f"tenant:{grouping}:x"))
assert effective.provenance is Provenance.GROUPING
assert isinstance(effective.value.amount, int)
assert not effective.is_fail_closed
def test_trial_defaults_to_zero_spend():
# ADR-0013's mandate, and the one row in the table that is canon
effective = resolve_limit("spend.monthly", tenant=tenant("tenant:trial:binky"))
assert effective.value.amount == 0
assert effective.provenance is Provenance.GROUPING
def test_agentic_ceiling_is_tighter_than_small():
defaults = SPEND_MONTHLY.grouping_defaults
assert defaults["agentic"].amount < defaults["small"].amount
def test_resolve_limits_covers_the_whole_registry():
resolved = resolve_limits(tenant=tenant("tenant:medium:acme"))
assert set(resolved) == set(DEFAULT_REGISTRY.keys)
# --- Resolution: precedence ----------------------------------------------
def test_override_beats_plan_beats_grouping():
t = tenant("tenant:small:acme")
grouping_only = resolve_limit("spend.monthly", tenant=t)
assert grouping_only.provenance is Provenance.GROUPING
with_plan = resolve_limit("spend.monthly", tenant=t, plan_limits={"spend.monthly": eur(60_000)})
assert with_plan.provenance is Provenance.PLAN
assert with_plan.value.amount == 60_000
with_override = resolve_limit(
"spend.monthly",
tenant=t,
plan_limits={"spend.monthly": eur(60_000)},
overrides={"spend.monthly": eur(1_000)},
)
assert with_override.provenance is Provenance.OVERRIDE
assert with_override.value.amount == 1_000
def test_precedence_is_per_key_not_per_set():
entity = LimitDefinition(
key="entity.workspace",
kind=LimitKind.ENTITY_COUNT,
floor=LimitValue(kind=LimitKind.ENTITY_COUNT, amount=0),
grouping_defaults={g: LimitValue(kind=LimitKind.ENTITY_COUNT, amount=3) for g in GROUPINGS},
reserved_default=LimitValue(kind=LimitKind.ENTITY_COUNT, amount=UNLIMITED),
owner="some-service",
)
registry = LimitRegistry((SPEND_MONTHLY, entity))
resolved = resolve_limits(
tenant=tenant("tenant:small:acme"),
plan_limits={"spend.monthly": eur(60_000)},
registry=registry,
)
# a plan supplying spend.monthly must not wipe out the grouping-derived
# entity limit
assert resolved["spend.monthly"].provenance is Provenance.PLAN
assert resolved["entity.workspace"].provenance is Provenance.GROUPING
assert resolved["entity.workspace"].value.amount == 3
def test_unmapped_grouping_fails_closed_visibly():
# second line of defence: the registry validates totality at construction,
# so this state is only reachable by bypassing it
broken = object.__new__(LimitDefinition)
object.__setattr__(broken, "key", "spend.monthly")
object.__setattr__(broken, "kind", LimitKind.SPEND)
object.__setattr__(broken, "period", "P1M")
object.__setattr__(broken, "floor", eur(0))
object.__setattr__(broken, "grouping_defaults", {})
object.__setattr__(broken, "reserved_default", eur(0))
object.__setattr__(broken, "owner", "tenant-engine")
effective = resolve_limit(
"spend.monthly", tenant=tenant("tenant:large:acme"), registry=LimitRegistry((broken,))
)
assert effective.provenance is Provenance.FAIL_CLOSED
assert effective.is_fail_closed
assert effective.value.amount == 0
# --- Resolution: reserved identifiers and lifecycle ----------------------
@pytest.mark.parametrize("identifier", sorted(RESERVED_IDENTIFIERS))
def test_reserved_identifiers_use_the_reserved_profile(identifier):
t = tenant(identifier)
assert t.is_reserved
effective = resolve_limit("spend.monthly", tenant=t)
# explicit profile, not a fall-through to the floor: falling through would
# clamp the platform's own identity to zero and take the platform down
assert effective.provenance is Provenance.RESERVED
assert effective.value.amount == 0
def test_reserved_counts_are_explicitly_unlimited():
assert unlimited_reserved_default(LimitKind.ENTITY_COUNT).is_unlimited
def test_retired_tenant_clamps_to_the_floor_but_still_reads():
effective = resolve_limit("spend.monthly", tenant=tenant("tenant:large:acme", retired=True))
assert effective.provenance is Provenance.LIFECYCLE
assert effective.value.amount == 0
def test_lifecycle_clamp_may_only_reduce():
# a retired tenant whose resolved value is already at the floor keeps its
# provenance -- the clamp reduces, it does not relabel
effective = resolve_limit("spend.monthly", tenant=tenant("tenant:trial:x", retired=True))
assert effective.value.amount == 0
assert effective.provenance is Provenance.GROUPING
def test_retired_tenant_override_cannot_loosen():
effective = resolve_limit(
"spend.monthly",
tenant=tenant("tenant:trial:x", retired=True),
overrides={"spend.monthly": eur(999_999)},
)
assert effective.value.amount == 0
assert effective.provenance is Provenance.LIFECYCLE
# --- Audit ---------------------------------------------------------------
def change(**kwargs) -> GuardrailChange:
base = dict(
change_id="c-1",
tenant_id="t-1",
limit_key="spend.monthly",
previous=eur(0),
current=eur(5_000),
changed_by="ops",
reason="upgrade",
correlation_id="corr-1",
changed_at=NOW,
)
return GuardrailChange(**{**base, **kwargs})
def test_guardrail_change_carries_actor_reason_and_correlation():
c = change()
assert (c.changed_by, c.reason, c.correlation_id) == ("ops", "upgrade", "corr-1")
assert not c.is_clear
def test_clearing_an_override_is_a_valid_change():
assert change(current=None).is_clear
def test_a_change_must_actually_change_something():
with pytest.raises(InvalidLimitError):
change(previous=eur(0), current=eur(0))
with pytest.raises(InvalidLimitError):
change(previous=None, current=None)
def test_a_change_may_not_alter_the_limit_shape():
with pytest.raises(ConflictingLimitError):
change(current=LimitValue(kind=LimitKind.ENTITY_COUNT, amount=5))
def test_a_change_requires_a_reason():
with pytest.raises(InvalidLimitError):
change(reason=" ")