Files
qtalker---/backend/tests/test_possession.py
Indiana 58c30b7273 feat: possession presentation layer — glitchy text + degraded audio
First sub-project of the "make contact feel real" arc (spec:
docs/superpowers/specs/2026-07-23-possession-presentation-design.md).
Direct Contact replies now feel like a spirit fighting through static to
hold the channel rather than a plain chat bubble:

- backend/app/possession.py: compute_stability(rarity, magnitude, rng) —
  a 0.05-0.98 score per reply (rarer entity + stronger triggering anomaly =
  cleaner signal), rng injectable for a later quantum-RNG source.
- ws.py sends stability on reply_start; audio synthesis for that reply gets
  noise/bitcrush scaled by instability (1 - stability) via a new
  instability param on synthesize_spirit_voice — effects.py itself is
  untouched, only the params fed into it.
- frontend/src/lib/possession.ts: renderPossessedText — pure, deterministic
  (tick-seeded, no Math.random) text corruption with self-correcting
  glitch bursts, wired into Transcript.tsx's streaming reply display.

Stored transcript/reply text is unaffected — this is presentation only.
78/78 backend, 137/137 frontend tests passing.
2026-07-23 05:48:02 +00:00

70 lines
2.6 KiB
Python

from app.possession import compute_stability
def test_rarity_ordering_holds_magnitude_and_rng_constant():
# Midpoint rng (0.5) zeroes out jitter, isolating the rarity base.
rng = lambda: 0.5
common = compute_stability("common", magnitude=50, rng=rng)
uncommon = compute_stability("uncommon", magnitude=50, rng=rng)
rare = compute_stability("rare", magnitude=50, rng=rng)
mythic = compute_stability("mythic", magnitude=50, rng=rng)
assert common < uncommon < rare < mythic
def test_unknown_rarity_falls_back_to_common_base():
rng = lambda: 0.5
assert compute_stability("legendary???", magnitude=50, rng=rng) == compute_stability(
"common", magnitude=50, rng=rng
)
def test_magnitude_bonus_increases_stability_up_to_cap():
rng = lambda: 0.5
low = compute_stability("common", magnitude=0, rng=rng)
mid = compute_stability("common", magnitude=10, rng=rng)
high = compute_stability("common", magnitude=1000, rng=rng)
assert low < mid < high
# Bonus caps at 0.15, so magnitude=15 and magnitude=1000 must land the
# same once base and jitter are held constant.
at_cap = compute_stability("common", magnitude=15, rng=rng)
past_cap = compute_stability("common", magnitude=1000, rng=rng)
assert at_cap == past_cap
def test_jitter_scaled_to_plus_minus_point_one():
# rng()=0 -> minimum jitter (-0.1); rng()=1 -> maximum jitter (+0.1).
base = 0.35 # common base
low_jitter = compute_stability("common", magnitude=0, rng=lambda: 0.0)
high_jitter = compute_stability("common", magnitude=0, rng=lambda: 1.0)
assert low_jitter == max(0.05, base - 0.1)
assert high_jitter == min(0.98, base + 0.1)
def test_clamped_at_lower_bound():
# common base 0.35 minus full jitter (0.1) minus nothing else is 0.25,
# comfortably above the floor -- force the floor with a negative-leaning
# setup instead: rarity base + jitter alone can't go below 0.05, but the
# clamp itself must still be exercised directly via a pathological rng.
stability = compute_stability("common", magnitude=0, rng=lambda: -100.0)
assert stability == 0.05
def test_clamped_at_upper_bound():
stability = compute_stability("mythic", magnitude=1000, rng=lambda: 100.0)
assert stability == 0.98
def test_deterministic_with_injected_rng():
rng = lambda: 0.73
first = compute_stability("rare", magnitude=42, rng=rng)
second = compute_stability("rare", magnitude=42, rng=rng)
assert first == second
def test_default_rng_produces_value_in_range():
stability = compute_stability("uncommon", magnitude=25)
assert 0.05 <= stability <= 0.98