Files
cmbot/tests/test_ai_outfit_panel.py
T
adminandClaude Opus 4.8 52bc6c0c50 feat(ai-outfit): 标题模型命中图片模型时开跑前拦截 (§19.21)
补齐缺口③:title_model 名字命中了、但那条是纯图片接口
(api_type=images/images_edits,如误指 GPT Image 2)时,过去会开跑后
逐行失败;现在 _find_title_model 在 api_config_errors 后用 detect_api_type
判类型,命中图片接口即返回错误「『{名字}』是图片模型({api_type}),不能
生成文字标题,请改选 chat/gemini 文本模型」→ _resolve_title_model_config
弹窗 + _start_title 中止,体验对齐①没配模型/②名字找不到。

边界:api_type=chat 但实际返图的模型(如 Nano Banana)类型上判不出,仍
运行时由「未找到文字标题」逐行暴露,固有限制。

测试:_find_title_model 命中 images_edits / URL 自动判图片 两例报错;
既有命中(chat)/缺失用例保持绿。离屏冒烟:title_model 指向图片模型 →
点「生成标题」前即弹窗中止、线程不启动。全套 py37 通过
(test_config_service 的 ai_models.json 顺序失败属并行 §19.13,与本改动无关)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:59:04 +08:00

212 lines
7.7 KiB
Python

