a551f52682
CI surfaced three issues:
- 'famous supply project' didn't substring-match 'Famous-Supply Work topics'
because the trailing filler word 'project' blocked the substring tier.
Strip {project, projects} from the query before the substring check.
- SequenceMatcher fallback against `combined` (title + description +
summary) diluted ratios to ~0.5 for plausible matches. Use title
directly; the 0.70 tier already handles description/summary mentions.
- Test patches used patch.object on a consumer module where
list_projects is imported locally — patch the source module instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
5.6 KiB
Python
146 lines
5.6 KiB
Python
"""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
|