from app.fsm import BotState, FSM, STATE_TRANSITIONS class TestFSM: def test_initial_state(self): FSM.clear_all() assert FSM.get_state(1) == BotState.IDLE def test_set_and_get_state(self): FSM.clear_all() FSM.set_state(1, BotState.NAME_REQUEST) assert FSM.get_state(1) == BotState.NAME_REQUEST def test_reset(self): FSM.clear_all() FSM.set_state(1, BotState.NAME_REQUEST) FSM.reset(1) assert FSM.get_state(1) == BotState.IDLE def test_valid_transition(self): FSM.clear_all() result = FSM.transition(1, "start") assert result == BotState.NAME_REQUEST def test_invalid_transition_returns_same(self): FSM.clear_all() # From IDLE, "unknown" should keep us at IDLE result = FSM.transition(1, "unknown_event") assert result == BotState.IDLE def test_can_transition_true(self): FSM.clear_all() assert FSM.can_transition(1, "start") is True def test_can_transition_false(self): FSM.clear_all() assert FSM.can_transition(1, "unknown_event") is False def test_full_greeting_flow(self): FSM.clear_all() assert FSM.transition(1, "start") == BotState.NAME_REQUEST assert FSM.transition(1, "name_provided") == BotState.CONSENT_REQUEST assert FSM.transition(1, "consent_given") == BotState.CONTACT_REQUEST assert FSM.transition(1, "contact_provided") == BotState.INQUIRY_REQUEST assert FSM.transition(1, "inquiry_provided") == BotState.CATEGORY_SELECT assert FSM.transition(1, "category_selected") == BotState.CONFIRMATION assert FSM.transition(1, "done") == BotState.IDLE def test_consent_refused_flow(self): FSM.clear_all() FSM.set_state(1, BotState.CONSENT_REQUEST) assert FSM.transition(1, "consent_refused") == BotState.IDLE def test_handoff_flow(self): FSM.clear_all() FSM.set_state(1, BotState.HANDOFF_REQUEST) assert FSM.transition(1, "confirmed") == BotState.HANDOFF_CONFIRMATION assert FSM.transition(1, "done") == BotState.IDLE def test_handoff_cancel_to_menu(self): FSM.clear_all() FSM.set_state(1, BotState.HANDOFF_REQUEST) assert FSM.transition(1, "cancel") == BotState.MAIN_MENU def test_main_menu_new_inquiry(self): FSM.clear_all() FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "new_inquiry") == BotState.NAME_REQUEST def test_main_menu_features(self): FSM.clear_all() FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "feature_risk_score") == BotState.FEATURE_RISK_SCORE FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "feature_sla_status") == BotState.FEATURE_SLA_STATUS FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "feature_photo_report") == BotState.FEATURE_PHOTO_REPORT FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "feature_appointment") == BotState.FEATURE_APPOINTMENT FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "feature_quality_rate") == BotState.FEATURE_QUALITY_RATE FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "feature_commercial") == BotState.FEATURE_COMMERCIAL FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "feature_my_objects") == BotState.FEATURE_MY_OBJECTS FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "sla_ticket_create") == BotState.SLA_TICKET_CREATE def test_feature_done_returns_to_menu(self): FSM.clear_all() FSM.set_state(1, BotState.FEATURE_RISK_SCORE) assert FSM.transition(1, "done") == BotState.MAIN_MENU FSM.set_state(1, BotState.FEATURE_RISK_SCORE) assert FSM.transition(1, "cancel") == BotState.MAIN_MENU def test_out_of_hours_flow(self): FSM.clear_all() FSM.set_state(1, BotState.OUT_OF_HOURS) assert FSM.transition(1, "leave_message") == BotState.INQUIRY_REQUEST FSM.set_state(1, BotState.OUT_OF_HOURS) assert FSM.transition(1, "callback") == BotState.NAME_REQUEST FSM.set_state(1, BotState.OUT_OF_HOURS) assert FSM.transition(1, "cancel") == BotState.IDLE FSM.set_state(1, BotState.OUT_OF_HOURS) assert FSM.transition(1, "main_menu") == BotState.MAIN_MENU def test_category_select_flow(self): FSM.clear_all() FSM.set_state(1, BotState.CATEGORY_SELECT) assert FSM.transition(1, "category_selected") == BotState.CONFIRMATION FSM.set_state(1, BotState.CATEGORY_SELECT) assert FSM.transition(1, "cancel") == BotState.IDLE def test_kb_search_found(self): FSM.clear_all() FSM.set_state(1, BotState.KB_SEARCH) assert FSM.transition(1, "found") == BotState.MAIN_MENU FSM.set_state(1, BotState.KB_SEARCH) assert FSM.transition(1, "not_found") == BotState.INQUIRY_REQUEST def test_waiting_1c_response(self): FSM.clear_all() FSM.set_state(1, BotState.WAITING_1C_RESPONSE) assert FSM.transition(1, "received") == BotState.MAIN_MENU FSM.set_state(1, BotState.WAITING_1C_RESPONSE) assert FSM.transition(1, "timeout") == BotState.IDLE def test_rating_flow(self): FSM.clear_all() FSM.set_state(1, BotState.AWAITING_RATING) assert FSM.transition(1, "rating_provided") == BotState.IDLE def test_new_user_start_via_callback(self): FSM.clear_all() # Simulate callback: from MAIN_MENU, cancel goes to IDLE FSM.set_state(1, BotState.MAIN_MENU) assert FSM.transition(1, "cancel") == BotState.IDLE def test_idle_handoff_event(self): FSM.clear_all() assert FSM.transition(1, "handoff") == BotState.HANDOFF_REQUEST def test_edit_from_confirmation(self): FSM.clear_all() FSM.set_state(1, BotState.CONFIRMATION) assert FSM.transition(1, "edit_name") == BotState.NAME_REQUEST FSM.set_state(1, BotState.CONFIRMATION) assert FSM.transition(1, "edit_contact") == BotState.CONTACT_REQUEST FSM.set_state(1, BotState.CONFIRMATION) assert FSM.transition(1, "edit_inquiry") == BotState.INQUIRY_REQUEST def test_clear_all(self): FSM.clear_all() FSM.set_state(1, BotState.NAME_REQUEST) FSM.set_state(2, BotState.CONSENT_REQUEST) FSM.clear_all() assert FSM.get_state(1) == BotState.IDLE assert FSM.get_state(2) == BotState.IDLE def test_all_states_have_transitions(self): for state in BotState: assert state in STATE_TRANSITIONS, f"{state} missing from STATE_TRANSITIONS" def test_all_transitions_go_to_valid_states(self): for state, events in STATE_TRANSITIONS.items(): for event, target in events.items(): assert target in BotState.__members__.values(), \ f"{state}.{event} -> {target} is not a valid BotState"