# SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc from __future__ import annotations import json import os import subprocess import sys from pathlib import Path from solstone.apps import AppRegistry from solstone.convey.icons import ( APP_LUCIDE_MAP, _lucide_tags, emoji_to_lucide, is_lucide_icon, lucide_svg, lucide_svg_for_emoji, resolve_icon_svg, search_lucide_icons, ) REPO_ROOT = Path(__file__).resolve().parents[1] def test_lucide_svg_hit_and_miss() -> None: assert " None: assert is_lucide_icon("house") is True assert is_lucide_icon("not-a-lucide-icon") is False assert is_lucide_icon("") is False def test_emoji_to_lucide_required_cases() -> None: assert emoji_to_lucide("📚") == "library" assert emoji_to_lucide("🤝") == "handshake" assert emoji_to_lucide("⚙️") == "settings" assert emoji_to_lucide("⚙") == "settings" assert emoji_to_lucide("⚙️") == emoji_to_lucide("⚙") assert emoji_to_lucide("🪮", default="fallback") == "fallback" assert emoji_to_lucide("") is None def test_emoji_to_lucide_preserves_raw_zwj_key() -> None: assert emoji_to_lucide("⛓‍💥") == "bone-fracture" def test_lucide_svg_for_emoji_hit_and_miss() -> None: assert " None: assert resolve_icon_svg("brain", "📚") == lucide_svg("brain") assert resolve_icon_svg(None, "📚") == lucide_svg("library") assert resolve_icon_svg("", "📚") == lucide_svg("library") assert resolve_icon_svg("definitely-not-an-icon", "📚") == lucide_svg("library") assert resolve_icon_svg("coins", "🪮") == lucide_svg("coins") def test_data_driven_l3_icon_names_resolve() -> None: import importlib import_routes = importlib.import_module("solstone.apps.import.routes") from solstone.apps.search.routes import AGENT_ICON_FALLBACK, AGENT_ICONS from solstone.think.activities import DEFAULT_ACTIVITIES import_icon_names = [source["icon"] for source in import_routes.SOURCE_METADATA] search_icon_names = [*AGENT_ICONS.values(), AGENT_ICON_FALLBACK] activity_icon_names = [activity["icon"] for activity in DEFAULT_ACTIVITIES] for icon_name in import_icon_names: assert lucide_svg(icon_name), f"import icon not found: {icon_name}" for icon_name in search_icon_names: assert lucide_svg(icon_name), f"search icon not found: {icon_name}" for icon_name in activity_icon_names: assert lucide_svg(icon_name), f"activity icon not found: {icon_name}" def test_activity_icon_overrides_use_explicit_lucide_names() -> None: from solstone.think.activities import DEFAULT_ACTIVITIES by_id = {activity["id"]: activity for activity in DEFAULT_ACTIVITIES} appointment = by_id["appointment"] assert resolve_icon_svg(appointment["icon"], appointment["emoji"]) == lucide_svg( "pin" ) assert resolve_icon_svg(appointment["icon"], appointment["emoji"]) != lucide_svg( "map-pin" ) event = by_id["event"] assert resolve_icon_svg(event["icon"], event["emoji"]) == lucide_svg("ticket") assert resolve_icon_svg(event["icon"], event["emoji"]) != lucide_svg("tags") def test_search_lucide_icons_lock_matches_name_or_tag() -> None: tags = _lucide_tags() results = search_lucide_icons("lock") assert results assert all( "lock" in result["name"] or any("lock" in tag for tag in tags.get(result["name"], [])) for result in results ) def test_search_lucide_icons_ranks_relevance_over_substring() -> None: # "lock" must surface the lock icon itself ahead of substring-only hits # like "alarm-clock"/"clock" (which only contain "lock" inside "clock"). names = [r["name"] for r in search_lucide_icons("lock", limit=40)] assert names[0] == "lock" assert "alarm-clock" not in names[:5] if "clock" in names and "alarm-clock" in names: assert names.index("lock") < names.index("clock") assert names.index("lock") < names.index("alarm-clock") # Exact name matches rank first for other queries too. assert search_lucide_icons("heart")[0]["name"] == "heart" assert search_lucide_icons("shield")[0]["name"] == "shield" def test_search_lucide_icons_empty_is_alphabetical_and_limited() -> None: results = search_lucide_icons("", limit=7) names = [result["name"] for result in results] assert len(results) == 7 assert names == sorted(names) def test_search_lucide_icons_respects_limit() -> None: assert len(search_lucide_icons("lock", limit=5)) <= 5 def test_search_lucide_icons_orders_name_matches_before_tag_only() -> None: results = search_lucide_icons("brain", limit=20) names = [result["name"] for result in results] tag_only_start = next(i for i, name in enumerate(names) if "brain" not in name) assert tag_only_start > 0 assert all("brain" in name for name in names[:tag_only_start]) assert all("brain" not in name for name in names[tag_only_start:]) def test_lucide_loading_is_package_relative(tmp_path: Path) -> None: env = os.environ.copy() pythonpath = env.get("PYTHONPATH") env["PYTHONPATH"] = ( f"{REPO_ROOT}{os.pathsep}{pythonpath}" if pythonpath else str(REPO_ROOT) ) result = subprocess.run( [ sys.executable, "-c", ( "from solstone.convey.icons import lucide_svg; " "raise SystemExit(0 if lucide_svg('house') else 1)" ), ], cwd=tmp_path, env=env, check=False, ) assert result.returncode == 0 def test_app_registry_is_covered_by_lucide_map() -> None: registry = AppRegistry() registry.discover() assert set(registry.apps) <= set(APP_LUCIDE_MAP) def test_app_lucide_map_values_exist_in_vendored_lucide_data() -> None: lucide_path = REPO_ROOT / "solstone" / "convey" / "static" / "icons" / "lucide.json" lucide_data = json.loads(lucide_path.read_text()) assert set(APP_LUCIDE_MAP.values()) <= set(lucide_data)