chore: import upstream snapshot with attribution
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:44 +08:00
commit 5a558eb09e
11579 changed files with 1795921 additions and 0 deletions
@@ -0,0 +1,51 @@
"""
Common test data constants for attachment tests.
This module contains base64-encoded and binary test data for various file formats
used across attachment decoder tests.
"""
import base64
# PNG Test Data
# 1x1 transparent PNG image
PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
# JPEG Test Data
# Minimal valid JPEG image (1x1 red pixel)
JPEG_BASE64 = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCRAP/Z"
# PDF Test Data
# Minimal valid PDF document with "Hello World"
PDF_BYTES: bytes = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/Resources <<\n/Font <<\n/F1 4 0 R\n>>\n>>\n/MediaBox [0 0 612 792]\n/Contents 5 0 R\n>>\nendobj\n4 0 obj\n<<\n/Type /Font\n/Subtype /Type1\n/BaseFont /Helvetica\n>>\nendobj\n5 0 obj\n<<\n/Length 44\n>>\nstream\nBT\n/F1 24 Tf\n100 700 Td\n(Hello World) Tj\nET\nendstream\nendobj\nxref\n0 6\n0000000000 65535 f\n0000000015 00000 n\n0000000074 00000 n\n0000000131 00000 n\n0000000277 00000 n\n0000000356 00000 n\ntrailer\n<<\n/Size 6\n/Root 1 0 R\n>>\nstartxref\n448\n%%EOF"
PDF_BASE64 = base64.b64encode(PDF_BYTES).decode("utf-8")
# GIF Test Data
# Minimal GIF89a image
GIF89_BYTES: bytes = (
b"GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;"
)
GIF89_BASE64 = base64.b64encode(GIF89_BYTES).decode("utf-8")
# WebP Test Data
# Minimal WebP image
WEBP_BYTES: bytes = b"RIFF\x1a\x00\x00\x00WEBPVP8 \x0e\x00\x00\x000\x01\x00\x9d\x01*\x01\x00\x01\x00\x00\x00\x00\x00"
WEBP_BASE64 = base64.b64encode(WEBP_BYTES).decode("utf-8")
# SVG Test Data
# Simple SVG image with a red circle
SVG_BYTES: bytes = b'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><circle cx="50" cy="50" r="40" fill="red"/></svg>'
SVG_BASE64 = base64.b64encode(SVG_BYTES).decode("utf-8")
# JSON Test Data
JSON_BYTES: bytes = b'{"key": "value", "number": 123, "array": [1, 2, 3]}'
JSON_BASE64 = base64.b64encode(JSON_BYTES).decode("utf-8")
# Plain Text Test Data
PLAIN_TEXT_BYTES: bytes = b"This is just plain text without special markers"
PLAIN_TEXT_BASE64 = base64.b64encode(PLAIN_TEXT_BYTES).decode("utf-8")
# Random Binary Data (octet-stream)
RANDOM_BINARY_BYTES: bytes = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a"
RANDOM_BINARY_BASE64 = base64.b64encode(RANDOM_BINARY_BYTES).decode("utf-8")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,240 @@
import base64
import uuid
import pytest
from opik.api_objects.attachment import base64_normalizer
from . import constants
def _to_urlsafe(standard_b64: str) -> str:
"""Re-encode a standard-base64 string into URL-safe form for fixtures."""
return base64.urlsafe_b64encode(base64.b64decode(standard_b64)).decode("utf-8")
# Bytes whose base64 encoding contains '+' / '/' (so URL-safe encoding contains
# '-' / '_'). Appended to fixtures to guarantee we actually exercise the
# URL-safe path rather than falling through the "no '-'/'_'" early exit.
_URLSAFE_FORCING_TAIL = b"\xfb\xff\xfe" * 8
def _urlsafe_image_fixture(signature: bytes) -> str:
"""Build a URL-safe base64 string that (a) decodes to bytes starting with
``signature`` and (b) is guaranteed to contain '-' or '_' so the
detector's URL-safe-alphabet pre-check actually runs.
"""
encoded = base64.urlsafe_b64encode(signature + _URLSAFE_FORCING_TAIL).decode(
"utf-8"
)
assert "-" in encoded or "_" in encoded, "fixture failed to force URL-safe chars"
return encoded
# ---------------------------------------------------------------------------
# urlsafe_to_standard_base64
# ---------------------------------------------------------------------------
class TestUrlsafeToStandardBase64:
def test_urlsafe_to_standard_base64__dash_and_underscore_chars__replaced_with_plus_and_slash(
self,
):
assert (
base64_normalizer.urlsafe_to_standard_base64("ab-cd_ef==") == "ab+cd/ef=="
)
def test_urlsafe_to_standard_base64__already_standard_alphabet__returns_unchanged(
self,
):
value = "iVBORw0KGgo+AB/CD=="
assert base64_normalizer.urlsafe_to_standard_base64(value) == value
def test_urlsafe_to_standard_base64__urlsafe_chars_with_padding__padding_preserved(
self,
):
assert base64_normalizer.urlsafe_to_standard_base64("ab-_==") == "ab+/=="
def test_urlsafe_to_standard_base64__empty_string__returns_empty(self):
assert base64_normalizer.urlsafe_to_standard_base64("") == ""
# ---------------------------------------------------------------------------
# is_urlsafe_base64_image
# ---------------------------------------------------------------------------
class TestIsUrlsafeBase64Image:
@pytest.mark.parametrize(
"signature",
[
b"\x89PNG\r\n\x1a\n",
b"\xff\xd8\xff",
b"GIF87a",
b"GIF89a",
b"RIFF\x00\x00\x00\x00WEBP",
],
ids=["png", "jpeg", "gif87a", "gif89a", "webp"],
)
def test_is_urlsafe_base64_image__urlsafe_encoded_image_signature__returns_true(
self, signature
):
"""Every supported image signature, when URL-safe-base64-encoded, is detected."""
urlsafe = _urlsafe_image_fixture(signature)
assert base64_normalizer.is_urlsafe_base64_image(urlsafe) is True
def test_is_urlsafe_base64_image__urlsafe_encoded_real_png__returns_true(self):
"""End-to-end sanity check using a real PNG fixture from constants.py."""
urlsafe = _to_urlsafe(constants.PNG_BASE64)
# The real PNG fixture happens to contain '+' / '/' in its standard
# encoding, so its URL-safe form contains '-' / '_' — exercise that.
assert "-" in urlsafe or "_" in urlsafe
assert base64_normalizer.is_urlsafe_base64_image(urlsafe) is True
def test_is_urlsafe_base64_image__standard_alphabet_image__returns_false(self):
"""Standard-alphabet base64 should short-circuit to False — nothing to rewrite."""
assert base64_normalizer.is_urlsafe_base64_image(constants.PNG_BASE64) is False
def test_is_urlsafe_base64_image__string_below_min_length__returns_false(self):
assert base64_normalizer.is_urlsafe_base64_image("ab-_cd==") is False
def test_is_urlsafe_base64_image__uuid_string__returns_false(self):
"""UUIDs share the URL-safe alphabet and contain '-', but aren't images."""
for _ in range(5):
assert base64_normalizer.is_urlsafe_base64_image(str(uuid.uuid4())) is False
def test_is_urlsafe_base64_image__urlsafe_non_image_binary__returns_false(self):
"""Non-image binary in URL-safe base64 (e.g. PDF) must be left alone."""
urlsafe_pdf = _to_urlsafe(constants.PDF_BASE64)
assert base64_normalizer.is_urlsafe_base64_image(urlsafe_pdf) is False
def test_is_urlsafe_base64_image__plain_text_with_dashes__returns_false(self):
text = "hello-world this is not base64-encoded at all"
assert base64_normalizer.is_urlsafe_base64_image(text) is False
def test_is_urlsafe_base64_image__non_base64_chars__returns_false(self):
# Contains '$' which is not in either alphabet
assert (
base64_normalizer.is_urlsafe_base64_image("iVBORw0KGgo$$$abcdefgh") is False
)
def test_is_urlsafe_base64_image__empty_string__returns_false(self):
assert base64_normalizer.is_urlsafe_base64_image("") is False
# ---------------------------------------------------------------------------
# normalize_urlsafe_base64_images_in_place
# ---------------------------------------------------------------------------
class TestNormalizeUrlsafeBase64ImagesInPlace:
def test_normalize_urlsafe_base64_images_in_place__top_level_dict_with_image__image_rewritten(
self,
):
urlsafe = _to_urlsafe(constants.PNG_BASE64)
payload = {"data": urlsafe}
base64_normalizer.normalize_urlsafe_base64_images_in_place(payload)
assert "-" not in payload["data"]
assert "_" not in payload["data"]
# Round-trips back to the original PNG bytes
original_bytes = base64.b64decode(constants.PNG_BASE64)
assert base64.b64decode(payload["data"]) == original_bytes
def test_normalize_urlsafe_base64_images_in_place__list_with_image__image_rewritten(
self,
):
urlsafe = _to_urlsafe(constants.JPEG_BASE64)
payload = ["leading text", urlsafe]
base64_normalizer.normalize_urlsafe_base64_images_in_place(payload)
assert payload[0] == "leading text"
assert "-" not in payload[1] and "_" not in payload[1]
def test_normalize_urlsafe_base64_images_in_place__deeply_nested_image__image_rewritten(
self,
):
urlsafe = _to_urlsafe(constants.PNG_BASE64)
payload = {
"role": "user",
"parts": [
{"text": "hello"},
{"inline_data": {"data": urlsafe, "mime_type": "image/png"}},
],
}
base64_normalizer.normalize_urlsafe_base64_images_in_place(payload)
normalized = payload["parts"][1]["inline_data"]["data"]
assert "-" not in normalized and "_" not in normalized
assert base64.b64decode(normalized) == base64.b64decode(constants.PNG_BASE64)
def test_normalize_urlsafe_base64_images_in_place__uuid_values__left_untouched(
self,
):
uid = str(uuid.uuid4())
payload = {"trace_id": uid, "nested": [{"span_id": uid}]}
base64_normalizer.normalize_urlsafe_base64_images_in_place(payload)
assert payload["trace_id"] == uid
assert payload["nested"][0]["span_id"] == uid
def test_normalize_urlsafe_base64_images_in_place__standard_base64_image__left_untouched(
self,
):
"""If the value is already standard-alphabet base64, it stays byte-for-byte identical."""
payload = {"data": constants.PNG_BASE64}
base64_normalizer.normalize_urlsafe_base64_images_in_place(payload)
assert payload["data"] == constants.PNG_BASE64
def test_normalize_urlsafe_base64_images_in_place__non_image_binary__left_untouched(
self,
):
"""URL-safe non-image binary (e.g. PDF) is not rewritten — only images are."""
urlsafe_pdf = _to_urlsafe(constants.PDF_BASE64)
payload = {"data": urlsafe_pdf}
base64_normalizer.normalize_urlsafe_base64_images_in_place(payload)
assert payload["data"] == urlsafe_pdf
def test_normalize_urlsafe_base64_images_in_place__non_string_leaves__ignored(self):
payload = {"a": 1, "b": None, "c": True, "d": 1.5, "e": [None, 2, False]}
snapshot = {"a": 1, "b": None, "c": True, "d": 1.5, "e": [None, 2, False]}
base64_normalizer.normalize_urlsafe_base64_images_in_place(payload)
assert payload == snapshot
def test_normalize_urlsafe_base64_images_in_place__mutated_payload__returns_none(
self,
):
urlsafe = _to_urlsafe(constants.PNG_BASE64)
payload = {"data": urlsafe}
result = base64_normalizer.normalize_urlsafe_base64_images_in_place(payload)
assert result is None
assert payload["data"] != urlsafe # was mutated
def test_normalize_urlsafe_base64_images_in_place__empty_containers__no_change(
self,
):
empty_dict: dict = {}
empty_list: list = []
base64_normalizer.normalize_urlsafe_base64_images_in_place(empty_dict)
base64_normalizer.normalize_urlsafe_base64_images_in_place(empty_list)
assert empty_dict == {}
assert empty_list == []
def test_normalize_urlsafe_base64_images_in_place__scalar_root__no_raise(self):
"""Scalar roots are valid inputs (no-op) — the walker should not raise."""
base64_normalizer.normalize_urlsafe_base64_images_in_place("plain string")
base64_normalizer.normalize_urlsafe_base64_images_in_place(42)
base64_normalizer.normalize_urlsafe_base64_images_in_place(None)
@@ -0,0 +1,481 @@
import os
import tempfile
from typing import Generator
from unittest import mock
import pytest
from opik.api_objects import attachment
from opik.api_objects.attachment import converters
from opik.message_processing import messages
@pytest.fixture
def original_file() -> Generator[str, None, None]:
"""Create a file with test content and clean up after test."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("test content")
path = f.name
yield path
if os.path.exists(path):
os.unlink(path)
@pytest.mark.parametrize(
"attachment_data,expected",
[
(attachment.Attachment(data="test.png", file_name=None), "image/png"),
(attachment.Attachment(data="test.png", file_name="test.jpg"), "image/jpeg"),
(
attachment.Attachment(
data="test.pdf", file_name=None, content_type="image/jpeg"
),
"image/jpeg",
),
],
)
def test_guess_attachment_type(attachment_data: attachment.Attachment, expected: str):
mimetype = converters.guess_attachment_type(attachment_data)
assert mimetype == expected
def test_attachment_to_message__no_temp_copy(original_file: str):
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
attachment_data = attachment.Attachment(data=original_file, create_temp_copy=False)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
assert message == messages.CreateAttachmentMessage(
file_path=original_file,
file_name=os.path.basename(original_file),
mime_type="text/plain",
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
encoded_url_override="aHR0cHM6Ly9leGFtcGxlLmNvbQ==",
)
def test_attachment_to_message__file_name(original_file: str):
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
attachment_data = attachment.Attachment(
data=original_file, file_name="test.jpg", create_temp_copy=False
)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
assert message == messages.CreateAttachmentMessage(
file_path=original_file,
file_name="test.jpg",
mime_type="image/jpeg",
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
encoded_url_override="aHR0cHM6Ly9leGFtcGxlLmNvbQ==",
)
def test_attachment_to_message__content_type(original_file: str):
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
attachment_data = attachment.Attachment(
data=original_file, content_type="image/jpeg", create_temp_copy=False
)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
assert message == messages.CreateAttachmentMessage(
file_path=original_file,
file_name=os.path.basename(original_file),
mime_type="image/jpeg",
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
encoded_url_override="aHR0cHM6Ly9leGFtcGxlLmNvbQ==",
)
def test_attachment_to_message__create_temp_copy(original_file: str):
"""Test that create_temp_copy creates a temporary file and sets delete_after_upload."""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
attachment_data = attachment.Attachment(
data=original_file,
file_name="test.txt",
content_type="text/plain",
create_temp_copy=True,
)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
# The file_path should be different from the original (it's a temp copy)
assert message.file_path != original_file
assert message.file_path.endswith(".txt")
assert os.path.exists(message.file_path)
# delete_after_upload should be True when create_temp_copy is used
assert message.delete_after_upload is True
# Other fields should be preserved
assert message.file_name == "test.txt"
assert message.mime_type == "text/plain"
assert message.entity_type == "trace"
assert message.entity_id == entity_id
assert message.project_name == project_name
# Verify the temp file has the same content
with open(message.file_path, "r") as f:
assert f.read() == "test content"
# Clean up the temp copy
os.unlink(message.file_path)
def test_attachment_to_message__create_temp_copy_fails_on_open__uses_original_file_and_does_not_delete(
original_file: str,
):
"""
Test that when create_temp_copy is True but opening the source file fails,
the original file is used and delete_after_upload is False.
"""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
attachment_data = attachment.Attachment(
data=original_file,
file_name="test.txt",
content_type="text/plain",
create_temp_copy=True,
)
# Patch open to fail when trying to read the source file for copying
original_open = open
def mock_open_fail_on_rb(file, mode="r", *args, **kwargs):
if mode == "rb" and file == original_file:
raise PermissionError("Cannot read source file")
return original_open(file, mode, *args, **kwargs)
with mock.patch("builtins.open", side_effect=mock_open_fail_on_rb):
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
# The file_path should be the original file (fallback behavior)
assert message.file_path == original_file
# delete_after_upload should be False to prevent deleting the original file
assert message.delete_after_upload is False
# Other fields should be preserved
assert message.file_name == "test.txt"
assert message.mime_type == "text/plain"
assert message.entity_type == "trace"
assert message.entity_id == entity_id
assert message.project_name == project_name
# Original file should still exist
assert os.path.exists(original_file)
# Clean up the temp copy
os.unlink(message.file_path)
def test_attachment_to_message__create_temp_copy_fails_with_delete_after_upload_true__still_does_not_delete(
original_file: str,
):
"""
Test that when create_temp_copy fails and delete_after_upload was explicitly True,
delete_after_upload is still set to False to protect the original file.
"""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
attachment_data = attachment.Attachment(
data=original_file,
file_name="test.txt",
content_type="text/plain",
create_temp_copy=True,
)
with mock.patch("shutil.copyfileobj", side_effect=IOError("Disk full")):
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
delete_after_upload=True, # Explicitly set to True
)
# Even though delete_after_upload was True, it should be False after failure
assert message.delete_after_upload is False
assert message.file_path == original_file
def test_attachment_to_message__bytes_data():
"""Test that bytes data is written to a temp file and marked for deletion."""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
data = b"binary content here"
attachment_data = attachment.Attachment(
data=data,
file_name="test.bin",
content_type="application/octet-stream",
)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
# A temp file should be created
assert os.path.exists(message.file_path)
assert message.file_path != "test.bin"
# delete_after_upload should be True for bytes data
assert message.delete_after_upload is True
# Verify file content matches the bytes
with open(message.file_path, "rb") as f:
assert f.read() == data
# Other fields should be preserved
assert message.file_name == "test.bin"
assert message.mime_type == "application/octet-stream"
assert message.entity_type == "trace"
assert message.entity_id == entity_id
assert message.project_name == project_name
assert message.encoded_url_override == "aHR0cHM6Ly9leGFtcGxlLmNvbQ=="
# Clean up
os.unlink(message.file_path)
def test_attachment_to_message__bytes_data_without_file_name():
"""Test bytes data without file_name uses temp file basename."""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
data = b"some binary data"
attachment_data = attachment.Attachment(
data=data,
content_type="image/png",
)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="span",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
# file_name should be the basename of the temp file
assert message.file_name == os.path.basename(message.file_path)
assert message.delete_after_upload is True
assert message.mime_type == "image/png"
assert message.entity_type == "span"
# Clean up
os.unlink(message.file_path)
def test_attachment_to_message__bytes_data_infers_mime_type_from_file_name():
"""Test that a mime type is inferred from file_name when data is bytes."""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
data = b"\x00\x01\x02\x03"
attachment_data = attachment.Attachment(
data=data,
file_name="image.png",
)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
assert message.file_name == "image.png"
assert message.mime_type == "image/png"
assert message.delete_after_upload is True
# Clean up
os.unlink(message.file_path)
def test_attachment_to_message__bytes_data_default_mime_type():
"""Test that bytes data without file_name or content_type use a binary mime type."""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
data = b"\x00\x01\x02\x03"
attachment_data = attachment.Attachment(
data=data,
)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
# Should use default binary mime type
assert message.mime_type == "application/octet-stream"
assert message.delete_after_upload is True
# Clean up
os.unlink(message.file_path)
def test_attachment_to_message__bytes_data_write_fails__error_reraised():
"""Test behavior when _write_file_like_to_temp_file fails and error reraised."""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
data = b"test data"
attachment_data = attachment.Attachment(
data=data,
file_name="test.bin",
content_type="application/octet-stream",
)
with mock.patch(
"opik.api_objects.attachment.converters._write_file_like_to_temp_file",
side_effect=OSError("Disk full"),
):
with pytest.raises(OSError):
converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
def test_attachment_to_message__base64_string(request):
"""Test that base64-encoded string is decoded and written to temp file."""
import base64
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
original_content = b"This is test content for base64 encoding"
base64_string = base64.b64encode(original_content).decode("utf-8")
attachment_data = attachment.Attachment(
data=base64_string,
file_name="test.txt",
content_type="text/plain",
)
message = converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
request.addfinalizer(
lambda: os.path.exists(message.file_path) and os.unlink(message.file_path)
)
# A temp file should be created
assert os.path.exists(message.file_path)
assert message.file_path != base64_string
# delete_after_upload should be True for base64 data
assert message.delete_after_upload is True
# Verify file content matches the decoded bytes
with open(message.file_path, "rb") as f:
assert f.read() == original_content
# Other fields should be preserved
assert message.file_name == "test.txt"
assert message.mime_type == "text/plain"
assert message.entity_type == "trace"
assert message.entity_id == entity_id
assert message.project_name == project_name
def test_attachment_to_message__invalid_data_raises_error():
"""Test that invalid attachment data (not bytes, file, or base64) raises ValueError."""
url_override = "https://example.com"
entity_id = "123"
project_name = "test-project"
invalid_data = "not-a-file-and-not-base64!@#$%"
attachment_data = attachment.Attachment(
data=invalid_data,
file_name="test.txt",
)
with pytest.raises(
ValueError,
match="Attachment data must be bytes, an existing file path, or a valid base64-encoded string",
):
converters.attachment_to_message(
attachment_data=attachment_data,
entity_type="trace",
entity_id=entity_id,
project_name=project_name,
url_override=url_override,
)
@@ -0,0 +1,286 @@
import base64
import os
import re
import pytest
from opik.api_objects.attachment import attachment, decoder_base64
from opik.api_objects.attachment import decoder_helpers
from . import constants
@pytest.fixture
def decoder():
"""Create a Base64AttachmentDecoder instance."""
return decoder_base64.Base64AttachmentDecoder()
def test_decode_png_success(decoder, files_to_remove):
"""Test successful decoding of PNG image."""
result = decoder.decode(constants.PNG_BASE64, context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert isinstance(result, attachment.Attachment)
assert result.content_type == "image/png"
assert result.file_name.startswith("input-attachment-")
assert result.file_name.endswith(".png")
assert os.path.exists(result.data)
def test_decode_jpeg_success(decoder, files_to_remove):
"""Test successful decoding of JPEG image."""
result = decoder.decode(constants.JPEG_BASE64, context="output")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert isinstance(result, attachment.Attachment)
assert result.content_type == "image/jpeg"
assert result.file_name.startswith("output-attachment-")
assert result.file_name.endswith(".jpg") or result.file_name.endswith(".jpeg")
assert os.path.exists(result.data)
def test_decode_pdf_success(decoder, files_to_remove):
"""Test successful decoding of PDF document."""
result = decoder.decode(constants.PDF_BASE64, context="metadata")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert isinstance(result, attachment.Attachment)
assert result.content_type == "application/pdf"
assert result.file_name.startswith("metadata-attachment-")
assert result.file_name.endswith(".pdf")
assert os.path.exists(result.data)
def test_decode_gif_success(decoder, files_to_remove):
"""Test successful decoding of GIF image."""
result = decoder.decode(constants.GIF89_BASE64, context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert isinstance(result, attachment.Attachment)
assert result.content_type == "image/gif"
assert result.file_name.endswith(".gif")
assert os.path.exists(result.data)
def test_decode_json_success(decoder, files_to_remove):
"""Test successful decoding of JSON data."""
result = decoder.decode(constants.JSON_BASE64, context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert isinstance(result, attachment.Attachment)
assert result.content_type == "application/json"
assert result.file_name.endswith(".json")
assert os.path.exists(result.data)
def test_decode_invalid_base64(decoder):
"""Test that an invalid base64 string returns None."""
invalid_base64 = "this is not valid base64!@#$%"
result = decoder.decode(invalid_base64, context="input")
assert result is None
def test_decode_non_string_input(decoder):
"""Test that non-string input returns None."""
result = decoder.decode(12345, context="input") # type: ignore
assert result is None
def test_decode_dict_input(decoder):
"""Test that dict input returns None."""
result = decoder.decode({"key": "value"}, context="input") # type: ignore
assert result is None
def test_decode_none_input(decoder):
"""Test that None input returns None."""
result = decoder.decode(None, context="input") # type: ignore
assert result is None
def test_decode_empty_string(decoder):
"""Test that empty string returns None."""
result = decoder.decode("", context="input")
assert result is None
def test_decode_octet_stream_returns_none(decoder):
"""Test that unrecognizable binary data (octet-stream) returns None."""
# Random binary data that won't match any known format
result = decoder.decode(constants.RANDOM_BINARY_BASE64, context="input")
assert result is None
def test_decode_plain_text_returns_none(decoder):
"""Test that plain text returns None."""
result = decoder.decode(constants.PLAIN_TEXT_BASE64, context="input")
assert result is None
def test_decode_creates_temp_file_with_correct_extension(decoder, files_to_remove):
"""Test that temporary file is created with the correct extension."""
result = decoder.decode(constants.PNG_BASE64, context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
# Check that the temp file path ends with .png (suffix parameter)
assert result.data.endswith("png")
assert os.path.exists(result.data)
# Verify file content
with open(result.data, "rb") as f:
content = f.read()
assert content[:8] == b"\x89PNG\r\n\x1a\n"
def test_decode_attachment_properties(decoder, files_to_remove):
"""Test that Attachment object has all required properties."""
result = decoder.decode(constants.PNG_BASE64, context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert hasattr(result, "data")
assert hasattr(result, "file_name")
assert hasattr(result, "content_type")
assert result.data is not None
assert result.file_name is not None
assert result.content_type is not None
def test_decode_filename_format(decoder, files_to_remove):
"""Test that the filename follows the expected format."""
result = decoder.decode(constants.PNG_BASE64, context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
# check that the filename matches a backend pattern
pattern = re.compile(decoder_helpers.ATTACHMENT_FILE_NAME_REGEX)
assert bool(pattern.fullmatch(result.file_name)) is True
def test_decode_different_contexts(decoder, files_to_remove):
"""Test that different contexts are reflected in the filename."""
contexts = ["input", "output", "metadata"]
results = []
for ctx in contexts:
result = decoder.decode(constants.PNG_BASE64, context=ctx)
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert result.file_name.startswith(f"{ctx}-attachment-")
results.append(result)
def test_decode_multiple_calls_create_unique_files(decoder, files_to_remove):
"""Test that multiple decode calls create unique filenames."""
result1 = decoder.decode(constants.PNG_BASE64, context="input")
result2 = decoder.decode(constants.PNG_BASE64, context="input")
assert result1 is not None
assert result2 is not None
# Register for cleanup
files_to_remove.append(result1.data)
files_to_remove.append(result2.data)
assert result1.file_name != result2.file_name
assert result1.data != result2.data
assert os.path.exists(result1.data)
assert os.path.exists(result2.data)
def test_decode_webp_success(decoder, files_to_remove):
"""Test successful decoding of WebP image."""
result = decoder.decode(constants.WEBP_BASE64, context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert result.content_type == "image/webp"
assert result.file_name.endswith(".webp")
def test_decode_svg_success(decoder, files_to_remove):
"""Test successful decoding of SVG image."""
result = decoder.decode(constants.SVG_BASE64, context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert result.content_type == "image/svg+xml"
assert result.file_name.endswith(".svg")
def test_decode_short_base64_data(decoder):
"""Test that very short base64 (< 4 bytes decoded) returns None."""
# Encode just 2 bytes
short_data = b"ab"
short_base64 = base64.b64encode(short_data).decode("utf-8")
result = decoder.decode(short_base64, context="input")
assert result is None
def test_decode_base64_with_whitespace(decoder, files_to_remove):
"""Test that base64 with whitespace is handled correctly."""
png_base64 = constants.PNG_BASE64
# Add whitespace
png_base64_with_whitespace = f" {png_base64} \n"
result = decoder.decode(png_base64_with_whitespace.strip(), context="input")
assert result is not None
# Register for cleanup
files_to_remove.append(result.data)
assert result.content_type == "image/png"
def test_decode_corrupted_base64_padding(decoder, files_to_remove):
"""Test base64 with incorrect padding.
Note: Python's base64 decoder is lenient and may still decode
strings with missing padding, so this test verifies proper handling.
"""
# Valid base64 but with missing padding
corrupted_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ" # Missing padding
result = decoder.decode(corrupted_base64, context="input")
# Python's base64 decoder is lenient, so this might still succeed
# If it succeeds, verify the result is valid and clean up
if result is not None:
files_to_remove.append(result.data)
@@ -0,0 +1,221 @@
import base64
from opik.api_objects.attachment import decoder_helpers
from . import constants
class TestDetectMimeType:
"""Test suite for detect_mime_type() function."""
def test_detect_png(self):
"""Test PNG image detection using magic bytes."""
# PNG magic bytes: 89 50 4E 47 0D 0A 1A 0A
png_data = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
assert decoder_helpers.detect_mime_type(png_data) == "image/png"
def test_detect_jpeg(self):
"""Test JPEG image detection using magic bytes."""
# Real JPEG SOI (FFD8) followed by an APP0/JFIF marker (FFE0), then
# body bytes, then FFD9 end marker.
jpeg_data = b"\xff\xd8\xff\xe0" + b"\x00" * 100 + b"\xff\xd9"
assert decoder_helpers.detect_mime_type(jpeg_data) == "image/jpeg"
def test_detect_jpeg_without_end_marker(self):
"""Test JPEG without proper end marker should not be detected as JPEG."""
# JPEG start but no proper end
jpeg_data = b"\xff\xd8" + b"\x00" * 100
assert decoder_helpers.detect_mime_type(jpeg_data) != "image/jpeg"
def test_detect_gif87a(self):
"""Test GIF87a format detection."""
gif_data = b"GIF87a" + b"\x00" * 100
assert decoder_helpers.detect_mime_type(gif_data) == "image/gif"
def test_detect_gif89a(self):
"""Test GIF89a format detection."""
assert decoder_helpers.detect_mime_type(constants.GIF89_BYTES) == "image/gif"
def test_detect_pdf(self):
"""Test PDF document detection."""
assert (
decoder_helpers.detect_mime_type(constants.PDF_BYTES) == "application/pdf"
)
def test_detect_webp(self):
"""Test WebP image detection."""
assert decoder_helpers.detect_mime_type(constants.WEBP_BYTES) == "image/webp"
def test_detect_svg(self):
"""Test SVG image detection."""
assert decoder_helpers.detect_mime_type(constants.SVG_BYTES) == "image/svg+xml"
def test_detect_svg_with_uppercase(self):
"""Test SVG detection with an uppercase tag."""
svg_data = b"<SVG>content</SVG>"
assert decoder_helpers.detect_mime_type(svg_data) == "image/svg+xml"
def test_detect_svg_with_mixed_case(self):
"""Test SVG detection with mixed case tag."""
svg_data = b"<SvG>content</SvG>"
assert decoder_helpers.detect_mime_type(svg_data) == "image/svg+xml"
def test_detect_mp4(self):
"""Test MP4 video detection."""
# MP4: ftyp at offset 4
mp4_data = b"\x00\x00\x00\x20ftypisom" + b"\x00" * 100
assert decoder_helpers.detect_mime_type(mp4_data) == "video/mp4"
def test_detect_json_object(self):
"""Test JSON object detection."""
json_data = b'{"key": "value", "number": 123}'
assert decoder_helpers.detect_mime_type(json_data) == "application/json"
def test_detect_json_array(self):
"""Test JSON array detection."""
json_data = b'["item1", "item2", "item3"]'
assert decoder_helpers.detect_mime_type(json_data) == "application/json"
def test_detect_json_with_whitespace(self):
"""Test JSON detection with leading whitespace."""
json_data = b' \n\t{"key": "value"}'
assert decoder_helpers.detect_mime_type(json_data) == "application/json"
def test_detect_invalid_json_like(self):
"""Test that invalid JSON-like content is not detected as JSON."""
# UTF-8 decoding should fail
invalid_data = b"\xff\xfe{invalid}"
assert (
decoder_helpers.detect_mime_type(invalid_data) == "application/octet-stream"
)
def test_detect_short_data(self):
"""Test that data shorter than 4 bytes returns octet-stream."""
short_data = b"abc"
assert (
decoder_helpers.detect_mime_type(short_data) == "application/octet-stream"
)
def test_detect_empty_data(self):
"""Test that empty data returns octet-stream."""
empty_data = b""
assert (
decoder_helpers.detect_mime_type(empty_data) == "application/octet-stream"
)
def test_detect_unknown_binary(self):
"""Test that unknown binary data returns octet-stream."""
assert (
decoder_helpers.detect_mime_type(constants.RANDOM_BINARY_BYTES)
== "application/octet-stream"
)
def test_detect_plain_text_not_json(self):
"""Test that plain text (non-JSON) returns octet-stream."""
assert (
decoder_helpers.detect_mime_type(constants.PLAIN_TEXT_BYTES)
== "application/octet-stream"
)
def test_real_png_base64(self):
"""Test with a real minimal PNG image (1x1 transparent pixel)."""
# 1x1 transparent PNG
png_data = base64.b64decode(constants.PNG_BASE64)
assert decoder_helpers.detect_mime_type(png_data) == "image/png"
def test_real_jpeg_base64(self):
"""Test with a real minimal JPEG image."""
# Minimal valid JPEG (1x1 red pixel)
jpeg_data = base64.b64decode(constants.JPEG_BASE64)
assert decoder_helpers.detect_mime_type(jpeg_data) == "image/jpeg"
def test_detect_svg_with_long_content(self):
"""Test SVG detection with content longer than 1024 bytes."""
# Create SVG content longer than 1024 bytes
long_content = "x" * 2000
svg_data = (
f'<svg xmlns="http://www.w3.org/2000/svg">{long_content}</svg>'.encode(
"utf-8"
)
)
assert decoder_helpers.detect_mime_type(svg_data) == "image/svg+xml"
def test_detect_webp_invalid(self):
"""Test that RIFF without WEBP marker is not detected as WebP."""
# RIFF but not WEBP
riff_data = b"RIFF\x00\x00\x00\x00XXXX" + b"\x00" * 100
assert decoder_helpers.detect_mime_type(riff_data) != "image/webp"
def test_detect_mp4_short_data(self):
"""Test that data too short for MP4 detection is not detected as MP4."""
short_mp4 = b"\x00\x00\x00\x20ftyp" # Only 9 bytes
assert decoder_helpers.detect_mime_type(short_mp4) != "video/mp4"
class TestGetFileExtension:
"""Test suite for get_file_extension() function."""
def test_get_extension_png(self):
"""Test PNG extension extraction."""
assert decoder_helpers.get_file_extension("image/png") == "png"
def test_get_extension_jpeg(self):
"""Test JPEG extension extraction (should return 'jpg' not 'jpe')."""
result = decoder_helpers.get_file_extension("image/jpeg")
# The function should return 'jpg' for image/jpeg
assert result in ("jpg", "jpeg")
def test_get_extension_pdf(self):
"""Test PDF extension extraction."""
assert decoder_helpers.get_file_extension("application/pdf") == "pdf"
def test_get_extension_gif(self):
"""Test GIF extension extraction."""
assert decoder_helpers.get_file_extension("image/gif") == "gif"
def test_get_extension_svg(self):
"""Test SVG extension extraction from 'image/svg+xml'."""
assert decoder_helpers.get_file_extension("image/svg+xml") == "svg"
def test_get_extension_webp(self):
"""Test WebP extension extraction."""
assert decoder_helpers.get_file_extension("image/webp") == "webp"
def test_get_extension_mp4(self):
"""Test MP4 extension extraction."""
assert decoder_helpers.get_file_extension("video/mp4") == "mp4"
def test_get_extension_json(self):
"""Test JSON extension extraction."""
assert decoder_helpers.get_file_extension("application/json") == "json"
def test_get_extension_with_parameters(self):
"""Test extension extraction with MIME type parameters."""
assert decoder_helpers.get_file_extension("image/jpeg; charset=utf-8") == "jpeg"
def test_get_extension_empty_string(self):
"""Test that an empty MIME type returns 'bin'."""
assert decoder_helpers.get_file_extension("") == "bin"
def test_get_extension_none(self):
"""Test that a None MIME type returns 'bin'."""
# This will fail the "if not mime_type:" check
result = decoder_helpers.get_file_extension("")
assert result == "bin"
def test_get_extension_invalid_format(self):
"""Test that an invalid MIME type format returns 'bin'."""
assert decoder_helpers.get_file_extension("invalidmimetype") == "bin"
def test_get_extension_unknown_type(self):
"""Test that an unknown MIME type extracts subtype."""
assert (
decoder_helpers.get_file_extension("application/x-custom-type")
== "x-custom-type"
)
def test_get_extension_removes_leading_dot(self):
"""Test that leading dots are removed from extensions."""
# mimetypes.guess_extension can return extensions with leading dots
result = decoder_helpers.get_file_extension("text/plain")
assert not result.startswith(".")