Files
qtalker---/backend/tests/test_entities.py
Indiana cf602ab3de feat: the moon shapes who answers; wire magnetometer; drop dead CSS
MOON INFLUENCE ON SUMMONING

Astronomy previously only decided whether a channel's familiar spirit
returned. It now shapes *who comes through*:

  - Rarity skews with real moon illumination. At full moon the rare and
    mythic weights roughly triple while common recedes, so a mythic
    summoning becomes a reason to go out on the right night rather than a
    flat lottery. Deliberately a skew and never a gate — every tier stays
    reachable on every night, because someone who can only play midweek
    should not be locked out of the good spirits.
  - Hidden traits take a small moonlit nudge: power and volatility rise,
    alignment drifts slightly darker. Capped at 0.12 and clamped to [0,1],
    so a full moon intensifies what a spirit already is instead of
    rewriting it. Deceptiveness is untouched — whether a spirit lies is its
    own nature, not the sky's doing.
  - The mint prompt is told the phase, and explicitly told the entity must
    never mention or seem aware of it. It shapes who they are, not their
    dialogue; a ghost remarking on the moonlight would break the illusion
    instantly.

Tests assert the outcomes shift in practice (mythic rate over 4000 draws,
rare-tier counts across 300 fallback profiles), not merely that the code
runs. test_mint_prompt_never_receives_traits now allows `sky` while still
forbidding `traits`: moon phase is public, observable state anyone can look
up, hidden ground truth is not.

GEOMAGNETIC (app/geomagnetic.py)

Real NOAA SWPC planetary K-index, verified against the live endpoint —
which caught a real bug: I had written the parser against an
array-of-arrays shape, and the actual feed serves a list of objects
(`estimated_kp` float, `kp_index` int, `kp` a display string with a letter
suffix). Fixed, and the tests now use the real captured shape. Cached,
never blocking, and a failed refresh keeps serving the last real value —
an hour-old genuine measurement beats nothing, and geomagnetic conditions
do not change fast enough for that to mislead.

MAGNETOMETER WIRED

MagnetometerListener existed but was never connected. The EMF panel now
runs it alongside the motion listener where the hardware exists, so the
"EMF meter" measures actual magnetic field in µT rather than only
inferring disturbance from movement. Additive: the motion path is
untouched and remains the only option on iOS. Its field jitter also feeds
the entropy pool.

DEAD CODE

Removed .evp-scope and .radio-waterfall, orphaned when both panels moved to
the shared SpectrumScope. Audited every other flagged export first and left
them alone — they are used internally, and "not imported elsewhere" is not
the same as dead.

Adds docs/CHANNELS.md recording what each channel measures and, honestly,
what has actually been verified against hardware versus only written
carefully.

311 backend + 355 frontend tests pass; i18n parity gate passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 13:31:15 +00:00

340 lines
13 KiB
Python

