"""Tests for lumina_skills.providers.setup.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.setup.capability_report import ( CapabilityReport, ConnectionStatus, ) from lumina_skills.providers.setup.fixture_provider import ( load_capability_fixture, load_fixture_metadata, ) # Paths to real fixture files. _FIXTURE_PATH = pathlib.Path(__file__).resolve().parents[2] / "data" / "fixtures" / "setup" / "capability_matrix.json" _ALL_CONNECTED_PATH = pathlib.Path(__file__).resolve().parents[2] / "data" / "fixtures" / "setup" / "capability_matrix_all_connected.json" _ERRORS_PATH = pathlib.Path(__file__).resolve().parents[2] / "data" / "fixtures" / "setup" / "capability_matrix_with_errors.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_capability_fixture ─────────────────────────────────────────────── def test_load_default_fixture(): """Load the default capability matrix fixture.""" report = load_capability_fixture(_FIXTURE_PATH) assert isinstance(report, CapabilityReport) assert report.salon_name == "Lumina Hair Studio & Spa" assert report.is_fixture is True assert len(report.capabilities) > 0 def test_load_fixture_statuses(): """Fixture statuses are correctly parsed.""" report = load_capability_fixture(_FIXTURE_PATH) statuses = {(c.area, c.provider): c.status for c in report.capabilities} assert statuses[("identity", "assistant_name")] == ConnectionStatus.CONNECTED assert statuses[("channels", "whatsapp")] == ConnectionStatus.CONNECTED assert statuses[("channels", "email")] == ConnectionStatus.SKIPPED assert statuses[("channels", "telegram")] == ConnectionStatus.LATER assert statuses[("scheduling", "vagaro")] == ConnectionStatus.OFFLINE def test_load_fixture_all_connected(): """All-connected fixture has all CONNECTED statuses.""" report = load_capability_fixture(_ALL_CONNECTED_PATH) assert report.all_connected() is True assert report.is_fixture is True def test_load_fixture_with_errors(): """Error fixture has ERROR statuses and reports has_errors.""" report = load_capability_fixture(_ERRORS_PATH) assert report.has_errors() is True assert report.error_count() >= 1 def test_load_fixture_is_fixture_flag(): """Fixture reports always have is_fixture=True.""" report = load_capability_fixture(_FIXTURE_PATH) assert report.is_fixture is True def test_load_fixture_missing_file(tmp_path: pathlib.Path): """FileNotFoundError for missing fixture.""" with pytest.raises(FileNotFoundError): load_capability_fixture(tmp_path / "nonexistent.json") def test_load_fixture_empty_capabilities(tmp_path: pathlib.Path): """Empty capabilities list returns empty report.""" data = { "salon_name": "Empty Salon", "is_fixture": True, "capabilities": [], } path = _make_fixture_file(tmp_path, data) report = load_capability_fixture(path) assert len(report.capabilities) == 0 assert report.salon_name == "Empty Salon" def test_load_fixture_unknown_status_raises(tmp_path: pathlib.Path): """Unknown status string raises ValueError.""" data = { "salon_name": "Test", "is_fixture": True, "capabilities": [{ "area": "channels", "provider": "whatsapp", "status": "typo_status", }], } path = _make_fixture_file(tmp_path, data) with pytest.raises(ValueError, match="Unknown capability status"): load_capability_fixture(path) def test_load_fixture_missing_area_field(tmp_path: pathlib.Path): """Missing 'area' field raises ValueError with clear message.""" data = { "salon_name": "Test", "is_fixture": True, "capabilities": [{ "provider": "whatsapp", "status": "connected", # Missing "area" }], } path = _make_fixture_file(tmp_path, data) with pytest.raises(ValueError, match="Capability entry 0 missing required field 'area'"): load_capability_fixture(path) def test_load_fixture_missing_provider_field(tmp_path: pathlib.Path): """Missing 'provider' field raises ValueError with clear message.""" data = { "salon_name": "Test", "is_fixture": True, "capabilities": [{ "area": "channels", "status": "connected", # Missing "provider" }], } path = _make_fixture_file(tmp_path, data) with pytest.raises(ValueError, match="Capability entry 0 missing required field 'provider'"): load_capability_fixture(path) def test_load_fixture_missing_status_field(tmp_path: pathlib.Path): """Missing 'status' field raises ValueError with clear message.""" data = { "salon_name": "Test", "is_fixture": True, "capabilities": [{ "area": "channels", "provider": "whatsapp", # Missing "status" }], } path = _make_fixture_file(tmp_path, data) with pytest.raises(ValueError, match="Capability entry 0 missing required field 'status'"): load_capability_fixture(path) def test_load_fixture_missing_field_reports_index(tmp_path: pathlib.Path): """Missing field error message includes the entry index.""" data = { "salon_name": "Test", "is_fixture": True, "capabilities": [ { "area": "channels", "provider": "whatsapp", "status": "connected", }, { "area": "books", # Missing "provider" and "status" in entry 1 }, ], } path = _make_fixture_file(tmp_path, data) with pytest.raises(ValueError, match="Capability entry 1 missing required field 'provider'"): load_capability_fixture(path) 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_capability_fixture(p) def test_load_fixture_default_salon_name(tmp_path: pathlib.Path): """Missing salon_name defaults to 'Unknown Salon'.""" data = { "is_fixture": True, "capabilities": [], } path = _make_fixture_file(tmp_path, data) report = load_capability_fixture(path) assert report.salon_name == "Unknown Salon" def test_load_fixture_details(): """Details field is loaded from fixture.""" report = load_capability_fixture(_FIXTURE_PATH) details = {(c.area, c.provider): c.details for c in report.capabilities} assert "fixture" in details[("scheduling", "vagaro")].lower() or "not yet" in details[("scheduling", "vagaro")].lower() # ── Fixture labeling: offline/fixture never silent as live ───────────────── def test_fixture_never_silent_as_live(): """Fixture reports must have is_fixture=True — never silent as live.""" report = load_capability_fixture(_FIXTURE_PATH) assert report.is_fixture is True, "Fixture report must always be labeled as fixture" report2 = load_capability_fixture(_ALL_CONNECTED_PATH) assert report2.is_fixture is True report3 = load_capability_fixture(_ERRORS_PATH) assert report3.is_fixture is True def test_fixture_explicit_false_still_respected(): """If fixture explicitly sets is_fixture=false, it is loaded as-is.""" with tempfile.TemporaryDirectory() as tmp: tmp_path = pathlib.Path(tmp) data = { "salon_name": "Test", "is_fixture": False, "capabilities": [{ "area": "channels", "provider": "whatsapp", "status": "connected", }], } path = _make_fixture_file(tmp_path, data) report = load_capability_fixture(path) # The loader respects the explicit value. assert report.is_fixture is False # ── load_fixture_metadata ───────────────────────────────────────────────── def test_load_metadata(): meta = load_fixture_metadata(_FIXTURE_PATH) assert meta["salon_name"] == "Lumina Hair Studio & Spa" assert meta["is_fixture"] is True assert "generated_at" in meta assert "note" in meta def test_load_metadata_missing_file(tmp_path: pathlib.Path): with pytest.raises(FileNotFoundError): load_fixture_metadata(tmp_path / "nonexistent.json")