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 @@
|
||||
"""Unit tests for Salon_Assistant deterministic logic."""
|
||||
@@ -0,0 +1,306 @@
|
||||
"""Tests for the deterministic board builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
)
|
||||
from lumina_skills.board_builder import (
|
||||
build_board,
|
||||
format_board_text,
|
||||
)
|
||||
|
||||
|
||||
def _apt(
|
||||
apt_id: str,
|
||||
start_h: int,
|
||||
start_m: int,
|
||||
end_h: int,
|
||||
end_m: int,
|
||||
client: str = "Client",
|
||||
service: str = "Service",
|
||||
staff: str = "Staff",
|
||||
status: AppointmentStatus = AppointmentStatus.CONFIRMED,
|
||||
needs_confirmation: bool = False,
|
||||
notes: str = "",
|
||||
) -> Appointment:
|
||||
"""Helper to create an Appointment quickly."""
|
||||
return Appointment(
|
||||
appointment_id=apt_id,
|
||||
start_time=datetime(2026, 7, 28, start_h, start_m),
|
||||
end_time=datetime(2026, 7, 28, end_h, end_m),
|
||||
client_name=client,
|
||||
service_name=service,
|
||||
staff_name=staff,
|
||||
status=status,
|
||||
notes=notes,
|
||||
needs_confirmation=needs_confirmation,
|
||||
)
|
||||
|
||||
|
||||
# ── build_board ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_build_board_basic():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
_apt("A2", 10, 0, 11, 0, "Bob", "Color", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon", source="fixtures")
|
||||
assert len(board.appointments) == 2
|
||||
assert board.total_booked_minutes == 120
|
||||
assert board.is_offline is True
|
||||
|
||||
|
||||
def test_build_board_excludes_cancelled():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
_apt("A2", 10, 0, 11, 0, "Bob", "Color", "Claire", status=AppointmentStatus.CANCELLED),
|
||||
_apt("A3", 11, 0, 12, 0, "Carol", "Style", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
assert len(board.appointments) == 2 # Cancelled excluded
|
||||
assert board.appointments[0].appointment_id == "A1"
|
||||
assert board.appointments[1].appointment_id == "A3"
|
||||
|
||||
|
||||
def test_build_board_sorts_by_time():
|
||||
apts = [
|
||||
_apt("A2", 10, 0, 11, 0, "Bob", "Color", "Claire"),
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
assert board.appointments[0].appointment_id == "A1"
|
||||
assert board.appointments[1].appointment_id == "A2"
|
||||
|
||||
|
||||
def test_build_board_confirmation_flags():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire", needs_confirmation=False),
|
||||
_apt("A2", 10, 0, 11, 0, "Bob", "Color", "Claire", needs_confirmation=True),
|
||||
_apt("A3", 11, 0, 12, 0, "Carol", "Style", "Claire", needs_confirmation=True),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
assert len(board.needs_confirmation) == 2
|
||||
assert board.needs_confirmation[0].appointment_id == "A2"
|
||||
assert board.needs_confirmation[1].appointment_id == "A3"
|
||||
|
||||
|
||||
def test_build_board_gaps_between_apts():
|
||||
"""Gap between two appointments on the same staff."""
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
_apt("A2", 11, 0, 12, 0, "Bob", "Color", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
assert len(board.gaps) == 1
|
||||
gap = board.gaps[0]
|
||||
assert gap.start_time == time(10, 0)
|
||||
assert gap.end_time == time(11, 0)
|
||||
assert gap.duration_minutes == 60
|
||||
assert gap.staff_name == "Claire"
|
||||
|
||||
|
||||
def test_build_board_no_small_gaps():
|
||||
"""Gaps under 30 minutes are not included."""
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
_apt("A2", 10, 15, 11, 15, "Bob", "Color", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
# 15-minute gap should be excluded.
|
||||
assert len(board.gaps) == 0
|
||||
|
||||
|
||||
def test_build_board_overlapping_appointments_warns():
|
||||
"""Overlapping appointments emit a warning and skip the negative gap."""
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 30, "Alice", "Cut", "Claire"),
|
||||
_apt("A2", 10, 0, 11, 0, "Bob", "Color", "Claire"), # starts 30 min before A1 ends
|
||||
]
|
||||
with pytest.warns(UserWarning, match="Overlapping appointments"):
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
# The negative gap should not appear in the board.
|
||||
assert all(g.duration_minutes >= 0 for g in board.gaps)
|
||||
# Both appointments still appear (overlap is a data issue, not a filter).
|
||||
assert len(board.appointments) == 2
|
||||
|
||||
|
||||
def test_build_board_boundary_gaps():
|
||||
"""Gaps from open→first and last→close when business_hours provided."""
|
||||
apts = [
|
||||
_apt("A1", 10, 0, 11, 0, "Alice", "Cut", "Claire"),
|
||||
_apt("A2", 15, 0, 16, 0, "Bob", "Color", "Claire"),
|
||||
]
|
||||
board = build_board(
|
||||
apts, "2026-07-28", "Test Salon",
|
||||
business_hours={"open": "09:00", "close": "18:00"},
|
||||
)
|
||||
# Should have: 09:00-10:00 (60 min), 11:00-15:00 (240 min), 16:00-18:00 (120 min)
|
||||
assert len(board.gaps) == 3
|
||||
assert board.gaps[0].start_time == time(9, 0)
|
||||
assert board.gaps[0].end_time == time(10, 0)
|
||||
assert board.gaps[2].start_time == time(16, 0)
|
||||
assert board.gaps[2].end_time == time(18, 0)
|
||||
|
||||
|
||||
def test_build_board_multi_staff_gaps():
|
||||
"""Gaps are computed per staff member."""
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
_apt("A2", 9, 0, 10, 0, "Bob", "Color", "Maya"),
|
||||
_apt("A3", 11, 0, 12, 0, "Carol", "Style", "Claire"),
|
||||
_apt("A4", 11, 0, 12, 0, "Dave", "Trim", "Maya"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
# Each staff has a 60-min gap.
|
||||
assert len(board.gaps) == 2
|
||||
staff_gaps = {g.staff_name: g.duration_minutes for g in board.gaps}
|
||||
assert staff_gaps["Claire"] == 60
|
||||
assert staff_gaps["Maya"] == 60
|
||||
|
||||
|
||||
def test_build_board_invalid_business_hours_warns():
|
||||
"""Malformed business_hours values warn and skip boundary gaps."""
|
||||
apts = [
|
||||
_apt("A1", 10, 0, 11, 0, "Alice", "Cut", "Claire"),
|
||||
]
|
||||
with pytest.warns(UserWarning, match="Invalid business_hours"):
|
||||
board = build_board(
|
||||
apts, "2026-07-28", "Test Salon",
|
||||
business_hours={"open": "nine", "close": "18:00"},
|
||||
)
|
||||
# Only the close boundary gap should appear (open was invalid).
|
||||
assert len(board.gaps) == 1
|
||||
assert board.gaps[0].start_time == time(11, 0)
|
||||
assert board.gaps[0].end_time == time(18, 0)
|
||||
|
||||
|
||||
def test_build_board_empty():
|
||||
board = build_board([], "2026-07-28", "Empty Salon")
|
||||
assert len(board.appointments) == 0
|
||||
assert len(board.gaps) == 0
|
||||
assert board.total_booked_minutes == 0
|
||||
assert board.total_gap_minutes == 0
|
||||
|
||||
|
||||
def test_build_board_source_labeling():
|
||||
"""Source is correctly set and is_offline derived."""
|
||||
board = build_board([], "2026-07-28", "Test", source="fixtures")
|
||||
assert board.source == "fixtures"
|
||||
assert board.is_offline is True
|
||||
|
||||
board2 = build_board([], "2026-07-28", "Test", source="offline")
|
||||
assert board2.is_offline is True
|
||||
|
||||
board3 = build_board([], "2026-07-28", "Test", source="vagaro")
|
||||
assert board3.is_offline is False
|
||||
|
||||
|
||||
def test_build_board_totals():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 30, "Alice", "Cut", "Claire"), # 90 min
|
||||
_apt("A2", 11, 0, 12, 0, "Bob", "Color", "Claire"), # 60 min
|
||||
]
|
||||
board = build_board(
|
||||
apts, "2026-07-28", "Test Salon",
|
||||
business_hours={"open": "09:00", "close": "18:00"},
|
||||
)
|
||||
assert board.total_booked_minutes == 150
|
||||
# Gaps: 10:30-11:00 (30 min), 12:00-18:00 (360 min)
|
||||
assert board.total_gap_minutes == 390
|
||||
|
||||
|
||||
# ── format_board_text ──────────────────────────────────────────────────────
|
||||
|
||||
def test_format_text_includes_offline_label():
|
||||
board = DayBoard(
|
||||
date="2026-07-28",
|
||||
salon_name="Test Salon",
|
||||
source="fixtures",
|
||||
is_offline=True,
|
||||
)
|
||||
text = format_board_text(board)
|
||||
assert "FIXTURE DATA" in text
|
||||
|
||||
|
||||
def test_format_text_includes_appointments():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
text = format_board_text(board)
|
||||
assert "Alice" in text
|
||||
assert "Cut" in text
|
||||
assert "Claire" in text
|
||||
assert "09:00" in text
|
||||
|
||||
|
||||
def test_format_text_includes_gaps():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
_apt("A2", 11, 0, 12, 0, "Bob", "Color", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
text = format_board_text(board)
|
||||
assert "Gaps" in text
|
||||
assert "60 min" in text
|
||||
|
||||
|
||||
def test_format_text_includes_confirmation_flags():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire", needs_confirmation=True),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
text = format_board_text(board)
|
||||
assert "CONFIRM" in text
|
||||
assert "Needs Confirmation" in text
|
||||
|
||||
|
||||
def test_format_text_includes_summary():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
text = format_board_text(board)
|
||||
assert "Summary" in text
|
||||
assert "60 min" in text
|
||||
|
||||
|
||||
def test_format_text_no_appointments():
|
||||
board = DayBoard(
|
||||
date="2026-07-28",
|
||||
salon_name="Empty Salon",
|
||||
source="fixtures",
|
||||
is_offline=True,
|
||||
)
|
||||
text = format_board_text(board)
|
||||
assert "No appointments" in text
|
||||
|
||||
|
||||
def test_format_text_no_gaps():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
text = format_board_text(board)
|
||||
assert "No significant gaps" in text
|
||||
|
||||
|
||||
def test_format_text_notes():
|
||||
apts = [
|
||||
_apt("A1", 9, 0, 10, 0, "Alice", "Cut", "Claire", notes="Allergic to ammonia"),
|
||||
]
|
||||
board = build_board(apts, "2026-07-28", "Test Salon")
|
||||
text = format_board_text(board)
|
||||
assert "Allergic to ammonia" in text
|
||||
@@ -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
|
||||
@@ -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