from __future__ import annotations import pytest from helpers import seed_state from slack_mock.server import search_messages def setup_function() -> None: seed_state( { "channels": { "C001": {"id": "C001", "name": "general", "is_private": False}, "C002": {"id": "C002", "name": "engineering", "is_private": False}, }, "messages": { "C001": [ { "ts": "1700000001.000", "text": "Hello world", "user": "U001", "type": "message", "reactions": [{"name": "wave", "users": ["U002"], "count": 1}], "permalink": "https://example.com/message/C001/1700000001.000", }, { "ts": "1700000002.000", "text": "Reply in thread", "user": "U002", "type": "message", "thread_ts": "1700000001.000", }, { "ts": "1700000003.000", "text": "Another message", "user": "U001", "type": "message", "is_starred": True, }, {"ts": "1700000004.000", "text": "Deployment is ready", "user": "U002", "type": "message"}, { "ts": "1700000004.500", "text": "Literal from:alice appears in this message", "user": "U002", "type": "message", }, ], "C002": [ {"ts": "1700000005.000", "text": "Hello from engineering", "user": "U002", "type": "message"}, { "ts": "1700000006.000", "text": "Code review needed", "user": "U001", "type": "message", "pinned_to": ["C002"], }, { "ts": "1700000007.000", "text": "Design handoff link https://example.com/spec", "user": "U003", "type": "message", }, { "ts": "1700000008.000", "text": "Multi marker message", "user": "U001", "type": "message", "is_starred": True, "reactions": [{"name": "eyes", "users": ["U002"], "count": 1}], }, { "ts": "1700000009.000", "text": "Ambiguous display marker", "user": "U004", "type": "message", }, ], }, "users": { "U001": { "id": "U001", "name": "alice", "real_name": "Alice Smith", "profile": {"display_name": "alice"}, }, "U002": {"id": "U002", "name": "bob", "real_name": "Bob Jones", "profile": {"display_name": "bob"}}, "U003": { "id": "U003", "name": "alicia.smith", "real_name": "Alicia Smith", "profile": {"display_name": "alicia.smith"}, }, "U004": { "id": "U004", "name": "charlie", "real_name": "Charles Example", "profile": {"display_name": "bob"}, }, }, } ) def texts(result: dict) -> list[str]: return [match["text"] for match in result["messages"]["matches"]] @pytest.mark.asyncio async def test_text_matching_scoping_sorting_and_context() -> None: result = await search_messages("Hello") assert result["ok"] is True assert result["messages"]["total"] == 2 assert all("hello" in match["text"].lower() for match in result["messages"]["matches"]) assert {match["channel"]["id"] for match in result["messages"]["matches"]} == {"C001", "C002"} assert [float(match["ts"]) for match in result["messages"]["matches"]] == sorted( [float(match["ts"]) for match in result["messages"]["matches"]], reverse=True ) assert all(match["channel"]["name"] for match in result["messages"]["matches"]) assert all(match["user_name"] != "Unknown" for match in result["messages"]["matches"]) assert all("username" in match and "display_name" in match for match in result["messages"]["matches"]) scoped = await search_messages("Hello", channel_id="C001") assert scoped["messages"]["total"] == 1 assert scoped["messages"]["matches"][0]["channel"]["id"] == "C001" assert (await search_messages("Hello", channel_id="INVALID"))["error"] == "channel_not_found" assert (await search_messages("zzzznonexistent"))["messages"]["matches"] == [] @pytest.mark.asyncio async def test_user_and_thread_matching() -> None: by_user_name = await search_messages("alice smith") assert by_user_name["messages"]["total"] > 0 assert all(match["user"] == "U001" for match in by_user_name["messages"]["matches"]) reply = await search_messages("Reply in thread") assert reply["messages"]["total"] == 1 assert reply["messages"]["matches"][0]["thread_ts"] == "1700000001.000" @pytest.mark.asyncio async def test_pagination_limits_and_cursors() -> None: first = await search_messages("Hello", limit=1) assert first["messages"]["total"] == 2 assert first["response_metadata"]["next_cursor"] second = await search_messages("Hello", limit=1, cursor=first["response_metadata"]["next_cursor"]) assert second["messages"]["matches"][0]["ts"] != first["messages"]["matches"][0]["ts"] capped = await search_messages("message", limit=999) assert "limit exceeds the maximum of 100; using 100." in capped["response_metadata"]["warnings"] malformed = await search_messages("hello", limit=1, cursor="not-a-number") assert malformed["messages"]["matches"][0]["text"] == "Hello from engineering" assert "Invalid cursor 'not-a-number'; using the first page." in malformed["response_metadata"]["warnings"] @pytest.mark.asyncio async def test_word_and_phrase_semantics() -> None: assert (await search_messages("world hello"))["messages"]["matches"][0]["text"] == "Hello world" assert (await search_messages("hello goodbye"))["messages"]["total"] == 0 assert (await search_messages('"hello world"'))["messages"]["matches"][0]["text"] == "Hello world" assert (await search_messages('"world hello"'))["messages"]["total"] == 0 assert (await search_messages('needed "code review"'))["messages"]["matches"][0]["text"] == "Code review needed" quoted = await search_messages('"from:alice"') assert quoted["messages"]["matches"][0]["text"] == "Literal from:alice appears in this message" assert quoted["response_metadata"]["warnings"] == [] mixed = await search_messages("deployment bob") assert mixed["messages"]["matches"][0]["text"] == "Deployment is ready" assert mixed["messages"]["matches"][0]["user"] == "U002" @pytest.mark.asyncio async def test_channel_and_user_filters() -> None: assert (await search_messages("in:#engineering hello"))["messages"]["matches"][0]["channel"]["id"] == "C002" assert (await search_messages("in:C001 hello"))["messages"]["matches"][0]["channel"]["id"] == "C001" assert (await search_messages("in:#engineering hello", channel_id="C001"))["error"] == "channel_scope_conflict" assert (await search_messages("in:#nonexistent hello"))["messages"]["total"] == 0 assert (await search_messages("in:#engineering hello", channel_id="INVALID"))["error"] == "channel_not_found" assert (await search_messages("from:@bob deployment"))["messages"]["matches"][0]["user"] == "U002" assert (await search_messages("from:U001 review"))["messages"]["matches"][0]["text"] == "Code review needed" assert (await search_messages("from:alicia link"))["messages"]["matches"][0]["user"] == "U003" assert {match["user"] for match in (await search_messages("from:@bob"))["messages"]["matches"]} == {"U002"} display_name_match = (await search_messages("from:bob ambiguous"))["messages"]["matches"][0] assert display_name_match["user"] == "U004" assert display_name_match["username"] == "charlie" assert display_name_match["display_name"] == "bob" from_me = await search_messages("from:me hello") assert from_me["messages"]["total"] == 0 assert ( "from:me is unsupported because caller identity is not available; it will match no users." in from_me["response_metadata"]["warnings"] ) @pytest.mark.asyncio async def test_date_filters() -> None: result = await search_messages("hello after:1700000002 before:1700000006") assert result["messages"]["matches"][0]["text"] == "Hello from engineering" assert (await search_messages("hello before:1700000005"))["messages"]["matches"][0]["text"] == "Hello world" assert (await search_messages("during:2023-11-14 hello"))["messages"]["total"] == 2 assert (await search_messages("during:2023-11 hello"))["messages"]["total"] == 2 assert (await search_messages("during:2023 hello"))["messages"]["total"] == 2 invalid = await search_messages("during:2023-Q1 hello") assert invalid["messages"]["total"] == 2 assert ( "Invalid during: date '2023-Q1'. Use YYYY, YYYY-MM, YYYY-MM-DD, or a parseable date." in invalid["response_metadata"]["warnings"] ) @pytest.mark.asyncio async def test_has_filters_and_empty_filters() -> None: unsupported = await search_messages("has:calendar hello") assert unsupported["messages"]["total"] == 0 assert ( "Unsupported has: value 'calendar'. Supported values: link, reaction, star, pin." in unsupported["response_metadata"]["warnings"] ) assert (await search_messages("has:link"))["messages"]["matches"][0][ "text" ] == "Design handoff link https://example.com/spec" assert (await search_messages('has:link "hello world"'))["messages"]["total"] == 0 assert set(texts(await search_messages("has:reaction"))) == {"Hello world", "Multi marker message"} assert "Another message" in texts(await search_messages("has:star")) assert (await search_messages("has:pin"))["messages"]["matches"][0]["text"] == "Code review needed" assert (await search_messages("has:reaction has:star"))["messages"]["matches"][0]["text"] == "Multi marker message" assert (await search_messages("has:reaction has:pin"))["messages"]["total"] == 0 with_text = await search_messages("from: hello") assert with_text["messages"]["total"] == 2 assert "Empty from: filter was ignored." in with_text["response_metadata"]["warnings"] assert (await search_messages("from:"))["error"] == "missing_query" assert "Empty in: filter was ignored." in (await search_messages("in:"))["response_metadata"]["warnings"] assert "Empty has: filter was ignored." in (await search_messages("has:"))["response_metadata"]["warnings"] combined = await search_messages("in:#general from:@alice has:reaction after:2023-11-14 hello") assert combined["messages"]["total"] == 1 assert combined["messages"]["matches"][0]["text"] == "Hello world"