"""Regression — like-notification dedup (repeated like must not spam). ``toggle_like`` returns ``(like_count, changed)`` and both callers (HTTP router + MCP ``feed_react_post``) gate ``notify_like_added`` on ``changed``. Before the fix, every repeated like call (double-click, LLM retry loop) re-delivered an "X liked your post" inbox notification to the author. """ from __future__ import annotations import pytest from sqlalchemy.ext.asyncio import AsyncSession from app.domains.feed.service import FeedService @pytest.mark.asyncio async def test_double_like_notifies_exactly_once( client, db_session: AsyncSession, admin_user, employee_user, monkeypatch, ) -> None: author, _ = admin_user liker, liker_token = employee_user # Eager-load org_memberships — ``create_user_post`` resolves the # author's primary department and a sync lazy-load would raise # MissingGreenlet under the async session. await db_session.refresh(author, attribute_names=["org_memberships"]) post, _ = await FeedService(db_session).create_user_post( author=author, body="hello world", visibility="public", category="general", mentioned_person_ids=[], attachments=[], ) await db_session.commit() calls: list[str] = [] async def _spy(db, *, post_id: str, liker) -> None: calls.append(post_id) monkeypatch.setattr( "app.domains.feed.notifications.notify_like_added", _spy, ) headers = {"Authorization": f"Bearer {liker_token}"} r1 = await client.post(f"/api/v1/feed/posts/{post.id}/like", headers=headers) r2 = await client.post(f"/api/v1/feed/posts/{post.id}/like", headers=headers) assert r1.status_code == 200 and r2.status_code == 200 assert r1.json()["data"]["like_count"] == 1 assert r2.json()["data"]["like_count"] == 1 # Exactly one notification: the duplicate like changed nothing. assert calls == [post.id] # Unlike never notifies. r3 = await client.delete(f"/api/v1/feed/posts/{post.id}/like", headers=headers) assert r3.status_code == 200 assert calls == [post.id]