feat(questionnaire): integrate revised intake and follow-up monitoring
CI / ruff · pytest (pull_request) Successful in 55s
CI / ruff · pytest (pull_request) Successful in 55s
Add gated TSQ, Russian C-SSRS Recent, durable responses and notifications, diaries, schedules and audited clinical review. Preserve manual assessment when the Russian follow-up source is unavailable. Refs #52
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Synthetic exhaustive scoring and calendar/diary boundary checks."""
|
||||
|
||||
from datetime import date, timedelta
|
||||
from itertools import product
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.intake_runtime import diary_flags, due_scales, load_runtime, month_before
|
||||
from app.core.questionnaire import IntakePolicy, alcohol_expansion, load_policy, score_alcohol
|
||||
from app.core.scales import score
|
||||
|
||||
|
||||
def test_release_is_closed():
|
||||
assert not load_runtime().released
|
||||
assert load_policy().approval is None
|
||||
|
||||
|
||||
def test_all_tsq_patterns():
|
||||
for answers in product((0, 1), repeat=10):
|
||||
result = score("tsq", {f"tsq_{i + 1}": v for i, v in enumerate(answers)})
|
||||
assert result.complete and result.total == sum(answers)
|
||||
assert (result.severity_code == "positive") == (sum(answers) >= 6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"day,expected",
|
||||
[
|
||||
(date(2024, 3, 31), date(2024, 2, 29)),
|
||||
(date(2025, 3, 31), date(2025, 2, 28)),
|
||||
(date(2026, 1, 1), date(2025, 12, 1)),
|
||||
],
|
||||
)
|
||||
def test_calendar_month(day, expected):
|
||||
assert month_before(day) == expected
|
||||
|
||||
|
||||
def test_medication_schedule_overlapping_and_delayed_completion():
|
||||
start = date(2026, 1, 1)
|
||||
last = {}
|
||||
for day in (6, 7, 13, 14, 27, 28, 55, 56):
|
||||
due = due_scales(
|
||||
today=start + timedelta(days=day), enrolled=start, last=last, route="self_help", medication_changed=start
|
||||
)
|
||||
assert ("phq9" in due) == (day in (7, 14, 28, 56))
|
||||
if "phq9" in due:
|
||||
last.update(phq9=start + timedelta(days=day), pcl5=start + timedelta(days=day))
|
||||
# A missed tick yields one form, not each historical milestone.
|
||||
assert (
|
||||
due_scales(
|
||||
today=start + timedelta(days=80), enrolled=start, last={}, route="self_help", medication_changed=start
|
||||
).count("phq9")
|
||||
== 1
|
||||
)
|
||||
assert "phq9" in due_scales(
|
||||
today=start + timedelta(days=7), enrolled=start, last={}, route="specialist", after_crisis=start
|
||||
)
|
||||
|
||||
|
||||
def test_diary_consecutive_days_and_baseline_do_not_include_bad_window():
|
||||
today = date(2026, 10, 3)
|
||||
history = {today - timedelta(days=i): {"mood": 8} for i in range(3, 10)}
|
||||
for i in range(3):
|
||||
history[today - timedelta(days=i)] = {"mood": 5, "sleep_hours": 3.9, "medication": "none"}
|
||||
assert set(diary_flags(history, today)) == {"phq9_due_mood", "sleep_review", "medication_contact"}
|
||||
history[today - timedelta(days=1)] = {}
|
||||
assert diary_flags(history, today) == ()
|
||||
history[today]["death_thoughts"] = 1
|
||||
assert diary_flags(history, today) == ("suicide_assessment_now",)
|
||||
|
||||
|
||||
def test_sleep_alternation_counts_but_threshold_is_strict():
|
||||
today = date(2026, 10, 3)
|
||||
h = {
|
||||
today: {"nightmares": 3},
|
||||
today - timedelta(days=1): {"sleep_hours": 3.5},
|
||||
today - timedelta(days=2): {"sleep_hours": 3.5},
|
||||
}
|
||||
assert "sleep_review" in diary_flags(h, today)
|
||||
h[today - timedelta(days=1)]["sleep_hours"] = 4
|
||||
assert "sleep_review" not in diary_flags(h, today)
|
||||
|
||||
|
||||
def test_total_threshold_and_zone_are_separate_decisions():
|
||||
data = load_policy().model_dump()
|
||||
data.update(cssrs_positive_policy="graded", rus_audit_s_expand_at=1)
|
||||
data["approval"] = dict(
|
||||
reviewer="Synthetic fixture",
|
||||
decision_reference="test",
|
||||
approved_at="2026-10-03T00:00:00Z",
|
||||
policy_version=data["version"],
|
||||
)
|
||||
policy = IntakePolicy.model_validate(data)
|
||||
r = score_alcohol({"rus_audit_3": 1, "rus_audit_9": 0, "rus_audit_10": 0}, short=True, sex="m")
|
||||
assert r.zone == 1 and alcohol_expansion(r, policy)
|
||||
with pytest.raises(ValueError):
|
||||
IntakePolicy.model_validate({**data, "rus_audit_s_expand_from_zone": 2})
|
||||
|
||||
|
||||
def test_module_rotation_never_drops_suicide_question():
|
||||
from app.core.intake_questions import diary_questions
|
||||
|
||||
first = {
|
||||
q.id for q in diary_questions(["veteran", "depression"], suicide_monitoring=False, weekday=0, day_index=100)
|
||||
}
|
||||
second = {
|
||||
q.id for q in diary_questions(["veteran", "depression"], suicide_monitoring=False, weekday=1, day_index=101)
|
||||
}
|
||||
assert "death_thoughts" in first & second
|
||||
assert "nightmares" in first and "nightmares" not in second
|
||||
assert "burden" in first and "burden" not in second
|
||||
Reference in New Issue
Block a user