Files

387 lines
16 KiB
Python
Raw Permalink Normal View History

2026-06-22 17:49:59 +08:00
"""Smoke tests for AI outfit panel defaults (offscreen Qt)."""
import os
import shutil
import sys
import tempfile
2026-07-04 16:47:12 +08:00
import time
2026-06-22 17:49:59 +08:00
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)
2026-06-23 16:58:16 +08:00
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)
2026-06-23 16:58:16 +08:00
2026-07-04 17:59:25 +08:00
def test_detail_status_column_uses_semantic_colors(self):
from core.models import OutfitTask
panel = self._panel()
task = OutfitTask(row_index=2, title="标题", product_id="SKU", garment_path="a.png")
panel._populate_table([task])
expected = {
"待处理": "#666666",
"生成中": "#0078d4",
"完成": "#107c10",
"失败": "#c42b1c",
"跳过": "#8a8a8a",
"停止": "#8a8a8a",
}
for status, color in expected.items():
panel._set_cell(0, 4, status)
self.assertEqual(panel._table.item(0, 4).foreground().color().name(), color)
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)
2026-07-04 16:47:12 +08:00
def test_title_wait_label_defaults_hidden(self):
panel = self._panel()
self.assertTrue(hasattr(panel, "_title_wait_label"))
self.assertTrue(panel._title_wait_label.isHidden())
self.assertEqual(panel._title_wait_label.text(), "")
def test_title_wait_timer_formats_elapsed_and_limit(self):
panel = self._panel()
panel._start_title_wait_timer(600)
panel._title_wait_started_at = time.monotonic() - 95
panel._refresh_title_wait_label()
self.assertFalse(panel._title_wait_label.isHidden())
self.assertEqual(panel._title_wait_label.text(), "等待中 01:35 / 10:00")
2026-07-04 17:04:23 +08:00
self.assertIn("#0078d4", panel._title_wait_label.styleSheet())
2026-07-04 16:47:12 +08:00
self.assertTrue(panel._title_wait_timer.isActive())
panel._reset_title_wait_label()
2026-07-04 17:04:23 +08:00
self.assertEqual(panel._title_wait_label.styleSheet(), "")
2026-07-04 16:47:12 +08:00
def test_title_wait_limit_uses_model_timeout_override(self):
from services.ai_image_service import AiModelConfig
panel = self._panel()
default_cfg = AiModelConfig(
url="https://r/v1/chat/completions", model="m", api_key="sk",
api_type="chat", timeout_seconds=0, connect_timeout_seconds=30)
override_cfg = AiModelConfig(
url="https://r/v1/chat/completions", model="m", api_key="sk",
api_type="chat", timeout_seconds=120, connect_timeout_seconds=30)
self.assertEqual(panel._title_wait_limit_for_model(default_cfg), 600)
self.assertEqual(panel._title_wait_limit_for_model(override_cfg), 120)
def test_title_progress_switches_wait_label_to_writing(self):
from core.models import OutfitTask, TitleResult
panel = self._panel()
task = OutfitTask(row_index=2, title="", product_id="SKU", garment_path="a.png")
panel._populate_table([task])
panel._start_title_wait_timer(600)
panel._on_title_progress(
1, 3, TitleResult(task=task, success=True, generated_title="新标题"))
self.assertFalse(panel._title_wait_timer.isActive())
self.assertFalse(panel._title_wait_label.isHidden())
self.assertEqual(panel._title_wait_label.text(), "写入中 1/3")
2026-07-04 17:04:23 +08:00
self.assertIn("#107c10", panel._title_wait_label.styleSheet())
panel._reset_title_wait_label()
def test_title_wait_finish_uses_success_and_error_colors(self):
panel = self._panel()
panel._finish_title_wait("完成 3 条", "success")
self.assertFalse(panel._title_wait_label.isHidden())
self.assertIn("#107c10", panel._title_wait_label.styleSheet())
panel._finish_title_wait("生成失败", "error")
self.assertIn("#c42b1c", panel._title_wait_label.styleSheet())
2026-07-04 16:47:12 +08:00
panel._reset_title_wait_label()
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")
2026-06-24 08:58:40 +08:00
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)
2026-06-22 17:49:59 +08:00
if __name__ == "__main__":
unittest.main()