diff --git a/agent/webhooks/slack.py b/agent/webhooks/slack.py index 842709d7..96b8e47f 100644 --- a/agent/webhooks/slack.py +++ b/agent/webhooks/slack.py @@ -290,6 +290,7 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st thread_ts, run_id, message_ts=trace_message_ts, + trace_message_ts=trace_message_ts, triggering_user_id=user_id, ) else: diff --git a/tests/test_slack_context.py b/tests/test_slack_context.py index 6772414c..3d24c881 100644 --- a/tests/test_slack_context.py +++ b/tests/test_slack_context.py @@ -42,6 +42,22 @@ class _FakeClient: self.threads = threads_client +class _FakeSlackMappingStore: + def __init__(self) -> None: + self.items: dict[tuple[tuple[str, ...], str], dict] = {} + + async def put_item(self, namespace: tuple[str, ...], key: str, value: dict) -> None: + self.items[(namespace, key)] = {"value": value} + + async def get_item(self, namespace: tuple[str, ...], key: str) -> dict | None: + return self.items.get((namespace, key)) + + +class _FakeSlackMappingClient: + def __init__(self) -> None: + self.store = _FakeSlackMappingStore() + + def test_generate_thread_id_from_slack_thread_is_deterministic() -> None: channel_id = "C12345" thread_ts = "1730900000.123456" @@ -51,6 +67,63 @@ def test_generate_thread_id_from_slack_thread_is_deterministic() -> None: assert len(first) == 36 +@pytest.mark.asyncio +async def test_slack_run_mapping_preserves_trace_message_ts() -> None: + client = _FakeSlackMappingClient() + + await slack_utils.store_slack_run_mapping( + client, + "C123", + "1.0", + "run-1", + message_ts="1.1", + triggering_user_id="U123", + trace_message_ts="1.1", + ) + await slack_utils.store_slack_message_run_mapping(client, "C123", "1.0", "1.2") + + thread_mapping = await slack_utils.lookup_slack_thread_run_mapping(client, "C123", "1.0") + message_mapping = await slack_utils.lookup_slack_run_mapping(client, "C123", "1.2") + assert thread_mapping is not None + assert thread_mapping["trace_message_ts"] == "1.1" + assert thread_mapping["triggering_user_id"] == "U123" + assert message_mapping is not None + assert message_mapping["trace_message_ts"] == "1.1" + assert message_mapping["message_ts"] == "1.2" + + +@pytest.mark.asyncio +async def test_slack_run_mapping_preserves_trace_message_ts_on_followup_mention() -> None: + """A subsequent Slack mention without trace_message_ts must not clobber the stored timestamp.""" + client = _FakeSlackMappingClient() + + # First mention stores the trace message ts. + await slack_utils.store_slack_run_mapping( + client, + "C123", + "1.0", + "run-1", + message_ts="1.1", + triggering_user_id="U123", + trace_message_ts="1.1", + ) + + # Follow-up mention (non-first) stores a new run_id without trace_message_ts. + await slack_utils.store_slack_run_mapping( + client, + "C123", + "1.0", + "run-2", + triggering_user_id="U456", + ) + + thread_mapping = await slack_utils.lookup_slack_thread_run_mapping(client, "C123", "1.0") + assert thread_mapping is not None + assert thread_mapping["run_id"] == "run-2" + assert thread_mapping["trace_message_ts"] == "1.1" + assert thread_mapping["triggering_user_id"] == "U456" + + def test_select_slack_context_messages_uses_thread_start_when_no_prior_mention() -> None: bot_user_id = "UBOT" messages = [