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,182 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user