This commit is contained in:
2026-04-12 01:06:31 +07:00
commit 10d660cbcb
1066 changed files with 228596 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,20 @@
# Core dependencies
google-genai>=0.2.0
python-dotenv>=1.0.0
# Image processing
pillow>=10.0.0
# PDF processing
pypdf>=3.0.0
# Document conversion
markdown>=3.5
# Testing
pytest>=7.4.0
pytest-cov>=4.1.0
pytest-mock>=3.12.0
# Optional dependencies for full functionality
# ffmpeg-python>=0.2.0 # For media optimization (requires ffmpeg installed)

View File

@@ -0,0 +1,74 @@
"""
Tests for document_converter.py
"""
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock, mock_open
sys.path.insert(0, str(Path(__file__).parent.parent))
import document_converter as dc
class TestAPIKeyFinder:
"""Test API key finding logic."""
@patch.dict('os.environ', {'GEMINI_API_KEY': 'test-key-from-env'})
def test_find_api_key_from_env(self):
"""Test finding API key from environment."""
api_key = dc.find_api_key()
assert api_key == 'test-key-from-env'
@patch.dict('os.environ', {}, clear=True)
@patch('document_converter.load_dotenv', None)
def test_find_api_key_no_key(self):
"""Test when no API key is available."""
api_key = dc.find_api_key()
assert api_key is None
class TestProjectRoot:
"""Test project root finding."""
@patch('pathlib.Path.exists')
def test_find_project_root_with_git(self, mock_exists):
"""Test finding project root with .git directory."""
root = dc.find_project_root()
assert isinstance(root, Path)
class TestMimeType:
"""Test MIME type detection."""
def test_pdf_mime_type(self):
"""Test PDF MIME type."""
assert dc.get_mime_type('document.pdf') == 'application/pdf'
def test_image_mime_types(self):
"""Test image MIME types."""
assert dc.get_mime_type('image.jpg') == 'image/jpeg'
assert dc.get_mime_type('image.png') == 'image/png'
def test_unknown_mime_type(self):
"""Test unknown file extension."""
assert dc.get_mime_type('file.unknown') == 'application/octet-stream'
class TestIntegration:
"""Integration tests."""
def test_mime_type_integration(self):
"""Test MIME type detection with various extensions."""
test_cases = [
('document.pdf', 'application/pdf'),
('image.jpg', 'image/jpeg'),
('unknown.xyz', 'application/octet-stream'),
]
for file_path, expected_mime in test_cases:
assert dc.get_mime_type(file_path) == expected_mime
if __name__ == '__main__':
pytest.main([__file__, '-v', '--cov=document_converter', '--cov-report=term-missing'])

View File

