80bef6f524
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.2 KiB
Python
74 lines
2.2 KiB
Python
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
import pytest
|
|
|
|
from bolt_pet.state import InvalidTransition, PetState, PetStateMachine
|
|
|
|
|
|
def test_starts_idle():
|
|
sm = PetStateMachine()
|
|
assert sm.state == PetState.IDLE
|
|
|
|
|
|
def test_happy_path_transitions():
|
|
sm = PetStateMachine()
|
|
sm.transition(PetState.LISTENING)
|
|
sm.transition(PetState.THINKING)
|
|
sm.transition(PetState.TALKING)
|
|
sm.transition(PetState.IDLE)
|
|
assert sm.state == PetState.IDLE
|
|
|
|
|
|
def test_idle_to_talking_is_allowed_for_proactive_announcements():
|
|
# The heartbeat poll can make the pet speak unprompted (a reminder
|
|
# firing, a nudge from the server) with no preceding listen/think leg.
|
|
sm = PetStateMachine()
|
|
sm.transition(PetState.TALKING)
|
|
assert sm.state == PetState.TALKING
|
|
|
|
|
|
def test_invalid_transition_raises():
|
|
sm = PetStateMachine()
|
|
with pytest.raises(InvalidTransition):
|
|
sm.transition(PetState.THINKING) # can't skip straight to thinking with no utterance
|
|
|
|
|
|
def test_same_state_transition_is_a_noop():
|
|
calls = []
|
|
sm = PetStateMachine(on_change=lambda old, new: calls.append((old, new)))
|
|
sm.transition(PetState.IDLE) # already idle
|
|
assert calls == []
|
|
|
|
|
|
def test_on_change_callback_fires_with_old_and_new():
|
|
calls = []
|
|
sm = PetStateMachine(on_change=lambda old, new: calls.append((old, new)))
|
|
sm.transition(PetState.LISTENING)
|
|
assert calls == [(PetState.IDLE, PetState.LISTENING)]
|
|
|
|
|
|
def test_every_state_can_reach_error_and_recover():
|
|
for path in (
|
|
[PetState.LISTENING],
|
|
[PetState.LISTENING, PetState.THINKING],
|
|
[PetState.LISTENING, PetState.THINKING, PetState.TALKING],
|
|
):
|
|
sm = PetStateMachine()
|
|
for step in path:
|
|
sm.transition(step)
|
|
sm.transition(PetState.ERROR)
|
|
sm.transition(PetState.IDLE)
|
|
assert sm.state == PetState.IDLE
|
|
|
|
|
|
def test_force_recovers_from_talking_directly_to_idle_without_validation():
|
|
sm = PetStateMachine()
|
|
sm.transition(PetState.LISTENING)
|
|
sm.transition(PetState.THINKING)
|
|
sm.transition(PetState.TALKING)
|
|
sm.force(PetState.LISTENING) # not in TALKING's allowed set, but force skips the check
|
|
assert sm.state == PetState.LISTENING
|