Files
bikoret/azazel_processor/test/test_eretz_gezerah.py
T
Scott KarpandClaude Opus 4.7 d06ecd244d azazel_processor: commit 1 — skeleton + stages (properties 11 & 12)
Pass 4 / Finding F, commit 1 of 10 from BUILD_AZAZEL.md. Lays the
load-bearing foundation: classification discipline (property 11) at
the type system level, and unmanaged-destination opacity (property
12) via a Python sentinel. Every later commit in the Azazel build
inherits these defenses for free.

CREATED:

  azazel_processor/
    pyproject.toml                      — Poetry config matching
                                          korban_processor; pythonpath
                                          includes ../korban_processor/src
                                          so the cross-package
                                          classification-discipline test
                                          can import both packages
    src/azazel_processor/
      __init__.py                       — public surface for commit 1:
                                          AzazelStage, AzazelStaged,
                                          AzazelStageError, ERETZ_GEZERAH,
                                          is_eretz_gezerah
      eretz_gezerah.py                  — the destination sentinel; module
                                          __getattr__ refuses any
                                          attribute beyond the two public
                                          names so introspection cannot
                                          discover further handles
      stages.py                          — AzazelStage enum (six
                                          checkpoints from the build doc:
                                          STAGED → CONFESSED → ROUTED →
                                          DESTROYED → ACKNOWLEDGED →
                                          RECORDED), AzazelStaged frozen
                                          dataclass, AzazelStageError
    test/
      test_eretz_gezerah.py             — 11 tests covering: sentinel
                                          identity stable across imports,
                                          not-a-string, no public attrs,
                                          repr leaks no destination info,
                                          identity (is) not equality (==),
                                          impostor with __eq__ rejected,
                                          module __getattr__ refuses
                                          unknown access, CI invariant
                                          forbidding destination-handle
                                          ASSIGNMENTS in source (catches
                                          regressions; mention in
                                          docstrings/__all__/getattr
                                          allowlist is fine)
      test_stages.py                    — 10 tests: intra-Azazel gating
                                          (happy path, mismatch raises
                                          naming both stages, frozen
                                          dataclass, six distinct
                                          checkpoints) and CROSS-PACKAGE
                                          classification discipline:
                                          KorbanStaged and AzazelStaged
                                          are distinct types; enum value
                                          spaces are disjoint; an Azazel
                                          function refuses a KorbanStaged
                                          input and vice versa; no
                                          conversion function exists in
                                          the public surface

PROPERTY COVERAGE THIS COMMIT:

  Property 11 (classification discipline):
    Defended by the type-distinctness of AzazelStaged vs.
    korban_processor.stages.Staged. Cross-package tests verify the
    two cannot be substituted at runtime; the absence of any
    korban_to_azazel/to_korban/from_korban/convert function in the
    public surface is verified by name-shape grep.

  Property 12 (unmanaged destination):
    Defended by the sentinel design: only object identity is exposed;
    no string handle exists; module __getattr__ refuses introspection;
    CI invariant catches assignment-shaped regressions across all of
    azazel_processor/src/.

PENDING (commits 2-10 per BUILD_AZAZEL.md):

  vidui (commit 2 — enumerative commit, property 3)
  shaliach + tevilah (commit 3 — role-separated operator, property 4)
  goral (commit 4 — blind routing, property 2)
  azazel orchestrator + state machine (commit 5 — properties 5, 7, 10)
  protocol envelope (commit 6 — protocol-restricted access, property 8)
  L3 azazel_records (commit 7 — operator-internal half of property 9)
  /peah/azazel_log endpoint (commit 8 — public half of property 9)
  Korban→Azazel routing on empty-vital + chatzotzrot wiring
    (commit 9 — properties 1, 6)
  Closure pass (commit 10)

Tests: azazel_processor 21 passed. korban_processor unchanged at
203 passed, 1 skipped (live-API). No regressions across the boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:07:44 -04:00

210 lines
7.4 KiB
Python