@@ -0,0 +1,362 @@
"""
Tests for gemini_batch_process.py
"""
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
import gemini_batch_process as gbp
class TestAPIKeyFinder:
"""Test API key detection."""
def test_find_api_key_from_env(self, monkeypatch):
"""Test finding API key from environment variable."""
monkeypatch.setenv('GEMINI_API_KEY', 'test_key_123')
assert gbp.find_api_key() == 'test_key_123'
@patch('gemini_batch_process.load_dotenv')
def test_find_api_key_not_found(self, mock_load_dotenv, monkeypatch):
"""Test when API key is not found."""
monkeypatch.delenv('GEMINI_API_KEY', raising=False)
# Mock load_dotenv to not actually load any files
mock_load_dotenv.return_value = None
assert gbp.find_api_key() is None
class TestMimeTypeDetection:
"""Test MIME type detection."""
def test_audio_mime_types(self):
"""Test audio file MIME types."""
assert gbp.get_mime_type('test.mp3') == 'audio/mp3'
assert gbp.get_mime_type('test.wav') == 'audio/wav'
assert gbp.get_mime_type('test.aac') == 'audio/aac'
assert gbp.get_mime_type('test.flac') == 'audio/flac'
def test_image_mime_types(self):
"""Test image file MIME types."""
assert gbp.get_mime_type('test.jpg') == 'image/jpeg'
assert gbp.get_mime_type('test.jpeg') == 'image/jpeg'
assert gbp.get_mime_type('test.png') == 'image/png'
assert gbp.get_mime_type('test.webp') == 'image/webp'
def test_video_mime_types(self):
"""Test video file MIME types."""
assert gbp.get_mime_type('test.mp4') == 'video/mp4'
assert gbp.get_mime_type('test.mov') == 'video/quicktime'
assert gbp.get_mime_type('test.avi') == 'video/x-msvideo'
def test_document_mime_types(self):
"""Test document file MIME types."""
assert gbp.get_mime_type('test.pdf') == 'application/pdf'
assert gbp.get_mime_type('test.txt') == 'text/plain'
def test_unknown_mime_type(self):
"""Test unknown file extension."""
assert gbp.get_mime_type('test.xyz') == 'application/octet-stream'
def test_case_insensitive(self):
"""Test case-insensitive extension matching."""
assert gbp.get_mime_type('TEST.MP3') == 'audio/mp3'
assert gbp.get_mime_type('Test.JPG') == 'image/jpeg'
class TestFileUpload:
"""Test file upload functionality."""
@patch('gemini_batch_process.genai.Client')
def test_upload_file_success(self, mock_client_class):
"""Test successful file upload."""
# Mock client and file
mock_client = Mock()
mock_file = Mock()
mock_file.state.name = 'ACTIVE'
mock_file.name = 'test_file'
mock_client.files.upload.return_value = mock_file
result = gbp.upload_file(mock_client, 'test.jpg', verbose=False)
assert result == mock_file
mock_client.files.upload.assert_called_once_with(file='test.jpg')
@patch('gemini_batch_process.genai.Client')
@patch('gemini_batch_process.time.sleep')
def test_upload_video_with_processing(self, mock_sleep, mock_client_class):
"""Test video upload with processing wait."""
mock_client = Mock()
# First call: PROCESSING, second call: ACTIVE
mock_file_processing = Mock()
mock_file_processing.state.name = 'PROCESSING'
mock_file_processing.name = 'test_video'
mock_file_active = Mock()
mock_file_active.state.name = 'ACTIVE'
mock_file_active.name = 'test_video'
mock_client.files.upload.return_value = mock_file_processing
mock_client.files.get.return_value = mock_file_active
result = gbp.upload_file(mock_client, 'test.mp4', verbose=False)
assert result.state.name == 'ACTIVE'
@patch('gemini_batch_process.genai.Client')
def test_upload_file_failed(self, mock_client_class):
"""Test failed file upload."""
mock_client = Mock()
mock_file = Mock()
mock_file.state.name = 'FAILED'
mock_client.files.upload.return_value = mock_file
mock_client.files.get.return_value = mock_file
with pytest.raises(ValueError, match="File processing failed"):
gbp.upload_file(mock_client, 'test.mp4', verbose=False)
class TestProcessFile:
"""Test file processing functionality."""
@patch('gemini_batch_process.genai.Client')
@patch('builtins.open', create=True)
@patch('pathlib.Path.stat')
def test_process_small_file_inline(self, mock_stat, mock_open, mock_client_class):
"""Test processing small file with inline data."""
# Mock small file
mock_stat.return_value.st_size = 10 * 1024 * 1024 # 10MB
# Mock file content
mock_open.return_value.__enter__.return_value.read.return_value = b'test_data'
# Mock client and response
mock_client = Mock()
mock_response = Mock()
mock_response.text = 'Test response'
mock_client.models.generate_content.return_value = mock_response
result = gbp.process_file(
client=mock_client,
file_path='test.jpg',
prompt='Describe this image',
model='gemini-2.5-flash',
task='analyze',
format_output='text',
verbose=False
)
assert result['status'] == 'success'
assert result['response'] == 'Test response'
@patch('gemini_batch_process.upload_file')
@patch('gemini_batch_process.genai.Client')
@patch('pathlib.Path.stat')
def test_process_large_file_api(self, mock_stat, mock_client_class, mock_upload):
"""Test processing large file with File API."""
# Mock large file
mock_stat.return_value.st_size = 50 * 1024 * 1024 # 50MB
# Mock upload and response
mock_file = Mock()
mock_upload.return_value = mock_file
mock_client = Mock()
mock_response = Mock()
mock_response.text = 'Test response'
mock_client.models.generate_content.return_value = mock_response
result = gbp.process_file(
client=mock_client,
file_path='test.mp4',
prompt='Summarize this video',
model='gemini-2.5-flash',
task='analyze',
format_output='text',
verbose=False
)
assert result['status'] == 'success'
mock_upload.assert_called_once()
@patch('gemini_batch_process.genai.Client')
@patch('builtins.open', create=True)
@patch('pathlib.Path.stat')
def test_process_file_error_handling(self, mock_stat, mock_open, mock_client_class):
"""Test error handling in file processing."""
mock_stat.return_value.st_size = 1024
# Mock file read
mock_file = MagicMock()
mock_file.__enter__.return_value.read.return_value = b'test_data'
mock_open.return_value = mock_file
mock_client = Mock()
mock_client.models.generate_content.side_effect = Exception("API Error")
result = gbp.process_file(
client=mock_client,
file_path='test.jpg',
prompt='Test',
model='gemini-2.5-flash',
task='analyze',
format_output='text',
verbose=False,
max_retries=1
)
assert result['status'] == 'error'
assert 'API Error' in result['error']
@patch('gemini_batch_process.genai.Client')
@patch('builtins.open', create=True)
@patch('pathlib.Path.stat')
def test_image_generation_with_aspect_ratio(self, mock_stat, mock_open, mock_client_class):
"""Test image generation with aspect ratio config."""
mock_stat.return_value.st_size = 1024
# Mock file read
mock_file = MagicMock()
mock_file.__enter__.return_value.read.return_value = b'test'
mock_open.return_value = mock_file
mock_client = Mock()
mock_response = Mock()
mock_response.candidates = [Mock()]
mock_response.candidates[0].content.parts = [
Mock(inline_data=Mock(data=b'fake_image_data'))
]
mock_client.models.generate_content.return_value = mock_response
result = gbp.process_file(
client=mock_client,
file_path='test.txt',
prompt='Generate mountain landscape',
model='gemini-2.5-flash-image',
task='generate',
format_output='text',
aspect_ratio='16:9',
verbose=False
)
# Verify config was called with correct structure
call_args = mock_client.models.generate_content.call_args
config = call_args.kwargs.get('config')
assert config is not None
assert result['status'] == 'success'
assert 'generated_image' in result
class TestBatchProcessing:
"""Test batch processing functionality."""
@patch('gemini_batch_process.find_api_key')
@patch('gemini_batch_process.process_file')
@patch('gemini_batch_process.genai.Client')
def test_batch_process_success(self, mock_client_class, mock_process, mock_find_key):
"""Test successful batch processing."""
mock_find_key.return_value = 'test_key'
mock_process.return_value = {'status': 'success', 'response': 'Test'}
results = gbp.batch_process(
files=['test1.jpg', 'test2.jpg'],
prompt='Analyze',
model='gemini-2.5-flash',
task='analyze',
format_output='text',
verbose=False,
dry_run=False
)
assert len(results) == 2
assert all(r['status'] == 'success' for r in results)
@patch('gemini_batch_process.find_api_key')
def test_batch_process_no_api_key(self, mock_find_key):
"""Test batch processing without API key."""
mock_find_key.return_value = None
with pytest.raises(SystemExit):
gbp.batch_process(
files=['test.jpg'],
prompt='Test',
model='gemini-2.5-flash',
task='analyze',
format_output='text',
verbose=False,
dry_run=False
)
@patch('gemini_batch_process.find_api_key')
def test_batch_process_dry_run(self, mock_find_key):
"""Test dry run mode."""
# API key not needed for dry run, but we mock it to avoid sys.exit
mock_find_key.return_value = 'test_key'
results = gbp.batch_process(
files=['test1.jpg', 'test2.jpg'],
prompt='Test',
model='gemini-2.5-flash',
task='analyze',
format_output='text',
verbose=False,
dry_run=True
)
assert results == []
class TestResultsSaving:
"""Test results saving functionality."""
@patch('builtins.open', create=True)
@patch('json.dump')
def test_save_results_json(self, mock_json_dump, mock_open):
"""Test saving results as JSON."""
results = [
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
{'file': 'test2.jpg', 'status': 'success', 'response': 'Test2'}
]
gbp.save_results(results, 'output.json', 'json')
mock_json_dump.assert_called_once()
@patch('builtins.open', create=True)
@patch('csv.DictWriter')
def test_save_results_csv(self, mock_csv_writer, mock_open):
"""Test saving results as CSV."""
results = [
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
{'file': 'test2.jpg', 'status': 'success', 'response': 'Test2'}
]
gbp.save_results(results, 'output.csv', 'csv')
# Verify CSV writer was used
mock_csv_writer.assert_called_once()
@patch('builtins.open', create=True)
def test_save_results_markdown(self, mock_open):
"""Test saving results as Markdown."""
mock_file = MagicMock()
mock_open.return_value.__enter__.return_value = mock_file
results = [
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
{'file': 'test2.jpg', 'status': 'error', 'error': 'Failed'}
]
gbp.save_results(results, 'output.md', 'markdown')
# Verify write was called
assert mock_file.write.call_count > 0
if __name__ == '__main__':
pytest.main([__file__, '-v', '--cov=gemini_batch_process', '--cov-report=term-missing'])

