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