import tempfile import pytest from pathlib import Path from fastapi.testclient import TestClient from app.engine import DuckDBEngine from app.models import EventCreateRequest, EventStopRequest def test_add_and_stop_sleep_event(tmp_path: Path): # Use isolated temp directory for write tests so we do not pollute wiki git repo engine = DuckDBEngine(data_dir=tmp_path) # 1. Add active sleep event (end=None) add_req = EventCreateRequest( kind="sleep.session", subject="mila", payload={"type": "nap", "location": "kinderwagen"}, actor="test-runner", note="Mittagsschlaf Test", ) res_add = engine.add_event(add_req) assert res_add["status"] == "success" event_id = res_add["id"] assert event_id is not None # Check that it appears in open sleep open_rows = engine.query(f"SELECT * FROM sleep WHERE id = '{event_id}' AND (isOpen = true OR endIso IS NULL)") assert len(open_rows) == 1 # 2. Stop the active sleep event stop_req = EventStopRequest( kind="sleep.session", subject="mila", event_id=event_id, actor="test-runner", ) res_stop = engine.stop_event(stop_req) assert res_stop["status"] == "success" assert res_stop["id"] == event_id # Check that it is now closed closed_rows = engine.query(f"SELECT isOpen, endIso FROM sleep WHERE id = '{event_id}'") assert len(closed_rows) == 1 assert closed_rows[0]["isOpen"] is False assert closed_rows[0]["endIso"] is not None def test_add_nutrition_and_diaper_event(tmp_path: Path): engine = DuckDBEngine(data_dir=tmp_path) # Add nutrition event nut_req = EventCreateRequest( kind="nutrition.intake", subject="moritz", payload={"item": "Apfel", "qty": 150.0, "kcal": 78.0, "protein_g": 0.5}, meal="snack", actor="test-runner", ) res_nut = engine.add_event(nut_req) assert res_nut["status"] == "success" nut_id = res_nut["id"] nut_rows = engine.query(f"SELECT * FROM nutrition WHERE id = '{nut_id}'") assert len(nut_rows) == 1 # Add diaper event diaper_req = EventCreateRequest( kind="diaper.change", subject="mila", payload={"pee": True, "poo": False}, actor="test-runner", ) res_diaper = engine.add_event(diaper_req) assert res_diaper["status"] == "success" diaper_id = res_diaper["id"] diaper_rows = engine.query(f"SELECT * FROM mila WHERE id = '{diaper_id}'") assert len(diaper_rows) == 1 def test_duplicate_open_sleep_rejected(tmp_path: Path): engine = DuckDBEngine(data_dir=tmp_path) # 1. Start first open sleep session req1 = EventCreateRequest( kind="sleep.session", subject="mila", payload={"type": "nap", "location": "bett"}, actor="parent", ) res1 = engine.add_event(req1) assert res1["status"] == "success" # 2. Attempt to start second open sleep session without stopping the first req2 = EventCreateRequest( kind="sleep.session", subject="mila", payload={"type": "nap", "location": "kinderwagen"}, actor="parent", ) with pytest.raises(ValueError, match="already active for subject 'mila'"): engine.add_event(req2) def test_duplicate_open_sleep_api_endpoint(client: TestClient): # 1. Start first session via API res1 = client.post("/duckdb-api/v1/events", json={ "kind": "sleep.session", "subject": "test_baby", "payload": {"type": "nap"}, }) assert res1.status_code == 200 # 2. Start second session via API -> must return 400 Bad Request res2 = client.post("/duckdb-api/v1/events", json={ "kind": "sleep.session", "subject": "test_baby", "payload": {"type": "nap"}, }) assert res2.status_code == 400 assert "already active for subject 'test_baby'" in res2.json()["detail"] # 3. Clean up by stopping stop_res = client.post("/duckdb-api/v1/events/stop", json={ "kind": "sleep.session", "subject": "test_baby", }) assert stop_res.status_code == 200 def test_agent_utc_wallclock_guard_and_mila_recipe_defaults(tmp_path: Path): engine = DuckDBEngine(data_dir=tmp_path) # 1. Agent passes +00:00 on local wall-clock times (e.g. 08:30 to 10:20) nap_req = EventCreateRequest( kind="sleep.session", subject="mila", actor="agent:mila_tracker", start="2026-10-17T08:30:00+00:00", end="2026-10-17T10:20:00+00:00", payload={"type": "nap", "location": "bett"}, ) res_nap = engine.add_event(nap_req) assert res_nap["status"] == "success" assert res_nap["record"]["startIso"] == "2026-10-17T08:30:00+02:00" assert res_nap["record"]["endIso"] == "2026-10-17T10:20:00+02:00" assert res_nap["record"]["sleepTimeSeconds"] == 6600 # 2. Solid food without amount_g gets standard recipe defaults brei_req = EventCreateRequest( kind="feeding.solid", subject="mila", actor="agent:mila_tracker", start="2026-10-17T12:20:00+00:00", meal="lunch", payload={"item": "mittagsbrei"}, ) res_brei = engine.add_event(brei_req) assert res_brei["record"]["start"] == "2026-10-17T12:20:00+02:00" assert res_brei["record"]["payload"]["amount_g"] == 190 assert res_brei["record"]["payload"]["kcal"] == 135 abend_req = EventCreateRequest( kind="feeding.solid", subject="mila", actor="agent:mila_tracker", start="2026-10-17T16:40:00+00:00", meal="dinner", payload={"item": "abendbrei"}, ) res_abend = engine.add_event(abend_req) assert res_abend["record"]["start"] == "2026-10-17T16:40:00+02:00" assert res_abend["record"]["payload"]["amount_g"] == 200 assert res_abend["record"]["payload"]["kcal"] == 110