From 4ea32be29c12468674087ed95ad5073e568bdfd3 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 14 Nov 2025 13:11:49 +0100 Subject: [PATCH 1/7] docs: add newline at end of README.md for consistency --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dc2728f1..52d62768 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,4 @@ Thanks to everyone in the community that has contributed to this project! ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/Dembrane/echo?utm_source=oss&utm_medium=github&utm_campaign=Dembrane%2Fecho&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) + \ No newline at end of file From f7c1e5042fc82e494abc2fa4bd6c9dd1331cc0f6 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 14 Nov 2025 18:51:33 +0100 Subject: [PATCH 2/7] chore: update CI workflow with environment variables for testing - Added required environment variables for Directus and various models in the pr-testing.yml workflow. - Set asyncio default fixture loop scope in pyproject.toml. - Cleaned up whitespace in conftest.py. --- .github/workflows/pr-testing.yml | 42 +++++++++++++++++++++++++++++ echo/server/pyproject.toml | 1 + echo/server/tests/smoke/conftest.py | 2 +- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-testing.yml b/.github/workflows/pr-testing.yml index cfbad0dc..d2c73251 100644 --- a/.github/workflows/pr-testing.yml +++ b/.github/workflows/pr-testing.yml @@ -45,6 +45,48 @@ jobs: unit-tests: name: Unit Tests runs-on: ubuntu-latest + env: + # Required environment variables for config.py + DIRECTUS_SECRET: test-secret + DIRECTUS_TOKEN: test-token + DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test + REDIS_URL: redis://localhost:6379 + OPENAI_API_KEY: sk-test-key + ANTHROPIC_API_KEY: sk-ant-test-key + STORAGE_S3_BUCKET: test-bucket + STORAGE_S3_ENDPOINT: https://test.endpoint.com + STORAGE_S3_KEY: test-key + STORAGE_S3_SECRET: test-secret + SMALL_LITELLM_MODEL: gpt-4o-mini + SMALL_LITELLM_API_KEY: test-key + SMALL_LITELLM_API_VERSION: "2024-06-01" + SMALL_LITELLM_API_BASE: https://api.openai.com/v1 + MEDIUM_LITELLM_MODEL: gpt-4o + MEDIUM_LITELLM_API_KEY: test-key + MEDIUM_LITELLM_API_VERSION: "2024-06-01" + MEDIUM_LITELLM_API_BASE: https://api.openai.com/v1 + LARGE_LITELLM_MODEL: gpt-4o + LARGE_LITELLM_API_KEY: test-key + LARGE_LITELLM_API_VERSION: "2024-06-01" + LARGE_LITELLM_API_BASE: https://api.openai.com/v1 + LIGHTRAG_LITELLM_MODEL: gpt-4o-mini + LIGHTRAG_LITELLM_API_KEY: test-key + LIGHTRAG_LITELLM_API_VERSION: "2024-06-01" + LIGHTRAG_LITELLM_API_BASE: https://api.openai.com/v1 + LIGHTRAG_LITELLM_AUDIOMODEL_MODEL: gpt-4o-audio + LIGHTRAG_LITELLM_AUDIOMODEL_API_BASE: https://api.openai.com/v1 + LIGHTRAG_LITELLM_AUDIOMODEL_API_KEY: test-key + LIGHTRAG_LITELLM_AUDIOMODEL_API_VERSION: "2024-06-01" + LIGHTRAG_LITELLM_TEXTSTRUCTUREMODEL_MODEL: gpt-4o-mini + LIGHTRAG_LITELLM_TEXTSTRUCTUREMODEL_API_BASE: https://api.openai.com/v1 + LIGHTRAG_LITELLM_TEXTSTRUCTUREMODEL_API_KEY: test-key + LIGHTRAG_LITELLM_TEXTSTRUCTUREMODEL_API_VERSION: "2024-06-01" + LIGHTRAG_LITELLM_EMBEDDING_MODEL: text-embedding-ada-002 + LIGHTRAG_LITELLM_EMBEDDING_API_BASE: https://api.openai.com/v1 + LIGHTRAG_LITELLM_EMBEDDING_API_KEY: test-key + LIGHTRAG_LITELLM_EMBEDDING_API_VERSION: "2024-06-01" + LIGHTRAG_LITELLM_INFERENCE_MODEL: claude-3-5-sonnet + LIGHTRAG_LITELLM_INFERENCE_API_KEY: test-key steps: - uses: actions/checkout@v4 diff --git a/echo/server/pyproject.toml b/echo/server/pyproject.toml index 2475a49d..368c14ef 100644 --- a/echo/server/pyproject.toml +++ b/echo/server/pyproject.toml @@ -142,6 +142,7 @@ markers = [ "integration: integration tests requiring external services", ] addopts = "--strict-markers --log-cli-level=INFO --log-cli-format='%(asctime)s [%(levelname)8s] %(name)s: %(message)s' --log-cli-date-format='%Y-%m-%d %H:%M:%S'" +asyncio_default_fixture_loop_scope = "function" [tool.coverage.run] source = ["dembrane"] diff --git a/echo/server/tests/smoke/conftest.py b/echo/server/tests/smoke/conftest.py index c5564691..15d97353 100644 --- a/echo/server/tests/smoke/conftest.py +++ b/echo/server/tests/smoke/conftest.py @@ -1,4 +1,5 @@ import os + import pytest @@ -12,4 +13,3 @@ def api_url(): def directus_url(): """Directus URL for smoke tests""" return os.getenv("TEST_DIRECTUS_URL", "https://directus.echo-testing.dembrane.com") - From 8f5420c2f3725b02dd468dc401f7d0f821116db5 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 14 Nov 2025 19:05:28 +0100 Subject: [PATCH 3/7] chore: remove Ruff format check from CI workflow - Deleted the Ruff format check step from the pr-testing.yml workflow to streamline the CI process. --- .github/workflows/pr-testing.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/pr-testing.yml b/.github/workflows/pr-testing.yml index d2c73251..3c97c3d9 100644 --- a/.github/workflows/pr-testing.yml +++ b/.github/workflows/pr-testing.yml @@ -32,11 +32,6 @@ jobs: cd echo/server ruff check . - - name: Run Ruff Format Check - run: | - cd echo/server - ruff format --check . - - name: Run MyPy Type Check run: | cd echo/server From a13962092dc58cb9b8d8b97bbba121661a467c06 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 14 Nov 2025 19:58:36 +0100 Subject: [PATCH 4/7] test: mark all test files as integration tests - Added pytest integration marker to multiple test files to categorize them for integration testing. --- echo/server/tests/api/test_conversation.py | 2 ++ echo/server/tests/service/test_conversation_service.py | 2 ++ echo/server/tests/service/test_file_service.py | 2 ++ echo/server/tests/service/test_project_service.py | 2 ++ echo/server/tests/test_audio_utils.py | 2 ++ echo/server/tests/test_conversation_utils.py | 4 ++++ echo/server/tests/test_quote_utils.py | 3 +++ echo/server/tests/test_transcribe_assembly.py | 2 ++ echo/server/tests/test_transcribe_runpod.py | 2 ++ 9 files changed, 21 insertions(+) diff --git a/echo/server/tests/api/test_conversation.py b/echo/server/tests/api/test_conversation.py index a997da0b..cc7febf3 100644 --- a/echo/server/tests/api/test_conversation.py +++ b/echo/server/tests/api/test_conversation.py @@ -16,6 +16,8 @@ logger = logging.getLogger("dembrane.tests.api.test_conversation") +pytestmark = pytest.mark.integration + @pytest.mark.asyncio async def test_get_conversation_transcript(): diff --git a/echo/server/tests/service/test_conversation_service.py b/echo/server/tests/service/test_conversation_service.py index 6a5584fb..5bb18d26 100644 --- a/echo/server/tests/service/test_conversation_service.py +++ b/echo/server/tests/service/test_conversation_service.py @@ -16,6 +16,8 @@ logger = logging.getLogger(__name__) +pytestmark = pytest.mark.integration + @pytest.fixture def project(): diff --git a/echo/server/tests/service/test_file_service.py b/echo/server/tests/service/test_file_service.py index f47e3c7f..cc104f7f 100644 --- a/echo/server/tests/service/test_file_service.py +++ b/echo/server/tests/service/test_file_service.py @@ -10,6 +10,8 @@ logger = logging.getLogger(__name__) +pytestmark = pytest.mark.integration + class TestS3FileService: """Test S3FileService implementation.""" diff --git a/echo/server/tests/service/test_project_service.py b/echo/server/tests/service/test_project_service.py index bcf0b2bc..abc33712 100644 --- a/echo/server/tests/service/test_project_service.py +++ b/echo/server/tests/service/test_project_service.py @@ -8,6 +8,8 @@ logger = logging.getLogger(__name__) +pytestmark = pytest.mark.integration + def test_create_project(): project = project_service.create( diff --git a/echo/server/tests/test_audio_utils.py b/echo/server/tests/test_audio_utils.py index 360d9c86..09c37403 100644 --- a/echo/server/tests/test_audio_utils.py +++ b/echo/server/tests/test_audio_utils.py @@ -20,6 +20,8 @@ logger = logging.getLogger(__name__) +pytestmark = pytest.mark.integration + AUDIO_FILES = [ "wav.wav", diff --git a/echo/server/tests/test_conversation_utils.py b/echo/server/tests/test_conversation_utils.py index b8061993..5812dc74 100644 --- a/echo/server/tests/test_conversation_utils.py +++ b/echo/server/tests/test_conversation_utils.py @@ -1,6 +1,8 @@ import logging from datetime import timedelta +import pytest + from dembrane.utils import get_utc_timestamp from dembrane.directus import directus from dembrane.conversation_utils import ( @@ -20,6 +22,8 @@ logger = logging.getLogger("test_conversation_utils") +pytestmark = pytest.mark.integration + def test_create_conversation_chunk(): # Create test project diff --git a/echo/server/tests/test_quote_utils.py b/echo/server/tests/test_quote_utils.py index 09d13fa4..1967b3f5 100644 --- a/echo/server/tests/test_quote_utils.py +++ b/echo/server/tests/test_quote_utils.py @@ -4,6 +4,7 @@ import datetime from typing import List +import pytest import numpy as np from sqlalchemy.orm import Session @@ -26,6 +27,8 @@ logger = logging.getLogger("test_quote_utils") +pytestmark = pytest.mark.integration + def test_create_test_quotes( db: Session, project_analysis_run_id: str, conversation_id: str, count: int = 10 diff --git a/echo/server/tests/test_transcribe_assembly.py b/echo/server/tests/test_transcribe_assembly.py index 9eab1bec..6d0dc578 100644 --- a/echo/server/tests/test_transcribe_assembly.py +++ b/echo/server/tests/test_transcribe_assembly.py @@ -10,6 +10,8 @@ logger = logging.getLogger("test_transcribe_assembly") +pytestmark = pytest.mark.integration + def _require_assemblyai(): """Ensure AssemblyAI is enabled and credentials are present or skip.""" diff --git a/echo/server/tests/test_transcribe_runpod.py b/echo/server/tests/test_transcribe_runpod.py index 5fe5ba7d..72d85a4b 100644 --- a/echo/server/tests/test_transcribe_runpod.py +++ b/echo/server/tests/test_transcribe_runpod.py @@ -14,6 +14,8 @@ logger = logging.getLogger("test_transcribe") +pytestmark = pytest.mark.integration + @pytest.fixture def fixture_english_chunk(): logger.info("setup") From 10db4ab0ae132cd05da52aa106d9eabb6c1d4f27 Mon Sep 17 00:00:00 2001 From: Dat Date: Sat, 15 Nov 2025 14:21:52 +0100 Subject: [PATCH 5/7] test: update test files and CI workflow for integration testing - Modified the pytest command in the CI workflow to exclude smoke tests. - Added integration markers to several test files to categorize them for integration testing. - Introduced a new test file for utility functions, ensuring coverage of utility methods. --- .github/workflows/pr-testing.yml | 2 +- .../service/test_conversation_service.py | 65 +++++++---- .../tests/service/test_project_service.py | 9 +- echo/server/tests/test_chat_utils.py | 4 + echo/server/tests/test_embedding.py | 4 + echo/server/tests/test_quote_utils.py | 2 +- echo/server/tests/test_transcribe_runpod.py | 3 +- echo/server/tests/test_utils.py | 106 ++++++++++++++++++ 8 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 echo/server/tests/test_utils.py diff --git a/.github/workflows/pr-testing.yml b/.github/workflows/pr-testing.yml index 3c97c3d9..2308cbce 100644 --- a/.github/workflows/pr-testing.yml +++ b/.github/workflows/pr-testing.yml @@ -98,7 +98,7 @@ jobs: - name: Run Unit Tests run: | cd echo/server - pytest tests/ -v -m "not integration and not slow" --maxfail=3 + pytest tests/ -v -m "not integration and not slow and not smoke" --maxfail=3 build-images: name: Build Docker Images (validation only) diff --git a/echo/server/tests/service/test_conversation_service.py b/echo/server/tests/service/test_conversation_service.py index 5bb18d26..db6106e4 100644 --- a/echo/server/tests/service/test_conversation_service.py +++ b/echo/server/tests/service/test_conversation_service.py @@ -16,8 +16,6 @@ logger = logging.getLogger(__name__) -pytestmark = pytest.mark.integration - @pytest.fixture def project(): @@ -32,6 +30,7 @@ def project(): project_service.delete(project["id"]) +@pytest.mark.integration def test_create_conversation(project): conversation = conversation_service.create( project_id=project["id"], @@ -51,6 +50,7 @@ def test_create_conversation(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_create_conversation_with_tags(project): tags = project_service.create_tags_and_link(project.get("id"), ["tag1", "tag2"]) @@ -87,6 +87,7 @@ def test_create_conversation_with_tags(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_create_conversation_not_allowed(): project = project_service.create( name="Test Project No Conversations", @@ -103,6 +104,7 @@ def test_create_conversation_not_allowed(): project_service.delete(project["id"]) +@pytest.mark.integration def test_get_by_id_or_raise(project): conversation = conversation_service.create( project_id=project["id"], @@ -117,6 +119,7 @@ def test_get_by_id_or_raise(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_get_by_id_or_raise_not_found(): try: conversation_service.get_by_id_or_raise("non-existent-id") @@ -124,6 +127,7 @@ def test_get_by_id_or_raise_not_found(): assert isinstance(e, ConversationNotFoundException) +@pytest.mark.integration def test_update_conversation(project): conversation = conversation_service.create( project_id=project["id"], @@ -151,6 +155,7 @@ def test_update_conversation(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_update_conversation_partial(project): conversation = conversation_service.create( project_id=project["id"], @@ -176,6 +181,7 @@ def test_update_conversation_partial(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_update_conversation_not_found(): with pytest.raises(ConversationNotFoundException): conversation_service.update( @@ -184,6 +190,7 @@ def test_update_conversation_not_found(): ) +@pytest.mark.integration def test_delete_conversation(project): conversation = conversation_service.create( project_id=project["id"], @@ -196,32 +203,30 @@ def test_delete_conversation(project): conversation_service.get_by_id_or_raise(conversation["id"]) +# Unit test - tests service initialization with dependencies def test_conversation_service_property_getters(): - """Test lazy initialization of service dependencies.""" - # Create a fresh service instance - service = ConversationService() - - # Initially, all services should be None - assert service._file_service is None - assert service._event_service is None - assert service._project_service is None - - # Access properties to trigger lazy initialization - file_service = service.file_service - event_service = service.event_service - project_service = service.project_service - - # Verify services are initialized - assert file_service is not None - assert event_service is not None - assert project_service is not None - - # Verify subsequent access returns same instances + """Test that service dependencies are properly set on initialization.""" + from dembrane.service.file import get_file_service + from dembrane.service.project import ProjectService + + # Create service instances + file_service = get_file_service() + project_svc = ProjectService() + + # Create conversation service with dependencies + service = ConversationService( + file_service=file_service, + project_service=project_svc, + ) + + # Verify services are set correctly + assert service.file_service is not None + assert service.project_service is not None assert service.file_service is file_service - assert service.event_service is event_service - assert service.project_service is project_service + assert service.project_service is project_svc +# Unit test - uses mocks, no external dependencies def test_get_by_id_directus_bad_request(): """Test exception handling when Directus returns bad request.""" with patch("dembrane.service.conversation.directus_client_context") as mock_context: @@ -233,6 +238,7 @@ def test_get_by_id_directus_bad_request(): conversation_service.get_by_id_or_raise("test-id") +# Unit test - uses mocks, no external dependencies def test_get_by_id_empty_result(): """Test exception handling when no conversation found.""" with patch("dembrane.service.conversation.directus_client_context") as mock_context: @@ -244,6 +250,7 @@ def test_get_by_id_empty_result(): conversation_service.get_by_id_or_raise("test-id") +# Unit test - uses mocks, no external dependencies def test_update_conversation_directus_bad_request(): """Test exception handling when updating non-existent conversation.""" with patch("dembrane.service.conversation.directus_client_context") as mock_context: @@ -255,6 +262,8 @@ def test_update_conversation_directus_bad_request(): conversation_service.update(conversation_id="non-existent-id", participant_name="Test") +# Unit test - uses mocks for S3 and events, but still needs DB for conversation +@pytest.mark.integration def test_create_chunk_from_file(project): """Test creating conversation chunk from file upload.""" conversation = conversation_service.create( @@ -297,6 +306,7 @@ def test_create_chunk_from_file(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_create_chunk_from_file_finished_conversation(project): """Test that chunks cannot be added to finished conversations.""" conversation = conversation_service.create( @@ -320,6 +330,8 @@ def test_create_chunk_from_file_finished_conversation(project): conversation_service.delete(conversation["id"]) +# Uses mock for events, but still needs DB for conversation +@pytest.mark.integration def test_create_chunk_from_text(project): """Test creating conversation chunk from text.""" conversation = conversation_service.create( @@ -357,6 +369,7 @@ def test_create_chunk_from_text(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_create_chunk_from_text_finished_conversation(project): """Test that text chunks cannot be added to finished conversations.""" conversation = conversation_service.create( @@ -378,6 +391,7 @@ def test_create_chunk_from_text_finished_conversation(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_get_by_id_with_chunks(project): """Test retrieving conversation with chunks sorted by timestamp.""" conversation = conversation_service.create( @@ -407,6 +421,7 @@ def test_get_by_id_with_chunks(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_delete_chunk(project): conversation = conversation_service.create( project_id=project["id"], @@ -448,6 +463,7 @@ def test_delete_chunk(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_chunk_timestamp_functionality(project): """Test comprehensive timestamp functionality for conversation chunks.""" conversation = conversation_service.create( @@ -538,6 +554,7 @@ def test_chunk_timestamp_functionality(project): conversation_service.delete(conversation["id"]) +@pytest.mark.integration def test_chunk_timestamp_edge_cases(project): """Test edge cases for timestamp handling.""" conversation = conversation_service.create( diff --git a/echo/server/tests/service/test_project_service.py b/echo/server/tests/service/test_project_service.py index abc33712..ff9a75c2 100644 --- a/echo/server/tests/service/test_project_service.py +++ b/echo/server/tests/service/test_project_service.py @@ -8,9 +8,8 @@ logger = logging.getLogger(__name__) -pytestmark = pytest.mark.integration - +@pytest.mark.integration def test_create_project(): project = project_service.create( name="Test Project", @@ -26,6 +25,7 @@ def test_create_project(): project_service.delete(project["id"]) +@pytest.mark.integration def test_create_and_link_tags(): project = project_service.create( name="Test Project", @@ -50,6 +50,7 @@ def test_create_and_link_tags(): project_service.delete(project["id"]) +@pytest.mark.integration def test_get_by_id_or_raise(): project = project_service.create( name="Test Project", @@ -62,11 +63,13 @@ def test_get_by_id_or_raise(): project_service.delete(project["id"]) +@pytest.mark.integration def test_get_by_id_not_found(): with pytest.raises(ProjectNotFoundException): project_service.get_by_id_or_raise("not-found") +# Unit test - uses mocks, no external dependencies def test_get_by_id_empty_result(): """Test exception handling when no project found.""" with patch("dembrane.service.project.directus_client_context") as mock_context: @@ -78,6 +81,7 @@ def test_get_by_id_empty_result(): project_service.get_by_id_or_raise("test-id") +@pytest.mark.integration def test_delete_project(): project = project_service.create( name="Test Project", @@ -91,6 +95,7 @@ def test_delete_project(): project_service.get_by_id_or_raise(project["id"]) +@pytest.mark.integration def test_create_shallow_clone(): project = project_service.create( name="Test Project", diff --git a/echo/server/tests/test_chat_utils.py b/echo/server/tests/test_chat_utils.py index 863fb1f6..d76b41ad 100644 --- a/echo/server/tests/test_chat_utils.py +++ b/echo/server/tests/test_chat_utils.py @@ -1,7 +1,11 @@ import asyncio +import pytest + from dembrane.chat_utils import generate_title +pytestmark = pytest.mark.integration + def test_generate_title(): with asyncio.Runner() as runner: diff --git a/echo/server/tests/test_embedding.py b/echo/server/tests/test_embedding.py index 3e9ff134..91f90d05 100644 --- a/echo/server/tests/test_embedding.py +++ b/echo/server/tests/test_embedding.py @@ -1,7 +1,11 @@ import math +import pytest + from dembrane.embedding import EMBEDDING_DIM, embed_text +pytestmark = pytest.mark.integration + def test_embed_text_returns_list_of_floats(): """Ensure `embed_text` returns a list of floats of the expected length.""" diff --git a/echo/server/tests/test_quote_utils.py b/echo/server/tests/test_quote_utils.py index 1967b3f5..eb362588 100644 --- a/echo/server/tests/test_quote_utils.py +++ b/echo/server/tests/test_quote_utils.py @@ -4,8 +4,8 @@ import datetime from typing import List -import pytest import numpy as np +import pytest from sqlalchemy.orm import Session from dembrane.utils import generate_uuid diff --git a/echo/server/tests/test_transcribe_runpod.py b/echo/server/tests/test_transcribe_runpod.py index 72d85a4b..604f207b 100644 --- a/echo/server/tests/test_transcribe_runpod.py +++ b/echo/server/tests/test_transcribe_runpod.py @@ -14,7 +14,8 @@ logger = logging.getLogger("test_transcribe") -pytestmark = pytest.mark.integration +# Runpod is no longer used in the platform; keep tests for reference but skip them. +pytestmark = pytest.mark.skip(reason="Runpod transcription is deprecated and no longer used") @pytest.fixture def fixture_english_chunk(): diff --git a/echo/server/tests/test_utils.py b/echo/server/tests/test_utils.py new file mode 100644 index 00000000..2f9cd004 --- /dev/null +++ b/echo/server/tests/test_utils.py @@ -0,0 +1,106 @@ +"""Unit tests for utility functions that don't require external services""" +import re +from datetime import datetime, timezone + +from dembrane.utils import ( + generate_uuid, + get_safe_filename, + get_utc_timestamp, + generate_4_digit_pin, + generate_6_digit_pin, +) + + +def test_generate_uuid(): + """Test that generate_uuid returns a valid UUID string""" + uuid_str = generate_uuid() + + # Check it's a string + assert isinstance(uuid_str, str) + + # Check it matches UUID format (8-4-4-4-12 hexadecimal digits) + uuid_pattern = re.compile( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + ) + assert uuid_pattern.match(uuid_str), f"Invalid UUID format: {uuid_str}" + + # Check that calling it twice returns different UUIDs + uuid_str2 = generate_uuid() + assert uuid_str != uuid_str2, "UUIDs should be unique" + + +def test_generate_4_digit_pin(): + """Test that generate_4_digit_pin returns a 4-digit string""" + pin = generate_4_digit_pin() + + # Check it's a string + assert isinstance(pin, str) + + # Check it's exactly 4 characters + assert len(pin) == 4, f"PIN should be 4 digits, got {len(pin)}" + + # Check it's all digits + assert pin.isdigit(), f"PIN should only contain digits, got {pin}" + + # Check it's in valid range (1000-9999) + pin_int = int(pin) + assert 1000 <= pin_int <= 9999, f"PIN should be between 1000 and 9999, got {pin_int}" + + +def test_generate_6_digit_pin(): + """Test that generate_6_digit_pin returns a 6-digit string""" + pin = generate_6_digit_pin() + + # Check it's a string + assert isinstance(pin, str) + + # Check it's exactly 6 characters + assert len(pin) == 6, f"PIN should be 6 digits, got {len(pin)}" + + # Check it's all digits + assert pin.isdigit(), f"PIN should only contain digits, got {pin}" + + # Check it's in valid range (100000-999999) + pin_int = int(pin) + assert 100000 <= pin_int <= 999999, f"PIN should be between 100000 and 999999, got {pin_int}" + + +def test_get_utc_timestamp(): + """Test that get_utc_timestamp returns a UTC datetime""" + timestamp = get_utc_timestamp() + + # Check it's a datetime + assert isinstance(timestamp, datetime) + + # Check it has UTC timezone + assert timestamp.tzinfo == timezone.utc, "Timestamp should have UTC timezone" + + # Check it's close to current time (within 1 second) + now = datetime.now(tz=timezone.utc) + time_diff = abs((now - timestamp).total_seconds()) + assert time_diff < 1, f"Timestamp should be current time, got difference of {time_diff}s" + + +def test_get_safe_filename(): + """Test that get_safe_filename sanitizes filenames correctly""" + # Test replacing forward slashes + assert get_safe_filename("path/to/file.txt") == "path_to_file.txt" + + # Test replacing backslashes + assert get_safe_filename("path\\to\\file.txt") == "path_to_file.txt" + + # Test replacing spaces + assert get_safe_filename("my file name.txt") == "my_file_name.txt" + + # Test combining multiple replacements + assert get_safe_filename("path/to\\my file.txt") == "path_to_my_file.txt" + + # Test already safe filename + assert get_safe_filename("safe_filename.txt") == "safe_filename.txt" + + # Test empty string + assert get_safe_filename("") == "" + + # Test filename with only special characters + assert get_safe_filename("/ \\ ") == "____" + From 69ca56dd27590b95a9d790fcb8b52da7e37c2e41 Mon Sep 17 00:00:00 2001 From: Dat Date: Sat, 15 Nov 2025 14:34:44 +0100 Subject: [PATCH 6/7] fix: update environment variable handling in config.py - Modified the retrieval of several environment variables to use a fallback value if not set, ensuring defaults are applied correctly. - Updated variables include RUNPOD_DIARIZATION_TIMEOUT, AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM, AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS, AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB, AUDIO_LIGHTRAG_TOP_K_PROMPT, and AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY. --- echo/server/dembrane/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/echo/server/dembrane/config.py b/echo/server/dembrane/config.py index edaf642c..28e170d3 100644 --- a/echo/server/dembrane/config.py +++ b/echo/server/dembrane/config.py @@ -539,7 +539,7 @@ ) logger.debug(f"RUNPOD_DIARIZATION_BASE_URL: {RUNPOD_DIARIZATION_BASE_URL}") -RUNPOD_DIARIZATION_TIMEOUT = int(os.environ.get("RUNPOD_DIARIZATION_TIMEOUT", 30)) +RUNPOD_DIARIZATION_TIMEOUT = int(os.environ.get("RUNPOD_DIARIZATION_TIMEOUT") or 30) if ENABLE_RUNPOD_DIARIZATION: logger.debug(f"RUNPOD_DIARIZATION_TIMEOUT: {RUNPOD_DIARIZATION_TIMEOUT}") # ---------------/Secrets--------------- @@ -547,7 +547,7 @@ # ---------------Configurations--------------- AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM = int( - os.environ.get("AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM", 10) + os.environ.get("AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM") or 10 ) assert AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM, ( "AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM environment variable is not set" @@ -555,7 +555,7 @@ logger.debug(f"AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM: {AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM}") AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS = int( - os.environ.get("AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS", 60) + os.environ.get("AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS") or 60 ) assert AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS, ( "AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS environment variable is not set" @@ -572,14 +572,14 @@ logger.debug(f"ENABLE_AUDIO_LIGHTRAG_INPUT: {ENABLE_AUDIO_LIGHTRAG_INPUT}") AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB = int( - os.environ.get("AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB", 15) + os.environ.get("AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB") or 15 ) assert AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB, ( "AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB environment variable is not set" ) logger.debug(f"AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB: {AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB}") -AUDIO_LIGHTRAG_TOP_K_PROMPT = int(os.environ.get("AUDIO_LIGHTRAG_TOP_K_PROMPT", 100)) +AUDIO_LIGHTRAG_TOP_K_PROMPT = int(os.environ.get("AUDIO_LIGHTRAG_TOP_K_PROMPT") or 100) assert AUDIO_LIGHTRAG_TOP_K_PROMPT, "AUDIO_LIGHTRAG_TOP_K_PROMPT environment variable is not set" logger.debug(f"AUDIO_LIGHTRAG_TOP_K_PROMPT: {AUDIO_LIGHTRAG_TOP_K_PROMPT}") @@ -601,7 +601,7 @@ ) logger.debug(f"AUDIO_LIGHTRAG_REDIS_LOCK_PREFIX: {AUDIO_LIGHTRAG_REDIS_LOCK_PREFIX}") -AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY = int(os.environ.get("AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY", 3600)) +AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY = int(os.environ.get("AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY") or 3600) assert AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY, ( "AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY environment variable is not set" ) From 36a24b1aa70aa0decac4878cdd7c24b9a8884315 Mon Sep 17 00:00:00 2001 From: Dat Date: Sat, 15 Nov 2025 14:40:52 +0100 Subject: [PATCH 7/7] fix: improve environment variable fallback in config.py - Updated the retrieval of RUNPOD_WHISPER_MAX_REQUEST_THRESHOLD to provide a default value of 100 if the environment variable is not set, ensuring more robust configuration handling. --- echo/server/dembrane/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/echo/server/dembrane/config.py b/echo/server/dembrane/config.py index 28e170d3..1bfae9ec 100644 --- a/echo/server/dembrane/config.py +++ b/echo/server/dembrane/config.py @@ -278,7 +278,7 @@ logger.debug(f"RUNPOD_WHISPER_PRIORITY_BASE_URL: {RUNPOD_WHISPER_PRIORITY_BASE_URL}") RUNPOD_WHISPER_MAX_REQUEST_THRESHOLD = int( - str(os.environ.get("RUNPOD_WHISPER_MAX_REQUEST_THRESHOLD")) + os.environ.get("RUNPOD_WHISPER_MAX_REQUEST_THRESHOLD") or 100 ) ENABLE_LITELLM_WHISPER_TRANSCRIPTION = os.environ.get(