"""State machine transition tests (happy path + 409 invalid transitions).""" from __future__ import annotations import pytest from app.statemachine import ( InvalidTransition, STATES, TransitionContext, check_transition, ) class TestStateMachineTransitions: """Test all valid transitions (happy path).""" def test_discovered_to_scored(self): ctx = TransitionContext( application_id="test", from_state="discovered", to_state="scored", has_score=True, ) check_transition(ctx) # should not raise def test_discovered_to_rejected(self): ctx = TransitionContext("test", "discovered", "rejected") check_transition(ctx) def test_scored_to_approved(self): ctx = TransitionContext("test", "scored", "approved") check_transition(ctx) def test_scored_to_rejected(self): ctx = TransitionContext("test", "scored", "rejected") check_transition(ctx) def test_approved_to_drafting(self): ctx = TransitionContext("test", "approved", "drafting") check_transition(ctx) def test_drafting_to_sent(self): ctx = TransitionContext( "test", "drafting", "sent", has_confirmed_approval=True, artifact_hash_match=True, ) check_transition(ctx) def test_sent_to_interviewing(self): ctx = TransitionContext("test", "sent", "interviewing") check_transition(ctx) def test_interviewing_to_offer(self): ctx = TransitionContext("test", "interviewing", "offer") check_transition(ctx) def test_interviewing_to_closed(self): ctx = TransitionContext("test", "interviewing", "closed") check_transition(ctx) def test_offer_to_closed(self): ctx = TransitionContext("test", "offer", "closed") check_transition(ctx) def test_scored_to_expired(self): ctx = TransitionContext("test", "scored", "expired") check_transition(ctx) def test_approved_to_expired(self): ctx = TransitionContext("test", "approved", "expired") check_transition(ctx) class TestStateMachineInvalidTransitions: """Test invalid transitions raise InvalidTransition.""" def test_discovered_to_approved(self): ctx = TransitionContext("test", "discovered", "approved") with pytest.raises(InvalidTransition): check_transition(ctx) def test_discovered_to_sent(self): ctx = TransitionContext("test", "discovered", "sent") with pytest.raises(InvalidTransition): check_transition(ctx) def test_rejected_to_scored(self): ctx = TransitionContext("test", "rejected", "scored") with pytest.raises(InvalidTransition): check_transition(ctx) def test_closed_to_sent(self): ctx = TransitionContext("test", "closed", "sent") with pytest.raises(InvalidTransition): check_transition(ctx) def test_unknown_from_state(self): ctx = TransitionContext("test", "nonexistent", "scored") with pytest.raises(InvalidTransition): check_transition(ctx) def test_unknown_to_state(self): ctx = TransitionContext("test", "discovered", "nonexistent") with pytest.raises(InvalidTransition): check_transition(ctx) def test_drafting_to_sent_no_approval(self): """Guard: drafting -> sent requires confirmed approval.""" ctx = TransitionContext( "test", "drafting", "sent", has_confirmed_approval=False, artifact_hash_match=False, ) with pytest.raises(InvalidTransition) as exc_info: check_transition(ctx) assert "guard failed" in str(exc_info.value) def test_drafting_to_sent_hash_mismatch(self): """Guard: drafting -> sent requires hash match.""" ctx = TransitionContext( "test", "drafting", "sent", has_confirmed_approval=True, artifact_hash_match=False, ) with pytest.raises(InvalidTransition): check_transition(ctx) def test_discovered_to_scored_no_score(self): """Guard: discovered -> scored requires scoring completed (has_score).""" ctx = TransitionContext( "test", "discovered", "scored", has_score=False, ) with pytest.raises(InvalidTransition) as exc_info: check_transition(ctx) assert "guard failed" in str(exc_info.value) class TestStateMachineHelpers: def test_all_states_present(self): assert len(STATES) == 10 def test_can_transition_valid(self): from app.statemachine import can_transition assert can_transition("discovered", "scored") assert can_transition("discovered", "rejected") def test_can_transition_invalid(self): from app.statemachine import can_transition assert not can_transition("discovered", "sent") assert not can_transition("closed", "discovered")