@@ -42,7 +42,7 @@ fn deep_preset_injects_strategy_section() {
4242 let dims = resolve (
4343 & p,
4444 & RefineStrategyPrefs :: default ( ) ,
45- Some ( & StrategyOverride { preset_id : Some ( "deep" . to_string ( ) ) , dims : HashMap :: new ( ) } ) ,
45+ Some ( & StrategyOverride { preset_id : Some ( "deep" . to_string ( ) ) , dims : HashMap :: new ( ) , custom_text : None } ) ,
4646 ) ;
4747 let s = strategy_instructions ( & dims, & p) ;
4848 assert ! ( s. contains( "深度改写" ) , "档级指令缺失: {}" , s) ;
@@ -62,7 +62,7 @@ fn unknown_preset_falls_back_to_standard() {
6262 let dims = resolve (
6363 & p,
6464 & RefineStrategyPrefs :: default ( ) ,
65- Some ( & StrategyOverride { preset_id : Some ( "no-such" . to_string ( ) ) , dims : HashMap :: new ( ) } ) ,
65+ Some ( & StrategyOverride { preset_id : Some ( "no-such" . to_string ( ) ) , dims : HashMap :: new ( ) , custom_text : None } ) ,
6666 ) ;
6767 assert_eq ! ( dims. preset_id, "standard" ) ;
6868 assert ! ( strategy_instructions( & dims, & p) . is_empty( ) ) ;
@@ -77,6 +77,7 @@ fn invalid_dim_value_falls_back_to_declared_default() {
7777 Some ( & StrategyOverride {
7878 preset_id : None ,
7979 dims : dims_map ( & [ ( "examples" , "bogus" ) , ( "concept" , "plain" ) ] ) ,
80+ custom_text : None ,
8081 } ) ,
8182 ) ;
8283 // bogus → 声明默认 standard;plain 合法保留
@@ -93,12 +94,13 @@ fn global_default_then_task_override_wins() {
9394 assert_eq ! ( d1. preset_id, "minimal" ) ;
9495 assert_eq ! ( d1. dims. get( "examples" ) . map( |s| s. as_str( ) ) , Some ( "condensed" ) ) ;
9596 // 任务覆盖档位:深度覆盖极简
96- let d2 = resolve ( & p, & prefs, Some ( & StrategyOverride { preset_id : Some ( "deep" . to_string ( ) ) , dims : HashMap :: new ( ) } ) ) ;
97+ let d2 = resolve ( & p, & prefs, Some ( & StrategyOverride { preset_id : Some ( "deep" . to_string ( ) ) , dims : HashMap :: new ( ) , custom_text : None } ) ) ;
9798 assert_eq ! ( d2. preset_id, "deep" ) ;
9899 // 单维覆盖只改目标维
99100 let d3 = resolve ( & p, & prefs, Some ( & StrategyOverride {
100101 preset_id : None ,
101102 dims : dims_map ( & [ ( "emotion" , "keep" ) ] ) ,
103+ custom_text : None ,
102104 } ) ) ;
103105 assert_eq ! ( d3. preset_id, "minimal" ) ;
104106 assert_eq ! ( d3. dims. get( "emotion" ) . map( |s| s. as_str( ) ) , Some ( "keep" ) ) ;
@@ -119,7 +121,7 @@ fn intent_meta_breaks_into_chips_and_keywords() {
119121fn preview_system_contains_strategy_and_matches_real_build ( ) {
120122 let p = NoteRefinePrompt :: bundled ( ) ;
121123 let prefs = RefineStrategyPrefs { default_ladder : "deep" . to_string ( ) , dim_overrides : HashMap :: new ( ) } ;
122- let over = StrategyOverride { preset_id : None , dims : dims_map ( & [ ( "examples" , "condensed" ) ] ) } ;
124+ let over = StrategyOverride { preset_id : None , dims : dims_map ( & [ ( "examples" , "condensed" ) ] ) , custom_text : None } ;
123125 let preview = preview_system ( & p, "lecture" , & prefs, Some ( & over) ) ;
124126 let dims = resolve ( & p, & prefs, Some ( & over) ) ;
125127 // 预览与实发同一 build_system 路径(逐字节一致)
@@ -142,5 +144,95 @@ fn meta_serializes_declaration_with_all_intents() {
142144 for want in [ "原文保真" , "考点浓缩" , "通俗转述" , "速查纲要" , "金句摘录" ] {
143145 assert ! ( labels. contains( & want) , "chips 书面命名缺失: {}(实得 {:?})" , want, labels) ;
144146 }
145- assert_eq ! ( meta. ladder_presets. len( ) , 4 , "四档阶梯应齐全" ) ;
147+ assert_eq ! ( meta. ladder_presets. len( ) , 5 , "五档阶梯应齐全(REQ-279 新增自定义)" ) ;
148+ assert ! ( meta. ladder_presets. iter( ) . any( |l| l. id == "custom" ) , "custom 档应声明" ) ;
149+ }
150+
151+ #[ test]
152+ fn standard_ignores_legacy_global_overrides ( ) {
153+ // REQ-279 污染通道③后端兜底:standard 档不受全局逐维覆盖影响——
154+ // 旧版 prefsFromDraft 曾把意图基准全维写入 dim_overrides,叠加后「标准」
155+ // 语义漂移;后端对 standard 恒不叠加(与零覆盖解析逐字段等价)。
156+ let p = NoteRefinePrompt :: bundled ( ) ;
157+ let prefs = RefineStrategyPrefs {
158+ default_ladder : String :: new ( ) ,
159+ dim_overrides : dims_map ( & [ ( "concept" , "plain" ) , ( "conclusion" , "summary_top" ) ] ) ,
160+ } ;
161+ let dirty = resolve ( & p, & prefs, None ) ;
162+ let clean = resolve ( & p, & RefineStrategyPrefs :: default ( ) , None ) ;
163+ assert_eq ! ( dirty. preset_id, "standard" ) ;
164+ assert_eq ! ( dirty, clean, "standard 必须与零覆盖逐字段等价(含指令与每维值)" ) ;
165+ assert ! ( strategy_instructions( & dirty, & p) . is_empty( ) ) ;
166+ // 对照:显式阶梯档(deep)仍允许全局覆盖折叠(设置页默认偏好的语义保留)
167+ let ladder_prefs = RefineStrategyPrefs {
168+ default_ladder : "deep" . to_string ( ) ,
169+ dim_overrides : dims_map ( & [ ( "examples" , "condensed" ) ] ) ,
170+ } ;
171+ let dl = resolve ( & p, & ladder_prefs, None ) ;
172+ assert_eq ! ( dl. preset_id, "deep" ) ;
173+ assert_eq ! ( dl. dims. get( "examples" ) . map( |s| s. as_str( ) ) , Some ( "condensed" ) ) ;
174+ }
175+
176+ #[ test]
177+ fn custom_preset_free_text_becomes_instruction_and_surfaces_in_info ( ) {
178+ // REQ-279:自定义档自由文本 → 档级指令(预览/实发同路径可见);无维度注入
179+ let p = NoteRefinePrompt :: bundled ( ) ;
180+ let dims = resolve (
181+ & p,
182+ & RefineStrategyPrefs :: default ( ) ,
183+ Some ( & StrategyOverride {
184+ preset_id : Some ( "custom" . to_string ( ) ) ,
185+ dims : HashMap :: new ( ) ,
186+ custom_text : Some ( "写得更口语一些,多举一个生活化例子。" . to_string ( ) ) ,
187+ } ) ,
188+ ) ;
189+ assert_eq ! ( dims. preset_id, "custom" ) ;
190+ assert_eq ! ( dims. custom_text, "写得更口语一些,多举一个生活化例子。" ) ;
191+ let s = strategy_instructions ( & dims, & p) ;
192+ assert ! ( s. contains( "本次为自定义档" ) , "档级指令缺失: {}" , s) ;
193+ assert ! ( s. contains( "生活化例子" ) , "自由文本未进指令: {}" , s) ;
194+ // 每维保持声明默认 → 无维度指令注入(纯文本档)
195+ assert_eq ! ( dims. dims. get( "conclusion" ) . map( |s| s. as_str( ) ) , Some ( "as_is" ) ) ;
196+ }
197+
198+ #[ test]
199+ fn custom_preset_empty_text_falls_back_to_standard ( ) {
200+ // 前端守卫 + 后端兜底统一口径:custom 无文本 = 无效 → standard(零变化)
201+ let p = NoteRefinePrompt :: bundled ( ) ;
202+ let dims = resolve (
203+ & p,
204+ & RefineStrategyPrefs :: default ( ) ,
205+ Some ( & StrategyOverride {
206+ preset_id : Some ( "custom" . to_string ( ) ) ,
207+ dims : HashMap :: new ( ) ,
208+ custom_text : Some ( " " . to_string ( ) ) ,
209+ } ) ,
210+ ) ;
211+ assert_eq ! ( dims. preset_id, "standard" ) ;
212+ assert ! ( dims. custom_text. is_empty( ) ) ;
213+ assert ! ( strategy_instructions( & dims, & p) . is_empty( ) ) ;
214+ }
215+
216+ #[ test]
217+ fn custom_preset_ignores_global_overrides_and_caps_text_length ( ) {
218+ // 净化:custom 与 standard 同属「用户显式文本」档——全局残留不得叠加;
219+ // 自由文本 500 字符截断(防提示词膨胀)
220+ let p = NoteRefinePrompt :: bundled ( ) ;
221+ let long: String = "a" . repeat ( 1200 ) ;
222+ let prefs = RefineStrategyPrefs {
223+ default_ladder : String :: new ( ) ,
224+ dim_overrides : dims_map ( & [ ( "concept" , "plain" ) ] ) ,
225+ } ;
226+ let dims = resolve (
227+ & p,
228+ & prefs,
229+ Some ( & StrategyOverride {
230+ preset_id : Some ( "custom" . to_string ( ) ) ,
231+ dims : HashMap :: new ( ) ,
232+ custom_text : Some ( long) ,
233+ } ) ,
234+ ) ;
235+ assert_eq ! ( dims. preset_id, "custom" ) ;
236+ assert_eq ! ( dims. custom_text. chars( ) . count( ) , crate :: ai_strategy:: MAX_CUSTOM_TEXT_CHARS ) ;
237+ assert_eq ! ( dims. dims. get( "concept" ) . map( |s| s. as_str( ) ) , Some ( "original" ) , "全局覆盖不得进 custom 档" ) ;
146238}
0 commit comments