#ifndef NOMINMAX #define NOMINMAX #endif #include "config/ime_config.h" #include "tests/includes/test_framework.h" #include #include #include TEST_CASE(font_fallback_upgrade_preserves_legacy_fonts_and_order) { const std::string next = "[appearance]\nfallback_fonts = [\"Noto Sans SC\", \"Microsoft YaHei\"]\n"; const std::string old = "[appearance]\nfont = \"SimSun\"\ndefault_font = \"DengXian\"\n"; const auto migrated = MergeConfigIntoTemplate(next, old, old); REQUIRE(migrated.find("[\"SimSun\", \"DengXian\"]") != std::string::npos); const std::string custom = "[appearance]\nfallback_fonts = [\n \"Font#1\", # comment\n \"Font]2\"\n]\n"; REQUIRE(MergeConfigIntoTemplate(next, custom, next).find("Font]2") != std::string::npos); REQUIRE(MergeConfigIntoTemplate(next, "[appearance]\nfallback_fonts = []\n", next).find("fallback_fonts = []") != std::string::npos); } TEST_CASE(shortcut_config_upgrade_preserves_word_selection_and_adds_defaults) { const std::string old_config = "[input]\nword_to_character = true\n\n[keybindings]\nswitch_language_shift = false\n"; const std::string new_template = "[input]\nword_to_character = false\nword_to_character_keys = \"brackets\"\n\n" "[keybindings]\nswitch_language_shift = true\ntoggle_character_set_ctrl_shift_f = true\n"; const auto merged = MergeConfigIntoTemplate(new_template, old_config, ""); REQUIRE(merged.find("word_to_character = true") != std::string::npos); REQUIRE(merged.find("word_to_character_keys = \"brackets\"") != std::string::npos); REQUIRE(merged.find("switch_language_shift = false") != std::string::npos); REQUIRE(merged.find("toggle_character_set_ctrl_shift_f = true") != std::string::npos); } TEST_CASE(candidate_key_config_rejects_invalid_groups_without_changing_state) { const auto keys = GetConfiguredWordToCharacterKeys(); const bool enabled = GetConfiguredWordToCharacterEnabled(); const bool brackets = GetConfiguredPagingBracketsEnabled(); const bool minus = GetConfiguredPagingMinusEqualEnabled(); REQUIRE(!SetConfiguredWordToCharacterKeys("unsupported")); REQUIRE_EQ(GetConfiguredWordToCharacterKeys(), keys); REQUIRE_EQ(GetConfiguredWordToCharacterEnabled(), enabled); REQUIRE_EQ(GetConfiguredPagingBracketsEnabled(), brackets); REQUIRE_EQ(GetConfiguredPagingMinusEqualEnabled(), minus); } TEST_CASE(config_merge_keeps_customized_values_and_adds_new_keys) { const std::string template_text = "[general]\n" "# 悬浮工具栏\n" "floating_toolbar = true\n" "cloud_candidates = true\n" "\n" "[input]\n" "schema = \"shuangpin\" # 可选:quanpin/shuangpin\n"; const std::string user_text = "[general]\n" "floating_toolbar = false\n" "\n" "[input]\n" "schema = \"quanpin\"\n"; const std::string baseline_text = "[general]\n" "floating_toolbar = true\n" "\n" "[input]\n" "schema = \"shuangpin\"\n"; const std::string expected = "[general]\n" "# 悬浮工具栏\n" "floating_toolbar = false\n" "cloud_candidates = true\n" "\n" "[input]\n" "schema = \"quanpin\" # 可选:quanpin/shuangpin\n"; REQUIRE_EQ(MergeConfigIntoTemplate(template_text, user_text, baseline_text), expected); } TEST_CASE(config_merge_lets_new_defaults_win_for_untouched_keys) { const std::string template_text = "[appearance]\npage_size = 9\nfont_size = 18\n"; const std::string user_text = "[appearance]\npage_size = 8\nfont_size = 20\n"; const std::string baseline_text = "[appearance]\npage_size = 8\nfont_size = 16\n"; // page_size 仍是旧默认值 -> 跟随新默认值;font_size 被用户改过 -> 保留。 REQUIRE_EQ(MergeConfigIntoTemplate(template_text, user_text, baseline_text), "[appearance]\npage_size = 9\nfont_size = 20\n"); } TEST_CASE(config_merge_drops_keys_and_sections_absent_from_template) { const std::string template_text = "[general]\nkept = true\n"; const std::string user_text = "[general]\nkept = false\nretired = 1\n\n[gone]\nvalue = \"x\"\n"; REQUIRE_EQ(MergeConfigIntoTemplate(template_text, user_text, std::string()), "[general]\nkept = false\n"); } TEST_CASE(config_merge_keeps_all_user_values_without_a_baseline) { // 从没有 config.base.toml 的旧版本升级时,无法区分「用户改的」和「旧默认值」。 const std::string template_text = "[voice_input]\nasr_token = \"\"\nlanguage = \"zh-cn\"\n"; const std::string user_text = "[voice_input]\nasr_token = \"sk-real-token\"\nlanguage = \"en\"\n"; REQUIRE_EQ(MergeConfigIntoTemplate(template_text, user_text, std::string()), "[voice_input]\nasr_token = \"sk-real-token\"\nlanguage = \"en\"\n"); } TEST_CASE(config_merge_handles_multiline_string_values) { const std::string template_text = "[ai_assistant]\n" "prompt = \"\"\"new\ndefault prompt\"\"\"\n" "model = \"v2\"\n"; const std::string user_text = "[ai_assistant]\n" "prompt = \"\"\"my\nown prompt\"\"\"\n" "model = \"v1\"\n"; const std::string baseline_text = "[ai_assistant]\n" "prompt = \"\"\"old\ndefault prompt\"\"\"\n" "model = \"v1\"\n"; const std::string expected = "[ai_assistant]\n" "prompt = \"\"\"my\nown prompt\"\"\"\n" "model = \"v2\"\n"; REQUIRE_EQ(MergeConfigIntoTemplate(template_text, user_text, baseline_text), expected); } TEST_CASE(config_merge_ignores_assignments_inside_multiline_strings) { const std::string template_text = "[ai_assistant]\n" "prompt = \"\"\"say hi\"\"\"\n" "model = \"v2\"\n"; // prompt 正文里的 model = ... 只是提示词内容,不能被当成一个键。 const std::string user_text = "[ai_assistant]\n" "prompt = \"\"\"say hi\nmodel = \"hijacked\"\n\"\"\"\n"; const std::string expected = "[ai_assistant]\n" "prompt = \"\"\"say hi\nmodel = \"hijacked\"\n\"\"\"\n" "model = \"v2\"\n"; REQUIRE_EQ(MergeConfigIntoTemplate(template_text, user_text, std::string()), expected); } TEST_CASE(config_merge_ignores_commented_out_keys) { const std::string template_text = "[input]\n# schema = \"wubi\"\nschema = \"shuangpin\"\n"; const std::string user_text = "[input]\n# schema = \"quanpin\"\nschema = \"wubi\"\n"; REQUIRE_EQ(MergeConfigIntoTemplate(template_text, user_text, std::string()), "[input]\n# schema = \"wubi\"\nschema = \"wubi\"\n"); } TEST_CASE(configured_voice_input_is_handed_out_as_a_snapshot) { // LoadImeConfig rewrites the whole voice config on the IPC worker thread (asr_tokens / polish_tokens are cleared // and refilled) while the voice control thread and the low-level keyboard hook read it on every keystroke. Handing // out a reference lets those readers walk strings and maps that are being rewritten, so this accessor must return a // snapshot by value. REQUIRE(!std::is_reference_v); REQUIRE((std::is_same_v)); const VoiceInputConfig snapshot = GetConfiguredVoiceInput(); REQUIRE_EQ(snapshot.commit_mode, VoiceInputConfig().commit_mode); } // 读取安装器真正分发的模板,防止开发配置齐全、安装模板漏项导致升级后丢失设置。 TEST_CASE(shipped_translation_settings_survive_template_upgrade) { std::ifstream input(MSIME_DEFAULT_CONFIG_PATH, std::ios::binary); REQUIRE(static_cast(input)); const std::string installed((std::istreambuf_iterator(input)), {}); const std::string configured = "[custom_translation]\nenabled = true\nendpoint = \"https://translation.example/translate\"\n" "api_key = \"test-translation-key\"\n[tencent_tmt]\ntarget_language = \"ja\"\n"; const std::string legacy_template = "[tencent_tmt]\nenabled = true\n"; // 同时覆盖旧版本缺少这些字段,以及新版本已经包含这些字段的升级。 for (const auto &baseline : {legacy_template, installed}) { const auto merged = toml::parse(MergeConfigIntoTemplate(installed, configured, baseline)); REQUIRE(merged["custom_translation"]["enabled"].value_or(false)); REQUIRE_EQ(merged["custom_translation"]["endpoint"].value_or(std::string()), std::string("https://translation.example/translate")); REQUIRE_EQ(merged["custom_translation"]["api_key"].value_or(std::string()), std::string("test-translation-key")); REQUIRE_EQ(merged["tencent_tmt"]["target_language"].value_or(std::string()), std::string("ja")); } }