feat(questionnaire): integrate revised intake and follow-up monitoring
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:
Pavel Eliseev
2026-10-03 18:19:34 +03:00
parent 8b61eecf1b
commit c42ef621e5
34 changed files with 2556 additions and 85 deletions
+3
View File
@@ -67,6 +67,9 @@ async def test_create_all_puts_tables_in_the_right_db(dbs: None) -> None:
"safety_plans",
"clock_state",
"audit_log",
"questionnaire_records",
"questionnaire_notices",
"questionnaire_tasks",
} == clinical_tables
await init_db() # idempotent
+407
View File
@@ -0,0 +1,407 @@
"""Synthetic released-config integration tests. Production approvals stay empty."""
import asyncio
import shutil
from datetime import UTC, datetime, timedelta
import pytest
import yaml
from sqlalchemy import select
from app.config import get_settings
from app.core import clock
from app.core.scales import clear_cache
from app.db import repo, session
from app.db.bootstrap import init_db
from app.db.intake import QuestionnaireNotice, QuestionnaireRecord, QuestionnaireTask
from app.dialog.contracts import Event
from app.dialog.engine import Engine
from app.integrations.base import MemoryNotifier, MemoryOutbox, integrations
from app.llm.gateway import LLMGateway
from app.llm.mock import MockTasks
from app.scheduler import jobs
from app.services import questionnaire as service
TOKEN = "b" * 32
NOW = datetime(2026, 10, 3, 17, tzinfo=UTC)
@pytest.fixture
async def released(tmp_path, monkeypatch):
config = tmp_path / "config"
shutil.copytree(get_settings().config_dir, config)
for name in ("questionnaire", "intake_runtime"):
path = config / f"{name}.yaml"
data = yaml.safe_load(path.read_text())
approval = dict(
reviewer="Synthetic test",
decision_reference="fixture-only",
approved_at=NOW.isoformat(),
policy_version=data["version"],
)
data["approval"] = approval
if name == "questionnaire":
data.update(cssrs_positive_policy="graded", rus_audit_s_expand_at=1)
else:
data.update(enabled=True, content_review=approval, contact_review=approval)
path.write_text(yaml.safe_dump(data, allow_unicode=True))
monkeypatch.setattr(get_settings(), "config_dir", config)
monkeypatch.setattr(clock, "now", lambda: NOW)
monkeypatch.setattr(integrations, "notifier", MemoryNotifier())
monkeypatch.setattr(integrations, "outbox", MemoryOutbox())
clear_cache()
await session.configure(identity_url="sqlite+aiosqlite:///:memory:", clinical_url="sqlite+aiosqlite:///:memory:")
await init_db()
async with session.get_clinical_session() as s:
await repo.upsert_profile(s, token=TOKEN, doctor_id="synthetic-doctor", modules=["veteran"], sex="m")
await repo.set_dialog_state(s, token=TOKEN, state="idle", data={"consent": True, "adult": True}, now=NOW)
yield Engine(gateway=LLMGateway(MockTasks()))
await session.configure()
clear_cache()
async def rows(cls=QuestionnaireRecord):
async with session.get_clinical_session() as s:
return list((await s.scalars(select(cls))).all())
async def press(e, data):
return await e.handle(TOKEN, Event(type="button", button=data))
def answer_button(replies, choices=None):
choices = choices or {}
for reply in reversed(replies):
for line in reply.buttons:
for b in line:
if b.data.startswith("iq:"):
item = b.data.split(":")[2]
index = choices.get(item, 1 if item.startswith("cssrs_") else 0)
for line2 in reply.buttons:
for other in line2:
if other.data.endswith(":" + str(index)) and other.data.startswith("iq:"):
return other.data
return None
async def complete_intake(e, choices=None):
replies = await press(e, "intake:start")
for _ in range(100):
button = answer_button(replies, choices)
if button is None:
return replies
replies = await press(e, button)
raise AssertionError("intake did not terminate")
async def test_minimal_intake_raw_data_not_imputed_and_restart(released):
await complete_intake(released, {"trauma": 1, "tbi": 1, "psychotropics": 1})
(row,) = await rows()
assert row.status == "complete"
assert row.answers["trauma"] == 0 and not any(k.startswith("tsq_") for k in row.answers)
assert row.details["totals"]["tsq"]["total"] is None
assert row.details["totals"]["phq9"]["total"] is None
assert row.details["totals"]["rus_audit_s"]["total"] == 0
assert row.details["totals"]["cssrs_recent"]["risk"] == "none"
assert row.details["decision"]["primary_route"] == "self_help"
async def test_pause_restart_and_duplicate_callbacks(released):
replies = await press(released, "intake:start")
old = answer_button(replies)
await asyncio.gather(press(released, old), press(released, old))
(row,) = await rows()
assert row.answers == {"cssrs_q1": 0}
await press(released, "intake:pause")
restarted = Engine(gateway=LLMGateway(MockTasks()))
replies = await press(restarted, "intake:resume")
assert "cssrs_q2" in answer_button(replies)
await press(restarted, old)
assert (await rows())[0].answers == {"cssrs_q1": 0}
assert all(len(b.data.encode()) <= 64 for r in replies for line in r.buttons for b in line)
@pytest.mark.parametrize("signal", ["help", "phq", "cssrs"])
async def test_immediate_stop_preserves_partial_answers(released, signal):
replies = await press(released, "intake:start")
if signal == "help":
replies = await press(released, "intake:help")
elif signal == "cssrs":
for item in ("cssrs_q1", "cssrs_q2", "cssrs_q3", "cssrs_q4"):
replies = await press(released, answer_button(replies, {item: 0}))
else:
replies = await complete_existing(released, replies, {"trauma": 1, "phq9_1": 2, "phq9_9": 1})
assert any(r.kind == "crisis" for r in replies)
(row,) = await rows()
assert row.status == "interrupted" and row.completed_at is None
notices = await rows(QuestionnaireNotice)
assert len(notices) == 1 and notices[0].status == "test_sink"
assert "уведомлён" not in replies[0].text
async def complete_existing(e, replies, choices):
for _ in range(100):
button = answer_button(replies, choices)
if not button:
return replies
replies = await press(e, button)
raise AssertionError("not terminated")
async def test_full_tsq_expands_at_six_and_carries_alcohol_answers(released):
choices = {"tsq_" + str(i): 0 if i <= 6 else 1 for i in range(1, 11)}
choices.update(rus_audit_3=1, rus_audit_1=1, tbi=1, psychotropics=1)
await complete_intake(released, choices)
(row,) = await rows()
assert row.status == "complete"
assert row.details["totals"]["tsq"]["total"] == 6
assert row.details["totals"]["pcl5"]["total"] == 0
assert row.details["totals"]["rus_audit"]["complete"]
assert row.answers["rus_audit_3"] == 1
assert len([k for k in row.answers if k.startswith("rus_audit_")]) == 10
async def test_alcohol_conflict_keeps_positive_short_answer(released):
await complete_intake(released, {"trauma": 1, "rus_audit_3": 1, "rus_audit_1": 0})
(row,) = await rows()
assert row.status == "review_required" and row.answers["rus_audit_3"] == 1
assert row.details["stop_reason"] == "answer_conflict"
async def test_low_risk_shows_safety_plan_but_moderate_stops(released):
replies = await press(released, "intake:start")
replies = await press(released, answer_button(replies, {"cssrs_q1": 0}))
replies = await press(released, answer_button(replies))
replies = await press(released, answer_button(replies))
assert any(r.kind == "safety_plan" for r in replies)
assert "phq9_1" in answer_button(replies)
async def test_notification_failure_retry_and_consent_cancellation(released, monkeypatch):
class Failing:
async def send(self, *args, **kwargs):
raise OSError("sensitive error content must not be stored")
monkeypatch.setattr(integrations, "notifier", Failing())
await press(released, "intake:start")
await press(released, "intake:help")
(notice,) = await rows(QuestionnaireNotice)
assert notice.status == "pending" and notice.error_code == "OSError" and notice.attempts == 1
monkeypatch.setattr(clock, "now", lambda: NOW + timedelta(minutes=3))
notifier = MemoryNotifier()
monkeypatch.setattr(integrations, "notifier", notifier)
await service.dispatch_notices()
await service.dispatch_notices()
assert len(notifier.sent) == 1
assert (await rows(QuestionnaireNotice))[0].attempts == 2
await press(released, "consent:revoke")
async with session.get_clinical_session() as s:
state = await repo.get_dialog_state(s, TOKEN)
assert state.data["consent"] is False and "questionnaire_record" not in state.data
async def test_scheduler_idempotent_and_failed_outbox_recovery(released, monkeypatch):
await complete_intake(released, {"trauma": 1, "tbi": 1, "psychotropics": 1})
class Failing:
async def push(self, *args):
raise OSError()
monkeypatch.setattr(integrations, "outbox", Failing())
await jobs.run_until_now()
tasks = await rows(QuestionnaireTask)
assert len(tasks) == 2
risk = next(t for t in tasks if t.kind == "cssrs_since_contact")
assert risk.delivery and risk.sent_at is None
outbox = MemoryOutbox()
monkeypatch.setattr(integrations, "outbox", outbox)
await jobs.run_until_now()
await jobs.run_until_now()
assert len(await rows(QuestionnaireTask)) == 2
assert len(outbox.drain(TOKEN)) == 1
assert len(await rows()) == 2
async def test_missing_since_contact_is_manual_review_not_recent(released):
async with session.get_clinical_session() as s:
task = await service.queue_task(s, TOKEN, "cssrs_since_contact", "synthetic", NOW)
task_id = task.id
replies = await released.handle(
TOKEN, Event(type="system", system="daily_checkin", params={"questionnaire_task": task_id})
)
(row,) = await rows()
assert row.kind == "cssrs_since_contact" and row.status == "review_required" and not row.answers
assert row.details["stop_reason"] == "cssrs_since_contact_required"
assert not answer_button(replies)
async def test_disabled_or_no_adult_confirmation_cannot_start(released):
async with session.get_clinical_session() as s:
state = await repo.get_dialog_state(s, TOKEN)
state.data = {"consent": True}
await press(released, "intake:start")
assert await rows() == []
async def review_risk_record(e):
from app.api.questionnaire import ReviewIn, review
risk = next(r for r in reversed(await rows()) if r.kind == "cssrs_since_contact")
return await review(
TOKEN,
risk.id,
ReviewIn(
doctor_id="synthetic-doctor", assessment_performed=True, decision="resume", reference="synthetic/assessment"
),
)
async def test_doctor_review_is_audited_and_unblocks_diary(released):
from fastapi import HTTPException
from app.api.questionnaire import ReviewIn, history, review
from app.db.audit import AuditLog
await complete_intake(released, {"trauma": 1, "tbi": 1, "psychotropics": 1})
await jobs.run_until_now()
risk = next(r for r in await rows() if r.kind == "cssrs_since_contact")
with pytest.raises(HTTPException) as exc:
await review(
TOKEN,
risk.id,
ReviewIn(doctor_id="other-doctor", assessment_performed=True, decision="resume", reference="test"),
)
assert exc.value.status_code == 404
await released.handle(TOKEN, Event(type="system", system="doctor_ack"))
async with session.get_clinical_session() as s:
assert (await repo.get_dialog_state(s, TOKEN)).data.get("intake_review_required")
await review_risk_record(released)
await jobs.run_until_now()
assert any(r.kind == "diary" and r.status == "active" for r in await rows())
await history(TOKEN, "synthetic-doctor")
audits = await rows(AuditLog)
assert {a.action for a in audits} >= {"view_questionnaires", "review_questionnaire"}
async def test_diary_numeric_skip_and_not_prescribed(released):
await complete_intake(released, {"trauma": 1, "tbi": 1, "psychotropics": 1})
await press(released, "menu:checkin")
await review_risk_record(released)
replies = await press(released, "menu:checkin")
for _ in range(30):
q = next(
(b.data.split(":")[2] for r in replies for line in r.buttons for b in line if b.data.startswith("iq:")),
None,
)
if q == "comment":
button = next(b.data for r in replies for line in r.buttons for b in line if b.data.endswith(":skip"))
replies = await press(released, button)
elif q:
replies = await press(released, answer_button(replies, {"medication": 3}))
elif any("часов" in r.text for r in replies):
invalid = await released.handle(TOKEN, Event(type="text", text="nan"))
assert any("число часов" in r.text for r in invalid)
replies = await released.handle(TOKEN, Event(type="text", text="6,5"))
else:
break
diary = next(r for r in await rows() if r.kind == "diary")
assert diary.status == "complete" and diary.answers["sleep_hours"] == 6.5
assert "medication_reason" not in diary.answers and "side_effects" not in diary.answers
assert diary.answers["comment"] is None
async def test_high_risk_diary_asks_safety_first_and_stops(released):
await complete_intake(released, {"trauma": 1, "tbi": 1, "psychotropics": 1})
async with session.get_clinical_session() as s:
(await repo.get_profile(s, TOKEN)).enhanced_monitoring = True
await press(released, "menu:checkin")
await review_risk_record(released)
replies = await press(released, "menu:checkin")
button = answer_button(replies)
assert "death_thoughts" in button
replies = await press(released, button)
assert any(r.kind == "crisis" for r in replies)
assert any(b.data == "safety_plan" for r in replies for line in r.buttons for b in line)
diary = next(r for r in await rows() if r.kind == "diary")
assert diary.answers == {"death_thoughts": 1} and diary.status == "interrupted"
async def test_unknown_sex_goes_to_review_without_a_zone(released):
async with session.get_clinical_session() as s:
(await repo.get_profile(s, TOKEN)).sex = None
await complete_intake(released, {"trauma": 1, "sex": 2})
(row,) = await rows()
assert row.status == "review_required" and row.answers["sex"] == "unknown"
async def test_version_change_blocks_resume_and_preserves_answers(released):
replies = await press(released, "intake:start")
await press(released, answer_button(replies))
await press(released, "intake:pause")
path = get_settings().config_dir / "instruments/intake_forms.yaml"
data = yaml.safe_load(path.read_text())
data["version"] = "synthetic-next-version"
path.write_text(yaml.safe_dump(data, allow_unicode=True))
await press(released, "intake:resume")
(row,) = await rows()
assert row.status == "review_required" and row.answers == {"cssrs_q1": 0}
async def test_followup_delta_uses_comparable_complete_baseline(released):
await complete_intake(released, {"trauma": 1, "phq9_1": 2, "tbi": 1, "psychotropics": 1})
async with session.get_clinical_session() as s:
task = await service.queue_task(s, TOKEN, "phq9", "synthetic-repeat", NOW)
task_id = task.id
replies = await released.handle(
TOKEN, Event(type="system", system="daily_checkin", params={"questionnaire_task": task_id})
)
replies = await complete_existing(released, replies, {"phq9_1": 3, "phq9_2": 3, "phq9_3": 1})
repeat = next(r for r in await rows() if r.kind == "phq9")
assert repeat.status == "complete" and repeat.details["delta"]["value"] == 5
assert repeat.details["delta"]["retriage"] is True
async def test_silence_uses_completed_checkins_not_empty_records(released, monkeypatch):
await complete_intake(released, {"trauma": 1, "tbi": 1, "psychotropics": 1})
async with session.get_clinical_session() as s:
(await repo.get_profile(s, TOKEN)).enhanced_monitoring = True
monkeypatch.setattr(clock, "now", lambda: NOW + timedelta(days=2))
await jobs.plan()
await jobs.plan()
notices = await rows(QuestionnaireNotice)
assert len([n for n in notices if n.event_key.startswith("silence:")]) == 1
async def test_stop_any_policy_interrupts_first_positive(released):
path = get_settings().config_dir / "questionnaire.yaml"
data = yaml.safe_load(path.read_text())
data["cssrs_positive_policy"] = "stop_any"
path.write_text(yaml.safe_dump(data, allow_unicode=True))
replies = await press(released, "intake:start")
replies = await press(released, answer_button(replies, {"cssrs_q1": 0}))
assert any(r.kind == "crisis" for r in replies)
assert (await rows())[0].answers == {"cssrs_q1": 1}
async def test_evening_schedule_uses_moscow_date(released, monkeypatch):
await complete_intake(released, {"trauma": 1, "tbi": 1, "psychotropics": 1})
monkeypatch.setattr(clock, "now", lambda: NOW.replace(hour=15, minute=59))
await jobs.plan()
assert await rows(QuestionnaireTask) == []
monkeypatch.setattr(clock, "now", lambda: NOW.replace(hour=16, minute=0))
await jobs.plan()
assert {t.kind for t in await rows(QuestionnaireTask)} == {"diary", "cssrs_since_contact"}
async def test_resumed_answer_keeps_original_period(released, monkeypatch):
replies = await press(released, "intake:start")
await press(released, answer_button(replies))
await press(released, "intake:pause")
monkeypatch.setattr(clock, "now", lambda: NOW + timedelta(days=1))
replies = await press(released, "intake:resume")
await press(released, answer_button(replies))
(record,) = await rows()
metadata = record.details["answer_metadata"]["cssrs_q2"]
assert metadata["period_start"] == "2026-09-03"
assert metadata["period_end"] == "2026-10-03"
assert metadata["answered_at"].startswith("2026-10-04")
+110
View File
@@ -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
+8 -7
View File
@@ -35,7 +35,7 @@ def zeros(scale):
def intake(**changes):
args = dict(
phq9={"phq9_1": 0, "phq9_2": 0},
pcptsd5={},
tsq={},
pcl5={},
alcohol=zeros("rus_audit_s"),
cssrs={"q1": 0, "q2": 0, "q6_lifetime": 0},
@@ -196,10 +196,10 @@ def test_phq2_and_trauma_expansion_do_not_turn_missing_into_negative():
r = intake(phq9={"phq9_1": 1, "phq9_2": 1})
assert r.status == "incomplete" and "phq9_9" in r.missing
assert intake(trauma_exposure=None).status == "incomplete"
assert intake(trauma_exposure=True, pcptsd5={"pcptsd5_1": 1}).status == "incomplete"
pc = {f"pcptsd5_{i}": int(i <= 3) for i in range(1, 6)}
assert intake(trauma_exposure=True, pcptsd5=pc).status == "incomplete"
assert intake(trauma_exposure=True, pcptsd5=pc, pcl5=zeros("pcl5")).primary_route == "psychologist"
assert intake(trauma_exposure=True, tsq={"tsq_1": 1}).status == "incomplete"
pc = {f"tsq_{i}": int(i <= 6) for i in range(1, 11)}
assert intake(trauma_exposure=True, tsq=pc).status == "incomplete"
assert intake(trauma_exposure=True, tsq=pc, pcl5=zeros("pcl5")).primary_route == "psychologist"
@pytest.mark.parametrize(
@@ -238,11 +238,12 @@ def test_alcohol_expansion_and_unknown_sex_block_routine_route():
assert r.status == "incomplete" and "alcohol_scoring_sex" in r.missing
def test_full_non_drinker_derivation_also_satisfies_short_stage():
def test_non_drinker_does_not_derive_missing_short_answer():
values = zeros("rus_audit")
del values["rus_audit_2"]
del values["rus_audit_3"]
assert intake(alcohol=values).status == "ready"
assert intake(alcohol=values).status == "incomplete"
assert "rus_audit_3" in intake(alcohol=values).missing
def test_crisis_keeps_other_flags_and_bad_data_cannot_hide_explicit_signal():
+5 -4
View File
@@ -159,9 +159,10 @@ def test_crisis_reply_patient_default() -> None:
assert isinstance(script, CrisisScript)
assert script.variant == "patient.default"
assert "112" in script.text
assert "8-800-2000-122" in script.text
assert "бесплатно, круглосуточно" in script.text
assert "уведомлён" in script.text
assert "+7 (495) 989-50-50" in script.text
assert "круглосуточно" in script.text
assert "по тарифу оператора" in script.text
assert "уведомлён" not in script.text
assert script.grounding in script.text
assert "психиатр (#8)" in script.status
@@ -169,7 +170,7 @@ def test_crisis_reply_patient_default() -> None:
def test_crisis_reply_variants() -> None:
dv = crisis_reply(["domestic_violence", "suicide"], audience="family")
assert dv.variant == "family.domestic_violence"
assert "потерпеть" in dv.text and "112" in dv.text and "8-800-2000-122" in dv.text
assert "потерпеть" in dv.text and "112" in dv.text and "+7 (495) 989-50-50" in dv.text
assert crisis_reply(["domestic_violence"]).variant == "patient.domestic_violence"
assert crisis_reply(["harm_others"]).variant == "patient.harm_others"
assert crisis_reply(["harm_others"], audience="family").variant == "family.default"
+1 -1
View File
@@ -30,7 +30,7 @@ def severity(scale_id: str, total: int, **kw: object) -> str:
def test_all_scales_load_with_expected_item_counts() -> None:
counts = {k: len(v.items) for k, v in load_all().items()}
expected = {"phq9": 9, "gad7": 7, "pcl5": 20, "auditc": 3, "pcptsd5": 5, "cssrs_screen": 6, "phq4": 4, "dar5": 5}
expected.update(rus_audit_s=3, rus_audit=10)
expected.update(rus_audit_s=3, rus_audit=10, tsq=10)
assert counts == expected