Files
PTSRaiAgent/tests/test_services_garden.py
ElebobaandClaude Opus 5.5 1cafaa87ec
CI / ruff · pytest (pull_request) Successful in 2m17s
feat(backend): patient garden — deterministic growth from activities
GET /api/v1/me/{token}/garden computes the garden on the fly from taken doses,
check-ins/diary and finished questionnaires (no new table). Rules and texts in
config/garden.yaml: per-day caps, points only add up, answers never count.

Refs #118

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:22:44 +03:00

89 lines
3.6 KiB
Python

"""Garden (#118): deterministic growth from activities, daily caps, never shrinks, ignores answers."""
from collections.abc import AsyncIterator
from datetime import date, datetime, timedelta
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from app.config import Settings
from app.core import clock
from app.db import session
from app.db.clinical import DailyCheckin, Dose
from app.main import app
from app.services.garden import compute, load_config
from scripts.seed_demo import seed
MEM = "sqlite+aiosqlite:///:memory:"
NOW = datetime(2026, 10, 5, 14, 0)
D = date(2026, 10, 5)
def test_points_steps_and_daily_caps() -> None:
cfg = load_config()
one_diary = compute([("diary", D)], D)
assert one_diary.points == cfg.activities["diary"].points
# Five diary entries on one day count as one; on five days — five times.
assert compute([("diary", D)] * 5, D).points == one_diary.points
five_days = compute([("diary", D - timedelta(days=i)) for i in range(5)], D)
assert five_days.points == 5 * one_diary.points
assert five_days.steps == five_days.points // cfg.step_points
assert five_days.points_in_step == five_days.points % cfg.step_points
def test_never_shrinks_and_future_ignored() -> None:
events = [("meds", D), ("diary", D), ("questionnaire", D)]
now = compute(events, D)
# A week without any activity takes nothing away.
assert compute(events, D + timedelta(days=7)).points == now.points
# Rows dated after the (demo) today are not counted yet.
assert compute([*events, ("diary", D + timedelta(days=1))], D).points == now.points
def test_plants_grow_one_after_another() -> None:
cfg = load_config()
per_plant = cfg.stages_per_plant
g = compute([("questionnaire", D - timedelta(days=i)) for i in range(100)], D)
stages = [p.stage for p in g.plants]
assert stages == sorted(stages, reverse=True) and stages[0] == per_plant
assert sum(stages) == min(g.steps, per_plant * cfg.max_plants)
assert compute([], D).title == cfg.titles[0].title
@pytest.fixture
async def client(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[AsyncClient]:
monkeypatch.setattr(clock, "get_settings", lambda: Settings(_env_file=None, demo_clock=True))
clock.set_now(NOW)
await session.configure(identity_url=MEM, clinical_url=MEM)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
yield c
await session.configure()
async def test_andrey_has_a_living_garden_that_grows_and_ignores_mood(client: AsyncClient) -> None:
token = (await seed(12)).andrey_token
url = f"/api/v1/me/{token}/garden"
before = (await client.get(url)).json()
assert before["steps"] >= 4 and before["plants"][0]["stage"] > 0
# Mood is an answer: a bad day and a good day grow the garden the same way.
async with session.get_clinical_session() as s:
rows = (await s.scalars(select(DailyCheckin).where(DailyCheckin.token == token))).all()
for r in rows:
r.mood = 1
assert (await client.get(url)).json()["points"] == before["points"]
# Taking a dose today adds points.
async with session.get_clinical_session() as s:
dose = Dose(token=token, prescription_id=1, planned_at=clock.now(), status="taken", answered_at=clock.now())
s.add(dose)
after = (await client.get(url)).json()
assert after["points"] >= before["points"]
assert any(i["activity"] == "diary" for i in after["today"])
async def test_unknown_patient_404(client: AsyncClient) -> None:
await seed(0)
assert (await client.get("/api/v1/me/nobody/garden")).status_code == 404