"""Tests for lumina_skills.setup.capability_report domain model and builder.""" from __future__ import annotations import json import sys import pathlib sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "skills" / "_lib")) from lumina_skills.setup.capability_report import ( CapabilityEntry, CapabilityReport, ConnectionStatus, build_capability_report, format_capability_report_text, status_label, ) # ── ConnectionStatus ─────────────────────────────────────────────────────── def test_connection_status_values(): """All status enum values are correct.""" assert ConnectionStatus.CONNECTED.value == "connected" assert ConnectionStatus.SKIPPED.value == "skipped" assert ConnectionStatus.LATER.value == "later" assert ConnectionStatus.ERROR.value == "error" assert ConnectionStatus.OFFLINE.value == "offline" def test_status_labels(): """Each status has an emoji label.""" assert "✅" in status_label(ConnectionStatus.CONNECTED) assert "⏭️" in status_label(ConnectionStatus.SKIPPED) assert "⏳" in status_label(ConnectionStatus.LATER) assert "❌" in status_label(ConnectionStatus.ERROR) assert "📋" in status_label(ConnectionStatus.OFFLINE) # ── CapabilityEntry ──────────────────────────────────────────────────────── def test_capability_entry_label(): """Entry label uses status_label.""" entry = CapabilityEntry( area="channels", provider="whatsapp", status=ConnectionStatus.CONNECTED, ) assert "✅" in entry.label def test_capability_entry_frozen(): """CapabilityEntry is immutable.""" entry = CapabilityEntry( area="channels", provider="whatsapp", status=ConnectionStatus.CONNECTED, ) try: entry.status = ConnectionStatus.ERROR assert False, "Should not be able to modify frozen dataclass" except Exception: pass # Expected # ── CapabilityReport ─────────────────────────────────────────────────────── def _make_entries(*statuses: ConnectionStatus) -> list[CapabilityEntry]: """Helper to create CapabilityEntry objects with given statuses.""" providers = ["whatsapp", "email", "telegram", "vagaro", "qbo"] areas = ["channels", "channels", "channels", "scheduling", "books"] entries = [] for i, status in enumerate(statuses): entries.append(CapabilityEntry( area=areas[i], provider=providers[i], status=status, )) return entries def test_report_connected_count(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTED, ConnectionStatus.SKIPPED, ConnectionStatus.OFFLINE, ConnectionStatus.ERROR, ) report = build_capability_report(entries, "Test Salon") assert report.connected_count() == 2 def test_report_offline_count(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.OFFLINE, ConnectionStatus.OFFLINE, ConnectionStatus.SKIPPED, ConnectionStatus.ERROR, ) report = build_capability_report(entries, "Test Salon") assert report.offline_count() == 2 def test_report_skipped_count(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.SKIPPED, ConnectionStatus.LATER, ConnectionStatus.OFFLINE, ConnectionStatus.ERROR, ) report = build_capability_report(entries, "Test Salon") assert report.skipped_count() == 2 # SKIPPED + LATER def test_report_error_count(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.ERROR, ConnectionStatus.ERROR, ConnectionStatus.SKIPPED, ConnectionStatus.OFFLINE, ) report = build_capability_report(entries, "Test Salon") assert report.error_count() == 2 def test_report_all_connected(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTED, ) report = build_capability_report(entries, "Test Salon") assert report.all_connected() is True def test_report_not_all_connected(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTED, ConnectionStatus.SKIPPED, ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTED, ) report = build_capability_report(entries, "Test Salon") assert report.all_connected() is False def test_report_has_errors(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.ERROR, ConnectionStatus.SKIPPED, ) report = build_capability_report(entries, "Test Salon") assert report.has_errors() is True def test_report_no_errors(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.SKIPPED, ConnectionStatus.OFFLINE, ) report = build_capability_report(entries, "Test Salon") assert report.has_errors() is False def test_report_empty(): report = build_capability_report([], "Empty Salon") assert report.connected_count() == 0 assert report.offline_count() == 0 assert report.skipped_count() == 0 assert report.error_count() == 0 assert report.all_connected() is False # empty report is not "all connected" assert report.has_errors() is False # ── to_dict ──────────────────────────────────────────────────────────────── def test_report_to_dict(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.OFFLINE, ConnectionStatus.SKIPPED, ) report = build_capability_report(entries, "Test Salon", is_fixture=True) d = report.to_dict() assert d["salon_name"] == "Test Salon" assert d["is_fixture"] is True assert len(d["capabilities"]) == 3 assert d["capabilities"][0]["status"] == "connected" assert d["capabilities"][1]["status"] == "offline" assert d["capabilities"][2]["status"] == "skipped" assert d["summary"]["connected"] == 1 assert d["summary"]["offline"] == 1 assert d["summary"]["skipped_or_later"] == 1 assert d["summary"]["errors"] == 0 assert d["summary"]["all_connected"] is False def test_report_to_dict_serializable(): """to_dict output must be JSON-serializable.""" entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.OFFLINE, ) report = build_capability_report(entries, "Test Salon") d = report.to_dict() # Should not raise. json.dumps(d) # ── format_capability_report_text ────────────────────────────────────────── def test_format_text_includes_salon_name(): entries = _make_entries(ConnectionStatus.CONNECTED) report = build_capability_report(entries, "Lumina Hair Studio & Spa") text = format_capability_report_text(report) assert "Lumina Hair Studio & Spa" in text def test_format_text_fixture_label(): entries = _make_entries(ConnectionStatus.OFFLINE) report = build_capability_report(entries, "Test Salon", is_fixture=True) text = format_capability_report_text(report) assert "FIXTURE DATA" in text def test_format_text_no_fixture_label_when_live(): entries = _make_entries(ConnectionStatus.CONNECTED) report = build_capability_report(entries, "Test Salon", is_fixture=False) text = format_capability_report_text(report) assert "FIXTURE DATA" not in text def test_format_text_includes_summary(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.OFFLINE, ConnectionStatus.SKIPPED, ) report = build_capability_report(entries, "Test Salon") text = format_capability_report_text(report) assert "Summary" in text assert "Connected:" in text def test_format_text_error_warning(): entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.ERROR, ) report = build_capability_report(entries, "Test Salon") text = format_capability_report_text(report) assert "needs attention" in text def test_format_text_fixture_disclaimer(): entries = _make_entries(ConnectionStatus.OFFLINE) report = build_capability_report(entries, "Test Salon", is_fixture=True) text = format_capability_report_text(report) assert "fixture" in text.lower() or "demo" in text.lower() def test_format_text_groups_by_area(): entries = [ CapabilityEntry("channels", "whatsapp", ConnectionStatus.CONNECTED), CapabilityEntry("channels", "email", ConnectionStatus.SKIPPED), CapabilityEntry("scheduling", "vagaro", ConnectionStatus.OFFLINE), CapabilityEntry("books", "quickbooks_online", ConnectionStatus.OFFLINE), ] report = build_capability_report(entries, "Test Salon") text = format_capability_report_text(report) assert "Channels" in text assert "Scheduling" in text assert "Books" in text def test_format_text_owner_safe(): """Formatted text must never contain forbidden keywords.""" forbidden = ["terminal", "docker", "nano", "shell", "bash", "sudo"] entries = _make_entries( ConnectionStatus.CONNECTED, ConnectionStatus.OFFLINE, ConnectionStatus.SKIPPED, ConnectionStatus.ERROR, ConnectionStatus.LATER, ) report = build_capability_report(entries, "Test Salon", is_fixture=True) text = format_capability_report_text(report).lower() for kw in forbidden: assert kw not in text, f"Formatted text contains forbidden keyword: {kw}"