View File

@@ -0,0 +1,373 @@
"""
Tests for media_optimizer.py
"""
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
import json
sys.path.insert(0, str(Path(__file__).parent.parent))
import media_optimizer as mo
class TestEnvLoading:
"""Test environment variable loading."""
@patch('media_optimizer.load_dotenv')
@patch('pathlib.Path.exists')
def test_load_env_files_success(self, mock_exists, mock_load_dotenv):
"""Test successful .env file loading."""
mock_exists.return_value = True
mo.load_env_files()
# Should be called for skill, skills, and claude dirs
assert mock_load_dotenv.call_count >= 1
@patch('media_optimizer.load_dotenv', None)
def test_load_env_files_no_dotenv(self):
"""Test when dotenv is not available."""
# Should not raise an error
mo.load_env_files()
class TestFFmpegCheck:
"""Test ffmpeg availability checking."""
@patch('subprocess.run')
def test_ffmpeg_installed(self, mock_run):
"""Test when ffmpeg is installed."""
mock_run.return_value = Mock()
assert mo.check_ffmpeg() is True
@patch('subprocess.run')
def test_ffmpeg_not_installed(self, mock_run):
"""Test when ffmpeg is not installed."""
mock_run.side_effect = FileNotFoundError()
assert mo.check_ffmpeg() is False
@patch('subprocess.run')
def test_ffmpeg_error(self, mock_run):
"""Test ffmpeg command error."""
mock_run.side_effect = Exception("Error")
assert mo.check_ffmpeg() is False
class TestMediaInfo:
"""Test media information extraction."""
@patch('media_optimizer.check_ffmpeg')
@patch('subprocess.run')
def test_get_video_info(self, mock_run, mock_check):
"""Test extracting video information."""
mock_check.return_value = True
mock_result = Mock()
mock_result.stdout = json.dumps({
'format': {
'size': '10485760',
'duration': '120.5',
'bit_rate': '691200'
},
'streams': [
{
'codec_type': 'video',
'width': 1920,
'height': 1080,
'r_frame_rate': '30/1'
},
{
'codec_type': 'audio',
'sample_rate': '48000',
'channels': 2
}
]
})
mock_run.return_value = mock_result
info = mo.get_media_info('test.mp4')
assert info['size'] == 10485760
assert info['duration'] == 120.5
assert info['width'] == 1920
assert info['height'] == 1080
assert info['sample_rate'] == 48000
@patch('media_optimizer.check_ffmpeg')
def test_get_media_info_no_ffmpeg(self, mock_check):
"""Test when ffmpeg is not available."""
mock_check.return_value = False
info = mo.get_media_info('test.mp4')
assert info == {}
@patch('media_optimizer.check_ffmpeg')
@patch('subprocess.run')
def test_get_media_info_error(self, mock_run, mock_check):
"""Test error handling in media info extraction."""
mock_check.return_value = True
mock_run.side_effect = Exception("Error")
info = mo.get_media_info('test.mp4')
assert info == {}
class TestVideoOptimization:
"""Test video optimization functionality."""
@patch('media_optimizer.check_ffmpeg')
@patch('media_optimizer.get_media_info')
@patch('subprocess.run')
def test_optimize_video_success(self, mock_run, mock_info, mock_check):
"""Test successful video optimization."""
mock_check.return_value = True
mock_info.side_effect = [
# Input info
{
'size': 50 * 1024 * 1024,
'duration': 120.0,
'bit_rate': 3500000,
'width': 1920,
'height': 1080
},
# Output info
{
'size': 25 * 1024 * 1024,
'duration': 120.0,
'width': 1920,
'height': 1080
}
]
result = mo.optimize_video(
'input.mp4',
'output.mp4',
quality=23,
verbose=False
)
assert result is True
mock_run.assert_called_once()
@patch('media_optimizer.check_ffmpeg')
def test_optimize_video_no_ffmpeg(self, mock_check):
"""Test video optimization without ffmpeg."""
mock_check.return_value = False
result = mo.optimize_video('input.mp4', 'output.mp4')
assert result is False
@patch('media_optimizer.check_ffmpeg')
@patch('media_optimizer.get_media_info')
def test_optimize_video_no_info(self, mock_info, mock_check):
"""Test video optimization when info cannot be read."""
mock_check.return_value = True
mock_info.return_value = {}
result = mo.optimize_video('input.mp4', 'output.mp4')
assert result is False
@patch('media_optimizer.check_ffmpeg')
@patch('media_optimizer.get_media_info')
@patch('subprocess.run')
def test_optimize_video_with_target_size(self, mock_run, mock_info, mock_check):
"""Test video optimization with target size."""
mock_check.return_value = True
mock_info.side_effect = [
{'size': 100 * 1024 * 1024, 'duration': 60.0, 'bit_rate': 3500000},
{'size': 50 * 1024 * 1024, 'duration': 60.0}
]
result = mo.optimize_video(
'input.mp4',
'output.mp4',
target_size_mb=50,
verbose=False
)
assert result is True
@patch('media_optimizer.check_ffmpeg')
@patch('media_optimizer.get_media_info')
@patch('subprocess.run')
def test_optimize_video_with_resolution(self, mock_run, mock_info, mock_check):
"""Test video optimization with custom resolution."""
mock_check.return_value = True
mock_info.side_effect = [
{'size': 50 * 1024 * 1024, 'duration': 120.0, 'bit_rate': 3500000},
{'size': 25 * 1024 * 1024, 'duration': 120.0}
]
result = mo.optimize_video(
'input.mp4',
'output.mp4',
resolution='1280x720',
verbose=False
)
assert result is True
class TestAudioOptimization:
"""Test audio optimization functionality."""
@patch('media_optimizer.check_ffmpeg')
@patch('media_optimizer.get_media_info')
@patch('subprocess.run')
def test_optimize_audio_success(self, mock_run, mock_info, mock_check):
"""Test successful audio optimization."""
mock_check.return_value = True
mock_info.side_effect = [
{'size': 10 * 1024 * 1024, 'duration': 300.0},
{'size': 5 * 1024 * 1024, 'duration': 300.0}
]
result = mo.optimize_audio(
'input.mp3',
'output.m4a',
bitrate='64k',
verbose=False
)
assert result is True
mock_run.assert_called_once()
@patch('media_optimizer.check_ffmpeg')
def test_optimize_audio_no_ffmpeg(self, mock_check):
"""Test audio optimization without ffmpeg."""
mock_check.return_value = False
result = mo.optimize_audio('input.mp3', 'output.m4a')
assert result is False
class TestImageOptimization:
"""Test image optimization functionality."""
@patch('PIL.Image.open')
@patch('pathlib.Path.stat')
def test_optimize_image_success(self, mock_stat, mock_image_open):
"""Test successful image optimization."""
# Mock image
mock_resized = Mock()
mock_resized.mode = 'RGB'
mock_img = Mock()
mock_img.width = 3840
mock_img.height = 2160
mock_img.mode = 'RGB'
mock_img.resize.return_value = mock_resized
mock_image_open.return_value = mock_img
# Mock file sizes
mock_stat.return_value.st_size = 5 * 1024 * 1024
result = mo.optimize_image(
'input.jpg',
'output.jpg',
max_width=1920,
quality=85,
verbose=False
)
assert result is True
# Since image is resized, save is called on the resized image
mock_resized.save.assert_called_once()
@patch('PIL.Image.open')
@patch('pathlib.Path.stat')
def test_optimize_image_resize(self, mock_stat, mock_image_open):
"""Test image resizing during optimization."""
mock_img = Mock()
mock_img.width = 3840
mock_img.height = 2160
mock_img.mode = 'RGB'
mock_resized = Mock()
mock_img.resize.return_value = mock_resized
mock_image_open.return_value = mock_img
mock_stat.return_value.st_size = 5 * 1024 * 1024
mo.optimize_image('input.jpg', 'output.jpg', max_width=1920, verbose=False)
mock_img.resize.assert_called_once()
@patch('PIL.Image.open')
@patch('pathlib.Path.stat')
def test_optimize_image_rgba_to_jpg(self, mock_stat, mock_image_open):
"""Test converting RGBA to RGB for JPEG."""
mock_img = Mock()
mock_img.width = 1920
mock_img.height = 1080
mock_img.mode = 'RGBA'
mock_img.split.return_value = [Mock(), Mock(), Mock(), Mock()]
mock_image_open.return_value = mock_img
mock_stat.return_value.st_size = 1024 * 1024
with patch('PIL.Image.new') as mock_new:
mock_rgb = Mock()
mock_new.return_value = mock_rgb
mo.optimize_image('input.png', 'output.jpg', verbose=False)
mock_new.assert_called_once()
def test_optimize_image_no_pillow(self):
"""Test image optimization without Pillow."""
with patch.dict('sys.modules', {'PIL': None}):
result = mo.optimize_image('input.jpg', 'output.jpg')
# Will fail to import but function handles it
assert result is False
class TestVideoSplitting:
"""Test video splitting functionality."""
@patch('media_optimizer.check_ffmpeg')
@patch('media_optimizer.get_media_info')
@patch('subprocess.run')
@patch('pathlib.Path.mkdir')
def test_split_video_success(self, mock_mkdir, mock_run, mock_info, mock_check):
"""Test successful video splitting."""
mock_check.return_value = True
mock_info.return_value = {'duration': 7200.0} # 2 hours
result = mo.split_video(
'input.mp4',
'./chunks',
chunk_duration=3600, # 1 hour chunks
verbose=False
)
# Duration 7200s / 3600s = 2, +1 for safety = 3 chunks
assert len(result) == 3
assert mock_run.call_count == 3
@patch('media_optimizer.check_ffmpeg')
@patch('media_optimizer.get_media_info')
def test_split_video_short_duration(self, mock_info, mock_check):
"""Test splitting video shorter than chunk duration."""
mock_check.return_value = True
mock_info.return_value = {'duration': 1800.0} # 30 minutes
result = mo.split_video(
'input.mp4',
'./chunks',
chunk_duration=3600, # 1 hour
verbose=False
)
assert result == ['input.mp4']
@patch('media_optimizer.check_ffmpeg')
def test_split_video_no_ffmpeg(self, mock_check):
"""Test video splitting without ffmpeg."""
mock_check.return_value = False
result = mo.split_video('input.mp4', './chunks')
assert result == []
if __name__ == '__main__':
pytest.main([__file__, '-v', '--cov=media_optimizer', '--cov-report=term-missing'])

