"""Workstream L: the pure Passage core (app/passage.py). Every draw here is PINNED — no `random`, no `veil_float`, no clock — so a twist that fires in this file fires on every run, forever. The only source of uncertainty in the real handler is `veil_float`, and it enters through `PassageDraw`, which these tests supply directly. """ import pytest from app.inventory import CROSS_OVER_ESSENCE from app.judgment import is_stuck_spirit from app.passage import ( COLLAPSIBLE_LAYERS, LAYER_ESSENCE, LAYERS, PassageDraw, collapse_chance, deceit_chance, next_layer, resolve_layer, ) # A spirit that genuinely qualifies for crossing (judgment.is_stuck_spirit: # alignment >= 0.5 AND volatility > 0.6) and is too incurious to lie # (deceptiveness 0.1 is well under DECEIT_FLOOR 0.55, so deceit_chance is # exactly 0 and no draw can make it lie). # # It CAN still collapse. COLLAPSE_FLOOR is 0.5, deliberately below # judgment's stuck bar of 0.6 (see passage.py), so volatility 0.61 gives a # real ~9.9% collapse chance on the collapsible layers. Tests that want an # uninterrupted rite must therefore hold the collapse draw — see # test_full_rite_on_a_calm_stuck_spirit_opens_every_layer_and_crosses. CALM_STUCK = {"alignment": 0.8, "power": 0.4, "volatility": 0.61, "deceptiveness": 0.1} # A demon: alignment below judgment's benevolence threshold. DEMON = {"alignment": 0.1, "power": 0.9, "volatility": 0.9, "deceptiveness": 0.9} # Maximally cagey (always able to lie) but perfectly steady (never collapses). LIAR = {"alignment": 0.9, "power": 0.5, "volatility": 0.2, "deceptiveness": 1.0} # Maximally unstable but perfectly straight. UNSTABLE = {"alignment": 0.9, "power": 0.5, "volatility": 1.0, "deceptiveness": 0.0} # Draw sentinels. 0.0 is below every possible chance (the twist fires); # 0.999 is above every possible chance (it does not). FIRES = 0.0 HOLDS = 0.999 NO_TWIST = PassageDraw(collapse=HOLDS, deceit=HOLDS) # --- shape ---------------------------------------------------------------- def test_layers_are_the_five_named_beats_in_order(): assert LAYERS == ("listen", "name", "unbind", "open", "release") def test_layer_rewards_escalate_and_the_last_pays_the_existing_cross_over_essence(): rewards = [LAYER_ESSENCE[layer] for layer in LAYERS] assert rewards == sorted(rewards) assert len(set(rewards)) == len(rewards) # strictly escalating assert LAYER_ESSENCE["release"] == CROSS_OVER_ESSENCE def test_next_layer_walks_the_track_and_ends(): assert [next_layer(layer) for layer in LAYERS] == [ "name", "unbind", "open", "release", None, ] def test_unknown_layer_raises(): with pytest.raises(ValueError): resolve_layer(CALM_STUCK, "banish", NO_TWIST) # --- the happy path: five beats, five reveals, one crossing ----------------- def test_full_rite_on_a_calm_stuck_spirit_opens_every_layer_and_crosses(): layer = LAYERS[0] seen = [] total = 0 # collapse HOLDS: volatility 0.61 is above COLLAPSE_FLOOR (0.5), so a # firing collapse draw would send every layer back to `listen` and this # loop would never terminate. deceit still FIRES, which is the point — # it proves a spirit under DECEIT_FLOOR cannot lie even when the draw # says it should. # The bound is a safety net, not a limit: five layers plus any collapse # should finish immediately, and an unbounded `while` turned a wrong # assertion into a hung test run rather than a failure. for _ in range(len(LAYERS) * 4): if layer is None: break outcome = resolve_layer(CALM_STUCK, layer, PassageDraw(collapse=HOLDS, deceit=FIRES)) seen.append((outcome.layer, outcome.result)) total += outcome.essence assert outcome.lied is False # deceptiveness 0.1 is under the floor layer = outcome.next_layer assert layer is None, "the rite never terminated" assert seen == [ ("listen", "opened"), ("name", "opened"), ("unbind", "opened"), ("open", "opened"), ("release", "crossed"), ] assert total == sum(LAYER_ESSENCE.values()) @pytest.mark.parametrize( "layer,trait", [ ("listen", "volatility"), ("name", "deceptiveness"), ("unbind", "power"), ("open", "alignment"), ], ) def test_each_opened_layer_reveals_that_layer_s_real_trait(layer, trait): outcome = resolve_layer(CALM_STUCK, layer, NO_TWIST) assert outcome.result == "opened" assert outcome.reveal is not None assert outcome.reveal.trait == trait # The REAL value off the entity's traits dict — no second hidden state. assert outcome.reveal.value == pytest.approx(CALM_STUCK[trait]) def test_release_has_no_trait_reveal_its_truth_is_the_crossing_itself(): outcome = resolve_layer(CALM_STUCK, "release", NO_TWIST) assert outcome.result == "crossed" assert outcome.reveal is None assert outcome.at_peace is True assert outcome.essence == CROSS_OVER_ESSENCE @pytest.mark.parametrize( "value,expected", [(0.9, "high"), (0.6, "high"), (0.5, "mid"), (0.4, "low"), (0.05, "low")], ) def test_reveal_bands_match_the_trait_value(value, expected): traits = {"alignment": 0.9, "power": value, "volatility": 0.7, "deceptiveness": 0.0} outcome = resolve_layer(traits, "unbind", NO_TWIST) assert outcome.reveal.band == expected # --- twist: low alignment (a demon) makes release resist -------------------- def test_a_demon_is_resisted_at_release_no_matter_what_the_draws_say(): # Every corner of the draw space — the invariant is checked before any # draw is consulted, so none of these can cross a demon. for collapse in (0.0, 0.5, 0.999): for deceit in (0.0, 0.5, 0.999): outcome = resolve_layer( DEMON, "release", PassageDraw(collapse=collapse, deceit=deceit) ) assert outcome.result == "resisted" assert outcome.at_peace is False assert outcome.essence == 0 assert outcome.next_layer is None def test_a_benevolent_but_not_stuck_spirit_also_resists_matching_judgment(): settled = {"alignment": 0.9, "power": 0.5, "volatility": 0.2, "deceptiveness": 0.0} assert is_stuck_spirit(settled) is False # same rule judgment.py uses outcome = resolve_layer(settled, "release", PassageDraw(collapse=FIRES, deceit=FIRES)) assert outcome.result == "resisted" assert outcome.at_peace is False def test_a_demon_can_still_walk_the_earlier_layers_and_learn_of_itself(): # The rite is not gated — a demon opens `open` and the reveal is its own # real (low) alignment, which is precisely the warning the seeker gets. outcome = resolve_layer(DEMON, "open", PassageDraw(collapse=HOLDS, deceit=HOLDS)) assert outcome.result == "opened" assert outcome.reveal.trait == "alignment" assert outcome.reveal.value == pytest.approx(0.1) assert outcome.reveal.band == "low" # --- twist: high deceptiveness lies ----------------------------------------- def test_high_deceptiveness_can_reveal_a_mirrored_false_trait(): outcome = resolve_layer(LIAR, "unbind", PassageDraw(collapse=HOLDS, deceit=FIRES)) assert outcome.result == "opened" assert outcome.lied is True # power is really 0.5; the lie mirrors it. assert outcome.reveal.value == pytest.approx(1.0 - LIAR["power"]) def test_the_lie_is_not_flagged_on_the_frame_that_carries_it(): outcome = resolve_layer(LIAR, "listen", PassageDraw(collapse=HOLDS, deceit=FIRES)) assert outcome.lied is True # `unreliable_layer` is what the client renders; it stays empty here, so # the seeker has no way to know at the time. assert outcome.unreliable_layer is None def test_the_previous_lie_surfaces_only_in_hindsight_on_the_next_layer(): outcome = resolve_layer(LIAR, "name", NO_TWIST, previous_lied=True) assert outcome.unreliable_layer == "listen" def test_hindsight_never_points_before_the_first_layer(): outcome = resolve_layer(LIAR, "listen", NO_TWIST, previous_lied=True) assert outcome.unreliable_layer is None def test_a_straight_spirit_can_never_lie_on_any_draw(): assert deceit_chance(UNSTABLE) == 0.0 for layer in ("listen", "name", "unbind", "open"): outcome = resolve_layer(UNSTABLE, layer, PassageDraw(collapse=HOLDS, deceit=FIRES)) assert outcome.lied is False assert outcome.reveal.value == pytest.approx(UNSTABLE[outcome.reveal.trait]) def test_deceit_chance_is_zero_at_the_floor_and_maxes_at_one(): assert deceit_chance({"deceptiveness": 0.55}) == 0.0 assert deceit_chance({"deceptiveness": 0.2}) == 0.0 assert deceit_chance({"deceptiveness": 1.0}) == pytest.approx(0.40) def test_a_draw_exactly_at_the_chance_does_not_lie(): # Strict `<`, pinned at the boundary: no off-by-one flakiness. chance = deceit_chance(LIAR) assert resolve_layer(LIAR, "listen", PassageDraw(collapse=HOLDS, deceit=chance)).lied is False # --- twist: high volatility collapses the rite ------------------------------ @pytest.mark.parametrize("layer", sorted(COLLAPSIBLE_LAYERS)) def test_high_volatility_collapses_a_middle_layer_back_to_listen(layer): outcome = resolve_layer(UNSTABLE, layer, PassageDraw(collapse=FIRES, deceit=HOLDS)) assert outcome.result == "collapsed" assert outcome.next_layer == "listen" assert outcome.reveal is None # No essence from the collapsed beat — but nothing is taken back either: # a collapse never produces a negative number for the caller to debit. assert outcome.essence == 0 assert outcome.at_peace is False def test_listen_never_collapses_a_collapse_back_to_itself_would_be_a_no_op(): assert collapse_chance(UNSTABLE, "listen") == 0.0 outcome = resolve_layer(UNSTABLE, "listen", PassageDraw(collapse=FIRES, deceit=HOLDS)) assert outcome.result == "opened" def test_release_never_collapses_so_the_crossable_are_not_the_uncrossable(): # UNSTABLE qualifies as stuck (alignment 0.9, volatility 1.0) — the very # profile with the highest collapse chance. It must still be able to # finish the rite. assert is_stuck_spirit(UNSTABLE) is True assert collapse_chance(UNSTABLE, "release") == 0.0 outcome = resolve_layer(UNSTABLE, "release", PassageDraw(collapse=FIRES, deceit=FIRES)) assert outcome.result == "crossed" def test_a_steady_spirit_can_never_collapse_on_any_draw(): assert collapse_chance(LIAR, "unbind") == 0.0 outcome = resolve_layer(LIAR, "unbind", PassageDraw(collapse=FIRES, deceit=HOLDS)) assert outcome.result == "opened" def test_collapse_chance_is_zero_at_the_floor_and_maxes_at_one(): assert collapse_chance({"volatility": 0.5}, "unbind") == 0.0 assert collapse_chance({"volatility": 1.0}, "unbind") == pytest.approx(0.45) def test_collapse_beats_the_lie_a_layer_that_falls_apart_reveals_nothing(): both = {"alignment": 0.9, "power": 0.5, "volatility": 1.0, "deceptiveness": 1.0} outcome = resolve_layer(both, "unbind", PassageDraw(collapse=FIRES, deceit=FIRES)) assert outcome.result == "collapsed" assert outcome.lied is False assert outcome.reveal is None # --- purity --------------------------------------------------------------- def test_resolve_layer_is_deterministic_and_never_mutates_its_traits(): traits = dict(LIAR) draw = PassageDraw(collapse=0.31, deceit=0.07) first = resolve_layer(traits, "unbind", draw) for _ in range(50): assert resolve_layer(traits, "unbind", draw) == first assert traits == LIAR