From cff49172627fd753f7db44fd95d7f69f349d4731 Mon Sep 17 00:00:00 2001 From: Sonny May Date: Thu, 5 Nov 2026 15:07:56 +0000 Subject: [PATCH] fix: exclude the ticket itself from AI suggestion matches A Resolved/Closed ticket was included in its own comparison set, so /ai/suggest could offer it as a 'similar past ticket' to itself. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0171zWziv1yV4nqFKxwETEZE --- backend/ai.py | 3 ++- backend/test_ai_suggest.py | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 backend/test_ai_suggest.py diff --git a/backend/ai.py b/backend/ai.py index d16befa..53f8115 100644 --- a/backend/ai.py +++ b/backend/ai.py @@ -125,7 +125,8 @@ def _extract_json(text: str) -> dict[str, Any]: def suggest_for_ticket(ticket_id: str) -> dict[str, Any]: ticket = _fetch_ticket(ticket_id) notes = _fetch_notes(ticket_id) - history = _fetch_recent_resolved(30) + # A Resolved/Closed ticket must not be offered as a "similar past ticket" to itself. + history = [t for t in _fetch_recent_resolved(30) if t.get("id") != ticket.get("id")] if not history: return {"matches": [], "note": "No resolved tickets available for comparison."} diff --git a/backend/test_ai_suggest.py b/backend/test_ai_suggest.py new file mode 100644 index 0000000..fed1ab2 --- /dev/null +++ b/backend/test_ai_suggest.py @@ -0,0 +1,53 @@ +"""Tests for ai.suggest_for_ticket (the real ai module, not the stub used in test_main).""" + +import importlib +import sys +from unittest.mock import MagicMock + +import pytest + + +@pytest.fixture +def ai(monkeypatch): + sys.modules.pop("ai", None) + module = importlib.import_module("ai") + yield module + sys.modules.pop("ai", None) + + +def test_suggest_excludes_the_ticket_itself(ai, monkeypatch): + target = {"id": "t1", "title": "Printer jam", "status": "Closed"} + other = {"id": "t2", "title": "Paper jam", "status": "Resolved"} + monkeypatch.setattr(ai, "_fetch_ticket", lambda _id: target) + monkeypatch.setattr(ai, "_fetch_notes", lambda _id: []) + monkeypatch.setattr(ai, "_fetch_recent_resolved", lambda limit=30: [dict(target), dict(other)]) + + captured = {} + + def fake_build(ticket, notes, history): + captured["history"] = history + return "prompt" + + monkeypatch.setattr(ai, "_build_prompt", fake_build) + client = MagicMock() + client.messages.create.return_value = MagicMock( + content=[MagicMock(type="text", text='{"matches": []}')] + ) + monkeypatch.setattr(ai, "_get_client", lambda: client) + + ai.suggest_for_ticket("t1") + + assert [t["id"] for t in captured["history"]] == ["t2"] + + +def test_suggest_with_only_itself_in_history_returns_note(ai, monkeypatch): + target = {"id": "t1", "title": "Printer jam", "status": "Closed"} + monkeypatch.setattr(ai, "_fetch_ticket", lambda _id: target) + monkeypatch.setattr(ai, "_fetch_notes", lambda _id: []) + monkeypatch.setattr(ai, "_fetch_recent_resolved", lambda limit=30: [dict(target)]) + monkeypatch.setattr(ai, "_get_client", lambda: pytest.fail("model must not be called")) + + result = ai.suggest_for_ticket("t1") + + assert result["matches"] == [] + assert "No resolved tickets" in result["note"]