2026-06-22 08:52:37 +08:00
|
|
|
|
"""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
|
2026-06-22 17:35:56 +08:00
|
|
|
|
import services.file_service as fs
|
2026-06-22 08:52:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-06-22 17:35:56 +08:00
|
|
|
|
self.app_dir = self.tmp / "app"
|
|
|
|
|
|
self.factory_config_dir = self.app_dir / "config"
|
|
|
|
|
|
self._orig_get_app_dir = fs.get_app_dir
|
|
|
|
|
|
fs.get_app_dir = lambda: self.app_dir
|
2026-06-22 08:52:37 +08:00
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
2026-06-22 17:35:56 +08:00
|
|
|
|
fs.get_app_dir = self._orig_get_app_dir
|
2026-06-22 08:52:37 +08:00
|
|
|
|
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")
|
|
|
|
|
|
|
2026-06-22 16:57:11 +08:00
|
|
|
|
def test_default_ai_models_template_is_loadable_and_has_no_keys(self):
|
|
|
|
|
|
template = Path(__file__).parent.parent / "packaging" / "default_config" / "ai_models.json"
|
|
|
|
|
|
shutil.copy2(str(template), str(self.config_dir / "ai_models.json"))
|
|
|
|
|
|
|
|
|
|
|
|
models = cs.load_ai_models()
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual([m["name"] for m in models], ["GPT Image 2", "Nano Banana 2"])
|
|
|
|
|
|
self.assertTrue(all(m.get("api_key") == "" for m in models))
|
|
|
|
|
|
|
2026-06-22 17:35:56 +08:00
|
|
|
|
def test_load_ai_models_seeds_missing_user_file_from_factory_template(self):
|
|
|
|
|
|
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
(self.factory_config_dir / "ai_models.json").write_text(
|
|
|
|
|
|
json.dumps({"models": [{"name": "factory", "url": "https://x"}]}),
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
models = cs.load_ai_models()
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(models[0]["name"], "factory")
|
|
|
|
|
|
user_file = self.config_dir / "ai_models.json"
|
|
|
|
|
|
self.assertTrue(user_file.exists())
|
|
|
|
|
|
copied = json.loads(user_file.read_text(encoding="utf-8"))
|
|
|
|
|
|
self.assertEqual(copied["models"][0]["name"], "factory")
|
|
|
|
|
|
|
|
|
|
|
|
def test_load_ai_models_does_not_overwrite_existing_user_file(self):
|
|
|
|
|
|
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
(self.factory_config_dir / "ai_models.json").write_text(
|
|
|
|
|
|
json.dumps({"models": [{"name": "factory"}]}), encoding="utf-8")
|
|
|
|
|
|
(self.config_dir / "ai_models.json").write_text(
|
|
|
|
|
|
json.dumps({"models": [{"name": "user", "api_key": "keep"}]}),
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
models = cs.load_ai_models()
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(models[0]["name"], "user")
|
|
|
|
|
|
kept = json.loads((self.config_dir / "ai_models.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
self.assertEqual(kept["models"][0]["api_key"], "keep")
|
|
|
|
|
|
|
|
|
|
|
|
def test_load_ai_models_missing_factory_template_returns_empty(self):
|
|
|
|
|
|
self.assertEqual(cs.load_ai_models(), [])
|
|
|
|
|
|
self.assertFalse((self.config_dir / "ai_models.json").exists())
|
|
|
|
|
|
|
2026-06-22 08:52:37 +08:00
|
|
|
|
# -- 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"))
|
|
|
|
|
|
|
2026-06-22 14:12:34 +08:00
|
|
|
|
# -- 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")
|
|
|
|
|
|
|
2026-06-22 08:52:37 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
unittest.main()
|