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>
This commit is contained in:
@@ -68,6 +68,49 @@ class TestOutfitConfigHelpers(unittest.TestCase):
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user