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:
@@ -0,0 +1,297 @@
|
||||
"""Tests for lumina_skills.setup.lesson_catalog."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "skills" / "_lib"))
|
||||
|
||||
from lumina_skills.setup.lesson_catalog import (
|
||||
FORBIDDEN_KEYWORDS,
|
||||
Lesson,
|
||||
LESSON_CATALOG,
|
||||
format_lesson_text,
|
||||
format_all_lessons_text,
|
||||
get_lesson,
|
||||
get_lessons_by_area,
|
||||
get_all_lessons,
|
||||
is_owner_safe,
|
||||
validate_lesson_owner_safe,
|
||||
validate_catalog_owner_safe,
|
||||
)
|
||||
|
||||
|
||||
# ── Lesson catalog structure ──────────────────────────────────────────────
|
||||
|
||||
def test_catalog_has_seven_lessons():
|
||||
"""Catalog must have exactly 7 lessons (matching SETUP_UX steps 1-7)."""
|
||||
assert len(LESSON_CATALOG) == 7
|
||||
|
||||
|
||||
def test_lesson_steps_are_sequential():
|
||||
"""Lesson steps must be 1 through 7."""
|
||||
steps = [l.step for l in LESSON_CATALOG]
|
||||
assert steps == list(range(1, 8))
|
||||
|
||||
|
||||
def test_all_lessons_returns_all():
|
||||
assert len(get_all_lessons()) == 7
|
||||
|
||||
|
||||
def test_get_lesson_by_step():
|
||||
lesson = get_lesson(1)
|
||||
assert lesson is not None
|
||||
assert lesson.step == 1
|
||||
assert "name" in lesson.title.lower() or "assistant" in lesson.title.lower()
|
||||
|
||||
|
||||
def test_get_lesson_out_of_range():
|
||||
assert get_lesson(0) is None
|
||||
assert get_lesson(8) is None
|
||||
assert get_lesson(-1) is None
|
||||
|
||||
|
||||
def test_get_lessons_by_area():
|
||||
channel_lessons = get_lessons_by_area("channels")
|
||||
assert len(channel_lessons) >= 1
|
||||
assert all(l.area == "channels" for l in channel_lessons)
|
||||
|
||||
|
||||
def test_get_lessons_by_area_empty():
|
||||
assert get_lessons_by_area("nonexistent_area") == []
|
||||
|
||||
|
||||
# ── Lesson content ────────────────────────────────────────────────────────
|
||||
|
||||
def test_lesson_1_identity():
|
||||
lesson = get_lesson(1)
|
||||
assert lesson is not None
|
||||
assert lesson.area == "identity"
|
||||
|
||||
|
||||
def test_lesson_2_profile():
|
||||
lesson = get_lesson(2)
|
||||
assert lesson is not None
|
||||
assert lesson.area == "profile"
|
||||
|
||||
|
||||
def test_lesson_3_channels():
|
||||
lesson = get_lesson(3)
|
||||
assert lesson is not None
|
||||
assert lesson.area == "channels"
|
||||
|
||||
|
||||
def test_lesson_4_scheduling():
|
||||
lesson = get_lesson(4)
|
||||
assert lesson is not None
|
||||
assert lesson.area == "scheduling"
|
||||
|
||||
|
||||
def test_lesson_5_books():
|
||||
lesson = get_lesson(5)
|
||||
assert lesson is not None
|
||||
assert lesson.area == "books"
|
||||
|
||||
|
||||
def test_lesson_6_expectations():
|
||||
lesson = get_lesson(6)
|
||||
assert lesson is not None
|
||||
assert lesson.area == "expectations"
|
||||
|
||||
|
||||
def test_lesson_7_report():
|
||||
lesson = get_lesson(7)
|
||||
assert lesson is not None
|
||||
assert lesson.area == "report"
|
||||
|
||||
|
||||
def test_lessons_have_non_empty_fields():
|
||||
"""All lessons must have non-empty title, description, and instructions."""
|
||||
for lesson in LESSON_CATALOG:
|
||||
assert lesson.title.strip(), f"Lesson {lesson.step} has empty title"
|
||||
assert lesson.description.strip(), f"Lesson {lesson.step} has empty description"
|
||||
assert lesson.instructions.strip(), f"Lesson {lesson.step} has empty instructions"
|
||||
assert lesson.what_it_enables.strip(), f"Lesson {lesson.step} has empty what_it_enables"
|
||||
|
||||
|
||||
def test_lessons_have_possible_outcomes():
|
||||
"""All lessons must have at least one possible outcome."""
|
||||
for lesson in LESSON_CATALOG:
|
||||
assert len(lesson.possible_outcomes) >= 1, f"Lesson {lesson.step} has no outcomes"
|
||||
|
||||
|
||||
# ── Owner-safe validation ─────────────────────────────────────────────────
|
||||
|
||||
def test_forbidden_keywords_not_empty():
|
||||
"""FORBIDDEN_KEYWORDS must contain expected keywords."""
|
||||
assert "terminal" in FORBIDDEN_KEYWORDS
|
||||
assert "docker" in FORBIDDEN_KEYWORDS
|
||||
assert "nano" in FORBIDDEN_KEYWORDS
|
||||
assert "bash" in FORBIDDEN_KEYWORDS
|
||||
assert "sudo" in FORBIDDEN_KEYWORDS
|
||||
assert "shell" in FORBIDDEN_KEYWORDS
|
||||
|
||||
|
||||
def test_is_owner_safe_clean_text():
|
||||
assert is_owner_safe("Log in to your Vagaro account in your browser.") is True
|
||||
assert is_owner_safe("Choose a name for your assistant.") is True
|
||||
assert is_owner_safe("Share the API key with your operator.") is True
|
||||
|
||||
|
||||
def test_is_owner_safe_forbidden_text():
|
||||
assert is_owner_safe("Run docker-compose up") is False
|
||||
assert is_owner_safe("Open a terminal and type") is False
|
||||
assert is_owner_safe("Edit with nano") is False
|
||||
assert is_owner_safe("Execute the bash script") is False
|
||||
|
||||
|
||||
def test_is_owner_safe_multi_word_no_false_positive():
|
||||
"""Multi-word keywords must not false-positive on unrelated text."""
|
||||
# "brew install" should NOT match "brew installation" or "homebrew installed"
|
||||
assert is_owner_safe("We use homebrew installed packages") is True
|
||||
assert is_owner_safe("The brew installation completed") is True
|
||||
assert is_owner_safe("I will install the app manually") is True
|
||||
# "pip install" should NOT match "pip installed" or "install pip"
|
||||
assert is_owner_safe("pip installed successfully") is True
|
||||
assert is_owner_safe("install pip from the store") is True
|
||||
# "git clone" should NOT match "clone git" (reversed)
|
||||
assert is_owner_safe("clone git repository") is True
|
||||
# "docker run" should NOT match "docker running" — but "docker" alone
|
||||
# IS a single-word forbidden keyword, so we test the multi-word phrase
|
||||
# in isolation by checking the phrase itself doesn't match a variant:
|
||||
assert is_owner_safe("the container is running in background") is True
|
||||
# "make install" should NOT match "make installation"
|
||||
assert is_owner_safe("make installation directory") is True
|
||||
# "npm install" should NOT match "npm installed"
|
||||
assert is_owner_safe("npm installed globally") is True
|
||||
|
||||
|
||||
def test_is_owner_safe_multi_word_true_positive():
|
||||
"""Multi-word keywords must still match the exact phrase."""
|
||||
assert is_owner_safe("brew install python") is False
|
||||
assert is_owner_safe("pip install requests") is False
|
||||
assert is_owner_safe("npm install express") is False
|
||||
assert is_owner_safe("git clone https://example.com") is False
|
||||
assert is_owner_safe("docker run nginx") is False
|
||||
assert is_owner_safe("make install all") is False
|
||||
assert is_owner_safe("docker exec container") is False
|
||||
assert is_owner_safe("git push origin main") is False
|
||||
assert is_owner_safe("git pull origin main") is False
|
||||
|
||||
|
||||
def test_is_owner_safe_single_word_boundaries():
|
||||
"""Single-word keywords use word boundaries."""
|
||||
# "docker" should match standalone but not inside unrelated words
|
||||
assert is_owner_safe("Use docker to containerize") is False
|
||||
# "nano" should match standalone
|
||||
assert is_owner_safe("Edit with nano") is False
|
||||
# "bash" should match standalone
|
||||
assert is_owner_safe("Run in bash") is False
|
||||
|
||||
|
||||
def test_is_owner_safe_case_insensitive():
|
||||
assert is_owner_safe("Use DOCKER to run") is False
|
||||
assert is_owner_safe("Open TERMINAL") is False
|
||||
|
||||
|
||||
def test_validate_lesson_owner_safe_clean():
|
||||
"""All catalog lessons must pass owner-safe validation."""
|
||||
for lesson in LESSON_CATALOG:
|
||||
violations = validate_lesson_owner_safe(lesson)
|
||||
assert violations == [], (
|
||||
f"Lesson {lesson.step} ({lesson.title}) contains forbidden keywords: {violations}"
|
||||
)
|
||||
|
||||
|
||||
def test_validate_catalog_owner_safe():
|
||||
"""Full catalog validation must return empty dict."""
|
||||
violations = validate_catalog_owner_safe()
|
||||
assert violations == {}, f"Catalog has violations: {violations}"
|
||||
|
||||
|
||||
def test_validate_lesson_owner_safe_detects_forbidden():
|
||||
"""Validation correctly detects forbidden keywords."""
|
||||
bad_lesson = Lesson(
|
||||
step=99,
|
||||
title="Bad Lesson",
|
||||
description="Run docker-compose up in your terminal",
|
||||
instructions="Open bash and type sudo nano config.yml",
|
||||
what_it_enables="Nothing",
|
||||
)
|
||||
violations = validate_lesson_owner_safe(bad_lesson)
|
||||
assert "docker" in violations
|
||||
assert "terminal" in violations
|
||||
assert "bash" in violations
|
||||
assert "sudo" in violations
|
||||
assert "nano" in violations
|
||||
|
||||
|
||||
def test_validate_lesson_owner_safe_includes_possible_outcomes():
|
||||
"""Validation checks possible_outcomes field for forbidden keywords."""
|
||||
lesson_with_bad_outcome = Lesson(
|
||||
step=99,
|
||||
title="Good Lesson",
|
||||
description="Clean description",
|
||||
instructions="Clean instructions",
|
||||
what_it_enables="Clean enables",
|
||||
possible_outcomes=["connected", "docker"], # "docker" in outcomes
|
||||
)
|
||||
violations = validate_lesson_owner_safe(lesson_with_bad_outcome)
|
||||
assert "docker" in violations, "possible_outcomes should be checked"
|
||||
|
||||
|
||||
def test_validate_lesson_owner_safe_clean_outcomes():
|
||||
"""Validation passes when possible_outcomes are clean."""
|
||||
clean_lesson = Lesson(
|
||||
step=99,
|
||||
title="Good Lesson",
|
||||
description="Clean description",
|
||||
instructions="Clean instructions",
|
||||
what_it_enables="Clean enables",
|
||||
possible_outcomes=["connected", "skipped", "later", "error"],
|
||||
)
|
||||
violations = validate_lesson_owner_safe(clean_lesson)
|
||||
assert violations == [], f"Clean lesson should have no violations: {violations}"
|
||||
|
||||
|
||||
# ── Formatting ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_format_lesson_text_includes_step():
|
||||
lesson = get_lesson(1)
|
||||
text = format_lesson_text(lesson)
|
||||
assert "Step 1" in text
|
||||
|
||||
|
||||
def test_format_lesson_text_includes_title():
|
||||
lesson = get_lesson(1)
|
||||
text = format_lesson_text(lesson)
|
||||
assert lesson.title in text
|
||||
|
||||
|
||||
def test_format_lesson_text_includes_instructions():
|
||||
lesson = get_lesson(4)
|
||||
text = format_lesson_text(lesson)
|
||||
assert "What to do:" in text
|
||||
assert lesson.instructions in text
|
||||
|
||||
|
||||
def test_format_lesson_text_owner_safe():
|
||||
"""Formatted lesson text must be owner-safe."""
|
||||
for lesson in LESSON_CATALOG:
|
||||
text = format_lesson_text(lesson)
|
||||
assert is_owner_safe(text), (
|
||||
f"Lesson {lesson.step} formatted text contains forbidden keywords"
|
||||
)
|
||||
|
||||
|
||||
def test_format_all_lessons_text_owner_safe():
|
||||
"""Full lessons text must be owner-safe."""
|
||||
text = format_all_lessons_text()
|
||||
assert is_owner_safe(text)
|
||||
|
||||
|
||||
def test_format_all_lessons_text_includes_all_steps():
|
||||
text = format_all_lessons_text()
|
||||
for i in range(1, 8):
|
||||
assert f"Step {i}" in text
|
||||
Reference in New Issue
Block a user