Files
cmbot/tests/test_config_service.py
2026-06-23 17:51:46 +08:00

267 lines
11 KiB
Python
Raw Permalink 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
import services.file_service as fs
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)
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
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)
# -- 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")
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", "GPT-5.5 文本"],
)
self.assertTrue(all(m.get("api_key") == "" for m in models))
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())
def test_load_ai_models_appends_missing_title_model_from_factory(self):
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
(self.factory_config_dir / "ai_models.json").write_text(
json.dumps({
"models": [
{"name": "GPT Image 2", "api_key": ""},
{"name": "GPT-5.5 文本", "api_type": "chat", "api_key": ""},
]
}),
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([m["name"] for m in models], ["user", "GPT-5.5 文本"])
self.assertEqual(models[0]["api_key"], "keep")
self.assertEqual(models[1]["api_key"], "")
saved = json.loads((self.config_dir / "ai_models.json").read_text(encoding="utf-8"))
self.assertEqual([m["name"] for m in saved["models"]], ["user", "GPT-5.5 文本"])
def test_load_ai_models_does_not_duplicate_existing_title_model(self):
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
(self.factory_config_dir / "ai_models.json").write_text(
json.dumps({"models": [{"name": "GPT-5.5 文本", "api_key": ""}]}),
encoding="utf-8",
)
(self.config_dir / "ai_models.json").write_text(
json.dumps({"models": [{"name": "GPT-5.5 文本", "api_key": "keep"}]}),
encoding="utf-8",
)
models = cs.load_ai_models()
self.assertEqual(len(models), 1)
self.assertEqual(models[0]["api_key"], "keep")
def test_load_ai_models_factory_without_title_model_does_not_append(self):
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
(self.factory_config_dir / "ai_models.json").write_text(
json.dumps({"models": [{"name": "other", "api_key": ""}]}),
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([m["name"] for m in models], ["user"])
# -- outfit_prompt.txt ----------------------------------------------
def test_prompt_default_when_missing(self):
self.assertEqual(cs.load_outfit_prompt(), cs.DEFAULT_OUTFIT_PROMPT)
def test_prompt_seeds_missing_user_file_from_factory_template(self):
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
(self.factory_config_dir / "outfit_prompt.txt").write_text(
"factory {title}", encoding="utf-8")
self.assertEqual(cs.load_outfit_prompt(), "factory {title}")
self.assertEqual(
(self.config_dir / "outfit_prompt.txt").read_text(encoding="utf-8"),
"factory {title}",
)
def test_prompt_does_not_overwrite_existing_user_file(self):
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
(self.factory_config_dir / "outfit_prompt.txt").write_text(
"factory", encoding="utf-8")
(self.config_dir / "outfit_prompt.txt").write_text("user", encoding="utf-8")
self.assertEqual(cs.load_outfit_prompt(), "user")
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")
# -- title_prompt.txt -----------------------------------------------
def test_title_prompt_default_when_missing(self):
self.assertEqual(cs.load_title_prompt(), cs.DEFAULT_TITLE_PROMPT)
def test_title_prompt_seeds_missing_user_file_from_factory_template(self):
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
(self.factory_config_dir / "title_prompt.txt").write_text(
"factory title prompt", encoding="utf-8")
self.assertEqual(cs.load_title_prompt(), "factory title prompt")
self.assertEqual(
(self.config_dir / "title_prompt.txt").read_text(encoding="utf-8"),
"factory title prompt",
)
def test_title_prompt_does_not_overwrite_existing_user_file(self):
self.factory_config_dir.mkdir(parents=True, exist_ok=True)
(self.factory_config_dir / "title_prompt.txt").write_text(
"factory", encoding="utf-8")
(self.config_dir / "title_prompt.txt").write_text("user", encoding="utf-8")
self.assertEqual(cs.load_title_prompt(), "user")
if __name__ == "__main__":
unittest.main()