View File

@@ -0,0 +1,232 @@
"""
Tests for minimax_api_client.py - HTTP utilities, auth, polling, downloads.
"""
import json
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
sys.path.insert(0, str(Path(__file__).parent.parent))
import minimax_api_client as mac
class TestFindMinimaxApiKey:
"""Test API key discovery."""
def test_find_key_from_env(self, monkeypatch):
monkeypatch.setenv('MINIMAX_API_KEY', 'test-minimax-key')
with patch.object(mac, 'CENTRALIZED_RESOLVER_AVAILABLE', False):
assert mac.find_minimax_api_key() == 'test-minimax-key'
def test_find_key_not_found(self, monkeypatch):
monkeypatch.delenv('MINIMAX_API_KEY', raising=False)
with patch.object(mac, 'CENTRALIZED_RESOLVER_AVAILABLE', False):
result = mac.find_minimax_api_key()
assert result is None
def test_find_key_via_centralized_resolver(self, monkeypatch):
mock_resolve = Mock(return_value='resolved-key')
with patch.object(mac, 'CENTRALIZED_RESOLVER_AVAILABLE', True), \
patch.object(mac, 'resolve_env', mock_resolve, create=True):
result = mac.find_minimax_api_key()
assert result == 'resolved-key'
mock_resolve.assert_called_once_with(
'MINIMAX_API_KEY', skill='ai-multimodal'
)
class TestGetHeaders:
"""Test header generation."""
def test_headers_contain_bearer_token(self):
headers = mac.get_headers('my-api-key')
assert headers['Authorization'] == 'Bearer my-api-key'
assert headers['Content-Type'] == 'application/json'
def test_headers_with_different_key(self):
headers = mac.get_headers('another-key-123')
assert 'another-key-123' in headers['Authorization']
class TestApiPost:
"""Test POST request handling."""
@patch('minimax_api_client.requests.post')
def test_successful_post(self, mock_post):
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {
"base_resp": {"status_code": 0},
"data": {"result": "ok"}
}
mock_post.return_value = mock_resp
result = mac.api_post("test_endpoint", {"key": "val"}, "api-key")
assert result["data"]["result"] == "ok"
mock_post.assert_called_once()
@patch('minimax_api_client.requests.post')
def test_http_error_raises(self, mock_post):
mock_resp = Mock()
mock_resp.status_code = 401
mock_resp.text = "Unauthorized"
mock_post.return_value = mock_resp
with pytest.raises(Exception, match="HTTP 401"):
mac.api_post("endpoint", {}, "bad-key")
@patch('minimax_api_client.requests.post')
def test_minimax_error_code_raises(self, mock_post):
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {
"base_resp": {"status_code": 1002, "status_msg": "Rate limit"}
}
mock_post.return_value = mock_resp
with pytest.raises(Exception, match="code 1002.*Rate limit"):
mac.api_post("endpoint", {}, "api-key")
@patch('minimax_api_client.requests.post')
def test_custom_timeout(self, mock_post):
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"base_resp": {"status_code": 0}}
mock_post.return_value = mock_resp
mac.api_post("endpoint", {}, "key", timeout=300)
_, kwargs = mock_post.call_args
assert kwargs['timeout'] == 300
@patch('minimax_api_client.requests.post')
def test_default_timeout_is_120(self, mock_post):
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"base_resp": {"status_code": 0}}
mock_post.return_value = mock_resp
mac.api_post("endpoint", {}, "key")
_, kwargs = mock_post.call_args
assert kwargs['timeout'] == 120
@patch('minimax_api_client.requests.post')
def test_verbose_prints_url(self, mock_post, capsys):
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"base_resp": {"status_code": 0}}
mock_post.return_value = mock_resp
mac.api_post("image_generation", {}, "key", verbose=True)
captured = capsys.readouterr()
assert "image_generation" in captured.err
class TestApiGet:
"""Test GET request handling."""
@patch('minimax_api_client.requests.get')
def test_successful_get(self, mock_get):
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"status": "Success", "file_id": "abc"}
mock_get.return_value = mock_resp
result = mac.api_get("query/video_generation", {"task_id": "t1"}, "key")
assert result["status"] == "Success"
@patch('minimax_api_client.requests.get')
def test_get_http_error(self, mock_get):
mock_resp = Mock()
mock_resp.status_code = 500
mock_resp.text = "Server Error"
mock_get.return_value = mock_resp
with pytest.raises(Exception, match="HTTP 500"):
mac.api_get("endpoint", {}, "key")
class TestPollAsyncTask:
"""Test async task polling."""
@patch('minimax_api_client.time.sleep')
@patch('minimax_api_client.api_get')
def test_poll_success_first_try(self, mock_get, mock_sleep):
mock_get.return_value = {"status": "Success", "file_id": "f123"}
result = mac.poll_async_task("task1", "video_generation", "key")
assert result["file_id"] == "f123"
mock_sleep.assert_not_called()
@patch('minimax_api_client.time.sleep')
@patch('minimax_api_client.api_get')
def test_poll_success_after_processing(self, mock_get, mock_sleep):
mock_get.side_effect = [
{"status": "Processing"},
{"status": "Processing"},
{"status": "Success", "file_id": "f456"}
]
result = mac.poll_async_task("task2", "video_generation", "key",
poll_interval=1)
assert result["file_id"] == "f456"
assert mock_sleep.call_count == 2
@patch('minimax_api_client.time.sleep')
@patch('minimax_api_client.api_get')
def test_poll_task_failed(self, mock_get, mock_sleep):
mock_get.return_value = {"status": "Failed", "error": "bad input"}
with pytest.raises(Exception, match="Task failed"):
mac.poll_async_task("task3", "video_generation", "key")
@patch('minimax_api_client.time.sleep')
@patch('minimax_api_client.api_get')
def test_poll_timeout(self, mock_get, mock_sleep):
mock_get.return_value = {"status": "Processing"}
with pytest.raises(TimeoutError, match="timed out"):
mac.poll_async_task("task4", "video_generation", "key",
poll_interval=1, max_wait=3)
class TestDownloadFile:
"""Test file download."""
@patch('minimax_api_client.requests.get')
@patch('minimax_api_client.api_get')
def test_download_success(self, mock_api_get, mock_req_get, tmp_path):
mock_api_get.return_value = {
"file": {"download_url": "https://cdn.minimax.io/video.mp4"}
}
mock_resp = Mock()
mock_resp.raise_for_status = Mock()
mock_resp.iter_content.return_value = [b"video_data"]
mock_req_get.return_value = mock_resp
output = str(tmp_path / "test.mp4")
result = mac.download_file("file123", "key", output)
assert result == output
assert Path(output).exists()
@patch('minimax_api_client.api_get')
def test_download_no_url_raises(self, mock_api_get):
mock_api_get.return_value = {"file": {}}
with pytest.raises(Exception, match="No download URL"):
mac.download_file("file123", "key", "/tmp/test.mp4")
class TestGetOutputDir:
"""Test output directory resolution."""
def test_returns_path_object(self):
result = mac.get_output_dir()
assert isinstance(result, Path)
def test_directory_exists(self):
result = mac.get_output_dir()
assert result.exists()
assert result.is_dir()

