"""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()