TrialPath / app /tests /test_state_manager.py
yakilee's picture
fix: make advance_journey idempotent for same-state transitions
6563704
"""Tests for app.services.state_manager — session state orchestration."""
import pytest
from app.services.state_manager import (
JOURNEY_STATES,
advance_journey,
can_advance_to,
get_current_journey_state,
init_session_state,
)
@pytest.fixture(autouse=True)
def mock_session_state(monkeypatch):
"""Replace st.session_state with a plain dict for every test."""
fake_state = {}
monkeypatch.setattr("app.services.state_manager.st.session_state", fake_state)
return fake_state
# ---------- JOURNEY_STATES constant ----------
def test_journey_states_order():
assert JOURNEY_STATES == [
"INGEST",
"PRESCREEN",
"VALIDATE_TRIALS",
"GAP_FOLLOWUP",
"SUMMARY",
]
# ---------- init_session_state ----------
def test_init_sets_all_defaults(mock_session_state):
init_session_state()
assert mock_session_state["journey_state"] == "INGEST"
assert mock_session_state["parlant_session_id"] is None
assert mock_session_state["parlant_agent_id"] is None
assert mock_session_state["patient_profile"] is None
assert mock_session_state["uploaded_files"] == []
assert mock_session_state["search_anchors"] is None
assert mock_session_state["trial_candidates"] == []
assert mock_session_state["eligibility_ledger"] == []
assert mock_session_state["last_event_offset"] == 0
def test_init_does_not_overwrite_existing(mock_session_state):
mock_session_state["journey_state"] = "PRESCREEN"
init_session_state()
assert mock_session_state["journey_state"] == "PRESCREEN"
# ---------- get_current_journey_state ----------
def test_get_current_journey_state_default(mock_session_state):
assert get_current_journey_state() == "INGEST"
def test_get_current_journey_state_returns_set_value(mock_session_state):
mock_session_state["journey_state"] = "GAP_FOLLOWUP"
assert get_current_journey_state() == "GAP_FOLLOWUP"
# ---------- advance_journey ----------
def test_advance_forward(mock_session_state):
mock_session_state["journey_state"] = "INGEST"
advance_journey("PRESCREEN")
assert mock_session_state["journey_state"] == "PRESCREEN"
def test_advance_multiple_steps_forward(mock_session_state):
mock_session_state["journey_state"] = "INGEST"
advance_journey("VALIDATE_TRIALS")
assert mock_session_state["journey_state"] == "VALIDATE_TRIALS"
def test_advance_refuses_backward(mock_session_state):
mock_session_state["journey_state"] = "VALIDATE_TRIALS"
with pytest.raises(ValueError, match="backward"):
advance_journey("INGEST")
assert mock_session_state["journey_state"] == "VALIDATE_TRIALS"
def test_advance_same_state_is_idempotent(mock_session_state):
mock_session_state["journey_state"] = "PRESCREEN"
advance_journey("PRESCREEN") # should not raise
assert mock_session_state["journey_state"] == "PRESCREEN"
def test_advance_invalid_state(mock_session_state):
mock_session_state["journey_state"] = "INGEST"
with pytest.raises(ValueError, match="Invalid"):
advance_journey("NONEXISTENT")
# ---------- can_advance_to ----------
def test_can_advance_to_prescreen_without_profile(mock_session_state):
init_session_state()
assert can_advance_to("PRESCREEN") is False
def test_can_advance_to_prescreen_with_profile(mock_session_state):
init_session_state()
mock_session_state["patient_profile"] = {"demographics": {"age": 62}}
assert can_advance_to("PRESCREEN") is True
def test_can_advance_to_validate_trials_without_profile(mock_session_state):
init_session_state()
mock_session_state["journey_state"] = "PRESCREEN"
assert can_advance_to("VALIDATE_TRIALS") is False
def test_can_advance_to_validate_trials_with_profile(mock_session_state):
init_session_state()
mock_session_state["journey_state"] = "PRESCREEN"
mock_session_state["patient_profile"] = {"demographics": {"age": 62}}
assert can_advance_to("VALIDATE_TRIALS") is True
def test_can_advance_to_gap_followup_without_candidates(mock_session_state):
init_session_state()
mock_session_state["journey_state"] = "VALIDATE_TRIALS"
mock_session_state["patient_profile"] = {"demographics": {"age": 62}}
assert can_advance_to("GAP_FOLLOWUP") is False
def test_can_advance_to_gap_followup_with_candidates(mock_session_state):
init_session_state()
mock_session_state["journey_state"] = "VALIDATE_TRIALS"
mock_session_state["patient_profile"] = {"demographics": {"age": 62}}
mock_session_state["trial_candidates"] = [{"nct_id": "NCT001"}]
assert can_advance_to("GAP_FOLLOWUP") is True
def test_can_advance_to_summary_without_ledger(mock_session_state):
init_session_state()
mock_session_state["journey_state"] = "GAP_FOLLOWUP"
mock_session_state["patient_profile"] = {"demographics": {"age": 62}}
mock_session_state["trial_candidates"] = [{"nct_id": "NCT001"}]
assert can_advance_to("SUMMARY") is False
def test_can_advance_to_summary_with_ledger(mock_session_state):
init_session_state()
mock_session_state["journey_state"] = "GAP_FOLLOWUP"
mock_session_state["patient_profile"] = {"demographics": {"age": 62}}
mock_session_state["trial_candidates"] = [{"nct_id": "NCT001"}]
mock_session_state["eligibility_ledger"] = [{"trial_id": "NCT001", "result": "ELIGIBLE"}]
assert can_advance_to("SUMMARY") is True
def test_can_advance_to_invalid_state(mock_session_state):
init_session_state()
assert can_advance_to("NONEXISTENT") is False
def test_can_advance_to_backward_state(mock_session_state):
init_session_state()
mock_session_state["journey_state"] = "VALIDATE_TRIALS"
assert can_advance_to("INGEST") is False