"""Tests for the tool-use fixes from the 2026-05-08 journal session.""" from types import SimpleNamespace from unittest.mock import AsyncMock, patch import pytest def _project(pid: int, title: str, description: str = "", auto_summary: str = ""): return SimpleNamespace( id=pid, title=title, description=description, auto_summary=auto_summary, ) @pytest.mark.asyncio async def test_search_projects_tool_returns_success_true(): """The dispatcher sets status from result['success']; without this key the call gets labeled 'error' even when data is returned.""" from fabledassistant.services.tools import projects as projects_tool fake_projects = [_project(5, "Famous-Supply Work topics", "AT&T fiber circuit")] # `list_projects` is imported locally inside search_projects_tool, so we # patch the source module rather than the consumer. with patch("fabledassistant.services.projects.list_projects", new=AsyncMock(return_value=fake_projects)): result = await projects_tool.search_projects_tool( user_id=1, arguments={"query": "famous supply"}, ) assert result["success"] is True assert result["type"] == "projects_list" assert len(result["data"]["projects"]) == 1 def test_strip_type_nouns_removes_task_word(): from fabledassistant.services.notes import _strip_type_nouns assert _strip_type_nouns("sebring secondary task") == ["sebring", "secondary"] def test_strip_type_nouns_removes_all_variants(): from fabledassistant.services.notes import _strip_type_nouns assert _strip_type_nouns("project notes task") == [] def test_strip_type_nouns_case_insensitive(): from fabledassistant.services.notes import _strip_type_nouns assert _strip_type_nouns("Sebring Task NOTES") == ["Sebring"] def test_strip_type_nouns_preserves_real_content_words(): from fabledassistant.services.notes import _strip_type_nouns assert _strip_type_nouns("circuit configuration") == ["circuit", "configuration"] def test_strip_type_nouns_handles_empty_string(): from fabledassistant.services.notes import _strip_type_nouns assert _strip_type_nouns("") == [] assert _strip_type_nouns(" ") == [] def test_score_project_match_exact_title_returns_1(): from fabledassistant.services.tools._helpers import score_project_match p = _project(5, "Famous-Supply Work topics") assert score_project_match("Famous-Supply Work topics", p) == 1.0 def test_score_project_match_colloquial_substring_at_least_85(): """'famous supply' is a substring of normalized 'famous supply work topics' after stripping the hyphen. Substring match returns 0.85.""" from fabledassistant.services.tools._helpers import score_project_match p = _project(5, "Famous-Supply Work topics", auto_summary="AT&T fiber circuit") score = score_project_match("famous supply project", p) assert score >= 0.85, f"expected substring tier (>=0.85), got {score}" def test_score_project_match_query_in_summary_returns_70(): from fabledassistant.services.tools._helpers import score_project_match p = _project(12, "Minstrel", auto_summary="self-hosted music server") assert score_project_match("music server", p) == 0.70 def test_score_project_match_unrelated_returns_low(): from fabledassistant.services.tools._helpers import score_project_match p = _project(12, "Minstrel", auto_summary="self-hosted music server") assert score_project_match("garden renovation", p) < 0.5 def test_score_project_match_empty_query_returns_zero(): from fabledassistant.services.tools._helpers import score_project_match p = _project(5, "Famous-Supply Work topics") assert score_project_match("", p) == 0.0 @pytest.mark.asyncio async def test_search_projects_tool_ranks_substring_match_above_others(): """Substring hits (score 0.85) must rank above SequenceMatcher misses for unrelated projects.""" from fabledassistant.services.tools import projects as projects_tool fake_projects = [ _project(12, "Minstrel", auto_summary="self-hosted music server"), _project(5, "Famous-Supply Work topics", auto_summary="AT&T fiber circuit"), _project(13, "ImageRepo", auto_summary="self-hosted Flask app"), ] with patch("fabledassistant.services.projects.list_projects", new=AsyncMock(return_value=fake_projects)): result = await projects_tool.search_projects_tool( user_id=1, arguments={"query": "famous supply project"}, ) top = result["data"]["projects"][0] assert top["id"] == 5, f"expected Famous-Supply to rank first, got {top}" assert top["score"] >= 0.85 @pytest.mark.asyncio async def test_resolve_project_finds_colloquial_match(monkeypatch): """resolve_project must surface 'Famous-Supply Work topics' when the user passes 'famous supply project' — substring match via the shared score helper, score 0.85 ≥ 0.55 threshold.""" from fabledassistant.services.tools import _helpers fake_projects = [ _project(12, "Minstrel", auto_summary="self-hosted music server"), _project(5, "Famous-Supply Work topics", auto_summary="AT&T fiber circuit"), ] async def fake_get_project_by_title(uid, name): return None # force the scored path async def fake_list_projects(uid): return fake_projects monkeypatch.setattr( "fabledassistant.services.projects.get_project_by_title", fake_get_project_by_title, ) monkeypatch.setattr( "fabledassistant.services.projects.list_projects", fake_list_projects, ) result = await _helpers.resolve_project(user_id=1, project_name="famous supply project") assert result is not None assert result.id == 5