112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
"""Idempotency and replay-safety tests for ``compute_tag_uuid`` (E.2.2).
|
|
|
|
The deterministic UUIDv5 derivation is the load-bearing primitive
|
|
behind ``INSERT OR IGNORE`` replay safety: the worker must be able to
|
|
re-process the same source events any number of times — crash recovery,
|
|
backfill, manual re-run — and converge to the same tag set.
|
|
|
|
The replay-safety lock asserts the *exact* parameter set so a future
|
|
contributor adding ``created_at``, ``os.getpid()``, ``random.random()``
|
|
or any other non-deterministic input cannot silently break replay
|
|
safety; the test breaks first.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
from typing import Optional
|
|
|
|
from hypothesis import given, settings
|
|
from hypothesis import strategies as st
|
|
|
|
from decnet.web.db.models.ttp import _TTP_TAG_NS, compute_tag_uuid
|
|
|
|
|
|
# ── Determinism ─────────────────────────────────────────────────────
|
|
|
|
|
|
_input_tuple = st.tuples(
|
|
st.text(min_size=1, max_size=32),
|
|
st.text(min_size=1, max_size=64),
|
|
st.text(min_size=1, max_size=32),
|
|
st.integers(min_value=0, max_value=10_000),
|
|
st.text(min_size=1, max_size=16),
|
|
st.one_of(st.none(), st.text(min_size=1, max_size=16)),
|
|
)
|
|
|
|
|
|
@given(t=_input_tuple)
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_compute_tag_uuid_is_deterministic(
|
|
t: tuple[str, str, str, int, str, Optional[str]],
|
|
) -> None:
|
|
a = compute_tag_uuid(*t)
|
|
b = compute_tag_uuid(*t)
|
|
assert a == b
|
|
|
|
|
|
# ── Collision resistance ────────────────────────────────────────────
|
|
|
|
|
|
@given(
|
|
tuples=st.lists(_input_tuple, min_size=2, max_size=200, unique=True),
|
|
)
|
|
@settings(max_examples=50, deadline=None)
|
|
def test_distinct_inputs_yield_distinct_uuids(
|
|
tuples: list[tuple[str, str, str, int, str, Optional[str]]],
|
|
) -> None:
|
|
uuids = {compute_tag_uuid(*t) for t in tuples}
|
|
# Distinct input tuples → distinct UUIDs in the practical input
|
|
# space. UUIDv5 is 122-bit; collisions in N≤200 are
|
|
# vanishingly unlikely.
|
|
assert len(uuids) == len(tuples)
|
|
|
|
|
|
# ── Golden-value lock ───────────────────────────────────────────────
|
|
|
|
|
|
def test_compute_tag_uuid_golden_value() -> None:
|
|
"""Pinned input → pinned UUID. Drift = breaking change.
|
|
|
|
Worked example from the design doc (``find_recursive_root`` / R0014
|
|
on ``cmd_42``). If this assertion ever needs to flip, every
|
|
existing tag UUID in production has been silently invalidated —
|
|
treat as a migration event.
|
|
"""
|
|
assert (
|
|
compute_tag_uuid("command", "cmd_42", "R0014", 2, "T1083", None)
|
|
== "9aa491e5-f03b-5d8f-9eb5-161becedcdd6"
|
|
)
|
|
assert (
|
|
compute_tag_uuid("command", "cmd_42", "R0015", 1, "T1548", "T1548.001")
|
|
== "4fd57b14-c135-544c-97d9-4fabc1051584"
|
|
)
|
|
|
|
|
|
def test_namespace_constant_is_pinned() -> None:
|
|
"""The namespace UUID itself is part of the contract — regenerating
|
|
it would invalidate every tag UUID."""
|
|
assert str(_TTP_TAG_NS) == "1ca31f08-5522-5aae-8371-fe81f0e39de3"
|
|
|
|
|
|
# ── Replay-safety lock: parameter set ───────────────────────────────
|
|
|
|
|
|
def test_compute_tag_uuid_parameter_set_is_locked() -> None:
|
|
"""The accepted inputs MUST be exactly the six identity fields.
|
|
|
|
Adding ``created_at``, a process PID, a random salt, or any other
|
|
non-deterministic input silently breaks replay safety. This test
|
|
is the trip-wire: a contributor must update it deliberately to
|
|
change the input set.
|
|
"""
|
|
sig = inspect.signature(compute_tag_uuid)
|
|
names = tuple(sig.parameters)
|
|
assert names == (
|
|
"source_kind",
|
|
"source_id",
|
|
"rule_id",
|
|
"rule_version",
|
|
"technique_id",
|
|
"sub_technique_id",
|
|
)
|