Files
cmbot/tests/test_ai_outfit_panel.py
T
adminandClaude Opus 4.8 81351c77b5 feat(ai-outfit): 话术按钮行微调 + 保留插入标题 + 最终提示词预览改弹窗 (§19.20)
话术组小改版(保留 {title} 占位符、不自动前置):
- 「保存话术」→「保存」,从编辑框下方上移到模板按钮行「重命名」之后
  (行变 新建/另存为/重命名/保存/删除)
- 保留「插入标题」按钮 + {title} 占位符(用户自定标题位置,不与旧话术重复)
- 最终提示词预览改按需弹窗:编辑框下方「预览最终提示词」按钮 → 非模态
  _OutfitPreviewDialog,含数据行下拉(read_all_rows,状态无关)+ 只读替换后
  提示词(含 §7.1 输出要求);话术/分辨率/数据行变化时实时刷新
- 左栏空间理由:标题生成组 + 话术组并存,常驻预览会逼出滚动;弹窗零常驻高度

render_prompt 与 DEFAULT_OUTFIT_PROMPT 不改(纯面板改动)。
测试:面板断言按钮行含「保存」、保留「插入标题」+「预览最终提示词」、
预览弹窗按选中行替换 {title} 并附输出要求。全套 py37 通过
(test_config_service 的 ai_models.json 顺序失败属并行 §19.13,与本改动无关)。

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

184 lines
6.6 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)
if __name__ == "__main__":
unittest.main()