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>
This commit is contained in:
1 parent
1fb7d33124
commit
d06ecd244d
7 files changed
+666
No files matched your search
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
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."
|
||||
)
|
||||
Reference in new issue
Block a user