"""
Tests for the eretz_gezerah sentinel — property 12 (unmanaged
destination opacity) from azazel_engineering_reading.md §14.
The destination MUST be opaque. There is no name, no URL, no queue
identifier, no string handle that encodes anything about where state
goes after disposal. The only legitimate reference is the sentinel
ERETZ_GEZERAH; identity comparison (`is`) is the only legitimate
predicate.
"""
import pathlib
import pytest
from azazel_processor.eretz_gezerah import ERETZ_GEZERAH, is_eretz_gezerah
# ============================================================
# The sentinel itself
# ============================================================
def test_sentinel_exists():
"""ERETZ_GEZERAH is importable and is the same object across imports."""
from azazel_processor.eretz_gezerah import ERETZ_GEZERAH as second_import
assert ERETZ_GEZERAH is second_import
def test_sentinel_is_a_plain_object_not_a_string():
"""A string would be searchable, interned, serializable. The sentinel
must NOT be a string. Property 12: opacity of name."""
assert not isinstance(ERETZ_GEZERAH, str)
assert not isinstance(ERETZ_GEZERAH, bytes)
def test_sentinel_has_no_meaningful_attributes():
"""A bare object() has no useful attributes for tooling to
discover. We don't grant the sentinel a name, a destination
string, or any other tracking handle."""
# Plain object() exposes only the dunder methods. No public attrs.
public = [a for a in dir(ERETZ_GEZERAH) if not a.startswith("_")]
assert public == []
def test_sentinel_repr_does_not_leak_destination_info():
"""The repr is just object()'s default — no operator-readable name
encoding 'where it goes'. Default Python repr is `<object object at
0x...>` which leaks only the address (changes per process)."""
r = repr(ERETZ_GEZERAH)
assert "<object object at 0x" in r
assert "azazel" not in r.lower()
assert "void" not in r.lower()
assert "null" not in r.lower()
# ============================================================
# is_eretz_gezerah/1
# ============================================================
def test_is_eretz_gezerah_recognizes_canonical_sentinel():
assert is_eretz_gezerah(ERETZ_GEZERAH)
def test_is_eretz_gezerah_rejects_other_objects():
assert not is_eretz_gezerah(None)
assert not is_eretz_gezerah(object())
assert not is_eretz_gezerah("eretz_gezerah")
assert not is_eretz_gezerah("ERETZ_GEZERAH")
assert not is_eretz_gezerah(0)
assert not is_eretz_gezerah([])
def test_is_eretz_gezerah_uses_identity_not_equality():
"""A class with __eq__ that returns True for anything must not
be confused with the sentinel. Identity is the gate; equality
would let any impostor in."""
class Impostor:
def __eq__(self, other):
return True
def __hash__(self):
return 0
impostor = Impostor()
assert impostor == ERETZ_GEZERAH # equality is True (impostor lies)
assert not is_eretz_gezerah(impostor) # but identity is not
# ============================================================
# Module-level __getattr__ refuses introspection
# ============================================================
def test_module_refuses_unknown_attribute_access():
"""The module exposes only ERETZ_GEZERAH and is_eretz_gezerah.
Any other attribute access must raise — there are no further
handles to discover."""
import azazel_processor.eretz_gezerah as mod
with pytest.raises(AttributeError) as exc:
mod.DESTINATION_URL # noqa: B018 — deliberate access for the test
assert "no further handles" in str(exc.value).lower() or \
"opaque" in str(exc.value).lower()
def test_module_refuses_invented_metadata_lookup():
import azazel_processor.eretz_gezerah as mod
for invented in ("destination", "endpoint", "queue_name", "host", "address"):
with pytest.raises(AttributeError):
getattr(mod, invented)
# ============================================================
# CI-style invariant: no destination string in the codebase
# ============================================================
#
# This is the grep-the-codebase check from BUILD_AZAZEL.md §12.
# It runs as a normal test so a regression at any layer of the
# build (a future contributor adding `DESTINATION_URL = "..."` to
# any module) fires immediately.
# Forbidden ASSIGNMENT-SHAPED patterns. The failure mode is someone
# adding a module-level constant that reintroduces a destination
# handle (e.g. `DESTINATION_URL = "..."`). Mere mention in a
# docstring, an `__all__` re-export, or a `__getattr__` allowlist is
# NOT the failure mode; what we defend against is binding a forbidden
# name to a value at module scope.
import re
_FORBIDDEN_ASSIGNMENT_PATTERNS = [
re.compile(rf"^\s*{name}\s*=", re.MULTILINE)
for name in (
"DESTINATION_URL",
"DESTINATION_HOST",
"AZAZEL_QUEUE",
"AZAZEL_ENDPOINT",
"DISPOSAL_HOST",
"DISPOSAL_URL",
"DISPOSAL_QUEUE",
"AZAZEL_RECIPIENT",
"DISPOSAL_RECIPIENT",
)
]
def _walk_source_files():
"""Yield every .py file under src/ — the source the operator has
to read to understand the channel. Tests are excluded (test
files legitimately reference forbidden patterns in assertions)."""
src_root = pathlib.Path(__file__).resolve().parent.parent / "src"
yield from src_root.rglob("*.py")
def test_no_destination_handle_assigned_in_source():
"""Property 12 enforced as a CI invariant. If a future contributor
adds `DESTINATION_URL = "..."` (or any of the other forbidden
handle-shaped names) to ANY module in azazel_processor/, this
test fails — they reintroduced the failure mode the sentinel
exists to prevent.
Catches assignment, not mention. Documenting "do not add
DESTINATION_URL" in a docstring is fine; `DESTINATION_URL = "x"`
is the regression.
"""
offenders = []
for path in _walk_source_files():
text = path.read_text()
for pattern in _FORBIDDEN_ASSIGNMENT_PATTERNS:
for match in pattern.finditer(text):
offenders.append((path.name, match.group().strip()))
assert offenders == [], (
f"Forbidden destination-handle assignment(s) in azazel_processor "
f"source: {offenders}. The destination is opaque by specification "
f"(property 12, azazel_engineering_reading.md §14). Use "
f"ERETZ_GEZERAH (the Python sentinel), not a string-valued "
f"module constant."
)
def test_sentinel_name_not_assigned_to_string_in_source():
"""A module-level `X = 'ERETZ_GEZERAH'` would treat the sentinel
name as if it were a string handle — re-introducing the very
string-tracking the sentinel exists to prevent. Mere mention in
`__all__` or `__getattr__`'s allowlist is fine; binding a name
to the string is not.
"""
string_assignment = re.compile(
r"""^\s*\w+\s*=\s*["']ERETZ_GEZERAH["']""", re.MULTILINE
)
offenders = []
for path in _walk_source_files():
text = path.read_text()
if string_assignment.search(text):
offenders.append(path.name)
assert offenders == [], (
f"Module-level assignment binding the string 'ERETZ_GEZERAH' "
f"to a name in: {offenders}. The sentinel is a Python object, "
f"not a string. Refer to it via the imported name, not via a "
f"string handle."
)