Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)

This commit is contained in:
InnovA AI
2026-03-16 21:47:37 +00:00
commit 42772a31ed
365 changed files with 103572 additions and 0 deletions

239
tests/test_bugfixes.py Normal file
View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
Tests for specific bug fixes.
- Issue #230: Bulk delete crash when recordings have speaker_snippets
- Issue #223: File monitor stability time env var
"""
import json
import os
import sys
import time
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.app import app, db
from src.models import User, Recording
from src.models.speaker_snippet import SpeakerSnippet
# Disable CSRF for testing
app.config['WTF_CSRF_ENABLED'] = False
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _get_or_create_user():
user = User.query.filter_by(username="bugfix_test_user").first()
if not user:
user = User(username="bugfix_test_user", email="bugfix@local.test")
db.session.add(user)
db.session.commit()
return user
def _create_recording_with_snippets(user):
"""Create a recording that has speaker_snippet records attached."""
rec = Recording(
user_id=user.id,
title="Recording with snippets",
status="COMPLETED",
transcription=json.dumps([
{"speaker": "SPEAKER_00", "sentence": "Hello there."},
]),
)
db.session.add(rec)
db.session.commit()
# We need a speaker to attach snippets to
from src.models import Speaker
speaker = Speaker.query.filter_by(user_id=user.id, name="BugfixTestSpeaker").first()
if not speaker:
speaker = Speaker(name="BugfixTestSpeaker", user_id=user.id)
db.session.add(speaker)
db.session.commit()
snippet = SpeakerSnippet(
speaker_id=speaker.id,
recording_id=rec.id,
segment_index=0,
text_snippet="Hello there.",
)
db.session.add(snippet)
db.session.commit()
return rec, speaker, snippet
# ---------------------------------------------------------------------------
# Issue #230: Deleting recordings with speaker_snippets
# ---------------------------------------------------------------------------
class TestIssue230BulkDeleteCascade:
"""Verify that deleting a recording with speaker_snippets doesn't crash."""
def test_single_delete_with_snippets(self):
"""Single DELETE /recording/<id> should succeed when snippets exist."""
with app.app_context():
user = _get_or_create_user()
rec, speaker, snippet = _create_recording_with_snippets(user)
rec_id = rec.id
snippet_id = snippet.id
with app.test_client() as client:
# Login
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
resp = client.delete(f'/recording/{rec_id}')
assert resp.status_code == 200, f"Delete failed: {resp.get_json()}"
data = resp.get_json()
assert data.get('success') is True
# Verify snippet was also deleted
orphan = db.session.get(SpeakerSnippet, snippet_id)
assert orphan is None, "Speaker snippet should have been deleted with recording"
# Cleanup speaker
db.session.delete(speaker)
db.session.commit()
def test_bulk_delete_with_snippets(self):
"""DELETE /api/recordings/bulk should succeed when snippets exist."""
with app.app_context():
user = _get_or_create_user()
rec, speaker, snippet = _create_recording_with_snippets(user)
rec_id = rec.id
snippet_id = snippet.id
with app.test_client() as client:
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
resp = client.delete(
'/api/recordings/bulk',
json={'recording_ids': [rec_id]},
content_type='application/json',
)
assert resp.status_code == 200, f"Bulk delete failed: {resp.get_json()}"
data = resp.get_json()
assert data.get('success') is True
assert rec_id in data.get('deleted_ids', [])
# Verify snippet was also deleted
orphan = db.session.get(SpeakerSnippet, snippet_id)
assert orphan is None, "Speaker snippet should have been deleted with recording"
# Cleanup speaker
db.session.delete(speaker)
db.session.commit()
def test_bulk_delete_multiple_with_snippets(self):
"""Bulk deleting multiple recordings (some with snippets) should succeed."""
with app.app_context():
user = _get_or_create_user()
rec1, speaker, snippet = _create_recording_with_snippets(user)
rec2 = Recording(user_id=user.id, title="No snippets", status="COMPLETED")
db.session.add(rec2)
db.session.commit()
rec1_id, rec2_id = rec1.id, rec2.id
with app.test_client() as client:
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
resp = client.delete(
'/api/recordings/bulk',
json={'recording_ids': [rec1_id, rec2_id]},
content_type='application/json',
)
assert resp.status_code == 200, f"Bulk delete failed: {resp.get_json()}"
data = resp.get_json()
assert data.get('deleted_count') == 2
# Cleanup speaker
db.session.delete(speaker)
db.session.commit()
# ---------------------------------------------------------------------------
# Issue #223: File monitor stability time
# ---------------------------------------------------------------------------
class TestIssue223StabilityTime:
"""Verify AUTO_PROCESS_STABILITY_TIME env var is respected."""
def test_default_stability_time(self):
"""Without env var, stability_time defaults to 5."""
from src.file_monitor import FileMonitor
monitor = FileMonitor.__new__(FileMonitor)
monitor.logger = MagicMock()
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
f.write(b'fake audio data')
tmp_path = Path(f.name)
try:
with patch.dict(os.environ, {}, clear=False):
# Remove the env var if it exists
os.environ.pop('AUTO_PROCESS_STABILITY_TIME', None)
with patch('time.sleep') as mock_sleep:
monitor._is_file_stable(tmp_path)
mock_sleep.assert_called_once_with(5)
finally:
tmp_path.unlink(missing_ok=True)
def test_custom_stability_time(self):
"""AUTO_PROCESS_STABILITY_TIME=15 should sleep for 15 seconds."""
from src.file_monitor import FileMonitor
monitor = FileMonitor.__new__(FileMonitor)
monitor.logger = MagicMock()
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
f.write(b'fake audio data')
tmp_path = Path(f.name)
try:
with patch.dict(os.environ, {'AUTO_PROCESS_STABILITY_TIME': '15'}):
with patch('time.sleep') as mock_sleep:
# _is_file_stable uses the default param, but the caller reads env
# So we test the caller path via _scan_user_directory indirectly
# or just call with explicit value
stability_time = int(os.environ.get('AUTO_PROCESS_STABILITY_TIME', '5'))
monitor._is_file_stable(tmp_path, stability_time)
mock_sleep.assert_called_once_with(15)
finally:
tmp_path.unlink(missing_ok=True)
def test_no_hardcoded_cap(self):
"""Stability time should NOT be capped at 2 seconds anymore."""
from src.file_monitor import FileMonitor
monitor = FileMonitor.__new__(FileMonitor)
monitor.logger = MagicMock()
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
f.write(b'fake audio data')
tmp_path = Path(f.name)
try:
with patch('time.sleep') as mock_sleep:
monitor._is_file_stable(tmp_path, stability_time=30)
# Should sleep for 30, NOT min(30, 2) = 2
mock_sleep.assert_called_once_with(30)
finally:
tmp_path.unlink(missing_ok=True)
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import pytest
sys.exit(pytest.main([__file__, "-v"]))