View File

@@ -0,0 +1,185 @@
"""
Tests for minimax_cli.py - CLI argument parsing and task dispatch.
"""
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
sys.path.insert(0, str(Path(__file__).parent.parent))
import minimax_cli as cli
class TestTaskDefaults:
"""Test task-to-model default mapping."""
def test_generate_defaults_to_image_01(self):
assert cli.TASK_DEFAULTS['generate'] == 'image-01'
def test_generate_video_defaults_to_hailuo(self):
assert cli.TASK_DEFAULTS['generate-video'] == 'MiniMax-Hailuo-2.3'
def test_generate_speech_defaults_to_speech_28_hd(self):
assert cli.TASK_DEFAULTS['generate-speech'] == 'speech-2.8-hd'
def test_generate_music_defaults_to_music_25(self):
assert cli.TASK_DEFAULTS['generate-music'] == 'music-2.5'
class TestPrintResult:
"""Test result formatting."""
def test_success_image(self, capsys):
result = {
"status": "success",
"generated_images": ["/path/to/img.png"],
"model": "image-01"
}
cli.print_result(result, "generate")
output = capsys.readouterr().out
assert "success" in output.lower()
assert "/path/to/img.png" in output
assert "image-01" in output
def test_success_video(self, capsys):
result = {
"status": "success",
"generated_video": "/path/to/vid.mp4",
"generation_time": 45.2,
"model": "MiniMax-Hailuo-2.3"
}
cli.print_result(result, "generate-video")
output = capsys.readouterr().out
assert "/path/to/vid.mp4" in output
assert "45.2s" in output
def test_success_audio(self, capsys):
result = {
"status": "success",
"generated_audio": "/path/to/audio.mp3",
"duration_ms": 140000,
"model": "music-2.5"
}
cli.print_result(result, "generate-music")
output = capsys.readouterr().out
assert "/path/to/audio.mp3" in output
assert "140.0s" in output
def test_error_result(self, capsys):
result = {"status": "error", "error": "Rate limit exceeded"}
cli.print_result(result, "generate")
output = capsys.readouterr().out
assert "Rate limit exceeded" in output
def test_unknown_status(self, capsys):
result = {"model": "image-01"}
cli.print_result(result, "generate")
output = capsys.readouterr().out
assert "unknown" in output.lower()
class TestMainCLI:
"""Test CLI main() argument parsing and dispatch."""
@patch('minimax_cli.find_minimax_api_key', return_value=None)
def test_no_api_key_exits(self, mock_key, capsys):
with patch('sys.argv', ['cli', '--task', 'generate', '--prompt', 'x']):
with pytest.raises(SystemExit) as exc_info:
cli.main()
assert exc_info.value.code == 1
@patch('minimax_cli.generate_image')
@patch('minimax_cli.find_minimax_api_key', return_value='test-key')
def test_generate_image_dispatch(self, mock_key, mock_gen):
mock_gen.return_value = {"status": "success", "generated_images": [],
"model": "image-01"}
with patch('sys.argv', ['cli', '--task', 'generate',
'--prompt', 'A cat']):
cli.main()
mock_gen.assert_called_once()
args = mock_gen.call_args
assert args[0][0] == 'test-key'
assert args[0][1] == 'A cat'
@patch('minimax_cli.generate_speech')
@patch('minimax_cli.find_minimax_api_key', return_value='test-key')
def test_generate_speech_dispatch(self, mock_key, mock_gen):
mock_gen.return_value = {"status": "success",
"generated_audio": "/x.mp3",
"model": "speech-2.8-hd"}
with patch('sys.argv', ['cli', '--task', 'generate-speech',
'--text', 'Hello world']):
cli.main()
mock_gen.assert_called_once()
@patch('minimax_cli.generate_speech')
@patch('minimax_cli.find_minimax_api_key', return_value='test-key')
def test_speech_uses_text_or_prompt(self, mock_key, mock_gen):
mock_gen.return_value = {"status": "success",
"generated_audio": "/x.mp3",
"model": "speech-2.8-hd"}
# --prompt should work as fallback for --text
with patch('sys.argv', ['cli', '--task', 'generate-speech',
'--prompt', 'Fallback text']):
cli.main()
call_args = mock_gen.call_args
assert call_args[0][1] == 'Fallback text'
@patch('minimax_cli.generate_music')
@patch('minimax_cli.find_minimax_api_key', return_value='test-key')
def test_generate_music_dispatch(self, mock_key, mock_gen):
mock_gen.return_value = {"status": "success",
"generated_audio": "/x.mp3",
"duration_ms": 60000,
"model": "music-2.5"}
with patch('sys.argv', ['cli', '--task', 'generate-music',
'--lyrics', 'La la la']):
cli.main()
mock_gen.assert_called_once()
@patch('minimax_cli.generate_video')
@patch('minimax_cli.find_minimax_api_key', return_value='test-key')
def test_generate_video_dispatch(self, mock_key, mock_gen):
mock_gen.return_value = {"status": "success",
"generated_video": "/x.mp4",
"generation_time": 30.0,
"model": "MiniMax-Hailuo-2.3"}
with patch('sys.argv', ['cli', '--task', 'generate-video',
'--prompt', 'A dancer']):
cli.main()
mock_gen.assert_called_once()
@patch('minimax_cli.find_minimax_api_key', return_value='test-key')
def test_auto_model_detection(self, mock_key):
with patch('sys.argv', ['cli', '--task', 'generate-speech',
'--text', 'hi']):
with patch('minimax_cli.generate_speech') as mock_gen:
mock_gen.return_value = {"status": "success",
"generated_audio": "/x.mp3",
"model": "speech-2.8-hd"}
cli.main()
# Model should be auto-detected
assert mock_gen.call_args[0][2] == 'speech-2.8-hd'
@patch('minimax_cli.find_minimax_api_key', return_value='test-key')
def test_explicit_model_override(self, mock_key):
with patch('sys.argv', ['cli', '--task', 'generate-speech',
'--text', 'hi', '--model', 'speech-2.8-turbo']):
with patch('minimax_cli.generate_speech') as mock_gen:
mock_gen.return_value = {"status": "success",
"generated_audio": "/x.mp3",
"model": "speech-2.8-turbo"}
cli.main()
assert mock_gen.call_args[0][2] == 'speech-2.8-turbo'
@patch('minimax_cli.generate_image')
@patch('minimax_cli.find_minimax_api_key', return_value='test-key')
def test_exception_exits_with_1(self, mock_key, mock_gen):
mock_gen.side_effect = Exception("API timeout")
with patch('sys.argv', ['cli', '--task', 'generate',
'--prompt', 'test']):
with pytest.raises(SystemExit) as exc_info:
cli.main()
assert exc_info.value.code == 1

