15f6a6c713
Add operator health checks (make doctor) wrapping platform CLIs, and the fixtures-only daily board skill library with unit tests (make verify).
157 lines
5.3 KiB
Python
157 lines
5.3 KiB
Python
"""Tests for the scheduling fixture provider."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import pathlib
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
import sys
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "skills" / "_lib"))
|
|
|
|
from lumina_skills.domain import Appointment, AppointmentStatus
|
|
from lumina_skills.providers.scheduling.fixture_provider import (
|
|
load_fixtures,
|
|
load_fixture_metadata,
|
|
)
|
|
|
|
# Path to the real fixture file.
|
|
_FIXTURE_PATH = pathlib.Path(__file__).resolve().parents[2] / "data" / "fixtures" / "scheduling" / "claire_bennett_2026-07-28.json"
|
|
|
|
|
|
def _make_fixture_file(tmp_path: pathlib.Path, data: dict) -> pathlib.Path:
|
|
"""Write a fixture dict to a temp JSON file."""
|
|
p = tmp_path / "test_fixture.json"
|
|
p.write_text(json.dumps(data), encoding="utf-8")
|
|
return p
|
|
|
|
|
|
# ── load_fixtures ──────────────────────────────────────────────────────────
|
|
|
|
def test_load_real_fixture():
|
|
"""Load the Claire Bennett fixture file."""
|
|
apts = load_fixtures(_FIXTURE_PATH)
|
|
assert len(apts) == 7 # 7 appointments in the fixture
|
|
assert all(isinstance(a, Appointment) for a in apts)
|
|
|
|
|
|
def test_load_fixture_statuses():
|
|
"""Fixture statuses are correctly parsed."""
|
|
apts = load_fixtures(_FIXTURE_PATH)
|
|
statuses = {a.appointment_id: a.status for a in apts}
|
|
assert statuses["APT-001"] == AppointmentStatus.CONFIRMED
|
|
assert statuses["APT-002"] == AppointmentStatus.PENDING
|
|
assert statuses["APT-007"] == AppointmentStatus.CANCELLED
|
|
|
|
|
|
def test_load_fixture_needs_confirmation():
|
|
"""needs_confirmation flag is loaded from fixture."""
|
|
apts = load_fixtures(_FIXTURE_PATH)
|
|
flags = {a.appointment_id: a.needs_confirmation for a in apts}
|
|
assert flags["APT-001"] is False
|
|
assert flags["APT-002"] is True
|
|
assert flags["APT-004"] is True
|
|
|
|
|
|
def test_load_fixture_notes():
|
|
"""Notes are loaded from fixture."""
|
|
apts = load_fixtures(_FIXTURE_PATH)
|
|
notes = {a.appointment_id: a.notes for a in apts}
|
|
assert "ammonia" in notes["APT-001"].lower()
|
|
assert "Wedding" in notes["APT-002"]
|
|
|
|
|
|
def test_load_fixture_missing_file(tmp_path: pathlib.Path):
|
|
"""FileNotFoundError for missing fixture."""
|
|
with pytest.raises(FileNotFoundError):
|
|
load_fixtures(tmp_path / "nonexistent.json")
|
|
|
|
|
|
def test_load_fixture_empty_appointments(tmp_path: pathlib.Path):
|
|
"""Empty appointments list returns empty list."""
|
|
data = {"salon_name": "Test", "date": "2026-01-01", "appointments": []}
|
|
path = _make_fixture_file(tmp_path, data)
|
|
apts = load_fixtures(path)
|
|
assert apts == []
|
|
|
|
|
|
def test_load_fixture_default_status(tmp_path: pathlib.Path):
|
|
"""Missing status field defaults to PENDING (via .get default)."""
|
|
data = {
|
|
"appointments": [{
|
|
"id": "APT-X",
|
|
"start": "2026-01-01T09:00:00",
|
|
"end": "2026-01-01T10:00:00",
|
|
"client_name": "Test",
|
|
"service_name": "Test",
|
|
"staff_name": "Test",
|
|
# No status field.
|
|
}]
|
|
}
|
|
path = _make_fixture_file(tmp_path, data)
|
|
apts = load_fixtures(path)
|
|
assert apts[0].status == AppointmentStatus.PENDING
|
|
|
|
|
|
def test_load_fixture_unknown_status_raises(tmp_path: pathlib.Path):
|
|
"""Unknown status string raises ValueError."""
|
|
data = {
|
|
"appointments": [{
|
|
"id": "APT-X",
|
|
"start": "2026-01-01T09:00:00",
|
|
"end": "2026-01-01T10:00:00",
|
|
"client_name": "Test",
|
|
"service_name": "Test",
|
|
"staff_name": "Test",
|
|
"status": "typo_status",
|
|
}]
|
|
}
|
|
path = _make_fixture_file(tmp_path, data)
|
|
with pytest.raises(ValueError, match="Unknown appointment status"):
|
|
load_fixtures(path)
|
|
|
|
|
|
def test_load_fixture_default_needs_confirmation(tmp_path: pathlib.Path):
|
|
"""Missing needs_confirmation defaults to False."""
|
|
data = {
|
|
"appointments": [{
|
|
"id": "APT-X",
|
|
"start": "2026-01-01T09:00:00",
|
|
"end": "2026-01-01T10:00:00",
|
|
"client_name": "Test",
|
|
"service_name": "Test",
|
|
"staff_name": "Test",
|
|
"status": "confirmed",
|
|
# No needs_confirmation field.
|
|
}]
|
|
}
|
|
path = _make_fixture_file(tmp_path, data)
|
|
apts = load_fixtures(path)
|
|
assert apts[0].needs_confirmation is False
|
|
|
|
|
|
def test_load_fixture_malformed_json(tmp_path: pathlib.Path):
|
|
"""ValueError for invalid JSON."""
|
|
p = tmp_path / "bad.json"
|
|
p.write_text("{not valid json}", encoding="utf-8")
|
|
with pytest.raises(ValueError):
|
|
load_fixtures(p)
|
|
|
|
|
|
# ── load_fixture_metadata ─────────────────────────────────────────────────
|
|
|
|
def test_load_metadata():
|
|
meta = load_fixture_metadata(_FIXTURE_PATH)
|
|
assert meta["salon_name"] == "Lumina Hair Studio & Spa"
|
|
assert meta["date"] == "2026-07-28"
|
|
assert meta["business_hours"]["open"] == "09:00"
|
|
assert meta["business_hours"]["close"] == "18:00"
|
|
assert len(meta["staff"]) == 2
|
|
|
|
|
|
def test_load_metadata_missing_file(tmp_path: pathlib.Path):
|
|
with pytest.raises(FileNotFoundError):
|
|
load_fixture_metadata(tmp_path / "nonexistent.json")
|