From 426bd8fe383b3bc0e09a1bf4804c4b2ca755c110 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Thu, 14 May 2026 11:16:52 +0800 Subject: [PATCH] Preserve legacy style mode during migration Constraint: legacy preferences may have defaultMode without activeStylePackId, and Raw style packs may still use the LLM path Rejected: defaulting missing activeStylePackId to builtin.light | silently changes Raw, Structured, and Formal users during upgrade Rejected: skipping script normalization for Raw LLM output | lets Simplified/Traditional preferences be bypassed by raw style packs Confidence: high Scope-risk: narrow Directive: treat missing activeStylePackId as a migration case derived from defaultMode, not as the new-user default Tested: cargo fmt --manifest-path src-tauri/Cargo.toml; git diff --check; cargo test --manifest-path src-tauri/Cargo.toml --lib types::tests::missing_active_style_pack_id_uses_legacy_default_mode; cargo test --manifest-path src-tauri/Cargo.toml --lib types::tests::explicit_active_style_pack_id_is_preserved; cargo test --manifest-path src-tauri/Cargo.toml --lib coordinator::dictation::tests::raw_llm_output_still_applies_script_preference; cargo test --manifest-path src-tauri/Cargo.toml --lib Not-tested: full app build; desktop runtime smoke on macOS/Windows --- .../src-tauri/src/coordinator/dictation.rs | 20 ++++++++- openless-all/app/src-tauri/src/types.rs | 42 +++++++++++++++---- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index c02008b2..f2343ce2 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -289,7 +289,7 @@ async fn run_streaming_polish( fn finalize_polished_text( polished: String, translation_active: bool, - raw_uses_llm: bool, + _raw_uses_llm: bool, mode: PolishMode, polish_error: &Option, chinese_script_preference: crate::types::ChineseScriptPreference, @@ -302,7 +302,7 @@ fn finalize_polished_text( let should_force_script = if translation_active { polish_error.is_some() } else { - (!raw_uses_llm && mode == PolishMode::Raw) || polish_error.is_some() + mode == PolishMode::Raw || polish_error.is_some() }; let polished = if should_force_script { apply_chinese_script_preference(&polished, chinese_script_preference) @@ -1650,6 +1650,22 @@ mod tests { assert_eq!(result, "Open AI"); } + #[test] + fn raw_llm_output_still_applies_script_preference() { + let result = finalize_polished_text( + "繁體".into(), + false, + true, + PolishMode::Raw, + &None, + ChineseScriptPreference::Simplified, + &[], + false, + ); + + assert_eq!(result, "繁体"); + } + #[test] fn non_streamed_output_still_applies_correction_rules() { let rules = vec![correction_rule("Open AI", "OpenAI")]; diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index d3354d23..a7525b85 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -644,8 +644,8 @@ struct UserPreferencesWire { dictation_hotkey: Option, default_mode: PolishMode, enabled_modes: Vec, - #[serde(default = "default_active_style_pack_id")] - active_style_pack_id: String, + #[serde(default)] + active_style_pack_id: Option, #[serde(default)] style_system_prompts: StyleSystemPrompts, #[serde(default)] @@ -711,7 +711,7 @@ impl Default for UserPreferencesWire { dictation_hotkey: None, default_mode: prefs.default_mode, enabled_modes: prefs.enabled_modes, - active_style_pack_id: prefs.active_style_pack_id, + active_style_pack_id: Some(prefs.active_style_pack_id), style_system_prompts: prefs.style_system_prompts, custom_style_prompts: prefs.custom_style_prompts, launch_at_login: prefs.launch_at_login, @@ -767,11 +767,10 @@ impl<'de> Deserialize<'de> for UserPreferences { dictation_hotkey, default_mode: wire.default_mode, enabled_modes: wire.enabled_modes, - active_style_pack_id: if wire.active_style_pack_id.trim().is_empty() { - builtin_style_pack_id(wire.default_mode).to_string() - } else { - wire.active_style_pack_id - }, + active_style_pack_id: wire + .active_style_pack_id + .filter(|id| !id.trim().is_empty()) + .unwrap_or_else(|| builtin_style_pack_id(wire.default_mode).to_string()), style_system_prompts: wire .style_system_prompts .with_legacy_custom_prompts(&wire.custom_style_prompts), @@ -1770,6 +1769,33 @@ mod tests { assert!(prefs.custom_style_prompts.has_for_mode(PolishMode::Formal)); } + #[test] + fn missing_active_style_pack_id_uses_legacy_default_mode() { + let prefs: UserPreferences = serde_json::from_str( + r#"{ + "defaultMode": "structured" + }"#, + ) + .unwrap(); + + assert_eq!(prefs.default_mode, PolishMode::Structured); + assert_eq!(prefs.active_style_pack_id, BUILTIN_STYLE_PACK_STRUCTURED_ID); + } + + #[test] + fn explicit_active_style_pack_id_is_preserved() { + let prefs: UserPreferences = serde_json::from_str( + r#"{ + "defaultMode": "formal", + "activeStylePackId": "custom.meeting" + }"#, + ) + .unwrap(); + + assert_eq!(prefs.default_mode, PolishMode::Formal); + assert_eq!(prefs.active_style_pack_id, "custom.meeting"); + } + #[test] fn legacy_custom_style_prompts_are_not_appended_twice() { let base = StyleSystemPrompts::default();