304 lines
12 KiB
Python
304 lines
12 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_detail_table_compacts_short_columns_and_stretches_title(self):
|
|
from PySide6.QtWidgets import QHeaderView
|
|
|
|
panel = self._panel()
|
|
header = panel._table.horizontalHeader()
|
|
|
|
self.assertEqual(header.sectionResizeMode(0), QHeaderView.Fixed)
|
|
self.assertEqual(header.sectionResizeMode(2), QHeaderView.Fixed)
|
|
self.assertEqual(header.sectionResizeMode(4), QHeaderView.Fixed)
|
|
self.assertEqual(header.sectionResizeMode(1), QHeaderView.Stretch)
|
|
self.assertLessEqual(panel._table.columnWidth(0), 56)
|
|
self.assertLessEqual(panel._table.columnWidth(2), 100)
|
|
self.assertLessEqual(panel._table.columnWidth(4), 90)
|
|
# §19.26: 衣服图 / 结果·原因 再收窄(仍可手拖)
|
|
self.assertLessEqual(panel._table.columnWidth(3), 90)
|
|
self.assertLessEqual(panel._table.columnWidth(5), 130)
|
|
|
|
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_image_model_combo_filters_configured_title_model(self):
|
|
"""§19.29: 图片 AI 模型下拉不显示 app_config.title_model 同名模型。"""
|
|
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": "GPT Image 2", "url": "https://r/v1/images/edits",
|
|
"model": "gpt-image", "api_key": "sk-y", "api_type": "images_edits"},
|
|
{"name": "Nano Banana 2", "url": "https://r/v1/chat/completions",
|
|
"model": "nano", "api_key": "sk-z", "api_type": "auto"},
|
|
]
|
|
panel._title_model_name = "GPT-5.5 文本"
|
|
|
|
panel._fill_model_combo(panel._model_combo, "GPT-5.5 文本")
|
|
|
|
names = [panel._model_combo.itemText(i) for i in range(panel._model_combo.count())]
|
|
self.assertNotIn("GPT-5.5 文本", names)
|
|
self.assertEqual(names, ["GPT Image 2", "Nano Banana 2"])
|
|
self.assertEqual(panel._model_combo.currentText(), "GPT Image 2")
|
|
self.assertTrue(panel._model_combo.isEnabled())
|
|
self.assertEqual(panel._model_combo.currentData()["name"], "GPT Image 2")
|
|
|
|
def test_selected_model_config_uses_filtered_combo_data(self):
|
|
"""Filtered combo indices no longer match self._models; use item data."""
|
|
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": "GPT Image 2", "url": "https://r/v1/images/edits",
|
|
"model": "gpt-image", "api_key": "sk-y", "api_type": "images_edits"},
|
|
{"name": "Nano Banana 2", "url": "https://r/v1/chat/completions",
|
|
"model": "nano", "api_key": "sk-z", "api_type": "auto"},
|
|
]
|
|
panel._title_model_name = "GPT-5.5 文本"
|
|
|
|
panel._fill_model_combo(panel._model_combo, "Nano Banana 2")
|
|
config = panel._selected_model_config()
|
|
|
|
self.assertEqual(config.model, "nano")
|
|
|
|
def test_image_model_combo_keeps_title_model_available_for_title_generation(self):
|
|
"""Filtering the image dropdown must not remove the title model from self._models."""
|
|
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": "GPT Image 2", "url": "https://r/v1/images/edits",
|
|
"model": "gpt-image", "api_key": "sk-y", "api_type": "images_edits"},
|
|
]
|
|
panel._title_model_name = "GPT-5.5 文本"
|
|
|
|
panel._fill_model_combo(panel._model_combo, "GPT Image 2")
|
|
config, error = panel._find_title_model()
|
|
|
|
self.assertIsNone(error)
|
|
self.assertIsInstance(config, AiModelConfig)
|
|
self.assertEqual(config.model, "gpt-5.5")
|
|
|
|
def test_image_model_combo_only_title_model_shows_placeholder(self):
|
|
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"},
|
|
]
|
|
panel._title_model_name = "GPT-5.5 文本"
|
|
|
|
panel._fill_model_combo(panel._model_combo, "")
|
|
|
|
self.assertEqual(panel._model_combo.count(), 1)
|
|
self.assertIn("未配置可用图片模型", panel._model_combo.currentText())
|
|
self.assertFalse(panel._model_combo.isEnabled())
|
|
self.assertIsNone(panel._model_combo.currentData())
|
|
|
|
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()
|