Files
Salon_Assistant/tests/unit/test_capability_report.py
T
Ty 76df8b7ab6 Implement E1 setup-education and capability report (fixtures).
Add owner-safe lesson catalog, fixture-backed capability statuses, CLI, and unit tests without live OAuth or connect scripts.
2026-07-27 13:10:57 -07:00

301 lines
10 KiB
Python

"""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}"