Files
qtalker---/backend/tests/test_passage.py
Indiana 09089f01d5 feat: the Passage — crossing over becomes a layered rite (Workstream L)
cross_over was one verdict with one outcome. It is now five beats — listen,
name, unbind, open, release — each revealing something true about the
spirit, each paying escalating essence, each able to twist.

Reveals use the entity's REAL traits; there is no second hidden state
invented for the rite. Twists are trait-driven, verified directly rather
than assumed: a demon collapses the rite 3.6x more often than a calm spirit
(0.360 vs 0.099 at `unbind`) and lies ~31% of the time, while a spirit
under DECEIT_FLOOR cannot lie on any draw. A demon still resists at
`release` and never crosses — the existing judgment rule is preserved, not
re-implemented. Every draw comes from veil_float on the room's physical
entropy, never `random`.

Essence is kept across a collapse. Clawing it back would punish a seeker
for the spirit's instability, which is not theirs to control.

FIXED AN INFINITE LOOP IN THE SALVAGED TESTS, not a flake:
test_full_rite_on_a_calm_stuck_spirit ran `while layer is not None` while
passing collapse=FIRES. Its fixture comment claimed "both twist chances are
0 at these values, so NO draw can make this entity lie or collapse" — that
is false. COLLAPSE_FLOOR is 0.5 (deliberately below judgment's stuck bar of
0.6, as passage.py explains), and the fixture's volatility is 0.61, giving a
real ~9.9% collapse chance. Forced to fire, every layer bounced back to
`listen` and the run hung forever instead of failing.

Three fixes: hold the collapse draw (deceit still fires, which is the
point — it proves a spirit under the floor cannot lie even when told to),
correct the false comment, and bound the loop so a future regression fails
in seconds rather than hanging a test run.

Also added the entire seance.passage i18n block in both languages — the
agent died before writing it, so the gate was failing on 35 missing keys —
and reworded a comment in PassagePanel that spelled out a translation call
in full: the coverage checker greps source text and cannot tell a comment
from real code, so it demanded a key for the placeholder.

34 passage tests pass; 385 frontend; i18n parity and typecheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 14:19:55 +00:00

293 lines
11 KiB
Python

"""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