Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 06:25:41 -06:00
commit 80bef6f524
63 changed files with 5674 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
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