import inspect
import pytest
from app.entities import (
fallback_profile,
normalize_profile,
parse_mint_response,
roll_traits,
signature_from_anomalies,
)
from app.llm import prompts as llm_prompts
TRAIT_KEYS = ("alignment", "power", "volatility", "deceptiveness")
def _anomaly(freq, mag):
return {"source": "radio", "frequency": freq, "magnitude": mag}
def test_signature_needs_enough_anomalies():
assert signature_from_anomalies([_anomaly(101.1, 5.0)]) is None
assert signature_from_anomalies([]) is None
def test_signature_is_deterministic_for_same_pattern():
anomalies = [_anomaly(101.1 + i, 5.0 + i) for i in range(6)]
assert signature_from_anomalies(anomalies) == signature_from_anomalies(list(anomalies))
def test_parse_mint_response_extracts_json():
raw = 'Sure! Here you go:\n{"name": "Vesper Wren", "epithet": "the Static Widow"}\nHope that helps'
profile = parse_mint_response(raw)
assert profile is not None
assert profile["name"] == "Vesper Wren"
def test_parse_mint_response_rejects_garbage():
assert parse_mint_response("no json here at all") is None
assert parse_mint_response('{"epithet": "nameless"}') is None
def test_normalize_profile_fills_and_clamps():
profile = normalize_profile(
{
"name": " Hollow Briar ",
"rarity": "legendary", # not a real tier -> common
"voice": {"voice_id": "nonexistent", "pitch": 99, "noise": -5},
"visual": {"form": "dragon", "hue": 9999},
"quotes": ["one", 2, "three"],
},
"abcdef0123456789",
)
assert profile["name"] == "Hollow Briar"
assert profile["rarity"] == "common"
assert profile["voice"]["voice_id"] != "nonexistent"
# Widened from +-6 when voices moved to coherent archetypes: a child
# and an elder man have to be able to land genuinely far apart.
assert -9 <= profile["voice"]["pitch"] <= 9
assert 0.01 <= profile["voice"]["noise"] <= 0.08
assert profile["visual"]["form"] in ("wisp", "banshee", "fairy", "shade")
assert 0 <= profile["visual"]["hue"] <= 360
assert profile["quotes"] == ["one", "three"]
def test_fallback_profile_is_deterministic_and_valid():
one = fallback_profile("0123456789abcdef")
two = fallback_profile("0123456789abcdef")
assert one == two
assert one["name"]
assert one["rarity"] in ("common", "uncommon", "rare", "mythic")
assert one["quotes"]
# --- hidden traits (Workstream A) -------------------------------------
def test_roll_traits_values_are_in_range():
traits = roll_traits("some-signature-1")
assert set(traits.keys()) == set(TRAIT_KEYS)
for key in TRAIT_KEYS:
assert isinstance(traits[key], float)
assert 0.0 <= traits[key] <= 1.0
def test_roll_traits_is_deterministic_for_same_signature():
one = roll_traits("repeatable-signature")
two = roll_traits("repeatable-signature")
assert one == two
def test_roll_traits_varies_by_signature():
values = {tuple(roll_traits(f"sig-{i}").values()) for i in range(20)}
# 20 distinct signatures should not collapse onto a single roll.
assert len(values) > 1
def test_normalize_profile_includes_traits_in_range():
profile = normalize_profile({"name": "Hollow Briar"}, "traits-norm-signature")
traits = profile["traits"]
assert set(traits.keys()) == set(TRAIT_KEYS)
for key in TRAIT_KEYS:
assert 0.0 <= traits[key] <= 1.0
def test_normalize_profile_traits_match_roll_traits_for_signature():
signature = "cross-check-signature"
profile = normalize_profile({"name": "X"}, signature)
assert profile["traits"] == roll_traits(signature)
def test_normalize_profile_traits_are_signature_deterministic():
one = normalize_profile({"name": "A"}, "det-signature")
two = normalize_profile({"name": "B", "rarity": "rare"}, "det-signature")
# Same signature -> same traits every call, regardless of the rest of
# the (LLM-supplied or fallback) profile passed in.
assert one["traits"] == two["traits"]
def test_fallback_profile_includes_traits_in_range():
profile = fallback_profile("fallback-traits-signature")
traits = profile["traits"]
assert set(traits.keys()) == set(TRAIT_KEYS)
for key in TRAIT_KEYS:
assert 0.0 <= traits[key] <= 1.0
def test_fallback_profile_traits_are_signature_deterministic():
one = fallback_profile("fallback-det-signature")
two = fallback_profile("fallback-det-signature")
assert one["traits"] == two["traits"]
def test_persona_is_independent_of_rolled_traits():
"""Persona text must stay fully decoupled from hidden truth: pairing the
exact same persona string with many different signatures should surface
a spread of alignment rolls, not a single value tied to the persona."""
fixed_persona = "A sweet old lady who just wants to chat about her garden."
alignments = set()
for i in range(40):
profile = normalize_profile(
{"name": "Nana", "persona": fixed_persona}, f"persona-independence-{i}"
)
assert profile["persona"] == fixed_persona
alignments.add(round(profile["traits"]["alignment"], 3))
# A meaningful spread (not clustered on one or two values) shows
# alignment isn't derived from — or gated by — the persona text.
assert len(alignments) > 20
def test_mint_prompt_never_receives_traits():
"""`mint_prompt` builds the LLM system/user prompt; traits must never be
one of its inputs, so hidden ground truth can't leak into persona
generation."""
params = set(inspect.signature(llm_prompts.mint_prompt).parameters)
assert "traits" not in params
# `sky` is public, observable state (moon phase — anyone can look up),
# so it may shape the persona. Hidden ground truth still must not.
assert params == {"signature", "channel", "anomaly_summary", "voice_ids", "sky"}
# --- voice archetypes -------------------------------------------------------
class TestVoiceArchetypes:
"""A voice must sound like a person, not like four random knobs.
These assert the *covariance* that makes an archetype legible — a child
being high AND fast AND clean — because that is exactly what
independently-rolled parameters would destroy.
"""
def _throats(self, n=200, language="en"):
import random as _r
from app.entities import roll_voice
return [roll_voice(_r.Random(i), language) for i in range(n)]
def test_every_archetype_is_reachable(self):
from app.entities import VOICE_ARCHETYPES
seen = {t["archetype"] for t in self._throats(400)}
assert seen == {a["key"] for a in VOICE_ARCHETYPES}
def test_all_parameters_stay_inside_the_synthesis_limits(self):
for t in self._throats(300):
assert -9 <= t["pitch"] <= 9
assert 0.75 <= t["rate"] <= 1.20
assert 0.01 <= t["noise"] <= 0.10
assert 0.0 <= t["echo"] <= 0.5
def test_only_installed_voices_are_chosen(self):
from app.tts.voices import VOICES
for t in self._throats(300):
assert t["voice_id"] in VOICES
def test_spanish_spirits_get_spanish_models(self):
# An English model reading Spanish text mangles it; better to carry
# the difference in shaping alone.
from app.tts.voices import ES_VOICE_IDS
for t in self._throats(120, language="es"):
assert t["voice_id"] in ES_VOICE_IDS
def test_children_are_higher_and_faster_than_elders(self):
throats = self._throats(600)
child = [t for t in throats if t["archetype"] == "child"]
elder = [t for t in throats if t["archetype"] == "elder_man"]
assert child and elder
assert min(t["pitch"] for t in child) > max(t["pitch"] for t in elder)
assert min(t["rate"] for t in child) > max(t["rate"] for t in elder)
def test_damaged_throats_are_noisier_than_clean_ones(self):
throats = self._throats(600)
burned = [t for t in throats if t["archetype"] == "burned"]
child = [t for t in throats if t["archetype"] == "child"]
assert burned and child
assert min(t["noise"] for t in burned) > max(t["noise"] for t in child)
def test_the_distant_archetype_is_the_most_reverberant(self):
throats = self._throats(600)
distant = [t for t in throats if t["archetype"] == "distant"]
young = [t for t in throats if t["archetype"] == "young_man"]
assert distant and young
assert min(t["echo"] for t in distant) > max(t["echo"] for t in young)
def test_two_spirits_rarely_share_a_throat(self):
# The whole point: distinguishable people, not a smear of one voice.
throats = self._throats(120)
fingerprints = {
(t["voice_id"], round(t["pitch"], 1), round(t["rate"], 2)) for t in throats
}
assert len(fingerprints) > 100
def test_normalize_profile_reports_the_archetype(self):
from app.entities import normalize_profile
profile = normalize_profile({"name": "Test"}, "sig-archetype")
assert profile["voice"]["archetype"]
# --- moon influence on summoning --------------------------------------------
class TestMoonInfluence:
"""The moon must change *who answers*, not just decorate the UI."""
def test_dark_sky_leaves_the_base_table_alone(self):
from app.entities import BASE_RARITY_WEIGHTS, rarity_weights_for_moon
assert rarity_weights_for_moon(0.0) == pytest.approx(BASE_RARITY_WEIGHTS)
def test_unknown_sky_falls_back_to_the_base_table(self):
from app.entities import BASE_RARITY_WEIGHTS, rarity_weights_for_moon
assert rarity_weights_for_moon(None) == BASE_RARITY_WEIGHTS
def test_a_full_moon_makes_rare_and_mythic_much_likelier(self):
from app.entities import rarity_weights_for_moon
dark = rarity_weights_for_moon(0.0)
full = rarity_weights_for_moon(1.0)
assert full[2] > dark[2] * 2 # rare
assert full[3] > dark[3] * 2 # mythic
assert full[0] < dark[0] # common recedes
def test_every_tier_stays_reachable_on_every_night(self):
# A skew, never a gate — someone who can only play midweek must not
# be locked out of the good spirits.
from app.entities import rarity_weights_for_moon
for lit in (0.0, 0.25, 0.5, 0.75, 1.0):
assert all(w > 0 for w in rarity_weights_for_moon(lit))
def test_rarity_odds_actually_shift_in_practice(self):
import random as _r
from app.entities import rarity_weights_for_moon
from app.models.entity import RARITY_TIERS
def mythic_rate(lit):
rng = _r.Random(1234)
weights = rarity_weights_for_moon(lit)
draws = [rng.choices(RARITY_TIERS, weights=weights)[0] for _ in range(4000)]
return draws.count("mythic") / len(draws)
assert mythic_rate(1.0) > mythic_rate(0.0) * 1.8
def test_moonlight_raises_power_and_volatility(self):
from app.entities import moon_trait_bias
base = {"alignment": 0.5, "power": 0.5, "volatility": 0.5, "deceptiveness": 0.5}
full = moon_trait_bias(base, 1.0)
assert full["power"] > base["power"]
assert full["volatility"] > base["volatility"]
assert full["alignment"] < base["alignment"]
def test_bias_is_a_thumb_on_the_scale_not_a_rewrite(self):
from app.entities import moon_trait_bias
base = {"alignment": 0.5, "power": 0.5, "volatility": 0.5, "deceptiveness": 0.5}
full = moon_trait_bias(base, 1.0)
for key in base:
assert abs(full[key] - base[key]) <= 0.12
def test_bias_never_escapes_the_unit_range(self):
from app.entities import moon_trait_bias
for extreme in (0.0, 1.0):
base = dict.fromkeys(
("alignment", "power", "volatility", "deceptiveness"), extreme
)
for lit in (0.0, 0.5, 1.0):
for v in moon_trait_bias(base, lit).values():
assert 0.0 <= v <= 1.0
def test_unknown_sky_leaves_traits_untouched(self):
from app.entities import moon_trait_bias
base = {"alignment": 0.3, "power": 0.7, "volatility": 0.2, "deceptiveness": 0.9}
assert moon_trait_bias(base, None) == base
def test_deceptiveness_is_not_moon_driven(self):
# Whether a spirit lies is its own nature, not the sky's doing.
from app.entities import moon_trait_bias
base = {"alignment": 0.5, "power": 0.5, "volatility": 0.5, "deceptiveness": 0.5}
assert moon_trait_bias(base, 1.0)["deceptiveness"] == 0.5
def test_fallback_profiles_respect_the_moon(self):
from app.entities import fallback_profile
dark = [fallback_profile(f"s{i}", None, 0.0)["rarity"] for i in range(300)]
full = [fallback_profile(f"s{i}", None, 1.0)["rarity"] for i in range(300)]
rare_dark = sum(r in ("rare", "mythic") for r in dark)
rare_full = sum(r in ("rare", "mythic") for r in full)
assert rare_full > rare_dark