191 lines
7.0 KiB
Python
191 lines
7.0 KiB
Python
"""
|
||
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()
|