""" Tests for vidui — property 3 (enumerative commit) of azazel_engineering_reading.md §14. The vidui must: - Have all THREE category slots present (avonot/peshaim/chatot), even when individual slots are empty - Refuse construction when ALL slots are empty (no untagged disposal — the failure mode property 3 defends against) - Be immutable after construction (the moment of confession is the moment of fixing) - Carry an attributable confessor (anonymous confession defeats property 7's authorized-routing requirement) - Render to readable text preserving the kol-three-times structure - Expose category counts WITHOUT the items themselves (for the public observability endpoint, commit 8 — property 9) """ from datetime import datetime, timedelta, timezone import pytest from azazel_processor.vidui import ( EmptyConfessionError, Vidui, confess, confess_boundary_violation, confess_empty_vital, confess_halt, confess_malformed_input, ) # ============================================================ # The three-fold structure # ============================================================ def test_vidui_has_exactly_the_five_expected_fields(): """Structural test: the dataclass has exactly avonot, peshaim, chatot, confessor, confessed_at — no silent additions and no missing fields. A future contributor who adds a sixth field (or removes one) breaks this and forces an explicit decision.""" fields = {f.name for f in Vidui.__dataclass_fields__.values()} assert fields == {"avonot", "peshaim", "chatot", "confessor", "confessed_at"} def test_confess_with_avonot_only_succeeds(): v = confess(avonot=("halt avodah_zarah",), confessor="kohen_gadol") assert v.avonot == ("halt avodah_zarah",) assert v.peshaim == () assert v.chatot == () def test_confess_with_peshaim_only_succeeds(): v = confess(peshaim=("ketiv/qere collapse",), confessor="kohen_gadol") assert v.peshaim == ("ketiv/qere collapse",) def test_confess_with_chatot_only_succeeds(): v = confess(chatot=("empty-vital korban",), confessor="kohen_gadol") assert v.chatot == ("empty-vital korban",) def test_confess_with_all_three_categories_filled_succeeds(): v = confess( avonot=("halt 1",), peshaim=("collapse 1",), chatot=("missing 1", "missing 2"), confessor="kohen_gadol", ) assert v.avonot == ("halt 1",) assert v.peshaim == ("collapse 1",) assert v.chatot == ("missing 1", "missing 2") # ============================================================ # Untagged disposal is forbidden # ============================================================ def test_confess_with_all_slots_empty_raises(): """Property 3: no untagged disposal. The act of naming what is being disposed is load-bearing. confess() with all three slots empty is the system trying to dispose without naming — refuse.""" with pytest.raises(EmptyConfessionError) as exc: confess(confessor="kohen_gadol") assert "without naming" in str(exc.value).lower() or "loss" in str(exc.value).lower() def test_confess_with_explicit_empty_tuples_in_all_slots_raises(): with pytest.raises(EmptyConfessionError): confess(avonot=(), peshaim=(), chatot=(), confessor="kohen_gadol") def test_vidui_construction_with_empty_string_item_raises(): """An empty string in a category list would degrade the enumeration to a count — refuse at construction.""" with pytest.raises(EmptyConfessionError): confess(avonot=("",), confessor="kohen_gadol") def test_vidui_construction_with_whitespace_only_item_raises(): with pytest.raises(EmptyConfessionError): confess(chatot=(" ",), confessor="kohen_gadol") def test_vidui_construction_with_non_string_item_raises(): with pytest.raises(EmptyConfessionError): confess(avonot=(None,), confessor="kohen_gadol") # type: ignore[arg-type] # ============================================================ # Confessor attribution # ============================================================ def test_confessor_required(): """Anonymous confession defeats property 7 (authorized routing). confess() must take a confessor (keyword-only) — no default.""" with pytest.raises(TypeError): # No confessor argument confess(avonot=("x",)) # type: ignore[call-arg] def test_confessor_must_be_non_empty_string(): with pytest.raises(EmptyConfessionError): confess(avonot=("x",), confessor="") def test_confessor_must_be_non_whitespace(): with pytest.raises(EmptyConfessionError): confess(avonot=("x",), confessor=" ") def test_confessor_recorded_on_vidui(): v = confess(avonot=("x",), confessor="aharon_ha_kohen") assert v.confessor == "aharon_ha_kohen" # ============================================================ # Immutability # ============================================================ def test_vidui_is_frozen(): """The moment of confession is the moment of fixing. No path to mutating the vidui after construction — that would let an operator re-write what they confessed.""" v = confess(avonot=("x",), confessor="kohen_gadol") with pytest.raises((AttributeError, Exception)): v.confessor = "someone_else" # type: ignore[misc] def test_vidui_categories_are_tuples_not_lists(): """Tuples are immutable; lists are not. Even though the dataclass is frozen, a list-valued field could be mutated in place (frozen=True doesn't propagate through container types). Tuples are the right shape for the enumeration.""" v = confess(avonot=("x",), peshaim=("y",), chatot=("z",), confessor="x") assert isinstance(v.avonot, tuple) assert isinstance(v.peshaim, tuple) assert isinstance(v.chatot, tuple) def test_iterable_input_normalized_to_tuple(): """confess() should accept any iterable for category lists and normalize to tuple at construction. A list passed in cannot be held as a list-valued field — that would re-introduce the mutability gap.""" v = confess(avonot=["x", "y"], confessor="kohen_gadol") assert isinstance(v.avonot, tuple) assert v.avonot == ("x", "y") # ============================================================ # Timestamp # ============================================================ def test_confessed_at_set_to_current_utc_time(): before = datetime.now(timezone.utc) v = confess(avonot=("x",), confessor="kohen_gadol") after = datetime.now(timezone.utc) assert before - timedelta(seconds=1) <= v.confessed_at <= after + timedelta(seconds=1) def test_confessed_at_is_timezone_aware_utc(): v = confess(avonot=("x",), confessor="kohen_gadol") assert v.confessed_at.tzinfo is not None assert v.confessed_at.utcoffset() == timedelta(0) # ============================================================ # Rendering # ============================================================ def test_to_text_preserves_kol_structure_for_all_three(): v = confess( avonot=("a1", "a2"), peshaim=("p1",), chatot=("c1",), confessor="kohen", ) text = v.to_text() assert "kol avonot" in text assert "kol pish'eihem" in text assert "l'kol chatotam" in text assert "a1" in text and "a2" in text assert "p1" in text assert "c1" in text def test_to_text_omits_empty_categories(): v = confess(avonot=("only-avonot",), confessor="kohen") text = v.to_text() assert "kol avonot" in text assert "kol pish'eihem" not in text assert "l'kol chatotam" not in text def test_category_summary_returns_counts_without_items(): """The shape exposed by the public observability endpoint — counts only, no item content. Property 9 at the public boundary.""" v = confess( avonot=("a1", "a2"), peshaim=("p1",), chatot=(), confessor="kohen", ) summary = v.category_summary() assert summary == {"avonot": 2, "peshaim": 1, "chatot": 0} # Items themselves are NOT in the summary — only counts. assert "a1" not in str(summary) assert "p1" not in str(summary) # ============================================================ # Convenience helpers # ============================================================ def test_confess_empty_vital_routes_to_chatot(): """The literal meaning of chet — missing the mark — is exactly the empty-vital case: the system fired and nothing landed.""" v = confess_empty_vital("Claude response on bereishit 1:1", confessor="x") assert v.chatot assert v.avonot == () assert v.peshaim == () assert "empty-vital" in v.chatot[0] def test_confess_halt_routes_to_avonot(): """Halt-trips are categorical orientation failures — yehareg-v'al-ya'avor cardinal sins.""" v = confess_halt("avodah_zarah", "claude_model treated as authority", confessor="x") assert v.avonot assert "avodah_zarah" in v.avonot[0] assert "claude_model" in v.avonot[0] def test_confess_boundary_violation_routes_to_peshaim(): """Boundary-keeping failures (gilui-shaped) — distinction collapses.""" v = confess_boundary_violation("ketiv/qere collapsed", confessor="x") assert v.peshaim assert "boundary collapse" in v.peshaim[0] def test_confess_malformed_input_routes_to_chatot(): """Different from empty-vital — empty-vital is content with no Torah; malformed is content with no SHAPE.""" v = confess_malformed_input("truncated JSON, missing closing brace", confessor="x") assert v.chatot assert "malformed" in v.chatot[0] def test_helpers_still_require_confessor(): """The convenience helpers do not weaken property-3's operator-attribution requirement.""" with pytest.raises(TypeError): confess_empty_vital("x") # type: ignore[call-arg]