Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .coverage
Binary file not shown.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.0.1a7 (2026-04-03)

Fixture replay tests and coverage hardening.

- All fixture replay tests pass (46 tests, 28 JSON fixtures copied from TS SDK)
- GChat: fix float startIndex in annotation parsing
- Postgres: atomic lock upsert to eliminate TOCTOU race
- Coverage tests added for 4 low-coverage modules
- Documentation updates (TESTING.md, ARCHITECTURE.md)

## 0.0.1a6 (2026-04-07)

Systematic port fidelity scan — 10 more bugs fixed.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Multi-platform async chat SDK for Python. Port of [Vercel Chat](https://github.com/vercel/chat).

> **Status: Alpha (0.0.1a6)** — API may change. Not yet tested in production.
> **Status: Alpha (0.0.1a7)** — API may change. Not yet tested in production.

## Why chat-sdk?

Expand Down
10 changes: 5 additions & 5 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,15 @@ def assert_no_camel_case_keys(d, path="", *, skip_raw=True):

This test suite is a regression guard. Without it, any refactoring of adapter dispatch code could reintroduce camelCase keys without any test failure. The test runs on every CI build and takes < 1 second.

## Plans: Webhook JSON Fixtures
## Webhook JSON Fixtures (Done)

The TS SDK repository contains recorded webhook JSON payloads used for testing. We plan to copy these fixtures into `tests/fixtures/` to enable:
28 webhook JSON fixtures were copied from the TS SDK repository into `tests/fixtures/` and 46 replay tests now pass against them. The fixtures enable:

1. **Cross-language parity testing**: Verify that the Python adapter produces the same normalized `Message` objects as the TS adapter for identical webhook payloads.
1. **Cross-language parity testing**: The Python adapter produces the same normalized `Message` objects as the TS adapter for identical webhook payloads.
2. **Platform version regression**: When platforms change their webhook format, the fixture files make it obvious what changed.
3. **Replay tests without mocks**: Drive the full adapter pipeline with real payloads instead of hand-constructed dicts.
3. **Replay tests without mocks**: The full adapter pipeline is driven with real payloads instead of hand-constructed dicts.

The fixture format will be:
The fixture layout:
```
tests/fixtures/
slack/
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "chat-sdk"
version = "0.0.1a6"
version = "0.0.1a7"
description = "Multi-platform async chat SDK for Python — port of Vercel Chat"
readme = "README.md"
license = {text = "MIT"}
Expand Down Expand Up @@ -34,7 +34,8 @@ discord = ["pynacl>=1.5", "aiohttp>=3.9"]
teams = ["aiohttp>=3.9"]
telegram = ["aiohttp>=3.9"]
whatsapp = ["aiohttp>=3.9"]
google-chat = ["aiohttp>=3.9", "google-auth>=2.0", "pyjwt>=2.8"]
google-chat = ["aiohttp>=3.9",
"pyjwt[crypto]>=2.8", "google-auth>=2.0", "pyjwt>=2.8"]
linear = ["aiohttp>=3.9"]
all = [
"slack-sdk>=3.27.0",
Expand All @@ -44,6 +45,7 @@ all = [
"cryptography>=42.0",
"pynacl>=1.5",
"aiohttp>=3.9",
"pyjwt[crypto]>=2.8",
"google-auth>=2.0",
]

Expand All @@ -70,5 +72,7 @@ dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23.0",
"pytest-cov>=5.0",
"aiohttp>=3.9",
"pyjwt[crypto]>=2.8",
"ruff>=0.4.0",
]
133 changes: 133 additions & 0 deletions tests/test_buffer_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Tests for buffer_utils -- targeting 80%+ coverage.

Ported from packages/adapter-shared/src/buffer-utils.test.ts.
"""

from __future__ import annotations

import pytest

from chat_sdk.shared.buffer_utils import buffer_to_data_uri, to_buffer, to_buffer_sync
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import ValidationError at the top level to adhere to PEP 8 and avoid repetitive local imports throughout the test methods.

Suggested change
from chat_sdk.shared.buffer_utils import buffer_to_data_uri, to_buffer, to_buffer_sync
from chat_sdk.shared.buffer_utils import buffer_to_data_uri, to_buffer, to_buffer_sync
from chat_sdk.shared.errors import ValidationError


# ---------------------------------------------------------------------------
# to_buffer (async)
# ---------------------------------------------------------------------------


class TestToBuffer:
async def test_bytes_passthrough(self):
data = b"hello"
result = await to_buffer(data, "slack")
assert result == b"hello"
assert isinstance(result, bytes)

async def test_bytearray_conversion(self):
data = bytearray(b"hello")
result = await to_buffer(data, "slack")
assert result == b"hello"
assert isinstance(result, bytes)

async def test_memoryview_conversion(self):
data = memoryview(b"hello")
result = await to_buffer(data, "slack")
assert result == b"hello"
assert isinstance(result, bytes)

async def test_raises_for_unsupported_type_string(self):
from chat_sdk.shared.errors import ValidationError

with pytest.raises(ValidationError):
await to_buffer("string", "slack")

async def test_raises_for_unsupported_type_int(self):
from chat_sdk.shared.errors import ValidationError

with pytest.raises(ValidationError):
await to_buffer(123, "slack")

async def test_raises_for_unsupported_type_dict(self):
from chat_sdk.shared.errors import ValidationError

with pytest.raises(ValidationError):
await to_buffer({}, "slack")

async def test_raises_for_unsupported_type_none(self):
from chat_sdk.shared.errors import ValidationError

with pytest.raises(ValidationError):
await to_buffer(None, "slack")

async def test_returns_none_when_throw_disabled(self):
result = await to_buffer("string", "teams", throw_on_unsupported=False)
assert result is None

async def test_includes_platform_in_error(self):
from chat_sdk.shared.errors import ValidationError

try:
await to_buffer("invalid", "slack")
except ValidationError as e:
assert e.adapter == "slack"
Comment on lines +65 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current try...except block will pass if to_buffer fails to raise a ValidationError. To ensure the exception is actually raised and to verify its attributes, use pytest.raises with the as excinfo pattern. The local import can be removed once moved to the top of the file.

        with pytest.raises(ValidationError) as excinfo:
            await to_buffer("invalid", "slack")
        assert excinfo.value.adapter == "slack"



# ---------------------------------------------------------------------------
# to_buffer_sync
# ---------------------------------------------------------------------------


class TestToBufferSync:
def test_bytes_passthrough(self):
data = b"hello"
result = to_buffer_sync(data, "slack")
assert result == b"hello"

def test_bytearray_conversion(self):
data = bytearray(b"hello")
result = to_buffer_sync(data, "slack")
assert result == b"hello"

def test_memoryview_conversion(self):
data = memoryview(b"hello")
result = to_buffer_sync(data, "slack")
assert result == b"hello"

def test_raises_for_unsupported_type(self):
from chat_sdk.shared.errors import ValidationError

with pytest.raises(ValidationError):
to_buffer_sync("string", "slack")

def test_returns_none_when_throw_disabled(self):
result = to_buffer_sync("string", "teams", throw_on_unsupported=False)
assert result is None

def test_includes_platform_in_error(self):
from chat_sdk.shared.errors import ValidationError

try:
to_buffer_sync("invalid", "teams")
except ValidationError as e:
assert e.adapter == "teams"
Comment on lines +105 to +110
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the async version, this try...except block should be replaced with pytest.raises to correctly verify that the exception is thrown and to inspect its attributes.

        with pytest.raises(ValidationError) as excinfo:
            to_buffer_sync("invalid", "teams")
        assert excinfo.value.adapter == "teams"



# ---------------------------------------------------------------------------
# buffer_to_data_uri
# ---------------------------------------------------------------------------


class TestBufferToDataUri:
def test_default_mime_type(self):
result = buffer_to_data_uri(b"hello")
assert result == "data:application/octet-stream;base64,aGVsbG8="

def test_custom_mime_type(self):
result = buffer_to_data_uri(b"hello", "text/plain")
assert result == "data:text/plain;base64,aGVsbG8="

def test_image_mime_type(self):
result = buffer_to_data_uri(bytes([0x89, 0x50, 0x4E, 0x47]), "image/png")
assert result.startswith("data:image/png;base64,")

def test_empty_buffer(self):
result = buffer_to_data_uri(b"")
assert result == "data:application/octet-stream;base64,"
Loading
Loading