Implement S6 doctor and A1 daily-board with fixtures.
Add operator health checks (make doctor) wrapping platform CLIs, and the fixtures-only daily board skill library with unit tests (make verify).
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user