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.
This commit is contained in:
Ty
2026-07-27 13:10:57 -07:00
parent 69b67498ff
commit 76df8b7ab6
16 changed files with 1955 additions and 7 deletions
+254
View File
@@ -0,0 +1,254 @@
"""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")