from hackathon_advisor.dashboard import build_dashboard_payload from hackathon_advisor.dashboard_repository import DashboardRepository from hackathon_advisor.dashboard_search import DashboardSearchIndex from hackathon_advisor.data import Project, ProjectIndex, build_index_payload def test_overview_reports_counts_clusters_and_quests() -> None: repository = analyzed_repository() overview = repository.overview() assert overview["project_count"] == 10 assert overview["cluster_count"] >= 1 assert overview["quest_status"] == "analyzed" assert overview["top_clusters"][0]["project_count"] >= 1 assert overview["top_quests"][0]["project_count"] >= 1 assert overview["most_liked"][0]["title"] == "Project 9" def test_overview_handles_not_analyzed_quests() -> None: repository = not_analyzed_repository() overview = repository.overview() assert overview["quest_status"] == "not_analyzed" assert overview["top_quests"] == [] def test_list_clusters_returns_labels_not_ids() -> None: repository = analyzed_repository() listing = repository.list_clusters() assert listing["cluster_count"] == len(listing["clusters"]) for cluster in listing["clusters"]: assert cluster["label"] assert not cluster["label"].startswith("cluster-") assert cluster["project_count"] >= 1 def test_cluster_detail_resolves_fuzzy_label_and_id() -> None: repository = analyzed_repository() first = repository.list_clusters()["clusters"][0] by_label = repository.cluster_detail(first["label"]) by_lower = repository.cluster_detail(first["label"].lower()) by_id = repository.cluster_detail("cluster-1") assert by_label is not None and by_label["label"] == first["label"] assert by_lower is not None and by_lower["label"] == first["label"] assert by_id is not None assert by_label["examples"] assert by_label["examples"][0]["id"].startswith("build-small-hackathon/") def test_cluster_detail_returns_none_for_unknown_label() -> None: repository = analyzed_repository() assert repository.cluster_detail("totally unrelated nonsense xyz") is None assert repository.cluster_detail("") is None def test_list_quests_sorted_by_coverage() -> None: repository = analyzed_repository() listing = repository.list_quests() counts = [quest["project_count"] for quest in listing["quests"]] assert listing["status"] == "analyzed" assert counts == sorted(counts, reverse=True) assert counts[0] >= 1 def test_quest_detail_resolves_aliases() -> None: repository = analyzed_repository() detail = repository.quest_detail("local first") assert detail is not None assert detail["id"] == "Off the Grid" assert detail["project_count"] >= 1 assert detail["examples"] def test_quest_detail_rejects_unknown_quest() -> None: repository = analyzed_repository() assert repository.quest_detail("not a quest at all") is None def test_quest_detail_spots_label_inside_a_question() -> None: repository = analyzed_repository() detail = repository.quest_detail("tell me about the Tiny Titan quest") assert detail is not None assert detail["id"] == "Tiny Titan" def test_top_by_quests_ranks_projects_by_quest_count() -> None: repository = analyzed_repository() leaderboard = repository.top_by_quests(limit=5) assert leaderboard["status"] == "analyzed" assert leaderboard["rows"] counts = [row["quest_count"] for row in leaderboard["rows"]] assert counts == sorted(counts, reverse=True) assert leaderboard["rows"][0]["quest_count"] == 2 assert leaderboard["rows"][0]["id"] == "build-small-hackathon/project-9" def test_top_by_quests_empty_when_not_analyzed() -> None: repository = not_analyzed_repository() leaderboard = repository.top_by_quests() assert leaderboard["status"] == "not_analyzed" assert leaderboard["rows"] == [] assert leaderboard["projects_with_quests"] == 0 def test_search_delegates_to_bm25_index() -> None: repository = analyzed_repository() result = repository.search("project 4 summary", limit=3) assert result["total"] >= 1 assert result["results"][0]["id"] == "build-small-hackathon/project-4" assert result["results"][0]["url"] def test_project_detail_returns_readme_and_app_excerpts() -> None: repository = analyzed_repository() by_title = repository.project_detail("Project 4") by_slug = repository.project_detail("project-4") embedded = repository.project_detail("how does Project 4 work?") assert by_title is not None assert by_title["id"] == "build-small-hackathon/project-4" assert "README evidence for project 4" in by_title["readme_excerpt"] assert "import gradio" in by_title["app_excerpt"] assert by_title["app_file"] == "app.py" assert by_title["cluster_label"] assert by_slug is not None and by_slug["id"] == by_title["id"] assert embedded is not None and embedded["id"] == by_title["id"] def test_project_detail_returns_none_for_unknown_project() -> None: repository = analyzed_repository() assert repository.project_detail("totally unknown thing") is None assert repository.project_detail("") is None def test_repository_survives_a_sparse_payload() -> None: """The docstring promises empty-but-well-formed results on degraded snapshots.""" project_index = fake_project_index() full_payload = build_dashboard_payload(project_index, generated_at="2026-06-08T00:00:00+00:00") search_index = DashboardSearchIndex(project_index.projects, full_payload) repository = DashboardRepository({}, search_index) assert repository.overview()["project_count"] == 0 assert repository.list_clusters()["clusters"] == [] assert repository.list_quests()["quests"] == [] assert repository.top_by_quests()["rows"] == [] assert repository.recent_activity()["projects"] == [] assert repository.cluster_detail("anything") is None assert repository.quest_detail("Off the Grid") is not None assert repository.search("planner")["results"] def test_recent_activity_sorts_by_last_modified() -> None: repository = analyzed_repository() recent = repository.recent_activity(limit=3) assert [project["id"] for project in recent["projects"]] == [ "build-small-hackathon/project-9", "build-small-hackathon/project-8", "build-small-hackathon/project-7", ] assert recent["projects"][0]["cluster_label"] def analyzed_repository() -> DashboardRepository: project_index = fake_project_index() quest_matches = {project.id: [] for project in project_index.projects} quest_matches["build-small-hackathon/project-9"] = [ {"quest": "Off the Grid", "confidence": 0.9, "evidence": "loads weights locally", "source": "readme"}, {"quest": "Tiny Titan", "confidence": 0.8, "evidence": "MiniCPM5-1B model", "source": "app_file"}, ] quest_matches["build-small-hackathon/project-4"] = [ {"quest": "Off the Grid", "confidence": 0.7, "evidence": "local llama.cpp runtime", "source": "readme"}, ] payload = build_dashboard_payload( project_index, quest_matches=quest_matches, quest_source="test-quest-run", generated_at="2026-06-08T00:00:00+00:00", ) return DashboardRepository(payload, DashboardSearchIndex(project_index.projects, payload)) def not_analyzed_repository() -> DashboardRepository: project_index = fake_project_index() payload = build_dashboard_payload(project_index, generated_at="2026-06-08T00:00:00+00:00") return DashboardRepository(payload, DashboardSearchIndex(project_index.projects, payload)) def fake_project_index() -> ProjectIndex: projects = [ Project( id=f"build-small-hackathon/project-{index}", title=f"Project {index}", summary=f"Offline project planner {index}", tags=("gradio", "local-first"), models=("tiny-model",), datasets=(), likes=index, sdk="gradio", license="mit", created_at="2026-06-01T00:00:00+00:00", last_modified=f"2026-06-{index + 1:02d}T00:00:00+00:00", host=f"https://project-{index}.hf.space", url=f"https://huggingface.co/spaces/build-small-hackathon/project-{index}", app_file="app.py", app_file_embedding_text=f"local inference gradio small model artifact project {index}", app_file_source=f'import gradio as gr\n\ndemo = gr.Interface(fn=str) # project {index}\ndemo.launch()', readme_body=f"README evidence for project {index}", ) for index in range(10) ] embeddings = [] for index in range(10): vector = [0.0] * 10 vector[index] = 1.0 embeddings.append(vector) generated_at = "2026-06-08T00:00:00+00:00" source = "https://example.test/spaces" return ProjectIndex( projects=projects, generated_at=generated_at, source=source, index_payload=build_index_payload(projects, generated_at, source, embeddings), )