View File

@@ -0,0 +1,393 @@
"""
Tests for minimax_generate.py - generation functions for image, video, speech, music.
"""
import json
import pytest
import sys
import time
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock, call
sys.path.insert(0, str(Path(__file__).parent.parent))
import minimax_generate as mg
class TestModelRegistries:
"""Test model set definitions."""
def test_image_models(self):
assert 'image-01' in mg.MINIMAX_IMAGE_MODELS
assert 'image-01-live' in mg.MINIMAX_IMAGE_MODELS
def test_video_models(self):
assert 'MiniMax-Hailuo-2.3' in mg.MINIMAX_VIDEO_MODELS
assert 'MiniMax-Hailuo-2.3-Fast' in mg.MINIMAX_VIDEO_MODELS
assert 'S2V-01' in mg.MINIMAX_VIDEO_MODELS
def test_speech_models(self):
assert 'speech-2.8-hd' in mg.MINIMAX_SPEECH_MODELS
assert 'speech-2.8-turbo' in mg.MINIMAX_SPEECH_MODELS
def test_music_models(self):
assert 'music-2.5' in mg.MINIMAX_MUSIC_MODELS
def test_all_models_is_union(self):
expected = (mg.MINIMAX_IMAGE_MODELS | mg.MINIMAX_VIDEO_MODELS |
mg.MINIMAX_SPEECH_MODELS | mg.MINIMAX_MUSIC_MODELS)
assert mg.ALL_MINIMAX_MODELS == expected
class TestIsMinimaxModel:
"""Test model detection."""
def test_known_image_model(self):
assert mg.is_minimax_model('image-01') is True
def test_known_video_model(self):
assert mg.is_minimax_model('MiniMax-Hailuo-2.3') is True
def test_known_speech_model(self):
assert mg.is_minimax_model('speech-2.8-hd') is True
def test_known_music_model(self):
assert mg.is_minimax_model('music-2.5') is True
def test_prefix_minimax(self):
assert mg.is_minimax_model('MiniMax-Future-Model') is True
def test_prefix_speech(self):
assert mg.is_minimax_model('speech-3.0-ultra') is True
def test_prefix_s2v(self):
assert mg.is_minimax_model('S2V-02') is True
def test_non_minimax_model(self):
assert mg.is_minimax_model('gemini-2.5-flash') is False
def test_non_minimax_imagen(self):
assert mg.is_minimax_model('imagen-4.0-generate-001') is False
class TestGenerateImage:
"""Test image generation."""
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {
"data": {"image_urls": ["https://cdn.minimax.io/img1.png"]}
}
with patch('requests.get') as mock_req_get:
mock_resp = Mock()
mock_resp.content = b'\x89PNG\r\n\x1a\n'
mock_resp.raise_for_status = Mock()
mock_req_get.return_value = mock_resp
result = mg.generate_image("key", "A cat", "image-01")
assert result["status"] == "success"
assert len(result["generated_images"]) == 1
assert result["model"] == "image-01"
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_no_images_returns_error(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {"data": {"image_urls": []}}
result = mg.generate_image("key", "A cat", "image-01")
assert result["status"] == "error"
@patch('minimax_generate.api_post')
def test_payload_structure(self, mock_post):
mock_post.return_value = {"data": {"image_urls": []}}
mg.generate_image("key", "A dog", "image-01", "16:9", 3)
payload = mock_post.call_args[0][1]
assert payload["model"] == "image-01"
assert payload["prompt"] == "A dog"
assert payload["aspect_ratio"] == "16:9"
assert payload["n"] == 3
assert payload["response_format"] == "url"
assert payload["prompt_optimizer"] is True
@patch('minimax_generate.api_post')
def test_num_images_capped_at_9(self, mock_post):
mock_post.return_value = {"data": {"image_urls": []}}
mg.generate_image("key", "test", "image-01", num_images=15)
payload = mock_post.call_args[0][1]
assert payload["n"] == 9
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_output_copy(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {
"data": {"image_urls": ["https://cdn.minimax.io/img.png"]}
}
with patch('requests.get') as mock_req_get:
mock_resp = Mock()
mock_resp.content = b'image_bytes'
mock_resp.raise_for_status = Mock()
mock_req_get.return_value = mock_resp
output_path = str(tmp_path / "custom_output.png")
result = mg.generate_image("key", "test", output=output_path)
assert Path(output_path).exists()
class TestGenerateVideo:
"""Test video generation (async workflow)."""
@patch('minimax_generate.download_file')
@patch('minimax_generate.poll_async_task')
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success(self, mock_post, mock_dir, mock_poll, mock_dl, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {"task_id": "vid-task-123"}
mock_poll.return_value = {"file_id": "file-456"}
# Create a fake video file so stat() works
mock_dl.side_effect = lambda fid, key, path, v: (
Path(path).write_bytes(b'fake_video') or path
)
result = mg.generate_video("key", "A dancer")
assert result["status"] == "success"
assert "generated_video" in result
assert result["model"] == "MiniMax-Hailuo-2.3"
mock_poll.assert_called_once()
@patch('minimax_generate.api_post')
def test_no_task_id_error(self, mock_post):
mock_post.return_value = {"error": "bad request"}
result = mg.generate_video("key", "test")
assert result["status"] == "error"
assert "No task_id" in result["error"]
@patch('minimax_generate.poll_async_task')
@patch('minimax_generate.api_post')
def test_no_file_id_error(self, mock_post, mock_poll):
mock_post.return_value = {"task_id": "t1"}
mock_poll.return_value = {"status": "Success"}
result = mg.generate_video("key", "test")
assert result["status"] == "error"
assert "No file_id" in result["error"]
@patch('minimax_generate.api_post')
def test_payload_with_first_frame(self, mock_post):
mock_post.return_value = {"task_id": None}
mg.generate_video("key", "test", first_frame="https://img.url/frame.png")
payload = mock_post.call_args[0][1]
assert payload["first_frame_image"] == "https://img.url/frame.png"
@patch('minimax_generate.api_post')
def test_payload_duration_resolution(self, mock_post):
mock_post.return_value = {"task_id": None}
mg.generate_video("key", "test", duration=10, resolution="720P")
payload = mock_post.call_args[0][1]
assert payload["duration"] == 10
assert payload["resolution"] == "720P"
class TestGenerateSpeech:
"""Test speech/TTS generation."""
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
# hex-encoded audio bytes
mock_post.return_value = {
"data": {"audio": "48656c6c6f"} # "Hello" in hex
}
result = mg.generate_speech("key", "Hello world")
assert result["status"] == "success"
assert "generated_audio" in result
assert result["model"] == "speech-2.8-hd"
# Verify file was written
audio_path = Path(result["generated_audio"])
assert audio_path.exists()
assert audio_path.read_bytes() == bytes.fromhex("48656c6c6f")
@patch('minimax_generate.api_post')
def test_no_audio_returns_error(self, mock_post):
mock_post.return_value = {"data": {}}
result = mg.generate_speech("key", "test")
assert result["status"] == "error"
@patch('minimax_generate.api_post')
def test_payload_structure(self, mock_post):
mock_post.return_value = {"data": {}}
mg.generate_speech("key", "Test text", "speech-2.8-turbo",
voice="English_Warm_Bestie", emotion="happy",
output_format="wav", rate=1.5)
payload = mock_post.call_args[0][1]
assert payload["model"] == "speech-2.8-turbo"
assert payload["text"] == "Test text"
assert payload["stream"] is False
assert payload["output_format"] == "hex"
assert payload["voice_setting"]["voice_id"] == "English_Warm_Bestie"
assert payload["voice_setting"]["speed"] == 1.5
assert payload["audio_setting"]["format"] == "wav"
assert payload["audio_setting"]["sample_rate"] == 32000
@patch('minimax_generate.api_post')
def test_text_truncated_at_10000(self, mock_post):
mock_post.return_value = {"data": {}}
long_text = "x" * 15000
mg.generate_speech("key", long_text)
payload = mock_post.call_args[0][1]
assert len(payload["text"]) == 10000
@patch('minimax_generate.api_post')
def test_uses_t2a_v2_endpoint(self, mock_post):
mock_post.return_value = {"data": {}}
mg.generate_speech("key", "test")
endpoint = mock_post.call_args[0][0]
assert endpoint == "t2a_v2"
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_wav_extension(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {"data": {"audio": "aabb"}}
result = mg.generate_speech("key", "test", output_format="wav")
assert result["generated_audio"].endswith(".wav")
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_pcm_defaults_to_mp3_ext(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {"data": {"audio": "aabb"}}
result = mg.generate_speech("key", "test", output_format="pcm")
assert result["generated_audio"].endswith(".mp3")
class TestGenerateMusic:
"""Test music generation."""
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success_with_url(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {
"data": {"audio": "https://cdn.minimax.io/music.mp3"},
"extra_info": {"music_duration": 120000}
}
with patch('requests.get') as mock_req_get:
mock_resp = Mock()
mock_resp.content = b'music_data'
mock_resp.raise_for_status = Mock()
mock_req_get.return_value = mock_resp
result = mg.generate_music("key", lyrics="La la la",
prompt="pop")
assert result["status"] == "success"
assert result["duration_ms"] == 120000
assert result["model"] == "music-2.5"
@patch('minimax_generate.get_output_dir')
@patch('minimax_generate.api_post')
def test_success_with_hex(self, mock_post, mock_dir, tmp_path):
mock_dir.return_value = tmp_path
mock_post.return_value = {
"data": {"audio": "deadbeef"},
"extra_info": {"music_duration": 60000}
}
result = mg.generate_music("key", lyrics="test")
assert result["status"] == "success"
audio_path = Path(result["generated_audio"])
assert audio_path.read_bytes() == bytes.fromhex("deadbeef")
@patch('minimax_generate.api_post')
def test_no_audio_returns_error(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
result = mg.generate_music("key", lyrics="test")
assert result["status"] == "error"
@patch('minimax_generate.api_post')
def test_payload_structure(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", lyrics="Verse 1\nHello",
prompt="upbeat pop", model="music-2.5",
output_format="wav")
payload = mock_post.call_args[0][1]
assert payload["model"] == "music-2.5"
assert payload["lyrics"] == "Verse 1\nHello"
assert payload["prompt"] == "upbeat pop"
assert payload["output_format"] == "url"
assert payload["audio_setting"]["format"] == "wav"
assert payload["audio_setting"]["sample_rate"] == 44100
@patch('minimax_generate.api_post')
def test_lyrics_truncated_at_3500(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", lyrics="x" * 5000)
payload = mock_post.call_args[0][1]
assert len(payload["lyrics"]) == 3500
@patch('minimax_generate.api_post')
def test_prompt_truncated_at_2000(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", prompt="y" * 3000)
payload = mock_post.call_args[0][1]
assert len(payload["prompt"]) == 2000
@patch('minimax_generate.api_post')
def test_uses_300s_timeout(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", lyrics="test")
# Check timeout kwarg passed to api_post
_, kwargs = mock_post.call_args
assert kwargs.get('timeout') == 300
@patch('minimax_generate.api_post')
def test_empty_lyrics_omitted(self, mock_post):
mock_post.return_value = {"data": {}, "extra_info": {}}
mg.generate_music("key", lyrics="", prompt="jazz")
payload = mock_post.call_args[0][1]
assert "lyrics" not in payload
assert payload["prompt"] == "jazz"