82 lines
2.7 KiB
Python
82 lines
2.7 KiB
Python
"""R4 mood wiring: events push state, heartbeat advances, restarts recover."""
|
|
import pytest
|
|
|
|
from cybergril.config import CharacterConfig
|
|
from cybergril.core.mood_runtime import MoodRuntime
|
|
from cybergril.memory.store import Store
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path):
|
|
s = Store(tmp_path / "mood.db")
|
|
yield s
|
|
s.close()
|
|
|
|
|
|
class Clock:
|
|
def __init__(self, start=1700000000.0):
|
|
self.now = start
|
|
|
|
def __call__(self):
|
|
return self.now
|
|
|
|
def advance(self, seconds):
|
|
self.now += seconds
|
|
|
|
|
|
def test_first_start_persists_baseline(store):
|
|
clock = Clock()
|
|
mood = MoodRuntime(store, CharacterConfig(identity={"y": 2000, "m": 1, "d": 1, "h": 12}), clock=clock)
|
|
mood.recover()
|
|
row = store.last_state()
|
|
assert row is not None and row["mood_word"] == "平静"
|
|
|
|
|
|
def test_events_push_state_and_it_persists(store):
|
|
clock = Clock()
|
|
mood = MoodRuntime(store, CharacterConfig(identity={"y": 2000, "m": 1, "d": 1, "h": 12}), clock=clock)
|
|
mood.recover()
|
|
before = mood.state.v
|
|
mood.impulse(valence=0.8, salience=0.9)
|
|
assert mood.state.v > before
|
|
assert mood.state.a > 0.5 # enough to satisfy reach_out.min_arousal
|
|
# persisted snapshot matches live state
|
|
row = store.last_state()
|
|
assert abs(row["valence"] - mood.state.v) < 1e-6
|
|
|
|
|
|
def test_heartbeat_decays_toward_baseline(store):
|
|
clock = Clock()
|
|
mood = MoodRuntime(store, CharacterConfig(identity={"y": 2000, "m": 1, "d": 1, "h": 12}), clock=clock)
|
|
mood.recover()
|
|
mood.impulse(valence=0.9, salience=1.0)
|
|
peak = mood.state.v
|
|
clock.advance(6 * 3600)
|
|
mood.advance()
|
|
assert mood.state.v < peak # decayed back toward the baseline
|
|
|
|
|
|
def test_restart_recovers_decayed_state_not_reset(store):
|
|
clock = Clock()
|
|
mood = MoodRuntime(store, CharacterConfig(identity={"y": 2000, "m": 1, "d": 1, "h": 12}), clock=clock)
|
|
mood.recover()
|
|
mood.impulse(valence=-0.9, salience=0.8)
|
|
low = mood.state.v
|
|
# "restart": a fresh runtime over the same database, hours later — the decay
|
|
# continues from the persisted snapshot toward the baseline (0.05), never a reset
|
|
clock.advance(3 * 3600)
|
|
revived = MoodRuntime(store, CharacterConfig(identity={"y": 2000, "m": 1, "d": 1, "h": 12}), clock=clock)
|
|
revived.recover()
|
|
assert revived.state.ts == int(clock.now * 1000)
|
|
assert low < revived.state.v < 0.05 # decayed toward baseline, still below it
|
|
|
|
|
|
def test_word_and_prompt_hint_never_expose_numbers(store):
|
|
clock = Clock()
|
|
mood = MoodRuntime(store, CharacterConfig(identity={"y": 2000, "m": 1, "d": 1, "h": 12}), clock=clock)
|
|
mood.recover()
|
|
hint = mood.prompt_hint()
|
|
assert "0." not in hint and mood.word() in hint
|
|
snap = mood.snapshot()
|
|
assert set(snap) == {"v", "a", "d", "e", "mood_word"}
|