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
2 changes: 1 addition & 1 deletion backend/routers/speech_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

@router.get('/v3/speech-profile', tags=['v3'])
def has_speech_profile(uid: str = Depends(auth.get_current_user_uid)):
return {'has_profile': get_user_has_speech_profile(uid, max_age_days=90)}
return {'has_profile': get_user_has_speech_profile(uid)}


@router.get('/v4/speech-profile', tags=['v3'])
Expand Down
1 change: 1 addition & 0 deletions backend/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pytest tests/unit/test_pusher_private_cloud_data_protection.py -v
pytest tests/unit/test_pusher_batch_upload.py -v
pytest tests/unit/test_storage_upload_audio_chunk_data_protection.py -v
pytest tests/unit/test_storage_opus_encoding.py -v
pytest tests/unit/test_speech_profile_existence.py -v
pytest tests/unit/test_storage_fanout_limits.py -v
pytest tests/unit/test_people_conversations_500s.py -v
pytest tests/unit/test_firestore_read_ops_cache.py -v
Expand Down
65 changes: 65 additions & 0 deletions backend/tests/unit/test_speech_profile_existence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Unit tests for the speech profile existence check (#5128).

/v3/speech-profile must report has_profile=true for ANY existing profile,
because the listen pipeline (routers/transcribe.py) uses the profile
regardless of age. A 90-day expiry applied only to this endpoint caused
users with older, actively-used profiles to be re-prompted to
"Teach Omi your voice" on every launch.
"""

import inspect
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

os.environ.setdefault("ENCRYPTION_SECRET", "omi_ZwB2ZNqB2HHpMK6wStk7sTpavJiPTFg7gXUHnc4tFABPU6pZ2c2DKgehtfgi4RZv")

# Mock heavy dependencies at sys.modules level before importing storage
sys.modules.setdefault("database._client", MagicMock())

_mock_gcs_storage = MagicMock()
_mock_gcs_storage.Client.return_value = MagicMock()
sys.modules.setdefault("google.cloud.storage", _mock_gcs_storage)
sys.modules.setdefault("google.cloud.storage.transfer_manager", MagicMock())
sys.modules.setdefault("google.cloud.exceptions", MagicMock())
sys.modules.setdefault("google.oauth2", MagicMock())
sys.modules.setdefault("google.oauth2.service_account", MagicMock())

from utils.other import storage as storage_mod


class TestGetUserHasSpeechProfile:
def _bucket_with_blob(self, exists: bool):
blob = MagicMock()
blob.exists.return_value = exists
bucket = MagicMock()
bucket.blob.return_value = blob
return bucket, blob

def test_existing_profile_counts_regardless_of_age(self):
"""An existing profile is reported as present — no age cutoff (#5128)."""
bucket, blob = self._bucket_with_blob(exists=True)
with patch.object(storage_mod, "_get_speech_profiles_bucket", return_value=bucket):
assert storage_mod.get_user_has_speech_profile("uid1") is True
# No metadata fetch for age checks — the old expiry code called blob.reload()
blob.reload.assert_not_called()

def test_missing_profile(self):
bucket, _ = self._bucket_with_blob(exists=False)
with patch.object(storage_mod, "_get_speech_profiles_bucket", return_value=bucket):
assert storage_mod.get_user_has_speech_profile("uid1") is False

def test_missing_bucket(self):
with patch.object(storage_mod, "_get_speech_profiles_bucket", return_value=None):
assert storage_mod.get_user_has_speech_profile("uid1") is False

def test_no_age_parameter_in_signature(self):
"""Guard against reintroducing an expiry knob on the existence check."""
params = inspect.signature(storage_mod.get_user_has_speech_profile).parameters
assert list(params) == ["uid"]

def test_endpoint_does_not_pass_age_cutoff(self):
"""The /v3/speech-profile router must not filter profiles by age (#5128)."""
router_src = Path(storage_mod.__file__).parents[2] / "routers" / "speech_profile.py"
assert "max_age_days" not in router_src.read_text()
19 changes: 5 additions & 14 deletions backend/utils/other/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,15 @@ def upload_profile_audio(file_path: str, uid: str):
return f'https://storage.googleapis.com/{speech_profiles_bucket}/{path}'


def get_user_has_speech_profile(uid: str, max_age_days: int = None) -> bool:
def get_user_has_speech_profile(uid: str) -> bool:
# No age cutoff: the listen pipeline (routers/transcribe.py) uses the profile
# regardless of age, so reporting an old profile as absent only causes the app
# to re-prompt users whose profile is still in active use (#5128).
bucket = _get_speech_profiles_bucket()
if bucket is None:
return False

blob = bucket.blob(f'{uid}/speech_profile.wav')
if not blob.exists():
return False

# Check age if max_age_days is specified
if max_age_days is not None:
blob.reload()
if blob.time_created:
age = datetime.datetime.now(datetime.timezone.utc) - blob.time_created
if age.days > max_age_days:
return False

return True
return bucket.blob(f'{uid}/speech_profile.wav').exists()


def get_profile_audio_if_exists(uid: str, download: bool = True) -> str:
Expand Down
Loading