""" tests/test_ppt_vision_service.py ───────────────────────────────────────────────────────────────── Operation Ollama-First v5.0 / Phase 14 — PPT vision (minicpm-v) 驗證 """ import os import tempfile import logging from unittest.mock import patch, MagicMock import pytest @pytest.fixture(autouse=True) def _reset_env(monkeypatch): monkeypatch.delenv('PPT_VISION_ENABLED', raising=False) yield @pytest.fixture def fake_image(): """產生 1KB 假 png 檔給 test 用""" f = tempfile.NamedTemporaryFile(suffix='.png', delete=False) f.write(b'\x89PNG\r\n\x1a\n' + b'\x00' * 1000) # PNG magic + 1KB padding f.close() yield f.name try: os.unlink(f.name) except Exception: pass def test_flag_off_returns_disabled_error(fake_image): """flag OFF 時 check_image 直接回 success=False(不打 HTTP)""" from services.ppt_vision_service import PPTVisionService svc = PPTVisionService() with patch('services.ollama_service.requests.post') as mock_post: result = svc.check_image(fake_image) assert result.success is False assert 'PPT_VISION_ENABLED=false' in (result.error or '') mock_post.assert_not_called() def test_missing_image_file(monkeypatch): monkeypatch.setenv('PPT_VISION_ENABLED', 'true') from services.ppt_vision_service import PPTVisionService svc = PPTVisionService() result = svc.check_image('/tmp/this_file_does_not_exist_xyz.png') assert result.success is False assert 'image not found' in (result.error or '') def test_no_issues_response(fake_image, monkeypatch): """minicpm-v 回「✅ 無視覺異常」→ issues_found 應為空 list""" monkeypatch.setenv('PPT_VISION_ENABLED', 'true') from services.ppt_vision_service import PPTVisionService fake_resp = MagicMock(status_code=200) fake_resp.json.return_value = {'response': '✅ 無視覺異常'} with patch('services.ollama_service.resolve_ollama_host', return_value='http://test:11434'), \ patch('services.ollama_service.requests.post', return_value=fake_resp) as mock_post: svc = PPTVisionService() result = svc.check_image(fake_image) assert result.success is True assert result.issues_found == [] assert result.confidence == 1.0 payload = mock_post.call_args.kwargs['json'] assert payload['model'] == 'minicpm-v:latest' assert payload['images'] and isinstance(payload['images'][0], str) def test_check_image_compresses_valid_png_before_ollama(monkeypatch, tmp_path): """有效截圖送 Ollama 前應轉成較輕的 JPEG payload。""" monkeypatch.setenv('PPT_VISION_ENABLED', 'true') import base64 Image = pytest.importorskip("PIL.Image") from services.ppt_vision_service import PPTVisionService image_path = tmp_path / 'slide.png' Image.new('RGB', (1800, 1000), color=(245, 238, 226)).save(image_path) fake_resp = MagicMock(status_code=200) fake_resp.json.return_value = {'response': '✅ 無視覺異常'} with patch('services.ollama_service.resolve_ollama_host', return_value='http://test:11434'), \ patch('services.ollama_service.requests.post', return_value=fake_resp) as mock_post: svc = PPTVisionService() result = svc.check_image(str(image_path)) payload = mock_post.call_args.kwargs['json'] encoded = base64.b64decode(payload['images'][0]) assert result.success is True assert encoded.startswith(b'\xff\xd8') assert len(encoded) < image_path.stat().st_size assert payload['options']['num_predict'] == 256 assert payload['keep_alive'] == '5m' def test_issues_detected(fake_image, monkeypatch): """minicpm-v 回多個 ⚠️ marker → issues_found 應含解析的問題""" monkeypatch.setenv('PPT_VISION_ENABLED', 'true') from services.ppt_vision_service import PPTVisionService fake_resp = MagicMock(status_code=200) fake_resp.json.return_value = { 'response': '⚠️ 圖表被切掉:右側長條圖超出邊界\n' '⚠️ 文字溢出:商品標題被遮擋\n' '其他無問題' } with patch('services.ollama_service.resolve_ollama_host', return_value='http://test:11434'), \ patch('services.ollama_service.requests.post', return_value=fake_resp): svc = PPTVisionService() result = svc.check_image(fake_image) assert result.success is True assert len(result.issues_found) == 2 assert any('圖表被切掉' in i for i in result.issues_found) assert any('文字溢出' in i for i in result.issues_found) assert result.confidence > 0.5 def test_http_500_marks_unhealthy(fake_image, monkeypatch): """HTTP 500 → success=False + mark_unhealthy 被呼叫""" monkeypatch.setenv('PPT_VISION_ENABLED', 'true') from services.ppt_vision_service import PPTVisionService fake_resp = MagicMock(status_code=500) fake_resp.text = 'oops' with patch('services.ollama_service.resolve_ollama_host', return_value='http://test:11434'), \ patch('services.ollama_service.mark_unhealthy') as mock_mark, \ patch('services.ollama_service.requests.post', return_value=fake_resp): svc = PPTVisionService() result = svc.check_image(fake_image) assert result.success is False assert 'HTTP 500' in (result.error or '') mock_mark.assert_called_once_with('http://test:11434') def test_http_500_preserves_error_when_mark_unhealthy_fails(fake_image, monkeypatch, caplog): monkeypatch.setenv('PPT_VISION_ENABLED', 'true') from services.ppt_vision_service import PPTVisionService fake_resp = MagicMock(status_code=500) fake_resp.text = 'oops' def broken_mark(_host): raise RuntimeError('mark failed') caplog.set_level(logging.DEBUG, logger="services.ollama_service") with patch('services.ollama_service.resolve_ollama_host', return_value='http://test:11434'), \ patch('services.ollama_service.mark_unhealthy', side_effect=broken_mark), \ patch('services.ollama_service.requests.post', return_value=fake_resp): svc = PPTVisionService() result = svc.check_image(fake_image) assert result.success is False assert 'HTTP 500' in (result.error or '') assert 'mark_unhealthy failed' in caplog.text def test_timeout_returns_failure(fake_image, monkeypatch): monkeypatch.setenv('PPT_VISION_ENABLED', 'true') from services.ppt_vision_service import PPTVisionService import requests with patch('services.ollama_service.resolve_ollama_host', return_value='http://test:11434'), \ patch('services.ollama_service.mark_unhealthy'), \ patch('services.ollama_service.requests.post', side_effect=requests.Timeout('60s')): svc = PPTVisionService() result = svc.check_image(fake_image) assert result.success is False assert 'timeout' in (result.error or '').lower()