feat(p26): PPT 視覺審核 daily 22:00 cron — minicpm-v 自動掃當天新生 .pptx
All checks were successful
CD Pipeline / deploy (push) Successful in 2m54s
All checks were successful
CD Pipeline / deploy (push) Successful in 2m54s
Operation Ollama-First v5.0 / Phase 26 — PPT 自我審視整合 services/ppt_vision_service.py 擴充: - check_ppt_file(pptx_path, max_slides=5) — 整檔視覺檢查 • LibreOffice headless 轉每張 slide 為 png • 對前 N 張跑 check_image • 彙總 issues + 平均 confidence • fail-safe:LibreOffice 不在 / 轉檔失敗 → 回 skip 不阻擋 - audit_recent_ppts(reports_dir, hours=24, max_files=10) • 掃 reports/ 過去 24h 新生 .pptx(getmtime filter) • 對每個檔跑 check_ppt_file • 彙總總 issues - push_ppt_audit_to_telegram(summary) • 有 issues 才推 Telegram(避免「無問題」洗版) • 每檔最多 3 張 slide / 每張 2 個 issue 列出 run_scheduler.py — 每日 22:00 cron - run_ppt_vision_audit task wrapper - PPT_VISION_ENABLED=false 時 service 內部 skip(不打 LLM) 設計哲學: 不動既有 5 個 prs.save() 呼叫點(risk 高)→ 改寫獨立 daily cron 集中處理 零侵入 PPT 生成主流程 / 零 risk regression / feature flag OFF 預設 部署需求: LibreOffice headless(apt install libreoffice)— 不在則 cron task 自動 skip + log tests/test_ppt_vision_audit.py (9 tests 全綠) - flag OFF skip / 目錄不存在 / 無 .pptx - 舊檔(>hours)filter / LibreOffice 不在 fail-safe - check_ppt_file flag/missing 容錯 - Telegram 推播:無 issues 不推 / 有 issues 推 regression: ppt_vision_service 既有 6 tests 全綠 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
149
tests/test_ppt_vision_audit.py
Normal file
149
tests/test_ppt_vision_audit.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
tests/test_ppt_vision_audit.py
|
||||
─────────────────────────────────────────────────────────────────
|
||||
Operation Ollama-First v5.0 / Phase 26 — PPT 視覺審核整合驗證
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_env(monkeypatch):
|
||||
monkeypatch.delenv('PPT_VISION_ENABLED', raising=False)
|
||||
yield
|
||||
|
||||
|
||||
def test_audit_disabled_by_default():
|
||||
"""flag OFF 時直接 skip(errors 含 disabled marker)"""
|
||||
from services.ppt_vision_service import audit_recent_ppts
|
||||
|
||||
summary = audit_recent_ppts(reports_dir='/tmp')
|
||||
assert summary['total_issues'] == 0
|
||||
assert any('PPT_VISION_ENABLED=false' in e for e in summary['errors'])
|
||||
|
||||
|
||||
def test_audit_missing_dir(monkeypatch):
|
||||
monkeypatch.setenv('PPT_VISION_ENABLED', 'true')
|
||||
from services.ppt_vision_service import audit_recent_ppts
|
||||
|
||||
summary = audit_recent_ppts(reports_dir='/tmp/nonexistent_xyz_ppts')
|
||||
assert summary['total_issues'] == 0
|
||||
assert any('not found' in e for e in summary['errors'])
|
||||
|
||||
|
||||
def test_audit_no_recent_ppts(monkeypatch):
|
||||
"""目錄存在但無 .pptx → 0 audit"""
|
||||
monkeypatch.setenv('PPT_VISION_ENABLED', 'true')
|
||||
from services.ppt_vision_service import audit_recent_ppts
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# 放一個 .txt 干擾
|
||||
with open(os.path.join(tmpdir, 'not_ppt.txt'), 'w') as f:
|
||||
f.write('hello')
|
||||
summary = audit_recent_ppts(reports_dir=tmpdir)
|
||||
|
||||
assert summary['audited_files'] == []
|
||||
assert summary['total_issues'] == 0
|
||||
|
||||
|
||||
def test_audit_filter_old_files(monkeypatch):
|
||||
"""超過 hours 視窗的舊檔不應 audit"""
|
||||
monkeypatch.setenv('PPT_VISION_ENABLED', 'true')
|
||||
from services.ppt_vision_service import audit_recent_ppts
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
old_path = os.path.join(tmpdir, 'old.pptx')
|
||||
with open(old_path, 'w') as f:
|
||||
f.write('fake')
|
||||
# 改 mtime 到 2 天前
|
||||
old_time = __import__('time').time() - 2 * 86400
|
||||
os.utime(old_path, (old_time, old_time))
|
||||
|
||||
summary = audit_recent_ppts(reports_dir=tmpdir, hours=24)
|
||||
|
||||
assert summary['audited_files'] == []
|
||||
|
||||
|
||||
def test_check_ppt_file_libreoffice_not_installed(monkeypatch):
|
||||
"""LibreOffice 不在 → fail-safe skip 不 raise"""
|
||||
monkeypatch.setenv('PPT_VISION_ENABLED', 'true')
|
||||
from services.ppt_vision_service import PPTVisionService
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.pptx', delete=False) as f:
|
||||
f.write(b'fake pptx')
|
||||
ppt_path = f.name
|
||||
|
||||
try:
|
||||
with patch('services.ppt_vision_service.subprocess.run',
|
||||
side_effect=FileNotFoundError('libreoffice')):
|
||||
svc = PPTVisionService()
|
||||
result = svc.check_ppt_file(ppt_path)
|
||||
|
||||
assert result['success'] is False
|
||||
assert 'libreoffice not installed' in (result['error'] or '')
|
||||
finally:
|
||||
os.unlink(ppt_path)
|
||||
|
||||
|
||||
def test_check_ppt_file_disabled():
|
||||
"""flag OFF check_ppt_file 也 skip"""
|
||||
from services.ppt_vision_service import PPTVisionService
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.pptx', delete=False) as f:
|
||||
f.write(b'fake pptx')
|
||||
ppt_path = f.name
|
||||
|
||||
try:
|
||||
svc = PPTVisionService()
|
||||
result = svc.check_ppt_file(ppt_path)
|
||||
assert result['success'] is False
|
||||
assert 'PPT_VISION_ENABLED=false' in (result['error'] or '')
|
||||
finally:
|
||||
os.unlink(ppt_path)
|
||||
|
||||
|
||||
def test_check_ppt_file_missing(monkeypatch):
|
||||
monkeypatch.setenv('PPT_VISION_ENABLED', 'true')
|
||||
from services.ppt_vision_service import PPTVisionService
|
||||
|
||||
svc = PPTVisionService()
|
||||
result = svc.check_ppt_file('/tmp/this_pptx_does_not_exist_xyz.pptx')
|
||||
assert result['success'] is False
|
||||
assert 'not found' in (result['error'] or '')
|
||||
|
||||
|
||||
def test_push_telegram_skips_when_no_issues():
|
||||
"""無 issues 不推(避免靜默無問題洗版)"""
|
||||
from services.ppt_vision_service import push_ppt_audit_to_telegram
|
||||
|
||||
summary = {'total_issues': 0, 'audited_files': [{'path': 'a.pptx', 'issues': 0}]}
|
||||
assert push_ppt_audit_to_telegram(summary) is False
|
||||
|
||||
|
||||
def test_push_telegram_with_issues():
|
||||
"""有 issues 才推 Telegram"""
|
||||
from services.ppt_vision_service import push_ppt_audit_to_telegram
|
||||
|
||||
summary = {
|
||||
'total_issues': 3,
|
||||
'audited_files': [
|
||||
{
|
||||
'path': '/tmp/test.pptx',
|
||||
'slides_checked': 2,
|
||||
'issues': 3,
|
||||
'issues_by_slide': [(1, ['⚠️ 圖表被切', '⚠️ 文字溢出']), (2, ['⚠️ 配色衝突'])],
|
||||
},
|
||||
],
|
||||
}
|
||||
with patch('services.telegram_templates._send_telegram_raw') as mock_send:
|
||||
result = push_ppt_audit_to_telegram(summary)
|
||||
|
||||
assert result is True
|
||||
mock_send.assert_called_once()
|
||||
msg = mock_send.call_args[0][0]
|
||||
assert 'PPT 視覺審核' in msg
|
||||
assert '3 issues' in msg or '3' in msg
|
||||
Reference in New Issue
Block a user