"""Smoke tests for AI outfit panel defaults (offscreen Qt)."""
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
import services.file_service as fs
from services.config_service import DEFAULT_CONFIG
class TestAiOutfitPanelDefaults(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
self._env = os.environ.get("CMBOT_DATA_DIR")
os.environ["CMBOT_DATA_DIR"] = str(self.tmp / "data")
self.app_dir = self.tmp / "app"
self.app_dir.mkdir(parents=True)
self._orig_get_app_dir = fs.get_app_dir
fs.get_app_dir = lambda: self.app_dir
self.app = self._app()
def tearDown(self):
fs.get_app_dir = self._orig_get_app_dir
if self._env is None:
os.environ.pop("CMBOT_DATA_DIR", None)
else:
os.environ["CMBOT_DATA_DIR"] = self._env
shutil.rmtree(str(self.tmp), ignore_errors=True)
def _app(self):
from PySide6.QtWidgets import QApplication
app = QApplication.instance()
if app is None:
app = QApplication([])
return app
def _panel(self):
from app.widgets.ai_outfit_panel import AiOutfitPanel
return AiOutfitPanel()
def test_empty_config_defaults_to_outfit_output_dir(self):
panel = self._panel()
cfg = dict(DEFAULT_CONFIG)
cfg["outfit_output_dir"] = ""
panel.apply_config(cfg)
self.assertEqual(Path(panel._output_edit.text()), self.app_dir / "穿搭图片")
def test_saved_outfit_output_dir_takes_precedence(self):
panel = self._panel()
saved = self.tmp / "custom-output"
cfg = dict(DEFAULT_CONFIG)
cfg["outfit_output_dir"] = str(saved)
panel.apply_config(cfg)
self.assertEqual(Path(panel._output_edit.text()), saved)
def test_generation_setting_label_is_image_concurrency(self):
from PySide6.QtWidgets import QLabel
panel = self._panel()
labels = [label.text() for label in panel.findChildren(QLabel)]
self.assertIn("图片并发数", labels)
def test_title_group_present_no_dropdown_preview_removed(self):
"""标题生成组存在;无标题模型下拉(配置定名 §17.3);预览块已移除。"""
from PySide6.QtWidgets import QLabel
panel = self._panel()
panel.apply_config(dict(DEFAULT_CONFIG))
labels = [label.text() for label in panel.findChildren(QLabel)]
self.assertIn("标题生成提示词", labels)
self.assertNotIn("标题模型", labels) # 下拉已去掉(§17.3 配置定名)
self.assertNotIn("最终生成要求预览", labels)
self.assertFalse(hasattr(panel, "_title_model_combo"))
self.assertFalse(hasattr(panel, "_preview_view"))
self.assertFalse(hasattr(panel, "_sample_combo"))
self.assertTrue(hasattr(panel, "_title_btn"))
def test_title_prompt_defaults_loaded(self):
from services.config_service import DEFAULT_TITLE_PROMPT
panel = self._panel()
panel.apply_config(dict(DEFAULT_CONFIG))
self.assertEqual(panel._title_prompt_edit.toPlainText(), DEFAULT_TITLE_PROMPT)
def test_prompt_buttons_save_row_insert_title_and_preview(self):
"""§19.20: 按钮行含「保存」;编辑框下方保留「插入标题」+「预览最终提示词」。"""
from PySide6.QtWidgets import QPushButton
panel = self._panel()
panel.apply_config(dict(DEFAULT_CONFIG))
texts = [b.text() for b in panel.findChildren(QPushButton)]
self.assertIn("保存", texts)
self.assertIn("插入标题", texts)
self.assertIn("预览最终提示词", texts)
self.assertNotIn("保存话术", texts) # 旧按钮已改名上移
def test_preview_dialog_renders_selected_row(self):
"""§7.3: 预览弹窗按选中数据行替换 {title} 并附加输出要求。"""
from openpyxl import Workbook
from app.widgets.ai_outfit_panel import _OutfitPreviewDialog
xlsx = self.tmp / "rows.xlsx"
wb = Workbook(); ws = wb.active
ws.append(["标题", "货号", "原始图片路径", "结果", "状态", "原因"])
ws.append(["纯棉短袖", "TY001", r"D:\img\a.png", "", "", ""])
wb.save(str(xlsx)); wb.close()
panel = self._panel()
panel.apply_config(dict(DEFAULT_CONFIG))
panel._excel_edit.setText(str(xlsx))
panel._prompt_edit.setPlainText("为 {title} 生成")
panel._set_combo(panel._resolution, "1K")
dialog = _OutfitPreviewDialog(panel) # __init__ fills rows + refresh
# Row dropdown picked up the valid row; pick it.
self.assertEqual(dialog._combo.count(), 1)
dialog._combo.setCurrentIndex(0)
text = dialog._view.toPlainText()
self.assertTrue(text.startswith("为 纯棉短袖 生成"))
self.assertIn("批量生成输出要求", text)
self.assertIn("参考解析度:1K", text)
dialog.deleteLater()
def test_find_title_model_resolves_by_config_name(self):
"""title_model 名字命中 ai_models.json → 返回 AiModelConfig。"""
from services.ai_image_service import AiModelConfig
panel = self._panel()
panel._models = [
{"name": "GPT-5.5 文本", "url": "https://r/v1/chat/completions",
"model": "gpt-5.5", "api_key": "sk-x", "api_type": "chat"},
{"name": "图片模型", "url": "https://r/v1/images/edits",
"model": "img", "api_key": "sk-y", "api_type": "images_edits"},
]
panel._title_model_name = "GPT-5.5 文本"
config, error = panel._find_title_model()
self.assertIsNone(error)
self.assertIsInstance(config, AiModelConfig)
self.assertEqual(config.model, "gpt-5.5")
def test_find_title_model_missing_name_errors(self):
panel = self._panel()
panel._models = [{"name": "别的模型", "url": "https://r/v1/chat/completions",
"model": "m", "api_key": "sk-x", "api_type": "chat"}]
panel._title_model_name = "GPT-5.5 文本"
config, error = panel._find_title_model()
self.assertIsNone(config)
self.assertIn("GPT-5.5 文本", error)
def test_find_title_model_no_models_errors(self):
panel = self._panel()
panel._models = []
panel._title_model_name = "GPT-5.5 文本"
config, error = panel._find_title_model()
self.assertIsNone(config)
self.assertTrue(error)
def test_find_title_model_image_model_rejected_upfront(self):
"""§19.21: title_model 命中图片模型(images_edits)→ 开跑前报错。"""
panel = self._panel()
panel._models = [
{"name": "GPT-5.5 文本", "url": "https://r/v1/images/edits",
"model": "gpt-image", "api_key": "sk-x", "api_type": "images_edits"},
]
panel._title_model_name = "GPT-5.5 文本"
config, error = panel._find_title_model()
self.assertIsNone(config)
self.assertIn("图片模型", error)
def test_find_title_model_image_model_by_url_autodetect_rejected(self):
"""api_type=auto 但 URL 是 images 端点 → 仍按图片模型拦截。"""
panel = self._panel()
panel._models = [
{"name": "标题模型", "url": "https://r/v1/images/generations",
"model": "x", "api_key": "sk-x", "api_type": "auto"},
]
panel._title_model_name = "标题模型"
config, error = panel._find_title_model()
self.assertIsNone(config)
self.assertIn("图片模型", error)
if __name__ == "__main__":
unittest.main()