Files
cmbot/tests/test_config_service.py
T
adminandClaude Opus 4.8 25b18ea342 feat(ai-outfit): multiple named prompt templates (§19.8)
- config_service: outfit_prompts.json load/save (utf-8-sig read, no-BOM write,
  corrupt/empty -> migrate legacy outfit_prompt.txt or seed 默认; always >=1;
  _normalize_prompts drops invalid). app_config gains outfit_prompt_name.
- ai_outfit_panel: 通用话术 group gets a template dropdown + 新建/另存为/重命名/
  删除 (keeps 插入标题/保存). Switching loads the template + refreshes preview;
  dirty edits prompt Save/Discard/Cancel before switching/new/save-as (cancel
  reverts the combo); unique names; can't delete the last one; selection
  persisted via config_changed; 开始生成 stores the editor into the template.
- tests: +6 config cases (14 total); offscreen CRUD/dirty/rename/delete verified;
  full suite (12 files) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:12:34 +08:00

117 lines
4.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for AI-outfit config helpers in config_service (no GUI)."""
import json
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
import services.config_service as cs
class TestOutfitConfigHelpers(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)
self.config_dir = self.tmp / "config"
self.config_dir.mkdir(parents=True, exist_ok=True)
def tearDown(self):
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)
# -- ai_models.json -------------------------------------------------
def test_load_ai_models_missing_returns_empty(self):
self.assertEqual(cs.load_ai_models(), [])
def test_load_ai_models_object_with_models_list(self):
(self.config_dir / "ai_models.json").write_text(
json.dumps({"models": [{"name": "m1", "url": "https://x"}]}),
encoding="utf-8")
models = cs.load_ai_models()
self.assertEqual(len(models), 1)
self.assertEqual(models[0]["name"], "m1")
def test_load_ai_models_bare_list(self):
(self.config_dir / "ai_models.json").write_text(
json.dumps([{"name": "a"}, {"name": "b"}]), encoding="utf-8")
self.assertEqual(len(cs.load_ai_models()), 2)
def test_load_ai_models_corrupt_returns_empty(self):
(self.config_dir / "ai_models.json").write_text("{ not json", encoding="utf-8")
self.assertEqual(cs.load_ai_models(), [])
def test_load_ai_models_tolerates_bom(self):
(self.config_dir / "ai_models.json").write_bytes(
"".encode("utf-8") + json.dumps([{"name": "z"}]).encode("utf-8"))
self.assertEqual(cs.load_ai_models()[0]["name"], "z")
# -- outfit_prompt.txt ----------------------------------------------
def test_prompt_default_when_missing(self):
self.assertEqual(cs.load_outfit_prompt(), cs.DEFAULT_OUTFIT_PROMPT)
def test_prompt_save_then_load_roundtrip(self):
cs.save_outfit_prompt("hello {title} {product_id}")
self.assertEqual(cs.load_outfit_prompt(), "hello {title} {product_id}")
def test_prompt_saved_without_bom(self):
cs.save_outfit_prompt("abc")
raw = (self.config_dir / "outfit_prompt.txt").read_bytes()
self.assertFalse(raw.startswith(b"\xef\xbb\xbf"))
# -- outfit_prompts.json (multi templates, §7.2) --------------------
def test_prompts_seed_default_when_missing(self):
prompts = cs.load_outfit_prompts()
self.assertEqual(len(prompts), 1)
self.assertEqual(prompts[0]["name"], cs.DEFAULT_OUTFIT_PROMPT_NAME)
self.assertEqual(prompts[0]["text"], cs.DEFAULT_OUTFIT_PROMPT)
def test_prompts_migrate_legacy_txt(self):
(self.config_dir / "outfit_prompt.txt").write_text(
"旧话术 {title}", encoding="utf-8")
prompts = cs.load_outfit_prompts()
self.assertEqual(len(prompts), 1)
self.assertEqual(prompts[0]["name"], "默认")
self.assertEqual(prompts[0]["text"], "旧话术 {title}")
def test_prompts_roundtrip_and_no_bom(self):
data = [{"name": "A", "text": "甲 {title}"}, {"name": "B", "text": "乙"}]
cs.save_outfit_prompts(data)
self.assertEqual(cs.load_outfit_prompts(), data)
raw = (self.config_dir / "outfit_prompts.json").read_bytes()
self.assertFalse(raw.startswith(b"\xef\xbb\xbf"))
def test_prompts_corrupt_falls_back(self):
(self.config_dir / "outfit_prompts.json").write_text("{ bad", encoding="utf-8")
prompts = cs.load_outfit_prompts()
self.assertEqual(len(prompts), 1)
self.assertEqual(prompts[0]["name"], cs.DEFAULT_OUTFIT_PROMPT_NAME)
def test_prompts_drop_invalid_entries(self):
import json
(self.config_dir / "outfit_prompts.json").write_text(
json.dumps([{"name": "", "text": "x"}, {"name": "ok", "text": "y"},
{"name": "bad", "text": 5}, "nope"]), encoding="utf-8")
prompts = cs.load_outfit_prompts()
self.assertEqual(prompts, [{"name": "ok", "text": "y"}])
def test_prompts_tolerate_bom(self):
import json
(self.config_dir / "outfit_prompts.json").write_bytes(
"".encode("utf-8") + json.dumps([{"name": "z", "text": "t"}]).encode("utf-8"))
self.assertEqual(cs.load_outfit_prompts()[0]["name"], "z")
if __name__ == "__main__":
unittest.main()