15f6a6c713
Add operator health checks (make doctor) wrapping platform CLIs, and the fixtures-only daily board skill library with unit tests (make verify).
183 lines
5.5 KiB
Python
183 lines
5.5 KiB
Python
"""Tests for lumina_skills.domain types."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, time
|
|
|
|
import pytest
|
|
|
|
import sys
|
|
import pathlib
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "skills" / "_lib"))
|
|
|
|
from lumina_skills.domain import (
|
|
Appointment,
|
|
AppointmentStatus,
|
|
DayBoard,
|
|
Gap,
|
|
)
|
|
|
|
|
|
# ── Appointment ────────────────────────────────────────────────────────────
|
|
|
|
def test_appointment_duration():
|
|
apt = Appointment(
|
|
appointment_id="APT-001",
|
|
start_time=datetime(2026, 7, 28, 9, 0),
|
|
end_time=datetime(2026, 7, 28, 10, 30),
|
|
client_name="Elena Rossi",
|
|
service_name="Balayage + Cut",
|
|
staff_name="Claire Bennett",
|
|
status=AppointmentStatus.CONFIRMED,
|
|
)
|
|
assert apt.duration_minutes() == 90
|
|
|
|
|
|
def test_appointment_duration_one_hour():
|
|
apt = Appointment(
|
|
appointment_id="APT-002",
|
|
start_time=datetime(2026, 7, 28, 13, 0),
|
|
end_time=datetime(2026, 7, 28, 14, 0),
|
|
client_name="Chris Nguyen",
|
|
service_name="Men's Cut",
|
|
staff_name="Claire Bennett",
|
|
status=AppointmentStatus.PENDING,
|
|
)
|
|
assert apt.duration_minutes() == 60
|
|
|
|
|
|
def test_appointment_time_only():
|
|
apt = Appointment(
|
|
appointment_id="APT-003",
|
|
start_time=datetime(2026, 7, 28, 11, 15),
|
|
end_time=datetime(2026, 7, 28, 12, 45),
|
|
client_name="Jasmine Patel",
|
|
service_name="Root Touch-Up",
|
|
staff_name="Maya Torres",
|
|
status=AppointmentStatus.CONFIRMED,
|
|
)
|
|
assert apt.start_time_only() == time(11, 15)
|
|
assert apt.end_time_only() == time(12, 45)
|
|
|
|
|
|
def test_appointment_frozen():
|
|
"""Appointment is immutable."""
|
|
apt = Appointment(
|
|
appointment_id="APT-001",
|
|
start_time=datetime(2026, 7, 28, 9, 0),
|
|
end_time=datetime(2026, 7, 28, 10, 0),
|
|
client_name="Test",
|
|
service_name="Test",
|
|
staff_name="Test",
|
|
status=AppointmentStatus.CONFIRMED,
|
|
)
|
|
with pytest.raises(Exception): # FrozenInstanceError
|
|
apt.client_name = "Hacker"
|
|
|
|
|
|
def test_appointment_needs_confirmation_default():
|
|
apt = Appointment(
|
|
appointment_id="APT-001",
|
|
start_time=datetime(2026, 7, 28, 9, 0),
|
|
end_time=datetime(2026, 7, 28, 10, 0),
|
|
client_name="Test",
|
|
service_name="Test",
|
|
staff_name="Test",
|
|
status=AppointmentStatus.PENDING,
|
|
)
|
|
assert apt.needs_confirmation is False
|
|
|
|
|
|
def test_appointment_status_enum():
|
|
assert AppointmentStatus.CONFIRMED.value == "confirmed"
|
|
assert AppointmentStatus.PENDING.value == "pending"
|
|
assert AppointmentStatus.CANCELLED.value == "cancelled"
|
|
assert AppointmentStatus.COMPLETED.value == "completed"
|
|
assert AppointmentStatus.NO_SHOW.value == "no_show"
|
|
|
|
|
|
# ── Gap ────────────────────────────────────────────────────────────────────
|
|
|
|
def test_gap_creation():
|
|
gap = Gap(
|
|
start_time=time(12, 0),
|
|
end_time=time(13, 30),
|
|
duration_minutes=90,
|
|
staff_name="Claire Bennett",
|
|
)
|
|
assert gap.duration_minutes == 90
|
|
assert gap.staff_name == "Claire Bennett"
|
|
|
|
|
|
def test_gap_with_references():
|
|
gap = Gap(
|
|
start_time=time(12, 0),
|
|
end_time=time(13, 0),
|
|
duration_minutes=60,
|
|
staff_name="Claire Bennett",
|
|
preceding_appointment_id="APT-001",
|
|
following_appointment_id="APT-002",
|
|
)
|
|
assert gap.preceding_appointment_id == "APT-001"
|
|
assert gap.following_appointment_id == "APT-002"
|
|
|
|
|
|
# ── DayBoard ───────────────────────────────────────────────────────────────
|
|
|
|
def test_dayboard_to_dict():
|
|
apt = Appointment(
|
|
appointment_id="APT-001",
|
|
start_time=datetime(2026, 7, 28, 9, 0),
|
|
end_time=datetime(2026, 7, 28, 10, 0),
|
|
client_name="Elena Rossi",
|
|
service_name="Cut",
|
|
staff_name="Claire Bennett",
|
|
status=AppointmentStatus.CONFIRMED,
|
|
needs_confirmation=False,
|
|
)
|
|
board = DayBoard(
|
|
date="2026-07-28",
|
|
salon_name="Lumina Hair Studio & Spa",
|
|
source="fixtures",
|
|
is_offline=True,
|
|
appointments=[apt],
|
|
gaps=[],
|
|
needs_confirmation=[],
|
|
total_booked_minutes=60,
|
|
total_gap_minutes=0,
|
|
)
|
|
d = board.to_dict()
|
|
assert d["date"] == "2026-07-28"
|
|
assert d["salon_name"] == "Lumina Hair Studio & Spa"
|
|
assert d["source"] == "fixtures"
|
|
assert d["is_offline"] is True
|
|
assert len(d["appointments"]) == 1
|
|
assert d["appointments"][0]["client"] == "Elena Rossi"
|
|
assert d["total_booked_minutes"] == 60
|
|
|
|
|
|
def test_dayboard_to_dict_serializable():
|
|
"""to_dict output must be JSON-serializable."""
|
|
board = DayBoard(
|
|
date="2026-07-28",
|
|
salon_name="Test Salon",
|
|
source="fixtures",
|
|
is_offline=True,
|
|
)
|
|
d = board.to_dict()
|
|
# Should not raise.
|
|
json.dumps(d)
|
|
|
|
|
|
def test_dayboard_empty():
|
|
board = DayBoard(
|
|
date="2026-07-28",
|
|
salon_name="Empty Salon",
|
|
source="offline",
|
|
is_offline=True,
|
|
)
|
|
assert len(board.appointments) == 0
|
|
assert len(board.gaps) == 0
|
|
assert board.total_booked_minutes == 0
|