Skip to content

Commit 3edf2e5

Browse files
address self-review findings on PR #48
- **Structural thread-ID validation**: reject `slack::`, `slack:::`, `slack::::` etc. via `any(seg for seg in remainder.split(":"))` BEFORE calling the adapter. Previously relied on `adapter.channel_id_from_thread_id()` returning exactly `"{adapter}:"` to catch the empty-remainder case, but different adapters return different things for malformed input (MockAdapter returns `"slack:"`, real Slack regex-matches and returns the full string unchanged, etc.). The structural check doesn't depend on adapter-specific behavior, so it's a single source of truth. - **Expand `test_empty_remainder_raises`** to cover `slack:::` and `slack::::` variants. - **Clean up `test_omits_history_when_adapter_does_not_persist`**: explicitly set `adapter.persist_message_history = None` instead of asserting on the mock's default. Prevents silent test regression if the mock default ever changes, and makes intent unambiguous.
1 parent f839e7a commit 3edf2e5

2 files changed

Lines changed: 37 additions & 21 deletions

File tree

src/chat_sdk/chat.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,25 +1404,34 @@ def thread(
14041404
14051405
Mirrors ``chat.thread(threadId)`` from the upstream TS SDK.
14061406
"""
1407-
# Validate thread ID shape: must be `{adapter}:{remainder}` with a
1408-
# non-empty adapter prefix AND non-empty remainder. Without the
1409-
# remainder check, `"slack:"` or `"slack::"` would construct a
1410-
# ThreadImpl with an empty channel ID that blows up on the first
1411-
# adapter call — surface the error here instead.
1407+
# Validate thread ID shape structurally, before calling the adapter.
1408+
# A valid ID is `{adapter}:{segment}[:{segment}...]` with:
1409+
# - non-empty adapter prefix
1410+
# - at least one non-empty segment after the prefix
1411+
# Rejects `"slack:"`, `"slack::"`, `"slack::::"` etc. at the SDK
1412+
# boundary so we don't rely on each adapter's
1413+
# `channel_id_from_thread_id` doing its own defense. (Different
1414+
# adapters return different things for malformed input; we need
1415+
# a single source of truth.)
14121416
adapter_name, sep, remainder = thread_id.partition(":")
1413-
if not sep or not adapter_name or not remainder:
1417+
if not sep or not adapter_name:
14141418
raise ChatError(f"Invalid thread ID: {thread_id}")
1419+
# Remainder is empty or only colons → no actual content.
1420+
if not remainder or not any(seg for seg in remainder.split(":")):
1421+
raise ChatError(f"Invalid thread ID: {thread_id}")
1422+
14151423
adapter = self._adapters.get(adapter_name)
14161424
if adapter is None:
14171425
raise ChatError(f'Adapter "{adapter_name}" not found for thread ID "{thread_id}"')
14181426

1419-
# Defer to the adapter to derive channel_id — it knows the
1420-
# platform-specific encoding. Empty channel_id also means malformed.
1427+
# Defer to the adapter to derive channel_id as a secondary sanity
1428+
# check — some platform-specific patterns can pass the structural
1429+
# check but still be malformed (e.g. wrong number of segments).
14211430
try:
14221431
derived_channel_id = adapter.channel_id_from_thread_id(thread_id)
14231432
except Exception as exc:
14241433
raise ChatError(f"Invalid thread ID: {thread_id}") from exc
1425-
if not derived_channel_id or derived_channel_id == f"{adapter_name}:":
1434+
if not derived_channel_id:
14261435
raise ChatError(f"Invalid thread ID: {thread_id}")
14271436

14281437
stub_message = (

tests/test_chat_resolver.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -504,14 +504,18 @@ def test_reuses_parent_chat_state_and_history(self):
504504
assert thread._message_history is chat._message_history
505505

506506
def test_omits_history_when_adapter_does_not_persist(self):
507-
"""Adapters with `persist_message_history=False`/None opt out of
508-
the shared history cache. `Chat.thread()` respects that: the
509-
Thread gets `None` for history rather than a shared cache the
510-
adapter won't populate.
507+
"""Adapters with `persist_message_history` falsy opt out of the
508+
shared history cache. `Chat.thread()` respects that: the Thread
509+
gets `None` for history rather than a shared cache the adapter
510+
won't populate.
511+
512+
Explicitly set `persist_message_history = None` rather than
513+
relying on the mock's default — prevents silent test regression
514+
if the mock default ever changes.
511515
"""
512516
chat = _make_chat("slack")
513517
adapter = chat._adapters["slack"]
514-
assert not adapter.persist_message_history # default on mock is None
518+
adapter.persist_message_history = None
515519

516520
thread = chat.thread("slack:C123:1234567890.123456")
517521
assert thread._state_adapter is chat._state_adapter
@@ -525,17 +529,20 @@ def test_invalid_thread_id_raises(self):
525529
chat.thread("no-colon-here")
526530

527531
def test_empty_remainder_raises(self):
528-
"""`slack:` or `slack::` would create a thread with empty channel
529-
ID that blows up on the first adapter call — surface the error
530-
at construction time instead.
532+
"""`slack:` or `slack::`, or any string where every segment after
533+
the adapter prefix is empty, would create a thread with no real
534+
content. Surface the error at construction time instead of
535+
relying on adapter-specific `channel_id_from_thread_id` to
536+
catch it (adapters differ in what they return for malformed
537+
input — some return the input, some return a partial prefix —
538+
so we validate structurally before dispatching to the adapter).
531539
"""
532540
from chat_sdk.errors import ChatError
533541

534542
chat = _make_chat("slack")
535-
with pytest.raises(ChatError, match="Invalid thread ID"):
536-
chat.thread("slack:")
537-
with pytest.raises(ChatError, match="Invalid thread ID"):
538-
chat.thread("slack::")
543+
for bad in ("slack:", "slack::", "slack:::", "slack::::"):
544+
with pytest.raises(ChatError, match="Invalid thread ID"):
545+
chat.thread(bad)
539546

540547
def test_unregistered_adapter_raises(self):
541548
from chat_sdk.errors import ChatError

0 commit comments

Comments
 (0)